From 42d69226f0a2e91c6a2c69e2631fd692a03f27e9 Mon Sep 17 00:00:00 2001 From: LDA Date: Sun, 23 Jun 2024 15:34:51 +0200 Subject: [PATCH] [ADD/MOD] XMPP->Matrix media bridge, small cleanup --- src/AS.c | 105 ++++++++++++++++- src/Events.c | 53 +++++++++ src/Main.c | 9 ++ src/{ParseeConfig.c => Parsee/Config.c} | 0 src/{ParseeData.c => Parsee/Data.c} | 9 ++ src/{ParseeUser.c => Parsee/User.c} | 0 src/XML/Elements.c | 23 ++++ src/XML/Parser.c | 13 ++- src/XMPP/Component.c | 3 + src/XMPPThread.c | 149 ++++++++++++++++-------- src/include/AS.h | 6 + src/include/Matrix.h | 3 + src/include/Parsee.h | 7 ++ src/include/XML.h | 1 + 14 files changed, 327 insertions(+), 54 deletions(-) rename src/{ParseeConfig.c => Parsee/Config.c} (100%) rename src/{ParseeData.c => Parsee/Data.c} (89%) rename src/{ParseeUser.c => Parsee/User.c} (100%) diff --git a/src/AS.c b/src/AS.c index f2b1302..ee6f74a 100644 --- a/src/AS.c +++ b/src/AS.c @@ -3,8 +3,10 @@ #include #include #include +#include #include +#include #include @@ -305,11 +307,11 @@ ASGetName(const ParseeConfig *c, char *room, char *user) reply = JsonDecode(HttpClientStream(ctx)); - JsonFree(reply); ret = StrDuplicate( JsonValueAsString(HashMapGet(reply, "displayname")) ); HttpClientContextFree(ctx); + JsonFree(reply); Free(path); if (!ret) @@ -321,3 +323,104 @@ ASGetName(const ParseeConfig *c, char *room, char *user) return NULL; } +char * +ASUpload(const ParseeConfig *c, Stream *from, unsigned int size) +{ + char *size_str, *path, *ret, *user; + int i; + HttpClientContext *ctx; + HashMap *reply; + if (!c || !from) + { + return NULL; + } + + size_str = StrInt(size); + user = StrConcat(4, "@",c->sender_localpart,":",c->homeserver_host); + path = StrConcat(2, + "/_matrix/media/v3/upload?user_id=", user + ); + ctx = ParseeCreateRequest(c, HTTP_POST, path); + ASAuthenticateRequest(c, ctx); + if (size) + { + HttpRequestHeader(ctx, "Content-Length", size_str); + } + HttpRequestSendHeaders(ctx); + + StreamCopy(from, HttpClientStream(ctx)); + HttpRequestSend(ctx); + + reply = JsonDecode(HttpClientStream(ctx)); + ret = StrDuplicate( + JsonValueAsString(HashMapGet(reply, "content_uri")) + ); + HttpClientContextFree(ctx); + JsonFree(reply); + Free(size_str); + Free(path); + Free(user); + + return ret; +} +char * +ASReupload(const ParseeConfig *c, char *from, char **mime) +{ + Uri *uri; + HttpClientContext *ctx; + unsigned short port; + int size = 0, flags = HTTP_FLAG_NONE; + int i; + char *ret, *content_len; + + if (!c || !from) + { + return NULL; + } + + uri = UriParse(from); + if (!uri) + { + return NULL; + } + if (uri->port) + { + port = uri->port; + } + else if (StrEquals(uri->proto, "https")) + { + port = 443; + } + else + { + port = 80; + } + + if (StrEquals(uri->proto, "https")) + { + flags |= HTTP_FLAG_TLS; + } + + ctx = HttpRequest( + HTTP_GET, flags, port, uri->host, uri->path + ); + HttpRequestSendHeaders(ctx); + HttpRequestSend(ctx); + + if (mime) + { + *mime = HashMapGet(HttpResponseHeaders(ctx), "content-type"); + *mime = StrDuplicate(*mime); + } + + content_len = HashMapGet(HttpResponseHeaders(ctx), "content-length"); + if (content_len) + { + size = strtol(content_len, NULL, 10); + } + ret = ASUpload(c, HttpClientStream(ctx), size); + + HttpClientContextFree(ctx); + UriFree(uri); + return ret; +} diff --git a/src/Events.c b/src/Events.c index b746b93..bbbbacf 100644 --- a/src/Events.c +++ b/src/Events.c @@ -1,5 +1,10 @@ #include +#include +#include + +#include + HashMap * MatrixCreateNotice(char *body) { @@ -61,3 +66,51 @@ MatrixCreateNickChange(char *nick) return map; } +HashMap * +MatrixCreateMedia(char *mxc, char *body, char *mime) +{ + HashMap *map; + char *mime_type = NULL, *matrix_type = NULL; + if (!mxc || !body) + { + return NULL; + } + + matrix_type = "m.file"; + if (mime) + { + size_t i; + mime_type = StrDuplicate(mime); + for (i = 0; i < strlen(mime); i++) + { + if (mime_type[i] == '/') + { + mime_type[i] = '\0'; + break; + } + } + + if (StrEquals(mime_type, "image")) + { + matrix_type = "m.image"; + } + else if (StrEquals(mime_type, "video")) + { + matrix_type = "m.video"; + } + else if (StrEquals(mime_type, "audio")) + { + matrix_type = "m.audio"; + } + + Free(mime_type); + } + + map = HashMapCreate(); + HashMapSet(map, "msgtype", JsonValueString(matrix_type)); + HashMapSet(map, "mimetype", JsonValueString(mime)); + HashMapSet(map, "body", JsonValueString(body)); + HashMapSet(map, "url", JsonValueString(mxc)); + + return map; +} diff --git a/src/Main.c b/src/Main.c index 5ff168c..58ce533 100644 --- a/src/Main.c +++ b/src/Main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,7 @@ Main(void) ParseeData *data = NULL; const ParseeConfig *parsee_conf; Stream *yaml; + Cron *cron = NULL; Log(LOG_INFO, "%s - v%s (Cytoplasm %s)", @@ -63,6 +65,7 @@ Main(void) Log(LOG_NOTICE, "Setting up local Matrix user..."); ASRegisterUser(parsee_conf, parsee_conf->sender_localpart); + memset(&conf, 0, sizeof(conf)); conf.port = parsee_conf->port; conf.threads = 4; @@ -70,6 +73,10 @@ Main(void) conf.handlerArgs = ParseeInitData(jabber); conf.handler = ParseeRequest; + Log(LOG_NOTICE, "Starting up local cronjobs..."); + cron = CronCreate( 10 SECONDS ); + CronEvery(cron, 30 MINUTES, ParseeCleanup, conf.handlerArgs); + Log(LOG_NOTICE, "Creating XMPP listener thread..."); if (pthread_create(&xmpp_thr, NULL, ParseeXMPPThread, conf.handlerArgs)) { @@ -98,6 +105,8 @@ end: Log(LOG_INFO, "Exiting..."); HttpServerFree(server); ParseeConfigFree(); + CronStop(cron); + CronFree(cron); ParseeFreeData(conf.handlerArgs); return 0; } diff --git a/src/ParseeConfig.c b/src/Parsee/Config.c similarity index 100% rename from src/ParseeConfig.c rename to src/Parsee/Config.c diff --git a/src/ParseeData.c b/src/Parsee/Data.c similarity index 89% rename from src/ParseeData.c rename to src/Parsee/Data.c index af0dbda..bea18ba 100644 --- a/src/ParseeData.c +++ b/src/Parsee/Data.c @@ -44,3 +44,12 @@ ParseeFreeData(ParseeData *data) HttpRouterFree(data->router); Free(data); } + +void +ParseeCleanup(void *datp) +{ + ParseeData *data = datp; + + Log(LOG_NOTICE, "Cleaning up..."); + /* TODO */ +} diff --git a/src/ParseeUser.c b/src/Parsee/User.c similarity index 100% rename from src/ParseeUser.c rename to src/Parsee/User.c diff --git a/src/XML/Elements.c b/src/XML/Elements.c index bcb138a..773e620 100644 --- a/src/XML/Elements.c +++ b/src/XML/Elements.c @@ -131,3 +131,26 @@ XMLookForUnique(XMLElement *parent, char *tag) return NULL; } +XMLElement * +XMLookForTKV(XMLElement *parent, char *tag, char *k, char *v) +{ + size_t i; + + if (!parent || !tag || !k || !v) + { + return NULL; + } + + for (i = 0; i < ArraySize(parent->children); i++) + { + XMLElement *child = ArrayGet(parent->children, i); + HashMap *attrs = child->attrs; + char *value = HashMapGet(attrs, k); + if (StrEquals(child->name, tag) && StrEquals(value, v)) + { + return child; + } + } + + return NULL; +} diff --git a/src/XML/Parser.c b/src/XML/Parser.c index f120459..e89e427 100644 --- a/src/XML/Parser.c +++ b/src/XML/Parser.c @@ -5,7 +5,6 @@ XMLElement * XMLDecode(Stream *stream, bool autofree) { - /* TODO: Use the existing SAX parser to decode everything */ #define push(x) ArrayAdd(stack, x) #define pop(x) ArrayDelete(stack, ArraySize(stack) - 1) #define peek(x) ArrayGet(stack, ArraySize(stack) - 1) @@ -14,6 +13,7 @@ XMLDecode(Stream *stream, bool autofree) XMLElement *ret = NULL; XMLElement *top; char *key, *val; + size_t i; Array *stack; @@ -85,6 +85,15 @@ XMLDecode(Stream *stream, bool autofree) event = NULL; } XMLFreeEvent(event); + for (i = 0; i < ArraySize(stack); i++) + { + XMLElement *e = ArrayGet(stack, i); + if (e == ret) + { + continue; + } + XMLFreeElement(e); + } ArrayFree(stack); XMLFreeLexer(lexer); return ret; @@ -158,8 +167,6 @@ XMLEncodeTag(Stream *stream, XMLElement *element) void XMLEncode(Stream *stream, XMLElement *element) { - /* TODO: Write the entire XML element. This shouldn't be - * too hard. */ if (!stream || !element) { return; diff --git a/src/XMPP/Component.c b/src/XMPP/Component.c index 082f47d..26114ba 100644 --- a/src/XMPP/Component.c +++ b/src/XMPP/Component.c @@ -160,6 +160,9 @@ XMPPAuthenticateCompStream(XMPPComponent *comp, char *shared) !StrEquals(ev->element, "handshake")) { Log(LOG_ERR, "Excepted empty handshake reply, got nonsense."); + Log(LOG_ERR, "Another service (possibly Parsee) may have taken over."); + Log(LOG_ERR, ""); + Log(LOG_ERR, "Simply jealous of that other service..."); Free(stream_id); Free(handshake); XMLFreeEvent(ev); diff --git a/src/XMPPThread.c b/src/XMPPThread.c index 146262a..b0d6ab0 100644 --- a/src/XMPPThread.c +++ b/src/XMPPThread.c @@ -33,6 +33,95 @@ ParseeDMHandler(char *room, char *from, XMLElement *data, const ParseeConfig *c) ); } +static bool +MessageStanza(ParseeData *args, XMLElement *stanza) +{ + XMPPComponent *jabber = args->jabber; + + XMLElement *body = NULL; + XMLElement *data = NULL; + XMLElement *stanza_id = NULL; + + char *to, *room, *from, *from_matrix; + char *chat_id, *mroom_id; + size_t i; + body = XMLookForUnique(stanza, "body"); + if (!body) + { + XMLFreeElement(stanza); + return false; + } + stanza_id = XMLookForUnique(stanza, "stanza-id"); + + to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); + from = HashMapGet(stanza->attrs, "from"); + from_matrix = ParseeEncodeJID(args->config, from, true); + room = ParseeFindDMRoom(args, to, from); + data = ArrayGet(body->children, 0); + + /* TODO: Consider having rich messages. */ + chat_id = ParseeGetFromMUCID(args, from); + mroom_id = ParseeGetRoomID(args, chat_id); + if (room) + { + ParseeDMHandler(room, from_matrix, data, args->config); + } + if (mroom_id && !XMPPIsParseeStanza(stanza)) + { + char *res = ParseeGetResource(from); + char *encoded = ParseeEncodeJID(args->config, from, false); + char *s_id_str = HashMapGet(stanza_id->attrs, "id"); + + /* TODO: Create smarter puppet names */ + if (ParseeVerifyStanza(args, chat_id, s_id_str)) + { + XMLElement *oob, *oob_data; + + ASRegisterUser(args->config, encoded); + ASSetName(args->config, encoded, res); + ASJoin(args->config, mroom_id, encoded); + + /* Check if it is a media link */ + oob = XMLookForTKV(stanza, "x", "xmlns", "jabber:x:oob"); + if (oob) + { + char *mxc, *mime = NULL; + HashMap *content = NULL; + oob_data = XMLookForUnique(oob, "url"); + oob_data = ArrayGet(oob_data->children, 0); + + mxc = ASReupload(args->config, oob_data->data, &mime); + content = MatrixCreateMedia(mxc, data->data, mime); + + ASSend( + args->config, mroom_id, encoded, + "m.room.message", content + ); + Free(mime); + Free(mxc); + } + else + { + ASSend( + args->config, mroom_id, encoded, + "m.room.message", MatrixCreateMessage(data->data) + ); + } + ParseePushStanza(args, chat_id, s_id_str); + } + + Free(res); + Free(encoded); + } + Free(chat_id); + Free(mroom_id); + Free(from_matrix); + Free(room); + Free(to); + + return true; +} + void * ParseeXMPPThread(void *argp) { @@ -41,9 +130,6 @@ ParseeXMPPThread(void *argp) XMLElement *stanza = NULL; while (true) { - XMLElement *body = NULL; - XMLElement *data = NULL; - XMLElement *stanza_id = NULL; char *to, *room, *from, *from_matrix; char *chat_id, *mroom_id; @@ -55,6 +141,7 @@ ParseeXMPPThread(void *argp) if (StrEquals(stanza->name, "presence")) { + /* TODO: Manage presence */ XMLFreeElement(stanza); continue; } @@ -80,56 +167,18 @@ ParseeXMPPThread(void *argp) continue; } } - body = XMLookForUnique(stanza, "body"); - if (!body) + + if (!MessageStanza(args, stanza)) { - XMLFreeElement(stanza); continue; } - stanza_id = XMLookForUnique(stanza, "stanza-id"); - - to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); - from = HashMapGet(stanza->attrs, "from"); - from_matrix = ParseeEncodeJID(args->config, from, true); - room = ParseeFindDMRoom(args, to, from); - data = ArrayGet(body->children, 0); - - /* TODO: Consider having rich messages, and move that out - * to separate functions */ - chat_id = ParseeGetFromMUCID(args, from); - mroom_id = ParseeGetRoomID(args, chat_id); - if (room) - { - ParseeDMHandler(room, from_matrix, data, args->config); - } - if (mroom_id && !XMPPIsParseeStanza(stanza)) - { - char *res = ParseeGetResource(from); - char *encoded = ParseeEncodeJID(args->config, from, false); - char *s_id_str = HashMapGet(stanza_id->attrs, "id"); - - /* TODO: Create smarter puppet names, and send presence - * in every room at Parsee's bootup. */ - if (ParseeVerifyStanza(args, chat_id, s_id_str)) - { - ASRegisterUser(args->config, encoded); - ASSetName(args->config, encoded, res); - ASJoin(args->config, mroom_id, encoded); - ASSend( - args->config, mroom_id, encoded, - "m.room.message", MatrixCreateMessage(data->data) - ); - ParseePushStanza(args, chat_id, s_id_str); - } - - Free(res); - Free(encoded); - } - Free(chat_id); - Free(mroom_id); - Free(from_matrix); - Free(room); - Free(to); + } + else if (StrEquals(stanza->name, "iq")) + { + /* TODO: Ought to manage info/query requests */ + Log(LOG_WARNING, "Unimplemented 'iq' stanza."); + Log(LOG_WARNING, "Odds are, this is the programmer's fault"); + Log(LOG_WARNING, "and this needs to be implemented later on."); } else { diff --git a/src/include/AS.h b/src/include/AS.h index f5b083b..2c2f7c7 100644 --- a/src/include/AS.h +++ b/src/include/AS.h @@ -43,4 +43,10 @@ extern void ASSetName(const ParseeConfig *c, char *user, char *name); /* Returns the user's name in a room, or a copy of the MXID itself, to be * Free'd. */ extern char * ASGetName(const ParseeConfig *c, char *room, char *user); + +/* Uploads data to Matrix to be used later */ +extern char * ASUpload(const ParseeConfig *c, Stream *from, unsigned int size); + +/* Reuploads a HTTP URL to Matrix, with an optional MIME type returned. */ +extern char * ASReupload(const ParseeConfig *c, char *from, char **mime); #endif diff --git a/src/include/Matrix.h b/src/include/Matrix.h index 0c0acce..e361c56 100644 --- a/src/include/Matrix.h +++ b/src/include/Matrix.h @@ -12,6 +12,9 @@ extern HashMap * MatrixCreateNotice(char *body); /* Creates the content for a normal message. */ extern HashMap * MatrixCreateMessage(char *body); +/* Creates the content for a media file. */ +extern HashMap * MatrixCreateMedia(char *mxc, char *body, char *mime); + /* Creates the content for a m.room.name state event */ extern HashMap * MatrixCreateNameState(char *name); diff --git a/src/include/Parsee.h b/src/include/Parsee.h index 0f46daf..2fa357d 100644 --- a/src/include/Parsee.h +++ b/src/include/Parsee.h @@ -44,6 +44,10 @@ typedef struct ParseeData { Db *db; } ParseeData; +#define SECONDS * 1000 +#define MINUTES * 60 SECONDS +#define HOURS * 60 MINUTES + /* Initialises a Parsee config from scratch, and writes to it * as JSON in the CWD. */ extern void ParseeConfigInit(void); @@ -150,4 +154,7 @@ extern bool ParseeVerifyStanza(ParseeData *, char *chat_id, char *stanza_id); extern void ParseeSendPresence(ParseeData *); extern bool ParseeInitialiseSignals(HttpServer *, pthread_t, XMPPComponent *); + +/* Job used to cleanup Parsee data that isn't necessary anymore. */ +extern void ParseeCleanup(void *data); #endif diff --git a/src/include/XML.h b/src/include/XML.h index 8114783..ec7b4ca 100644 --- a/src/include/XML.h +++ b/src/include/XML.h @@ -64,6 +64,7 @@ extern void XMLAddAttr(XMLElement *element, char *key, char *val); extern void XMLAddChild(XMLElement *element, XMLElement *child); extern XMLElement * XMLookForUnique(XMLElement *parent, char *tag); +extern XMLElement * XMLookForTKV(XMLElement *parent, char *tag, char *k, char *v); /* Frees an XML element properly. */ extern void XMLFreeElement(XMLElement *element);