[ADD/MOD] XMPP->Matrix media bridge, small cleanup

This commit is contained in:
LDA 2024-06-23 15:34:51 +02:00
commit 42d69226f0
14 changed files with 327 additions and 54 deletions

105
src/AS.c
View file

@ -3,8 +3,10 @@
#include <Cytoplasm/Memory.h> #include <Cytoplasm/Memory.h>
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <Cytoplasm/Log.h> #include <Cytoplasm/Log.h>
#include <Cytoplasm/Uri.h>
#include <string.h> #include <string.h>
#include <stdlib.h>
#include <Matrix.h> #include <Matrix.h>
@ -305,11 +307,11 @@ ASGetName(const ParseeConfig *c, char *room, char *user)
reply = JsonDecode(HttpClientStream(ctx)); reply = JsonDecode(HttpClientStream(ctx));
JsonFree(reply);
ret = StrDuplicate( ret = StrDuplicate(
JsonValueAsString(HashMapGet(reply, "displayname")) JsonValueAsString(HashMapGet(reply, "displayname"))
); );
HttpClientContextFree(ctx); HttpClientContextFree(ctx);
JsonFree(reply);
Free(path); Free(path);
if (!ret) if (!ret)
@ -321,3 +323,104 @@ ASGetName(const ParseeConfig *c, char *room, char *user)
return NULL; 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;
}

View file

@ -1,5 +1,10 @@
#include <Matrix.h> #include <Matrix.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Str.h>
#include <string.h>
HashMap * HashMap *
MatrixCreateNotice(char *body) MatrixCreateNotice(char *body)
{ {
@ -61,3 +66,51 @@ MatrixCreateNickChange(char *nick)
return map; 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;
}

View file

@ -2,6 +2,7 @@
#include <Cytoplasm/Cytoplasm.h> #include <Cytoplasm/Cytoplasm.h>
#include <Cytoplasm/Memory.h> #include <Cytoplasm/Memory.h>
#include <Cytoplasm/Util.h> #include <Cytoplasm/Util.h>
#include <Cytoplasm/Cron.h>
#include <Cytoplasm/Log.h> #include <Cytoplasm/Log.h>
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
@ -26,6 +27,7 @@ Main(void)
ParseeData *data = NULL; ParseeData *data = NULL;
const ParseeConfig *parsee_conf; const ParseeConfig *parsee_conf;
Stream *yaml; Stream *yaml;
Cron *cron = NULL;
Log(LOG_INFO, Log(LOG_INFO,
"%s - v%s (Cytoplasm %s)", "%s - v%s (Cytoplasm %s)",
@ -63,6 +65,7 @@ Main(void)
Log(LOG_NOTICE, "Setting up local Matrix user..."); Log(LOG_NOTICE, "Setting up local Matrix user...");
ASRegisterUser(parsee_conf, parsee_conf->sender_localpart); ASRegisterUser(parsee_conf, parsee_conf->sender_localpart);
memset(&conf, 0, sizeof(conf)); memset(&conf, 0, sizeof(conf));
conf.port = parsee_conf->port; conf.port = parsee_conf->port;
conf.threads = 4; conf.threads = 4;
@ -70,6 +73,10 @@ Main(void)
conf.handlerArgs = ParseeInitData(jabber); conf.handlerArgs = ParseeInitData(jabber);
conf.handler = ParseeRequest; 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..."); Log(LOG_NOTICE, "Creating XMPP listener thread...");
if (pthread_create(&xmpp_thr, NULL, ParseeXMPPThread, conf.handlerArgs)) if (pthread_create(&xmpp_thr, NULL, ParseeXMPPThread, conf.handlerArgs))
{ {
@ -98,6 +105,8 @@ end:
Log(LOG_INFO, "Exiting..."); Log(LOG_INFO, "Exiting...");
HttpServerFree(server); HttpServerFree(server);
ParseeConfigFree(); ParseeConfigFree();
CronStop(cron);
CronFree(cron);
ParseeFreeData(conf.handlerArgs); ParseeFreeData(conf.handlerArgs);
return 0; return 0;
} }

View file

@ -44,3 +44,12 @@ ParseeFreeData(ParseeData *data)
HttpRouterFree(data->router); HttpRouterFree(data->router);
Free(data); Free(data);
} }
void
ParseeCleanup(void *datp)
{
ParseeData *data = datp;
Log(LOG_NOTICE, "Cleaning up...");
/* TODO */
}

View file

@ -131,3 +131,26 @@ XMLookForUnique(XMLElement *parent, char *tag)
return NULL; 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;
}

View file

@ -5,7 +5,6 @@
XMLElement * XMLElement *
XMLDecode(Stream *stream, bool autofree) XMLDecode(Stream *stream, bool autofree)
{ {
/* TODO: Use the existing SAX parser to decode everything */
#define push(x) ArrayAdd(stack, x) #define push(x) ArrayAdd(stack, x)
#define pop(x) ArrayDelete(stack, ArraySize(stack) - 1) #define pop(x) ArrayDelete(stack, ArraySize(stack) - 1)
#define peek(x) ArrayGet(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 *ret = NULL;
XMLElement *top; XMLElement *top;
char *key, *val; char *key, *val;
size_t i;
Array *stack; Array *stack;
@ -85,6 +85,15 @@ XMLDecode(Stream *stream, bool autofree)
event = NULL; event = NULL;
} }
XMLFreeEvent(event); XMLFreeEvent(event);
for (i = 0; i < ArraySize(stack); i++)
{
XMLElement *e = ArrayGet(stack, i);
if (e == ret)
{
continue;
}
XMLFreeElement(e);
}
ArrayFree(stack); ArrayFree(stack);
XMLFreeLexer(lexer); XMLFreeLexer(lexer);
return ret; return ret;
@ -158,8 +167,6 @@ XMLEncodeTag(Stream *stream, XMLElement *element)
void void
XMLEncode(Stream *stream, XMLElement *element) XMLEncode(Stream *stream, XMLElement *element)
{ {
/* TODO: Write the entire XML element. This shouldn't be
* too hard. */
if (!stream || !element) if (!stream || !element)
{ {
return; return;

View file

@ -160,6 +160,9 @@ XMPPAuthenticateCompStream(XMPPComponent *comp, char *shared)
!StrEquals(ev->element, "handshake")) !StrEquals(ev->element, "handshake"))
{ {
Log(LOG_ERR, "Excepted empty handshake reply, got nonsense."); 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(stream_id);
Free(handshake); Free(handshake);
XMLFreeEvent(ev); XMLFreeEvent(ev);

View file

@ -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 * void *
ParseeXMPPThread(void *argp) ParseeXMPPThread(void *argp)
{ {
@ -41,9 +130,6 @@ ParseeXMPPThread(void *argp)
XMLElement *stanza = NULL; XMLElement *stanza = NULL;
while (true) while (true)
{ {
XMLElement *body = NULL;
XMLElement *data = NULL;
XMLElement *stanza_id = NULL;
char *to, *room, *from, *from_matrix; char *to, *room, *from, *from_matrix;
char *chat_id, *mroom_id; char *chat_id, *mroom_id;
@ -55,6 +141,7 @@ ParseeXMPPThread(void *argp)
if (StrEquals(stanza->name, "presence")) if (StrEquals(stanza->name, "presence"))
{ {
/* TODO: Manage presence */
XMLFreeElement(stanza); XMLFreeElement(stanza);
continue; continue;
} }
@ -80,56 +167,18 @@ ParseeXMPPThread(void *argp)
continue; continue;
} }
} }
body = XMLookForUnique(stanza, "body");
if (!body) if (!MessageStanza(args, stanza))
{ {
XMLFreeElement(stanza);
continue; continue;
} }
stanza_id = XMLookForUnique(stanza, "stanza-id"); }
else if (StrEquals(stanza->name, "iq"))
to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); {
from = HashMapGet(stanza->attrs, "from"); /* TODO: Ought to manage info/query requests */
from_matrix = ParseeEncodeJID(args->config, from, true); Log(LOG_WARNING, "Unimplemented 'iq' stanza.");
room = ParseeFindDMRoom(args, to, from); Log(LOG_WARNING, "Odds are, this is the programmer's fault");
data = ArrayGet(body->children, 0); Log(LOG_WARNING, "and this needs to be implemented later on.");
/* 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 else
{ {

View file

@ -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 /* Returns the user's name in a room, or a copy of the MXID itself, to be
* Free'd. */ * Free'd. */
extern char * ASGetName(const ParseeConfig *c, char *room, char *user); 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 #endif

View file

@ -12,6 +12,9 @@ extern HashMap * MatrixCreateNotice(char *body);
/* Creates the content for a normal message. */ /* Creates the content for a normal message. */
extern HashMap * MatrixCreateMessage(char *body); 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 */ /* Creates the content for a m.room.name state event */
extern HashMap * MatrixCreateNameState(char *name); extern HashMap * MatrixCreateNameState(char *name);

View file

@ -44,6 +44,10 @@ typedef struct ParseeData {
Db *db; Db *db;
} ParseeData; } ParseeData;
#define SECONDS * 1000
#define MINUTES * 60 SECONDS
#define HOURS * 60 MINUTES
/* Initialises a Parsee config from scratch, and writes to it /* Initialises a Parsee config from scratch, and writes to it
* as JSON in the CWD. */ * as JSON in the CWD. */
extern void ParseeConfigInit(void); extern void ParseeConfigInit(void);
@ -150,4 +154,7 @@ extern bool ParseeVerifyStanza(ParseeData *, char *chat_id, char *stanza_id);
extern void ParseeSendPresence(ParseeData *); extern void ParseeSendPresence(ParseeData *);
extern bool ParseeInitialiseSignals(HttpServer *, pthread_t, XMPPComponent *); extern bool ParseeInitialiseSignals(HttpServer *, pthread_t, XMPPComponent *);
/* Job used to cleanup Parsee data that isn't necessary anymore. */
extern void ParseeCleanup(void *data);
#endif #endif

View file

@ -64,6 +64,7 @@ extern void XMLAddAttr(XMLElement *element, char *key, char *val);
extern void XMLAddChild(XMLElement *element, XMLElement *child); extern void XMLAddChild(XMLElement *element, XMLElement *child);
extern XMLElement * XMLookForUnique(XMLElement *parent, char *tag); extern XMLElement * XMLookForUnique(XMLElement *parent, char *tag);
extern XMLElement * XMLookForTKV(XMLElement *parent, char *tag, char *k, char *v);
/* Frees an XML element properly. */ /* Frees an XML element properly. */
extern void XMLFreeElement(XMLElement *element); extern void XMLFreeElement(XMLElement *element);