From 6b0f08c49e921cb9ba6778963fc85ed580453de5 Mon Sep 17 00:00:00 2001 From: LDA Date: Tue, 2 Jul 2024 02:21:31 +0200 Subject: [PATCH] [ADD/WIP] One-way chatstates, capabilities No avatarwerk today because avatars are an absolute sore. --- README.MD | 7 ++ XEPS-TBD.TXT | 2 + src/AS.c | 30 +++++ src/Parsee/Data.c | 6 +- src/XMPP/MUC.c | 1 + src/XMPP/Stanza.c | 21 ++++ src/XMPPThread.c | 292 +++++++++++++++++++++++++++++++++++++++++---- src/include/AS.h | 1 + src/include/Bot.h | 8 ++ src/include/XMPP.h | 5 + 10 files changed, 349 insertions(+), 24 deletions(-) create mode 100644 src/include/Bot.h diff --git a/README.MD b/README.MD index 58c6fe4..fdfd5d3 100644 --- a/README.MD +++ b/README.MD @@ -32,3 +32,10 @@ TODO ## DOCS TODO + +## TODOS +- Look at XEPS-TBD.TXT for XEPs to be done +- Achievements +### Why? +> "[...] and it [BoVeX] has an achievement system, because another thing I don't like is when software will not acknowledge that you've reached an obscure error state, essentially outsmarting it." + - Tom Murphy VII diff --git a/XEPS-TBD.TXT b/XEPS-TBD.TXT index 0c1e217..5003e6b 100644 --- a/XEPS-TBD.TXT +++ b/XEPS-TBD.TXT @@ -1,6 +1,8 @@ XEPs current supported are in src/XMPPThread.c, at the IQ disco advertising. Somewhat implemented XEPs: + ~ https://xmpp.org/extensions/xep-0085.html + Only XMPP->Matrix ~ https://xmpp.org/extensions/xep-0444.html This allows reactions, which Matrix also has support to. The two systems don't seem *too* restrictive on one-another (unlike some diff --git a/src/AS.c b/src/AS.c index 47f704d..cc37230 100644 --- a/src/AS.c +++ b/src/AS.c @@ -602,3 +602,33 @@ ASReupload(const ParseeConfig *c, char *from, char **mime) UriFree(uri); return ret; } +void +ASType(const ParseeConfig *c, char *user, char *room, bool status) +{ + HttpClientContext *ctx = NULL; + HashMap *json; + char *path, *full; + if (!c || !user || !room) + { + return; + } + + user = HttpUrlEncode(user); + path = StrConcat(6, + "/_matrix/client/v3/rooms/", + room, "/typing/", user, + "?user_id=", user + ); + + json = HashMapCreate(); + HashMapSet(json, "typing", JsonValueBoolean(status)); + HashMapSet(json, "timeout", JsonValueBoolean(1 MINUTES)); + ctx = ParseeCreateRequest(c, HTTP_PUT, path); + Free(path); + ASAuthenticateRequest(c, ctx); + ParseeSetRequestJSON(ctx, json); + JsonFree(json); + + HttpClientContextFree(ctx); + Free(user); +} diff --git a/src/Parsee/Data.c b/src/Parsee/Data.c index 4f34406..306f3a9 100644 --- a/src/Parsee/Data.c +++ b/src/Parsee/Data.c @@ -107,9 +107,9 @@ ParseeCleanup(void *datp) } \ while (0) - CleanupField(stanza, 1 HOURS); - CleanupField(event, 1 HOURS); - CleanupField(id, 1 HOURS); + CleanupField(stanza, 3 HOURS); + CleanupField(event, 3 HOURS); + CleanupField(id, 3 HOURS); DbUnlock(data->db, ref); } diff --git a/src/XMPP/MUC.c b/src/XMPP/MUC.c index 196714a..ba2e5cf 100644 --- a/src/XMPP/MUC.c +++ b/src/XMPP/MUC.c @@ -46,6 +46,7 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out) /* Except an IQ reply */ iq_query = XMLDecode(jabber->stream, false); + /* TODO: I've spotted presence requests spawning there. */ if (!iq_query || !StrEquals(iq_query->name, "iq")) { XMLFreeElement(iq_query); diff --git a/src/XMPP/Stanza.c b/src/XMPP/Stanza.c index 3958f63..9da16e8 100644 --- a/src/XMPP/Stanza.c +++ b/src/XMPP/Stanza.c @@ -175,6 +175,7 @@ XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc) XMLAddAttr(x, "xmlns", "http://jabber.org/protocol/muc"); XMLAddChild(presence, x); + XMPPAnnotatePresence(presence); XMLEncode(comp->stream, presence); StreamFlush(comp->stream); @@ -279,3 +280,23 @@ XMPPGetReply(XMLElement *elem) return HashMapGet(rep->attrs, "id"); } +void +XMPPAnnotatePresence(XMLElement *presence) +{ + XMLElement *c; + char *ver; + if (!presence) + { + return; + } + + ver = XMPPGenerateVer(); + c = XMLCreateTag("c"); + XMLAddAttr(c, "xmlns", "http://jabber.org/protocol/caps"); + XMLAddAttr(c, "hash", "sha-1"); + XMLAddAttr(c, "node", REPOSITORY); + XMLAddAttr(c, "ver", ver); + + Free(ver); + XMLAddChild(presence, c); +} diff --git a/src/XMPPThread.c b/src/XMPPThread.c index 7901583..c759f24 100644 --- a/src/XMPPThread.c +++ b/src/XMPPThread.c @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include #include @@ -13,15 +15,142 @@ #include #define IQ_ADVERT \ + AdvertiseSimple("http://jabber.org/protocol/caps") \ AdvertiseSimple("http://jabber.org/protocol/chatstates") \ AdvertiseSimple("urn:xmpp:message-correct:0") \ AdvertiseSimple("urn:xmpp:reactions:0") \ AdvertiseSimple("urn:xmpp:styling:0") \ AdvertiseSimple("urn:xmpp:reply:0") \ AdvertiseSimple("jabber:x:oob") \ + AdvertiseSimple("vcard-temp") \ + AdvertiseSimple("urn:xmpp:avatar:metadata+notify") \ + AdvertiseSimple("jabber:iq:version") \ AdvertiseSimple("urn:parsee:x-parsee:0") \ AdvertiseSimple("urn:parsee:jealousy:0") +/* TODO: More identities */ +#define IQ_IDENTITY \ + IdentitySimple("gateway", "matrix", "Parsee Matrix Gateway") \ + IdentitySimple("client", "pc", NAME " v" VERSION " bridge") \ + IdentitySimple("component", "generic", "Parsee's component") + +typedef struct XMPPIdentity { + char *category, *type, *lang, *name; +} XMPPIdentity; + +static int +ICollate(unsigned char *cata, unsigned char *catb) +{ + size_t al = cata ? strlen(cata) : 0; + size_t bl = catb ? strlen(catb) : 0; + + if (!al && !bl) + { + return 0; + } + while (true) + { + if (!al && bl) + { + return -1; + } + else if (al && !bl) + { + return 1; + } + + if (*cata == *catb) + { + cata++; + catb++; + al--; + bl--; + continue; + } + else if (*cata < *catb) + { + return -1; + } + return 1; + } + return 0; +} + +static int +IdentitySort(void *idap, void *idbp) +{ + XMPPIdentity *ida = idap; + XMPPIdentity *idb = idbp; + unsigned char *cata = ida->category; + unsigned char *catb = idb->category; + + return ICollate(cata, catb); +} + +/* Generates a SHA-256 hash of the ver field. */ +char * +XMPPGenerateVer(void) +{ + char *S = NULL; + unsigned char *Sha = NULL; + Array *identities = ArrayCreate(); + Array *features = ArrayCreate(); + size_t i; + + /* Initialise identity table, to be sorted */ +#define IdentitySimple(cat, Type, Name) { \ + XMPPIdentity *id = Malloc(sizeof(*id)); \ + id->category = cat; \ + id->lang = NULL; \ + id->type = Type; \ + id->name = Name; \ + ArrayAdd(identities, id); } + IQ_IDENTITY +#undef IdentitySimple +#define AdvertiseSimple(feature) ArrayAdd(features, feature); + IQ_ADVERT +#undef AdvertiseSimple + ArraySort(identities, IdentitySort); + for (i = 0; i < ArraySize(identities); i++) + { + XMPPIdentity *identity = ArrayGet(identities, i); + char *id_chunk = StrConcat(7, + identity->category, "/", + identity->type, "/", + identity->lang, "/", + identity->name); + char *tmp = S; + S = StrConcat(3, S, id_chunk, "<"); + Free(tmp); + Free(id_chunk); + } + + ArraySort(features, ((int (*) (void *, void *)) ICollate)); + for (i = 0; i < ArraySize(features); i++) + { + char *feature = ArrayGet(features, i); + char *tmp = S; + S = StrConcat(3, S, feature, "<"); + Free(tmp); + } + + Sha = Sha1(S); + Free(S); + S = Base64Encode(Sha, 20); + Free(Sha); + + ArrayFree(features); + for (i = 0; i < ArraySize(identities); i++) + { + XMPPIdentity *identity = ArrayGet(identities, i); + /* We don't have to do anything here. */ + Free(identity); + } + ArrayFree(identities); + + return S; +} + static pthread_mutex_t cond_var_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER; @@ -42,6 +171,8 @@ ParseeDMHandler(char *room, char *from, XMLElement *data, const ParseeConfig *c) )); } +/* TODO: Clean up all of this. We are currently separating DMs from MUCs, + * where we could unify all our code, and generalise everything. */ static bool MessageStanza(ParseeData *args, XMLElement *stanza) { @@ -54,6 +185,39 @@ MessageStanza(ParseeData *args, XMLElement *stanza) char *to, *room, *from, *from_matrix, *decode_from; char *chat_id, *mroom_id; size_t i; + + from = HashMapGet(stanza->attrs, "from"); + +#define CHAT_STATES "http://jabber.org/protocol/chatstates" + if (XMLookForTKV(stanza, "composing", "xmlns", CHAT_STATES)) + { + decode_from = ParseeLookupJID(from); + from_matrix = ParseeEncodeJID(args->config, decode_from, true); + chat_id = ParseeGetFromMUCID(args, from); + mroom_id = ParseeGetRoomID(args, chat_id); + + ASType(args->config, from_matrix, mroom_id, true); + + Free(decode_from); + Free(from_matrix); + Free(mroom_id); + Free(chat_id); + } + else if (XMLookForTKV(stanza, "active", "xmlns", CHAT_STATES)) + { + decode_from = ParseeLookupJID(from); + from_matrix = ParseeEncodeJID(args->config, decode_from, true); + chat_id = ParseeGetFromMUCID(args, from); + mroom_id = ParseeGetRoomID(args, chat_id); + + ASType(args->config, from_matrix, mroom_id, false); + + Free(decode_from); + Free(from_matrix); + Free(mroom_id); + Free(chat_id); + } +#undef CHAT_STATES body = XMLookForUnique(stanza, "body"); if (!body) { @@ -61,14 +225,13 @@ MessageStanza(ParseeData *args, XMLElement *stanza) return false; } - to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); - from = HashMapGet(stanza->attrs, "from"); /* TODO: On semi-anonymous MUCs, it might be preferable to use a * form of the occupant ID as the base, as it is more unique, and * less prone to trigger the character limit on Matrix. * * See: https://xmpp.org/extensions/xep-0421.html */ + to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); decode_from = ParseeLookupJID(from); from_matrix = ParseeEncodeJID(args->config, decode_from, true); room = ParseeFindDMRoom(args, to, from); @@ -205,6 +368,39 @@ MessageStanza(ParseeData *args, XMLElement *stanza) } #define DISCO "http://jabber.org/protocol/disco#info" +static XMLElement * +IQGenerateQuery(void) +{ + XMLElement *query = XMLCreateTag("query"); + XMLAddAttr(query, "xmlns", DISCO); + { + XMLElement *feature; +#define IdentitySimple(c,t,n) do \ + { \ + feature = XMLCreateTag("identity"); \ + XMLAddAttr(feature, "category", c); \ + XMLAddAttr(feature, "type", t); \ + XMLAddAttr(feature, "name", n); \ + XMLAddChild(query, feature); \ + } \ + while (0); + IQ_IDENTITY +#undef IdentitySimple +#define AdvertiseSimple(f) do \ + { \ + feature = XMLCreateTag("feature"); \ + XMLAddAttr(feature, "var", f); \ + XMLAddChild(query, feature); \ + } \ + while (0); + + /* TODO: Advertise more things */ + IQ_ADVERT +#undef AdvertiseSimple + } + + return query; +} static void IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza) { @@ -221,25 +417,17 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza) XMLAddAttr(iq_reply, "from", to); XMLAddAttr(iq_reply, "type", "result"); XMLAddAttr(iq_reply, "id", id); - { - query = XMLCreateTag("query"); - XMLAddAttr(query, "xmlns", DISCO); - { - XMLElement *feature; -#define AdvertiseSimple(f) do \ - { \ - feature = XMLCreateTag("feature"); \ - XMLAddAttr(feature, "var", f); \ - XMLAddChild(query, feature); \ - } \ - while (0); - /* TODO: Advertise more things */ - IQ_ADVERT -#undef AdvertiseSimple - } - XMLAddChild(iq_reply, query); + query = IQGenerateQuery(); + { + char *ver = XMPPGenerateVer(); + char *node = StrConcat(3, REPOSITORY, "#", ver); + XMLAddAttr(query, "node", node); + + Free(node); + Free(ver); } + XMLAddChild(iq_reply, query); pthread_mutex_lock(&jabber->write_lock); XMLEncode(jabber->stream, iq_reply); @@ -255,6 +443,45 @@ IQGet(ParseeData *args, XMLElement *stanza) { IQDiscoGet(args, jabber, stanza); } + else if (XMLookForTKV(stanza, "query", "xmlns", "jabber:iq:version")) + { + XMLElement *iq_reply, *query; + XMLElement *name, *version, *val; + char *from = HashMapGet(stanza->attrs, "from"); + char *to = HashMapGet(stanza->attrs, "to"); + char *id = HashMapGet(stanza->attrs, "id"); + + iq_reply = XMLCreateTag("iq"); + XMLAddAttr(iq_reply, "to", from); + XMLAddAttr(iq_reply, "from", to); + XMLAddAttr(iq_reply, "type", "result"); + XMLAddAttr(iq_reply, "id", id); + + query = XMLCreateTag("query"); + XMLAddAttr(query, "xmlns", "jabber:iq:version"); + { + name = XMLCreateTag("name"); + version = XMLCreateTag("version"); + + XMLAddChild(name, XMLCreateText(NAME)); + XMLAddChild(version, XMLCreateText(VERSION)); + } + XMLAddChild(query, name); + XMLAddChild(query, version); + XMLAddChild(iq_reply, query); + + pthread_mutex_lock(&jabber->write_lock); + XMLEncode(jabber->stream, iq_reply); + pthread_mutex_unlock(&jabber->write_lock); + XMLFreeElement(iq_reply); + } + else + { + Log(LOG_WARNING, "Unknown I/Q received:"); + XMLEncode(StreamStdout(), stanza); + StreamPrintf(StreamStdout(),"\n"); + StreamFlush(StreamStdout()); + } } #undef DISCO @@ -282,18 +509,41 @@ PresenceStanza(ParseeData *args, XMLElement *stanza) { #define MUC_USER_NS "http://jabber.org/protocol/muc#user" XMLElement *user_info; + XMLElement *vc = XMLookForTKV(stanza, "x", "xmlns", "vcard-temp:x:update"); + char *oid = HashMapGet(stanza->attrs, "from"); if ((user_info = XMLookForTKV(stanza, "x", "xmlns", MUC_USER_NS))) { XMLElement *item = XMLookForUnique(user_info, "item"); char *jid = item ? HashMapGet(item->attrs, "jid") : NULL; - char *oid = HashMapGet(stanza->attrs, "from"); - if (jid) { ParseePushJIDTable(oid, jid); } } + else if (vc) + { + XMLElement *photo = XMLookForUnique(vc, "photo"); + XMLElement *p_dat = photo ? ArrayGet(photo->children, 0) : NULL; + char *room_id = NULL, *chat_id = NULL; + + if (!p_dat) + { + return; + } + + chat_id = ParseeGetFromMUCID(args, oid); + room_id = ParseeGetRoomID(args, chat_id); + + if (!room_id) + { + Free(chat_id); + Free(room_id); + } + /* TODO: Get the media, and shove it to the room */ + Free(chat_id); + Free(room_id); + } #undef MUC_USER_NS } diff --git a/src/include/AS.h b/src/include/AS.h index d910a32..cdc6619 100644 --- a/src/include/AS.h +++ b/src/include/AS.h @@ -38,6 +38,7 @@ extern HashMap * ASFind(const ParseeConfig *, char *, char *); /* Sends a message event with a specific type and body. * Said body is freed during the function's execution. */ extern char * ASSend(const ParseeConfig *, char *, char *, char *, HashMap *); +extern void ASType(const ParseeConfig *, char *, char *, bool); /* Sets a state event with a specific type and body */ extern void ASSetState(const ParseeConfig *conf, char *id, char *type, char *key, char *mask, HashMap *event); diff --git a/src/include/Bot.h b/src/include/Bot.h new file mode 100644 index 0000000..782fedf --- /dev/null +++ b/src/include/Bot.h @@ -0,0 +1,8 @@ +#ifndef PARSEE_BOT_H +#define PARSEE_BOT_H + +#include + +#define BotInitialise() profile = + +#endif diff --git a/src/include/XMPP.h b/src/include/XMPP.h index d5537b8..3d39718 100644 --- a/src/include/XMPP.h +++ b/src/include/XMPP.h @@ -73,4 +73,9 @@ extern char * XMPPGetReplacedID(XMLElement *); /* Get the replied-to stanza ID, if existent. */ extern char * XMPPGetReply(XMLElement *elem); +/* Generate the B64-encoded SHA-256 hash for the 'ver' field in caps. */ +extern char * XMPPGenerateVer(void); + +/* Annotates a presence with https://xmpp.org/extensions/xep-0115.html */ +extern void XMPPAnnotatePresence(XMLElement *presence); #endif