Chat-O-Matic/protocols/irc/IrcProtocol.cpp
Jaidyn Ann 4905dbbe6c Redesign add-on disconnection
Currently, add-ons are disconnected when ChatProtocol::Shutdown() is
called, which the add-on can do by itself― but there is no standard way
for add-ons to notify the app about their Shutdown. Because of this,
they tend to not call Shutdown()― instead (as in the case of the Jabber
add-on), they display a BAlert (IM_ERROR) notifying the user of the
connection error, but the account is considered active by Cardie (and
its threads are still existant, including its ProtocolLooper).

Zombies are bad, so this is redesigned somewhat with this commit:
Protocols should no longer call ChatProtocol::Shutdown() themselves,
they must send an IM_MESSAGE of IM_PROTOCOL_DISABLE to the app.

This will delete its ProtocolLooper, which in turn will send a
notification to the user and delete the ChatProtocol, and so
calling ChatProtocol::Shutdown().

In the included protocols, an IM_ERROR is sent right before
IM_PROTOCOL_DISABLE is sent if due to a connection error. This is not
required, but it is courteous to inform your user about the "why." :)
2021-07-18 17:52:36 -05:00

474 lines
9.3 KiB
C++

/*
* Copyright 2021, Jaidyn Levesque <jadedctrl@teknik.io>
* All rights reserved. Distributed under the terms of the MIT license.
*/
#include "IrcProtocol.h"
#include <cstdio>
#include <StringList.h>
#include <ChatProtocolMessages.h>
IrcProtocol* current_proto;
status_t
connect_thread(void* data)
{
IrcProtocol* proto = (IrcProtocol*)data;
current_proto = proto;
BMessage* settings = proto->fSettings;
if (!settings)
return B_ERROR;
const char* nick = settings->FindString("nick");
const char* ident = 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 = false;
if (!nick || !ident || !server)
return B_ERROR;
// libircclient wants a "#" in front of SSL addresses
BString joinServer;
if (ssl == true)
joinServer << "#";
joinServer << server;
// Now create the session
irc_callbacks_t callbacks = get_callbacks();
irc_session_t* session = irc_create_session(&callbacks);
proto->fSession = session;
irc_ctx_t ctx;
ctx.nick.SetTo(nick);
irc_set_ctx(session, &ctx);
if (!session)
return B_ERROR;
// Start connection
if (irc_connect(session, joinServer.String(), port, password, nick, ident,
real_name))
{
BMessage error(IM_ERROR);
error.AddString("error", "Could not connect");
error.AddString("details", irc_strerror(irc_errno(session)));
_SendMessage(&error);
_SendMessage(new BMessage(IM_PROTOCOL_DISABLE));
return B_ERROR;
}
// Start network loop
if (irc_run(session)) {
BMessage error(IM_ERROR);
error.AddString("error", "Connection or I/O error");
error.AddString("details", irc_strerror(irc_errno(session)));
_SendMessage(&error);
_SendMessage(new BMessage(IM_PROTOCOL_DISABLE));
return B_ERROR;
}
return B_OK;
}
IrcProtocol::IrcProtocol()
{
}
status_t
IrcProtocol::Init(ChatProtocolMessengerInterface* interface)
{
fMessenger = interface;
return B_OK;
}
status_t
IrcProtocol::Shutdown()
{
return B_OK;
}
status_t
IrcProtocol::Process(BMessage* msg)
{
BString chat_id;
if (msg->FindString("chat_id", &chat_id) != B_OK)
return B_ERROR;
switch (msg->FindInt32("im_what"))
{
case IM_SEND_MESSAGE:
{
BString body;
if (msg->FindString("body", &body) != B_OK)
return B_ERROR;
irc_cmd_msg(fSession, chat_id.String(), body.String());
irc_ctx_t* ctx = (irc_ctx_t*)irc_get_ctx(fSession);
BMessage sendMsg(*msg);
sendMsg.ReplaceInt32("im_what", IM_MESSAGE_SENT);
sendMsg.AddString("user_id", ctx->id);
_SendMessage(&sendMsg);
break;
}
case IM_JOIN_ROOM:
case IM_CREATE_ROOM:
{
irc_cmd_join(fSession, chat_id.String(), "");
break;
}
case IM_LEAVE_ROOM:
{
irc_cmd_part(fSession, chat_id.String());
break;
}
case IM_GET_ROOM_PARTICIPANTS:
{
irc_cmd_names(fSession, chat_id.String());
break;
}
case IM_ROOM_SEND_INVITE:
{
BString user_id;
if (msg->FindString("user_id", &user_id) != B_OK)
return B_ERROR;
irc_cmd_invite(fSession, user_id.String(), chat_id.String());
break;
}
case IM_ROOM_KICK_PARTICIPANT:
{
BString user_id;
if (msg->FindString("user_id", &user_id) != B_OK)
return B_ERROR;
irc_cmd_kick(fSession, user_id.String(), chat_id.String(),
msg->FindString("body"));
break;
}
}
return B_OK;
}
status_t
IrcProtocol::UpdateSettings(BMessage* msg)
{
fSettings = msg;
fServerThread = spawn_thread(connect_thread, "connect_thread",
B_NORMAL_PRIORITY, (void*)this);
if (fServerThread < B_OK)
return B_ERROR;
resume_thread(fServerThread);
return B_OK;
}
BMessage
IrcProtocol::SettingsTemplate(const char* name)
{
size_t size;
BMessage temp;
const void* buff = fResources.LoadResource(B_MESSAGE_TYPE, name, &size);
if (buff != NULL)
temp.Unflatten((const char*)buff);
return temp;
}
BObjectList<BMessage>
IrcProtocol::Commands()
{
return BObjectList<BMessage>();
}
BObjectList<BMessage>
IrcProtocol::UserPopUpItems()
{
return BObjectList<BMessage>();
}
BObjectList<BMessage>
IrcProtocol::ChatPopUpItems()
{
return BObjectList<BMessage>();
}
BObjectList<BMessage>
IrcProtocol::MenuBarItems()
{
return BObjectList<BMessage>();
}
const char*
IrcProtocol::Signature() const
{
return "irc";
}
const char*
IrcProtocol::FriendlySignature() const
{
return "IRC";
}
BBitmap*
IrcProtocol::Icon() const
{
return NULL;
}
void
IrcProtocol::SetAddOnPath(BPath path)
{
fAddOnPath = path;
fResources.SetTo(path.Path());
}
BPath
IrcProtocol::AddOnPath()
{
return fAddOnPath;
}
const char*
IrcProtocol::GetName()
{
return fName.String();
}
void
IrcProtocol::SetName(const char* name)
{
fName.SetTo(name);
}
uint32
IrcProtocol::GetEncoding()
{
return 0xffff;
}
ChatProtocolMessengerInterface*
IrcProtocol::MessengerInterface() const
{
return fMessenger;
}
irc_callbacks_t
get_callbacks()
{
irc_callbacks_t callbacks;
callbacks.event_connect = event_connect;
callbacks.event_numeric = event_numeric;
callbacks.event_join = event_join;
callbacks.event_part = event_part;
callbacks.event_channel = event_channel;
callbacks.event_privmsg = event_privmsg;
callbacks.event_nick = event_nick;
return callbacks;
}
void
event_connect(irc_session_t* session, const char* event,
const char* origin, const char** params, unsigned int count)
{
BMessage readyMsg(IM_MESSAGE);
readyMsg.AddInt32("im_what", IM_PROTOCOL_READY);
_SendMessage(&readyMsg);
}
void
event_numeric(irc_session_t* session, unsigned int event,
const char* origin, const char** params, unsigned int count)
{
irc_ctx_t* ctx = (irc_ctx_t*)irc_get_ctx(session);
switch (event) {
case LIBIRC_RFC_RPL_NAMREPLY:
{
BStringList user_id;
BString user_list(params[3]);
user_list.Split(" ", false, user_id);
BStringList user_name(user_id);
int32 index = user_name.IndexOf(ctx->nick);
if (index >= 0)
user_id.Replace(index, BString(ctx->id));
BMessage list(IM_MESSAGE);
list.AddInt32("im_what", IM_ROOM_PARTICIPANTS);
list.AddString("chat_id", params[2]);
list.AddStrings("user_id", user_id);
list.AddStrings("user_name", user_name);
_SendMessage(&list);
break;
}
case LIBIRC_RFC_RPL_TOPIC:
{
BMessage topic(IM_MESSAGE);
topic.AddInt32("im_what", IM_ROOM_SUBJECT_SET);
topic.AddString("chat_id", params[1]);
topic.AddString("subject", params[2]);
_SendMessage(&topic);
break;
}
}
}
void
event_join(irc_session_t* session, const char* event, const char* joiner,
const char** channel, unsigned int count)
{
int32 im_what = IM_ROOM_PARTICIPANT_JOINED;
irc_ctx_t* ctx = (irc_ctx_t*)irc_get_ctx(session);
if (_IsOwnUser(joiner, session) == true)
im_what = IM_ROOM_JOINED;
BMessage joinedMsg(IM_MESSAGE);
joinedMsg.AddInt32("im_what", im_what);
joinedMsg.AddString("user_id", joiner);
joinedMsg.AddString("chat_id", channel[0]);
_SendMessage(&joinedMsg);
}
void
event_part(irc_session_t* session, const char* event, const char* quitter,
const char** chanReason, unsigned int count)
{
int32 im_what = IM_ROOM_PARTICIPANT_LEFT;
if (_IsOwnUser(quitter, session) == true)
im_what = IM_ROOM_LEFT;
BString body;
if (chanReason[1] != NULL)
body << chanReason[1];
BMessage quitMsg(IM_MESSAGE);
quitMsg.AddInt32("im_what", im_what);
quitMsg.AddString("user_id", quitter);
quitMsg.AddString("chat_id", chanReason[0]);
if (body.IsEmpty() == false)
quitMsg.AddString("body", body);
_SendMessage(&quitMsg);
}
void
event_channel(irc_session_t* session, const char* event, const char* sender,
const char** chanBody, unsigned int count)
{
BMessage msgMsg(IM_MESSAGE);
msgMsg.AddInt32("im_what", IM_MESSAGE_RECEIVED);
msgMsg.AddString("user_id", sender);
msgMsg.AddString("chat_id", chanBody[0]);
msgMsg.AddString("body", chanBody[1]);
_SendMessage(&msgMsg);
}
void
event_privmsg(irc_session_t* session, const char* event, const char* sender,
const char** selfBody, unsigned int count)
{
BMessage msgMsg(IM_MESSAGE);
msgMsg.AddInt32("im_what", IM_MESSAGE_RECEIVED);
msgMsg.AddString("user_id", sender);
msgMsg.AddString("chat_id", _UserNick(sender));
msgMsg.AddString("body", selfBody[1]);
_SendMessage(&msgMsg);
}
// TODO: Nick-change API
void
event_nick(irc_session_t* session, const char* event, const char* oldNick,
const char** newNick, unsigned int count)
{
irc_ctx_t* ctx = (irc_ctx_t*)irc_get_ctx(session);
if (_IsOwnUser(oldNick, session) == true) {
ctx->nick.SetTo(newNick[0]);
irc_set_ctx(session, ctx);
}
}
BString
_UserNick(const char* userId)
{
BStringList split;
BString id(userId);
id.Split("!", false, split);
BString name = split.StringAt(0);
if (name.StartsWith("@") == true)
name.RemoveFirst("@");
return name;
}
bool
_IsOwnUser(const char* userId, irc_session_t* session)
{
bool ret = false;
irc_ctx_t* ctx = (irc_ctx_t*)irc_get_ctx(session);
if (ctx->nick == _UserNick(userId)) {
ret = true;
if (ctx->id.IsEmpty() == true) {
ctx->id.SetTo(userId);
irc_set_ctx(session, ctx);
// The app hasn't been informed of user_id, then!
BMessage ownInfo(IM_MESSAGE);
ownInfo.AddInt32("im_what", IM_OWN_CONTACT_INFO);
ownInfo.AddString("user_id", userId);
_SendMessage(&ownInfo);
}
}
return ret;
}
void
_SendMessage(BMessage* msg)
{
if (!msg)
return;
msg->AddString("protocol", current_proto->Signature());
current_proto->MessengerInterface()->SendMessage(msg);
}