558 lines
12 KiB
C++
558 lines
12 KiB
C++
/*
|
|
* Copyright 2021, Jaidyn Levesque <jadedctrl@teknik.io>
|
|
* All rights reserved. Distributed under the terms of the MIT license.
|
|
*/
|
|
|
|
#include "IrcProtocol.h"
|
|
|
|
#include <iostream>
|
|
|
|
#include <Catalog.h>
|
|
#include <Resources.h>
|
|
#include <SecureSocket.h>
|
|
#include <Socket.h>
|
|
|
|
#include <ChatProtocolMessages.h>
|
|
#include <Flags.h>
|
|
|
|
#include "Numerics.h"
|
|
|
|
|
|
#undef B_TRANSLATION_CONTEXT
|
|
#define B_TRANSLATION_CONTEXT "IrcProtocol"
|
|
|
|
|
|
status_t
|
|
connect_thread(void* data)
|
|
{
|
|
IrcProtocol* protocol = (IrcProtocol*)data;
|
|
status_t status = protocol->Loop();
|
|
exit(status);
|
|
}
|
|
|
|
|
|
IrcProtocol::IrcProtocol()
|
|
:
|
|
fSocket(NULL),
|
|
fNick(NULL),
|
|
fIdent(NULL),
|
|
fReady(false)
|
|
{
|
|
}
|
|
|
|
|
|
IrcProtocol::~IrcProtocol()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
|
|
status_t
|
|
IrcProtocol::Init(ChatProtocolMessengerInterface* interface)
|
|
{
|
|
fMessenger = interface;
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
status_t
|
|
IrcProtocol::Shutdown()
|
|
{
|
|
BString cmd = "QUIT :";
|
|
cmd << fPartText << "\n";
|
|
_SendIrc(cmd);
|
|
|
|
kill_thread(fRecvThread);
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
status_t
|
|
IrcProtocol::UpdateSettings(BMessage* settings)
|
|
{
|
|
fNick = settings->FindString("nick");
|
|
fPartText = settings->GetString("part", "Cardie[0.1]: i've been liquified!");
|
|
fUser = settings->FindString("ident");
|
|
const char* real_name = settings->FindString("real_name");
|
|
const char* server = settings->FindString("server");
|
|
const char* password = settings->FindString("password");
|
|
int32 port = settings->FindInt32("port");
|
|
bool ssl = settings->GetBool("ssl", false);
|
|
|
|
fSocket = ssl ? new BSecureSocket : new BSocket;
|
|
|
|
if (fSocket->Connect(BNetworkAddress(server, port)) != B_OK)
|
|
return B_ERROR;
|
|
|
|
if (password != NULL) {
|
|
BString passMsg = "PASS ";
|
|
passMsg << password << "\n";
|
|
_SendIrc(passMsg);
|
|
}
|
|
|
|
BString userMsg = "USER ";
|
|
userMsg << fUser << " * 0 :" << real_name << "\n";
|
|
_SendIrc(userMsg);
|
|
|
|
BString nickMsg = "NICK ";
|
|
nickMsg << fNick << "\n";
|
|
_SendIrc(nickMsg);
|
|
|
|
fRecvThread = spawn_thread(connect_thread, "what_a_tangled_web_we_weave",
|
|
B_NORMAL_PRIORITY, (void*)this);
|
|
|
|
if (fRecvThread < B_OK)
|
|
return B_ERROR;
|
|
|
|
resume_thread(fRecvThread);
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
status_t
|
|
IrcProtocol::Process(BMessage* msg)
|
|
{
|
|
int32 im_what = msg->FindInt32("im_what");
|
|
switch (im_what) {
|
|
case IM_JOIN_ROOM:
|
|
{
|
|
BString chat_id;
|
|
if (msg->FindString("chat_id", &chat_id) == B_OK) {
|
|
BString cmd = "JOIN ";
|
|
cmd << chat_id << "\n";
|
|
_SendIrc(cmd);
|
|
}
|
|
break;
|
|
}
|
|
case IM_GET_ROOM_METADATA:
|
|
{
|
|
BString chat_id;
|
|
if (msg->FindString("chat_id", &chat_id) == B_OK) {
|
|
BMessage meta(IM_MESSAGE);
|
|
meta.AddInt32("im_what", IM_ROOM_METADATA);
|
|
meta.AddString("chat_id", chat_id);
|
|
meta.AddInt32("room_default_flags",
|
|
ROOM_AUTOJOIN | ROOM_LOG_LOCALLY | ROOM_POPULATE_LOGS);
|
|
_SendMsg(&meta);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
std::cerr << "Unhandled message for IRC:\n";
|
|
msg->PrintToStream();
|
|
}
|
|
return B_ERROR;
|
|
}
|
|
|
|
|
|
BMessage
|
|
IrcProtocol::SettingsTemplate(const char* name)
|
|
{
|
|
BMessage settings;
|
|
if (strcmp(name, "account") == 0)
|
|
settings = _AccountTemplate();
|
|
else if (strcmp(name, "join_room") == 0 || strcmp(name, "create_room") == 0)
|
|
settings = _RoomTemplate();
|
|
return settings;
|
|
}
|
|
|
|
|
|
status_t
|
|
IrcProtocol::Loop()
|
|
{
|
|
while (fSocket != NULL && fSocket->IsConnected() == true)
|
|
_ProcessLine(_ReadUntilNewline(fSocket, &fRemainingBuf));
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
void
|
|
IrcProtocol::_ProcessLine(BString line)
|
|
{
|
|
BStringList words;
|
|
line.RemoveCharsSet("\n\r");
|
|
line.Split(" ", true, words);
|
|
BString sender = _LineSender(words);
|
|
BString code = _LineCode(words);
|
|
BStringList params = _LineParameters(words, line);
|
|
|
|
int32 numeric;
|
|
if ((numeric = atoi(code.String())) > 0)
|
|
_ProcessNumeric(numeric, sender, params);
|
|
else
|
|
_ProcessCommand(code, sender, params);
|
|
}
|
|
|
|
|
|
void
|
|
IrcProtocol::_ProcessNumeric(int32 numeric, BString sender, BStringList params)
|
|
{
|
|
if (numeric > 400) {
|
|
_ProcessNumericError(numeric, sender, params);
|
|
return;
|
|
}
|
|
|
|
switch (numeric) {
|
|
case RPL_WELCOME:
|
|
{
|
|
BString cmd("WHO ");
|
|
cmd << fNick << "\n";
|
|
_SendIrc(cmd);
|
|
break;
|
|
}
|
|
case RPL_WHOREPLY:
|
|
{
|
|
BString channel = params.StringAt(1);
|
|
BString user = params.StringAt(2);
|
|
BString host = params.StringAt(3);
|
|
BString nick = params.StringAt(5);
|
|
BString ident = user;
|
|
ident << "@" << host;
|
|
|
|
// Contains the user's contact info― protocol ready!
|
|
if (fReady == false) {
|
|
fUser = user.String();
|
|
fIdent = ident;
|
|
|
|
fReady = true;
|
|
BMessage ready(IM_MESSAGE);
|
|
ready.AddInt32("im_what", IM_PROTOCOL_READY);
|
|
ready.PrintToStream();
|
|
_SendMsg(&ready);
|
|
|
|
BMessage self(IM_MESSAGE);
|
|
self.AddInt32("im_what", IM_OWN_CONTACT_INFO);
|
|
self.AddString("user_id", fIdent);
|
|
self.AddString("user_name", fNick);
|
|
self.PrintToStream();
|
|
_SendMsg(&self);
|
|
|
|
_SendIrc("MOTD\n");
|
|
}
|
|
|
|
// Used to populate a room's userlist
|
|
if (fWhoRequested == false && channel != "*") {
|
|
BMessage user(IM_MESSAGE);
|
|
user.AddInt32("im_what", IM_ROOM_PARTICIPANTS);
|
|
user.AddString("chat_id", channel);
|
|
user.AddString("user_id", ident);
|
|
user.AddString("user_name", nick);
|
|
_SendMsg(&user);
|
|
}
|
|
break;
|
|
}
|
|
case RPL_ENDOFWHO:
|
|
fWhoRequested = false;
|
|
break;
|
|
case RPL_TOPIC:
|
|
{
|
|
BString chat_id = params.StringAt(1);
|
|
BString subject = params.Last();
|
|
|
|
BMessage topic(IM_MESSAGE);
|
|
topic.AddInt32("im_what", IM_ROOM_SUBJECT_SET);
|
|
topic.AddString("subject", subject);
|
|
topic.AddString("chat_id", chat_id);
|
|
_SendMsg(&topic);
|
|
break;
|
|
}
|
|
case RPL_MOTDSTART:
|
|
case RPL_MOTD:
|
|
case RPL_ENDOFMOTD:
|
|
{
|
|
BString body = params.Last();
|
|
if (numeric == RPL_MOTDSTART)
|
|
body = "――MOTD start――";
|
|
else if (numeric == RPL_ENDOFMOTD)
|
|
body = "――MOTD end――";
|
|
BMessage send(IM_MESSAGE);
|
|
send.AddInt32("im_what", IM_MESSAGE_RECEIVED);
|
|
send.AddString("chat_id", "*server*");
|
|
send.AddString("body", body);
|
|
_SendMsg(&send);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
IrcProtocol::_ProcessNumericError(int32 numeric, BString sender,
|
|
BStringList params)
|
|
{
|
|
switch (numeric) {
|
|
case ERR_NICKNAMEINUSE:
|
|
{
|
|
fNick << "_";
|
|
BString cmd("NICK ");
|
|
cmd << fNick << "\n";
|
|
_SendIrc(cmd);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
IrcProtocol::_ProcessCommand(BString command, BString sender,
|
|
BStringList params)
|
|
{
|
|
if (command == "PING")
|
|
{
|
|
BString cmd = "PONG ";
|
|
cmd << params.Last() << "\n";
|
|
_SendIrc(cmd);
|
|
}
|
|
else if (command == "NOTICE")
|
|
{
|
|
BString chat_id = params.First();
|
|
if (chat_id == "AUTH" || chat_id == "*") {
|
|
chat_id = "*server*";
|
|
sender = "";
|
|
}
|
|
BMessage send(IM_MESSAGE);
|
|
send.AddInt32("im_what", IM_MESSAGE_RECEIVED);
|
|
send.AddString("chat_id", chat_id);
|
|
if (sender.IsEmpty() == false)
|
|
send.AddString("user_id", sender);
|
|
send.AddString("body", params.Last());
|
|
_SendMsg(&send);
|
|
}
|
|
else if (command == "TOPIC")
|
|
{
|
|
BString chat_id = params.First();
|
|
BString subject = params.Last();
|
|
|
|
BMessage topic(IM_MESSAGE);
|
|
topic.AddInt32("im_what", IM_ROOM_SUBJECT_SET);
|
|
topic.AddString("subject", subject);
|
|
topic.AddString("chat_id", chat_id);
|
|
_SendMsg(&topic);
|
|
}
|
|
else if (command == "JOIN")
|
|
{
|
|
BString chat_id = params.First();
|
|
|
|
BMessage joined(IM_MESSAGE);
|
|
joined.AddString("chat_id", chat_id);
|
|
if (_SenderIdent(sender) == fIdent) {
|
|
joined.AddInt32("im_what", IM_ROOM_JOINED);
|
|
// Populate the userlist
|
|
BString cmd("WHO ");
|
|
cmd << chat_id << "\n";
|
|
_SendIrc(cmd);
|
|
}
|
|
else {
|
|
joined.AddInt32("im_what", IM_ROOM_PARTICIPANT_JOINED);
|
|
joined.AddString("user_id", _SenderIdent(sender));
|
|
joined.AddString("user_name", _SenderNick(sender));
|
|
}
|
|
_SendMsg(&joined);
|
|
}
|
|
}
|
|
|
|
|
|
BString
|
|
IrcProtocol::_LineSender(BStringList words)
|
|
{
|
|
BString sender;
|
|
if (words.CountStrings() > 1)
|
|
sender = words.First().RemoveChars(0, 1);
|
|
return sender;
|
|
}
|
|
|
|
|
|
BString
|
|
IrcProtocol::_LineCode(BStringList words)
|
|
{
|
|
BString code;
|
|
if (words.CountStrings() > 2)
|
|
code = words.StringAt(1);
|
|
return code;
|
|
}
|
|
|
|
|
|
BStringList
|
|
IrcProtocol::_LineParameters(BStringList words, BString line)
|
|
{
|
|
BStringList params;
|
|
BString current;
|
|
for (int i = 2; i < words.CountStrings(); i++)
|
|
if ((current = words.StringAt(i)).StartsWith(":") == false)
|
|
params.Add(current);
|
|
else
|
|
break;
|
|
|
|
// Last parameter is preceded by a colon
|
|
int32 index = line.RemoveChars(0, 1).FindFirst(":");
|
|
if (index != B_ERROR)
|
|
params.Add(line.RemoveChars(0, index + 1));
|
|
return params;
|
|
}
|
|
|
|
|
|
void
|
|
IrcProtocol::_SendMsg(BMessage* msg)
|
|
{
|
|
msg->AddString("protocol", Signature());
|
|
if (fReady == true)
|
|
fMessenger->SendMessage(msg);
|
|
else {
|
|
std::cerr << "Tried sending message when not ready: \n";
|
|
msg->PrintToStream();
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
IrcProtocol::_SendIrc(BString cmd)
|
|
{
|
|
if (fSocket != NULL && fSocket->IsConnected() == true)
|
|
fSocket->Write(cmd.String(), cmd.CountChars());
|
|
else {
|
|
BMessage disable(IM_MESSAGE);
|
|
disable.AddInt32("im_what", IM_PROTOCOL_DISABLE);
|
|
}
|
|
}
|
|
|
|
|
|
BString
|
|
IrcProtocol::_SenderNick(BString sender)
|
|
{
|
|
BStringList split;
|
|
sender.Split("!", true, split);
|
|
return split.First();
|
|
}
|
|
|
|
|
|
BString
|
|
IrcProtocol::_SenderIdent(BString sender)
|
|
{
|
|
BStringList split;
|
|
sender.Split("!", true, split);
|
|
return split.Last();
|
|
}
|
|
|
|
|
|
BString
|
|
IrcProtocol::_ReadUntilNewline(BDataIO* io, BString* extraBuffer)
|
|
{
|
|
BString total;
|
|
char buf[1024] = { '\0' };
|
|
|
|
// Use buffer from last read if any text remains
|
|
if (extraBuffer->IsEmpty() == false) {
|
|
BString trimRet = _TrimStringToNewline(extraBuffer);
|
|
if (trimRet.IsEmpty() == true)
|
|
total << extraBuffer;
|
|
else
|
|
return trimRet;
|
|
}
|
|
|
|
while (!(strstr(buf, "\n"))) {
|
|
io->Read(buf, 1023);
|
|
std::cerr << buf << std::endl;
|
|
total << buf;
|
|
}
|
|
|
|
BString currentLine = _TrimStringToNewline(&total);
|
|
extraBuffer->SetTo(total);
|
|
return currentLine;
|
|
}
|
|
|
|
|
|
BString
|
|
IrcProtocol::_TrimStringToNewline(BString* str)
|
|
{
|
|
BString line;
|
|
int32 lineEnd = str->FindFirst('\n');
|
|
|
|
if (lineEnd != B_ERROR) {
|
|
str->CopyCharsInto(line, 0, lineEnd + 1);
|
|
str->RemoveChars(0, lineEnd + 1);
|
|
}
|
|
return line;
|
|
}
|
|
|
|
|
|
BMessage
|
|
IrcProtocol::_AccountTemplate()
|
|
{
|
|
BMessage settings;
|
|
|
|
BMessage server;
|
|
server.AddString("name", "server");
|
|
server.AddString("description", B_TRANSLATE("Server:"));
|
|
server.AddString("default", "irc.oftc.net");
|
|
server.AddString("error", B_TRANSLATE("Please enter a valid server address."));
|
|
server.AddInt32("type", B_STRING_TYPE);
|
|
settings.AddMessage("setting", &server);
|
|
|
|
BMessage port;
|
|
port.AddString("name", "port");
|
|
port.AddString("description", B_TRANSLATE("Port:"));
|
|
port.AddInt32("default", 6697);
|
|
port.AddString("error", B_TRANSLATE("We need a port-number to know which door to knock on! Likely 6667/6697."));
|
|
port.AddInt32("type", B_INT32_TYPE);
|
|
settings.AddMessage("setting", &port);
|
|
|
|
BMessage ssl;
|
|
ssl.AddString("name", "ssl");
|
|
ssl.AddString("description", B_TRANSLATE("SSL:"));
|
|
ssl.AddBool("default", true);
|
|
ssl.AddInt32("type", B_BOOL_TYPE);
|
|
settings.AddMessage("setting", &ssl);
|
|
|
|
BMessage nick;
|
|
nick.AddString("name", "nick");
|
|
nick.AddString("description", B_TRANSLATE("Nickname:"));
|
|
nick.AddString("default", "Haikunaut");
|
|
nick.AddString("error", B_TRANSLATE("You need a default nickname― The Nameless are not welcome on IRC."));
|
|
nick.AddInt32("type", B_STRING_TYPE);
|
|
settings.AddMessage("setting", &nick);
|
|
|
|
BMessage ident;
|
|
ident.AddString("name", "ident");
|
|
ident.AddString("description", B_TRANSLATE("Username:"));
|
|
ident.AddString("error", B_TRANSLATE("You need a username in order to connect!"));
|
|
ident.AddInt32("type", B_STRING_TYPE);
|
|
settings.AddMessage("setting", &ident);
|
|
|
|
BMessage password;
|
|
password.AddString("name", "password");
|
|
password.AddString("description", B_TRANSLATE("Password:"));
|
|
password.AddInt32("type", B_STRING_TYPE);
|
|
settings.AddMessage("setting", &password);
|
|
|
|
BMessage realName;
|
|
realName.AddString("name", "real_name");
|
|
realName.AddString("description", B_TRANSLATE("Real name:"));
|
|
realName.AddInt32("type", B_STRING_TYPE);
|
|
settings.AddMessage("setting", &realName);
|
|
|
|
BMessage part;
|
|
part.AddString("name", "part");
|
|
part.AddString("description", B_TRANSLATE("Part message:"));
|
|
part.AddInt32("type", B_STRING_TYPE);
|
|
part.AddString("default", "Cardie[0.1]: i've been liquified!");
|
|
settings.AddMessage("setting", &part);
|
|
|
|
return settings;
|
|
}
|
|
|
|
|
|
BMessage
|
|
IrcProtocol::_RoomTemplate()
|
|
{
|
|
BMessage settings;
|
|
|
|
BMessage id;
|
|
id.AddString("name", "chat_id");
|
|
id.AddString("description", B_TRANSLATE("Channel:"));
|
|
id.AddString("error", B_TRANSLATE("Please enter a channel― skipping it doesn't make sense!"));
|
|
id.AddInt32("type", B_STRING_TYPE);
|
|
settings.AddMessage("setting", &id);
|
|
|
|
return settings;
|
|
}
|