Parsee/src/MatrixEventHandler.c
LDA 6167732e83 [FIX] BMP for MUC nicks, dip toes in vCard avatars
Can, your, Bifrost, Do, That. -lh
2024-09-21 13:18:59 +02:00

554 lines
15 KiB
C

#include <Parsee.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Log.h>
#include <string.h>
#include <stdlib.h>
#include <StanzaBuilder.h>
#include <Unistring.h>
#include <Matrix.h>
#include <AS.h>
#include <ctype.h>
static const char *
GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to);
static char *
JoinMUC(ParseeData *data, HashMap *event, char *jid, char *muc, char *name)
{
char *sender = GrabString(event, 1, "sender");
Unistr *uninick = UnistrCreate(name);
Unistr *filtered = UnistrFilter(uninick, UnistrIsBMP);
char *nick = UnistrC(filtered);
char *rev = StrConcat(3, muc, "/", nick);
int nonce = 0;
Log(LOG_DEBUG, "MUCJOINER: filtered '%s' to '%s'", name, nick);
UnistrFree(uninick);
UnistrFree(filtered);
while (!XMPPJoinMUC(data->jabber, jid, rev, true) && nonce < 32)
{
char *nonce_str = StrInt(nonce);
char *input = StrConcat(3, sender, name, nonce_str);
char *hex = ParseeHMACS(data->id, input);
if (strlen(hex) >= 8)
{
hex[8] = '\0';
}
Free(nick);
Free(rev);
nick = StrConcat(4, name, "[", hex, "]");
rev = StrConcat(3, muc, "/", nick);
nonce++;
Free(nonce_str);
Free(input);
Free(hex);
}
ParseePushNickTable(muc, sender, nick);
Free(nick);
return (rev);
}
static void
ParseeMemberHandler(ParseeData *data, HashMap *event)
{
char *state_key = GrabString(event, 1, "state_key");
char *membership = GrabString(event, 2, "content", "membership");
char *room_id = GrabString(event, 1, "room_id");
char *sender = GrabString(event, 1, "sender");
char *chat_id;
char *local = data->config->sender_localpart;
const ParseeConfig *conf = data->config;
if (StrEquals(membership, "invite") && ParseeIsPuppet(conf, state_key))
{
DbRef *ref = NULL;
HashMap *json;
char *jid;
bool direct = GrabBoolean(event, 2, "content", "is_direct");
bool bot = !strncmp(sender + 1, local, strlen(local));
Free(ASJoin(conf, room_id, state_key));
jid = ParseeDecodeLocalJID(conf, state_key);
if (direct && !bot)
{
ref = DbCreate(data->db, 3, "rooms", room_id, "data");
json = DbJson(ref);
HashMapSet(json, "is_direct", JsonValueBoolean(direct && !bot));
HashMapSet(json, "xmpp_user", JsonValueString(jid));
DbUnlock(data->db, ref);
ParseePushDMRoom(data, sender, jid, room_id);
}
if (jid)
{
Free(jid);
}
}
else if (StrEquals(membership, "join") && !ParseeIsPuppet(conf, state_key))
{
char *jid = ParseeEncodeMXID(state_key);
chat_id = ParseeGetFromRoomID(data, room_id);
if (chat_id)
{
char *muc = ParseeGetMUCID(data, chat_id);
char *name = ASGetName(data->config, room_id, state_key);
char *avatar = ASGetAvatar(data->config, room_id, state_key);
char *jabber = JoinMUC(data, event, jid, muc, name);
Log(LOG_DEBUG, "MATRIX: Joining as '%s' (avatar=%s)", jabber, avatar);
Free(avatar);
Free(jabber);
Free(name);
Free(muc);
/* TODO: XEP-0084 magic to advertise a new avatar if possible. */
}
else
{
char *avatar = ASGetAvatar(data->config, room_id, state_key);
char *sha = NULL, *mime = NULL, *url = NULL;
char *full_jid = StrConcat(3,
jid, "@", data->config->component_host
);
XMLElement *elem, *pevent, *items, *item, *meta, *info;
Log(LOG_DEBUG, "MATRIX: Got local user '%s'(mxid=%s avatar=%s)", jid, state_key, avatar);
url = ParseeToUnauth(data, avatar);
elem = XMLCreateTag("message");
ASGetMIMESHA(data->config, avatar, &mime, &sha);
{
#define PUBSUB "http://jabber.org/protocol/pubsub"
#define AVATAR "urn:xmpp:avatar:metadata"
pevent = XMLCreateTag("event");
XMLAddAttr(pevent, "xmlns", PUBSUB "#event");
{
items = XMLCreateTag("items");
item = XMLCreateTag("item");
XMLAddAttr(items, "node", AVATAR);
XMLAddAttr(item, "id", sha);
{
meta = XMLCreateTag("metadata");
info = XMLCreateTag("info");
XMLAddAttr(meta, "xmlns", AVATAR);
XMLAddAttr(info, "id", sha);
XMLAddAttr(info, "url", url);
XMLAddAttr(info, "type", mime);
XMLAddChild(meta, info);
XMLAddChild(item, meta);
}
XMLAddChild(items, item);
XMLAddChild(pevent, items);
}
XMLAddChild(elem, pevent);
#undef PUBSUB
}
/* TODO: Broadcast PEP avatar change */
ParseeBroadcastStanza(data, full_jid, elem);
XMLFreeElement(elem);
Free(full_jid);
Free(avatar);
Free(mime);
Free(sha);
Free(url);
}
Free(jid);
Free(chat_id);
}
else if ((StrEquals(membership, "leave") ||
StrEquals(membership, "ban"))
&& !ParseeIsPuppet(conf, state_key))
{
/* TODO: Manage bans on their own, rather than a spicy leave.
* Then again, this could cause desync issues. */
XMPPComponent *jabber = data->jabber;
char *jid = ParseeEncodeMXID(state_key);
char *name = NULL, *rev = NULL, *muc_id = NULL;
char *reason = GrabString(event, 2, "content", "reason");
/* Try to find the chat ID */
chat_id = ParseeGetFromRoomID(data, room_id);
muc_id = ParseeGetMUCID(data, chat_id);
if (!chat_id)
{
/* If it can't be found, try to see if it's as a DM */
char *info_from = NULL, *info_to = NULL;
const char *type = GetXMPPInformation(data, event, &info_from, &info_to);
if (StrEquals(type, "chat"))
{
char *jid_to = ParseeTrimJID(info_to);
Log(LOG_DEBUG, "('%s'->'%s') is gone.", state_key, info_to);
/* TODO: Send a last DM, signifying that all is gone. */
ParseeDeleteDM(data, state_key, jid_to);
Free(jid_to);
}
Free(info_from);
Free(info_to);
goto end;
}
name = StrDuplicate(ParseeLookupNick(muc_id, sender));
rev = StrConcat(3, muc_id, "/", name);
XMPPLeaveMUC(jabber, jid, rev, reason);
ParseePushNickTable(muc_id, sender, NULL);
end:
Free(chat_id);
Free(muc_id);
Free(name);
Free(rev);
Free(jid);
}
}
static void
ParseeBotHandler(ParseeData *data, HashMap *event)
{
char *msgtype = GrabString(event, 2, "content", "msgtype");
char *body = GrabString(event, 2, "content", "body");
char *id = GrabString(event, 1, "room_id");
char *sender = GrabString(event, 1, "sender");
char *profile = ParseeMXID(data);
Command *cmd = NULL;
ParseeCmdArg arg = {
.data = data,
.event = event
};
if (StrEquals(msgtype, "m.notice"))
{
Free(profile);
return;
}
if (*body != '!')
{
/* All commands are to be marked with a ! */
Free(ASSend(
data->config, id, profile,
"m.room.message",
MatrixCreateNotice("Please enter a valid command")
));
Free(profile);
return;
}
if (!ParseeIsAdmin(data, sender))
{
Free(ASSend(
data->config, id, profile,
"m.room.message",
MatrixCreateNotice("You are not authorised to do this.")
));
Free(profile);
return;
}
body++;
cmd = CommandParse(body);
RouteCommand(data->handler, cmd, &arg);
Free(profile);
CommandFree(cmd);
}
static const char *
GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to)
{
const char *type = NULL;
char *room_id = GrabString(event, 1, "room_id");
char *matrix_sender = GrabString(event, 1, "sender");
char *chat_id = NULL, *muc_id = NULL;
char *user;
DbRef *room_data;
HashMap *data_json;
bool direct = false;
if (!data || !event || !from || !to)
{
return NULL;
}
*from = NULL;
*to = NULL;
chat_id = ParseeGetFromRoomID(data, room_id);
room_data = DbLockIntent(data->db,
DB_HINT_READONLY,
3, "rooms", room_id, "data"
);
data_json = DbJson(room_data);
direct = GrabBoolean(data_json, 1, "is_direct");
type = direct ? "chat" : "groupchat";
user = GrabString(data_json, 1, "xmpp_user");
*from = ParseeEncodeMXID(matrix_sender);
if (direct)
{
*to = StrDuplicate(user);
Free(chat_id);
}
else
{
char *matrix_name;
muc_id = ParseeGetMUCID(data, chat_id);
if (!chat_id)
{
/* muc_id is already implied to be freed by this point */
Free(*from);
*from = NULL;
DbUnlock(data->db, room_data);
return NULL;
}
matrix_name = ASGetName(data->config, room_id, matrix_sender);
Free(JoinMUC(data, event, *from, muc_id, matrix_name));
*to = muc_id;
Free(matrix_name);
}
Free(chat_id);
DbUnlock(data->db, room_data);
return type;
}
static void
ParseeMessageHandler(ParseeData *data, HashMap *event)
{
XMPPComponent *jabber = data->jabber;
StanzaBuilder *builder;
DbRef *ref = NULL;
HashMap *json;
char *body = GrabString(event, 2, "content", "body");
char *id = GrabString(event, 1, "room_id");
char *ev_id = GrabString(event, 1, "event_id");
char *m_sender = GrabString(event, 1, "sender");
char *chat_id, *muc_id;
char *reply_id = MatrixGetReply(event);
char *xepd = ParseeXMPPify(event);
char *type, *user, *xmppified_user = NULL, *to = NULL;
char *unauth = NULL;
char *origin_id = NULL, *stanza = NULL;
char *sender = NULL;
char *unedited_id = MatrixGetEdit(event);
char *url = GrabString(event, 2, "content", "url");
char *encoded_from = NULL;
bool direct = false;
if (ParseeIsPuppet(data->config, m_sender) ||
ParseeManageBan(data, m_sender, id))
{
Free(reply_id);
Free(xepd);
Free(unedited_id);
return;
}
chat_id = ParseeGetFromRoomID(data, id);
ref = DbLockIntent(data->db, DB_HINT_READONLY, 3, "rooms", id, "data");
json = DbJson(ref);
direct = JsonValueAsBoolean(HashMapGet(json, "is_direct"));
if (!direct && !chat_id)
{
ParseeBotHandler(data, event);
DbUnlock(data->db, ref);
ref = NULL;
Free(chat_id);
Free(reply_id);
Free(xepd);
Free(unedited_id);
return;
}
type = direct ? "chat" : "groupchat";
user = GrabString(json, 1, "xmpp_user");
unauth = ParseeToUnauth(data, url);
encoded_from = ParseeEncodeMXID(m_sender);
xmppified_user = StrConcat(3,
encoded_from, "@", jabber->host
);
if (direct)
{
to = StrDuplicate(user);
Free(chat_id);
}
else
{
char *name;
/* Try to find the chat ID */
muc_id = ParseeGetMUCID(data, chat_id);
if (!chat_id)
{
goto end;
}
name = ASGetName(data->config, id, m_sender);
Free(JoinMUC(data, event, encoded_from, muc_id, name));
to = muc_id;
Free(name);
}
if (reply_id)
{
/* TODO: Monocles chat DM users HATE this trick!
* Replies don't work there. Go figure why. */
if (!ParseeGetStanzaInfo(data, chat_id, reply_id, &stanza, &sender))
{
ParseeGetDMStanzaInfo(data, id, reply_id, &stanza, &sender);
}
}
else if (unedited_id)
{
if (!ParseeGetOrigin(data, chat_id, unedited_id, &origin_id))
{
ParseeGetDMOrigin(data, id, unedited_id, &origin_id);
}
}
if (direct && sender)
{
char *sndr_tmp = sender;
sender = ParseeTrimJID(sender);
Free(sndr_tmp);
}
{
char *xmpp_ident = StrRandom(32);
builder = CreateStanzaBuilder(xmppified_user, to, xmpp_ident);
SetStanzaType(builder, type);
SetStanzaBody(builder, unauth ? unauth : (xepd ? xepd : body));
SetStanzaReply(builder, stanza, sender);
SetStanzaLink(builder, unauth);
SetStanzaEdit(builder, origin_id);
SetStanzaXParsee(builder, event);
WriteoutStanza(builder, jabber);
DestroyStanzaBuilder(builder);
if (direct)
{
ParseePushDMStanza(
data, id, NULL,
xmpp_ident, ev_id,
xmppified_user
);
}
Free(xmpp_ident);
}
end:
Free(origin_id);
Free(xmppified_user);
Free(chat_id);
Free(to);
Free(reply_id);
Free(xepd);
Free(stanza);
Free(sender);
Free(unauth);
Free(unedited_id);
Free(encoded_from);
DbUnlock(data->db, ref);
ref = NULL;
}
void
ParseeEventHandler(ParseeData *data, HashMap *event)
{
char *event_type, *event_id, *room_id, *sender;
char *parsee = ParseeMXID(data);
event_id = GrabString(event, 1, "event_id");
event_type = GrabString(event, 1, "type");
room_id = GrabString(event, 1, "room_id");
sender = GrabString(event, 1, "sender");
if (!data || !event || ParseeIsPuppet(data->config, sender))
{
Free(parsee);
return;
}
ParseePushHeadTable(room_id, event_id);
if (StrEquals(event_type, "m.room.member"))
{
ParseeMemberHandler(data, event);
Free(parsee);
return;
}
else if (StrEquals(event_type, "m.room.message") ||
StrEquals(event_type, "m.sticker")) /* TODO: Actual sticker
* support here... */
{
ParseeMessageHandler(data, event);
Free(parsee);
return;
}
else if (StrEquals(event_type, "m.room.redaction") &&
!StrEquals(sender, parsee))
{
char *from, *to;
char *redacted = GrabString(event, 1, "redacts");
char *redacted_stanza = NULL;
char *chat_id;
XMPPComponent *jabber = data->jabber;
const char *type = GetXMPPInformation(data, event, &from, &to);
chat_id = ParseeGetFromRoomID(data, room_id);
if (!ParseeGetOrigin(data, chat_id, redacted, &redacted_stanza))
{
ParseeGetDMOrigin(data, room_id, redacted, &redacted_stanza);
}
/* Some clients don't support retractions *at all*, which smell.
* This therefore serves as a fallback, just in case that fails.
*
* TODO: BAD IDEA. XMPP CLIENTS, FUCKING IMPLEMENT IT YOU LAZY
* FUCKTARDS.
*/
XMPPRetract(jabber, from, to, (char *) type, redacted_stanza);
Free(redacted_stanza);
Free(chat_id);
Free(from);
Free(to);
}
Free(parsee);
}