#include #include #include #include #include #include #include #include #include #include #include #include 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); }