From e54332e3763a32d596e41abdcc1e74e0717a2636 Mon Sep 17 00:00:00 2001 From: LDA Date: Thu, 19 Sep 2024 23:24:46 +0200 Subject: [PATCH] [MOD] Basic work to get XMPP avatars through PEP Attaboy! --- CHANGELOG.md | 2 +- README.MD | 2 +- src/AS/Media.c | 93 ++++++++++++++++++++++++++ src/AS/Profile.c | 69 ++++++++++++++++++++ src/Main.c | 2 + src/MatrixEventHandler.c | 72 ++++++++++++++++++--- src/XMPPThread/PEP.c | 3 +- src/XMPPThread/PresenceSub.c | 108 +++++++++++++++++++++++++++---- src/XMPPThread/Stanzas/IQ.c | 63 ++++++++++++++++++ src/XMPPThread/Stanzas/Message.c | 11 ++++ src/include/AS.h | 20 ++++++ src/include/Parsee.h | 6 ++ tools/aya.c | 2 +- 13 files changed, 428 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf82211..3076eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ of Parsee. May occasionally deadlock. Fixes some media metadata things, and replaces the build system of Parsee. #### New things -*NONE* +- Start dealing with some basic PEP-based avatars. #### Bugfixes - Adds more information to media events so that clients can behave. - Fixes issues where SIGPIPE can actually just kill Parsee. diff --git a/README.MD b/README.MD index 5205590..51cc590 100644 --- a/README.MD +++ b/README.MD @@ -69,7 +69,7 @@ Currently, the main sources of documentation are the Ayadocs(for headers) and th ## TODOS before 1.0 rolls around - PROPER FUCKING AVATARS - XMPP->Matrix is decent, Matrix->XMPP is effectiveny not done + XMPP->Matrix is decent, Matrix->XMPP is effectively a WIP - Add [libomemo](https://github.com/gkdr/libomemo) or something as an optional dependency. - It depends on more stuff anyways, and I don't want to weigh down the dependency list of Parsee for that. diff --git a/src/AS/Media.c b/src/AS/Media.c index fceefa9..1742189 100644 --- a/src/AS/Media.c +++ b/src/AS/Media.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -127,3 +128,95 @@ ASReupload(const ParseeConfig *c, char *from, char **mime) return ret; } +bool +ASGetMIMESHA(const ParseeConfig *c, char *mxc, char **mime, char **sha) +{ + HttpClientContext *cctx; + Stream *stream; + Stream *fake; + Uri *uri; + char *path, *buf = NULL; + unsigned char *sha1; + size_t len; + bool ret; + if (!c || !mxc || !mime || !sha) + { + return false; + } + *mime = NULL; + *sha = NULL; + + if (!(uri = UriParse(mxc)) || !StrEquals(uri->proto, "mxc")) + { + return false; + } + + path = StrConcat(3, "/_matrix/media/v3/download/", uri->host, uri->path); + cctx = ParseeCreateRequest(c, HTTP_GET, path); + ASAuthenticateRequest(c, cctx); + HttpRequestSendHeaders(cctx); + HttpRequestSend(cctx); + + *mime = StrDuplicate( + HashMapGet(HttpResponseHeaders(cctx), "content-type") + ); + stream = HttpClientStream(cctx); + fake = StreamFile(open_memstream(&buf, &len)); + StreamCopy(stream, fake); + StreamClose(fake); + + sha1 = Sha1Raw(buf, len); + free(buf); + *sha = ShaToHex(sha1, HASH_SHA1); + Free(sha1); + + HttpClientContextFree(cctx); + UriFree(uri); + Free(path); + return true; +} +bool +ASGrab(const ParseeConfig *c, char *mxc, char **mime, char **out, size_t *len) +{ + HttpClientContext *cctx; + Stream *stream; + Stream *fake; + Uri *uri; + char *path, *buf = NULL; + bool ret; + if (!c || !mxc || !mime || !out || !len) + { + return false; + } + *mime = NULL; + *out = NULL; + *len = 0; + + if (!(uri = UriParse(mxc)) || !StrEquals(uri->proto, "mxc")) + { + return false; + } + + path = StrConcat(3, "/_matrix/media/v3/download/", uri->host, uri->path); + cctx = ParseeCreateRequest(c, HTTP_GET, path); + ASAuthenticateRequest(c, cctx); + HttpRequestSendHeaders(cctx); + HttpRequestSend(cctx); + + *mime = StrDuplicate( + HashMapGet(HttpResponseHeaders(cctx), "content-type") + ); + stream = HttpClientStream(cctx); + fake = StreamFile(open_memstream(&buf, len)); + StreamCopy(stream, fake); + StreamClose(fake); + + *out = Malloc(*len); + memcpy(*out, buf, *len); + free(buf); + + HttpClientContextFree(cctx); + UriFree(uri); + Free(path); + return true; +} diff --git a/src/AS/Profile.c b/src/AS/Profile.c index 73e9b4b..f536300 100644 --- a/src/AS/Profile.c +++ b/src/AS/Profile.c @@ -136,3 +136,72 @@ ASGetName(const ParseeConfig *c, char *room, char *user) } return ret; } +char * +ASGetAvatar(const ParseeConfig *c, char *room, char *user) +{ + HttpClientContext *ctx; + HashMap *reply; + char *path = NULL, *ret = NULL; + char *u2 = user; + if (!c || !user) + { + return NULL; + } + + if (room) + { + user = HttpUrlEncode(user); + room = HttpUrlEncode(room); + path = StrConcat(4, + "/_matrix/client/v3/rooms/", room, + "/state/m.room.member/", user + ); + ctx = ParseeCreateRequest(c, HTTP_GET, path); + Free(user); + Free(room); + ASAuthenticateRequest(c, ctx); + HttpRequestSendHeaders(ctx); + HttpRequestSend(ctx); + + reply = JsonDecode(HttpClientStream(ctx)); + + ret = StrDuplicate( + JsonValueAsString(HashMapGet(reply, "avatar_url")) + ); + HttpClientContextFree(ctx); + JsonFree(reply); + Free(path); + + user = u2; + + Log(LOG_DEBUG, "ASGetAvatar: trying to grab avatar from room, got %s", ret); + } + + if (!ret) + { + user = HttpUrlEncode(user); + path = StrConcat(3, + "/_matrix/client/v3/profile/", user, "/avatar_url" + ); + ctx = ParseeCreateRequest(c, HTTP_GET, path); + Free(user); + user = u2; + ASAuthenticateRequest(c, ctx); + HttpRequestSendHeaders(ctx); + HttpRequestSend(ctx); + + reply = JsonDecode(HttpClientStream(ctx)); + + ret = StrDuplicate( + JsonValueAsString(HashMapGet(reply, "avatar_url")) + ); + StreamFlush(StreamStderr()); + HttpClientContextFree(ctx); + JsonFree(reply); + Free(path); + + Log(LOG_DEBUG, "ASGetAvatar: trying to grab avatar from profile, got %s", ret); + } + + return ret; +} diff --git a/src/Main.c b/src/Main.c index aa47822..fe976bf 100644 --- a/src/Main.c +++ b/src/Main.c @@ -78,6 +78,8 @@ Main(Array *args, HashMap *env) ); ParseePrintASCII(); Log(LOG_INFO, "======================="); + Log(LOG_INFO, "(C)opyright 2023 LDA"); + Log(LOG_INFO, "(This program is free software, see LICENSE.)"); LogConfigIndent(LogConfigGlobal()); { diff --git a/src/MatrixEventHandler.c b/src/MatrixEventHandler.c index 82d4cf8..3835fe8 100644 --- a/src/MatrixEventHandler.c +++ b/src/MatrixEventHandler.c @@ -16,7 +16,7 @@ static const char * GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to); -static void +static char * JoinMUC(ParseeData *data, HashMap *event, char *jid, char *muc, char *name) { char *sender = GrabString(event, 1, "sender"); @@ -50,7 +50,7 @@ JoinMUC(ParseeData *data, HashMap *event, char *jid, char *muc, char *name) ParseePushNickTable(muc, sender, nick); Free(nick); - Free(rev); + return (rev); } static void @@ -98,13 +98,71 @@ ParseeMemberHandler(ParseeData *data, HashMap *event) { 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); - 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); } @@ -263,7 +321,7 @@ GetXMPPInformation(ParseeData *data, HashMap *event, char **from, char **to) } matrix_name = ASGetName(data->config, room_id, matrix_sender); - JoinMUC(data, event, *from, muc_id, matrix_name); + Free(JoinMUC(data, event, *from, muc_id, matrix_name)); *to = muc_id; Free(matrix_name); @@ -350,12 +408,8 @@ ParseeMessageHandler(ParseeData *data, HashMap *event) goto end; } - /* TODO: Check the name's validity. - * Is there a good way to check for that that isn't - * just "await on join and try again?" */ name = ASGetName(data->config, id, m_sender); - - JoinMUC(data, event, encoded_from, muc_id, name); + Free(JoinMUC(data, event, encoded_from, muc_id, name)); to = muc_id; diff --git a/src/XMPPThread/PEP.c b/src/XMPPThread/PEP.c index a8868ec..f49dcd0 100644 --- a/src/XMPPThread/PEP.c +++ b/src/XMPPThread/PEP.c @@ -40,7 +40,7 @@ static bool IsPubsubRequest(XMLElement *stanza) { char *type = HashMapGet(stanza ? stanza->attrs : NULL, "type"); - XMLElement *pubsub, *subscribe; + XMLElement *pubsub; if (!stanza) { return false; @@ -58,7 +58,6 @@ IsPubsubRequest(XMLElement *stanza) return false; } - Log(LOG_INFO, "WOAH"); return XMLookForUnique(pubsub, "subscribe"); } diff --git a/src/XMPPThread/PresenceSub.c b/src/XMPPThread/PresenceSub.c index da070c4..453df40 100644 --- a/src/XMPPThread/PresenceSub.c +++ b/src/XMPPThread/PresenceSub.c @@ -1,5 +1,14 @@ #include "XMPPThread/internal.h" +#include +#include +#include +#include +#include +#include + +#include + static char * SubscriptionHash(ParseeData *data, char *from, char *to) { @@ -10,14 +19,28 @@ SubscriptionHash(ParseeData *data, char *from, char *to) len = strlen(from) + 1 + strlen(to); sum = Malloc(len); memset(sum, 0x00, len); - memcpy(sum[0], from, strlen(from)): - memcpy(sum[strlen(from) + 1], to, strlen(to)); + memcpy(&sum[0], from, strlen(from)); + memcpy(&sum[strlen(from) + 1], to, strlen(to)); - hash = ParseeHMAC(data->id, sum, len); + hash = Base64Encode(sum, len); Free(sum); return hash; } +static void +DecodeSubscription(ParseeData *data, char *hash, char **from, char **to) +{ + char *sum; + if (!data || !hash || !from || !to) + { + return; + } + + sum = Base64Decode(hash, strlen(hash)); + *from = StrDuplicate(sum); + *to = StrDuplicate(sum + strlen(sum) + 1); + Free(sum); +} void AddPresenceSubscriber(ParseeData *data, char *from, char *to) @@ -32,13 +55,18 @@ AddPresenceSubscriber(ParseeData *data, char *from, char *to) database = data->db; hash = SubscriptionHash(data, from, to); - ref = DbCreate(database, 2, "subscriptions", hash); + ref = DbCreate(database, 2, "subs", hash); + if (!ref) + { + goto end; + } - HashMapSet(DbRef(ref), "from", JsonValueString(from)); - HashMapSet(DbRef(ref), "to", JsonValueString(to)); + HashMapSet(DbJson(ref), "from", JsonValueString(from)); + HashMapSet(DbJson(ref), "to", JsonValueString(to)); /* I don't think we need more information right now */ - DbClose(database, ref); +end: + DbUnlock(database, ref); Free(hash); } @@ -48,15 +76,73 @@ IsSubscribed(ParseeData *data, char *user, char *to) Db *database; char *hash; bool ret; - if (!data || !from || !to) + if (!data || !user || !to) { - return; + return false; } database = data->db; - hash = SubscriptionHash(data, from, to); - ret = DbExists(database, 2, "subscriptions", hash); + hash = SubscriptionHash(data, user, to); + ret = DbExists(database, 2, "subs", hash); Free(hash); return ret; } + +void +ParseeBroadcastStanza(ParseeData *data, char *from, XMLElement *stanza) +{ + XMPPComponent *jabber = data ? data->jabber : NULL; + Array *entries; + size_t i; + if (!data || !from || !stanza) + { + return; + } + + /* Copy our stanza so that we can freely modify it */ + stanza = XMLCopy(stanza); + + /* Start doing a storm on Mt. Subs. */ + entries = DbList(data->db, 1, "subs"); + for (i = 0; i < ArraySize(entries); i++) + { + char *entry = ArrayGet(entries, i); + char *entry_from = NULL, *entry_to = NULL; + char *storm_id; /* ooe */ + XMLElement *sub; + + DecodeSubscription(data, entry, &entry_from, &entry_to); + + if (!StrEquals(entry_to, from)) + { + goto end; + } + + Log(LOG_DEBUG, + "PRESENCE SYSTEM: " + "We should be brotkasting straight to %s (from %s)", + entry_from, from + ); + sub = XMLCopy(stanza); + XMLAddAttr(sub, "from", from); + XMLAddAttr(sub, "to", entry_from); + + /* TODO: Should we store IDs somewhere? */ + XMLAddAttr(sub, "id", (storm_id = StrRandom(16))); + + pthread_mutex_lock(&jabber->write_lock); + XMLEncode(jabber->stream, sub); + StreamFlush(jabber->stream); + pthread_mutex_unlock(&jabber->write_lock); + + XMLFreeElement(sub); + Free(storm_id); + end: + Free(entry_from); + Free(entry_to); + } + DbListFree(entries); + XMLFreeElement(stanza); + +} diff --git a/src/XMPPThread/Stanzas/IQ.c b/src/XMPPThread/Stanzas/IQ.c index 125c5e1..6058ba4 100644 --- a/src/XMPPThread/Stanzas/IQ.c +++ b/src/XMPPThread/Stanzas/IQ.c @@ -327,6 +327,7 @@ void IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) { XMPPComponent *jabber = args->jabber; + XMLElement *pubsub; char *from = HashMapGet(stanza->attrs, "from"); char *to = HashMapGet(stanza->attrs, "to"); char *id = HashMapGet(stanza->attrs, "id"); @@ -395,6 +396,68 @@ IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr) XMLFreeElement(iqVCard); } } +#define PS "http://jabber.org/protocol/pubsub" + else if ((pubsub = XMLookForTKV(stanza, "pubsub", "xmlns", PS))) + { + /* TODO: Pass this through the PEP manager */ + XMLElement *a_items = XMLookForTKV(pubsub, + "items", "node", "urn:xmpp:avatar:data" + ); + if (a_items) + { + /* Do, without regret, start shoving an avatar out the bus */ + char *to_matrix = ParseeDecodeMXID(to); + char *avatar = ASGetAvatar(args->config, NULL, to_matrix); + char *buf, *mime; + char *b64; + size_t len; + XMLElement *reply; + + ASGrab(args->config, avatar, &mime, &buf, &len); + b64 = Base64Encode(buf, len); + Free(buf); + + Log(LOG_INFO, "FM=%s", to_matrix); + Log(LOG_INFO, "B=%s (%dB)", b64, (int) len); + /* Strike back with a response */ + reply = XMLCreateTag("iq"); + XMLAddAttr(reply, "type", "result"); + XMLAddAttr(reply, "to", from); + XMLAddAttr(reply, "from", to); + XMLAddAttr(reply, "id", HashMapGet(stanza->attrs, "id")); + { + XMLElement *ps = XMLCreateTag("pubsub"); + XMLElement *items = XMLCreateTag("items"); + XMLAddAttr(ps, "xmlns", PS); + XMLAddAttr(items, "node", "urn:xmpp:avatar:data"); + { + XMLElement *item = XMLCreateTag("item"); + XMLElement *data = XMLCreateTag("data"); + XMLAddAttr(item, "id", "TODO"); + XMLAddAttr(data, "xmlns", "urn:xmpp:avatar:data"); + + XMLAddChild(data, XMLCreateText(b64)); + + XMLAddChild(item, data); + XMLAddChild(items, item); + } + XMLAddChild(ps, items); + XMLAddChild(reply, ps); + } + + pthread_mutex_lock(&jabber->write_lock); + XMLEncode(jabber->stream, reply); + StreamFlush(jabber->stream); + pthread_mutex_unlock(&jabber->write_lock); + XMLFreeElement(reply); + + Free(to_matrix); + Free(avatar); + Free(mime); + Free(b64); + } + } +#undef PS else if (XMLookForTKV(stanza, "query", "xmlns", DISCO)) { IQDiscoGet(args, jabber, stanza); diff --git a/src/XMPPThread/Stanzas/Message.c b/src/XMPPThread/Stanzas/Message.c index 75722e5..9237aec 100644 --- a/src/XMPPThread/Stanzas/Message.c +++ b/src/XMPPThread/Stanzas/Message.c @@ -97,6 +97,17 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr) Log(LOG_DEBUG, " usage=%d (%s:%d)", MemoryAllocated(), __FILE__, __LINE__); return false; } + /*{ + XMLElement *foo = XMLCreateTag("message"); + XMLElement *body = XMLCreateTag("body"); + XMLAddAttr(foo, "type", "chat"); + XMLAddChild(foo, body); + XMLAddChild(body, XMLCreateText("Storm on Mt. Ooe (sorry if you see this)")); + + BroadcastStanza(args, HashMapGet(stanza->attrs, "to"), foo); + XMLFreeElement(foo); + }*/ + if (ServerHasXEP421(args, from)) { diff --git a/src/include/AS.h b/src/include/AS.h index 1d993e1..ad91e60 100644 --- a/src/include/AS.h +++ b/src/include/AS.h @@ -141,6 +141,14 @@ extern void ASSetStatus(const ParseeConfig *c, char *user, UserStatus status, ch * Modifies: NOTHING */ extern char * ASGetName(const ParseeConfig *c, char *room, char *user); +/** Returns the user's avatar in a room, or a the global user avatar, to be + * Free'd + * ------------- + * Returns: The user's name in the [HEAP] | NULL + * Thrasher: Free + * Modifies: NOTHING */ +extern char * ASGetAvatar(const ParseeConfig *c, char *room, char *user); + /** Uploads data to Matrix to be used later * ---------------- * Returns: A valid MXC URI[HEAP] | NULL @@ -170,4 +178,16 @@ extern Array * ASGetRelations(const ParseeConfig *c, size_t n, char *room, char * Thrashes: {relations} * See-Also: ASGetRelations */ extern void ASFreeRelations(Array *relations); + +/** Returns the MIME and SHA-1 hash of a media entry, in one fell swoop. + * ----------------- + * Returns: whenever the media exists + * Modifies: {mime}[HEAP], {sha}[HEAP] */ +extern bool ASGetMIMESHA(const ParseeConfig *c, char *mxc, char **mime, char **sha); + +/** Retrieves media off an MXC link. + * ------------ + * Returns: whenever the media exists + * Modifies {mime}[HEAP], {out}[HEAP], {len} */ +extern bool ASGrab(const ParseeConfig *c, char *mxc, char **mime, char **out, size_t *len); #endif diff --git a/src/include/Parsee.h b/src/include/Parsee.h index f64df82..34077cd 100644 --- a/src/include/Parsee.h +++ b/src/include/Parsee.h @@ -401,4 +401,10 @@ extern void ParseeSetThreads(int xmpp, int http); extern char * ParseeHMAC(char *key, uint8_t *msg, size_t msglen); #define ParseeHMACS(key, msg) ParseeHMAC(key, (uint8_t *) msg, strlen(msg)) +/** Broadcasts a stanza from a user to any that may be interested by it + * (PEP or subscription) + * ------------------------------------- + * Returns: NOTHING */ +extern void ParseeBroadcastStanza(ParseeData *data, char *from, XMLElement *s); + #endif diff --git a/tools/aya.c b/tools/aya.c index 69371d1..b49fd6b 100644 --- a/tools/aya.c +++ b/tools/aya.c @@ -109,7 +109,7 @@ ParseAyadoc(char *raw) if (parsing_notes) { char *del = strchr(line_content, ':'); - char *val = del + 1; + char *val = del ? del + 1 : NULL; if (del && strlen(del) >= 1) { while (*val && isspace(*val))