[ADD/WIP] XMPP->Matrix avatar, start bridging bans

I still hate XEP-0084.
This commit is contained in:
LDA 2024-07-05 03:10:43 +02:00
commit 1f658ece76
9 changed files with 587 additions and 27 deletions

171
src/AS.c
View file

@ -188,6 +188,41 @@ ASBan(const ParseeConfig *conf, char *id, char *banned)
JsonFree(json);
}
void
ASKick(const ParseeConfig *conf, char *id, char *banned)
{
HttpClientContext *ctx = NULL;
HashMap *json = NULL;
char *path, *bridge;
if (!conf || !id || !banned)
{
return;
}
bridge = StrConcat(4,
"@", conf->sender_localpart,
":", conf->homeserver_host
);
path = StrConcat(5,
"/_matrix/client/v3/rooms/", id, "/kick",
"?user_id=", bridge
);
Free(bridge);
ctx = ParseeCreateRequest(
conf,
HTTP_POST, path
);
Free(path);
json = HashMapCreate();
HashMapSet(json, "user_id", JsonValueString(banned));
HashMapSet(json, "reason", JsonValueString("Parsee felt jealous."));
ASAuthenticateRequest(conf, ctx);
ParseeSetRequestJSON(ctx, json);
HttpClientContextFree(ctx);
JsonFree(json);
}
void
ASJoin(const ParseeConfig *conf, char *id, char *masquerade)
{
HttpClientContext *ctx = NULL;
@ -331,6 +366,35 @@ ASCreateRoom(const ParseeConfig *conf, char *by, char *alias)
return id;
}
void
ASSetAvatar(const ParseeConfig *conf, char *user, char *mxc)
{
HttpClientContext *ctx = NULL;
HashMap *json;
char *path;
if (!conf || !user || !mxc)
{
return;
}
user = HttpUrlEncode(user);
path = StrConcat(6,
"/_matrix/client/v3/profile/",
user, "/avatar_url", "?",
"user_id=", user
);
json = HashMapCreate();
HashMapSet(json, "avatar_url", JsonValueString(mxc));
ctx = ParseeCreateRequest(conf, HTTP_PUT, path);
Free(path);
ASAuthenticateRequest(conf, ctx);
ParseeSetRequestJSON(ctx, json);
HttpClientContextFree(ctx);
JsonFree(json);
Free(user);
}
void
ASSetName(const ParseeConfig *conf, char *user, char *name)
{
HttpClientContext *ctx = NULL;
@ -510,6 +574,7 @@ ASUpload(const ParseeConfig *c, Stream *from, unsigned int size)
HashMap *reply;
if (!c || !from)
{
Log(LOG_INFO, "Obvious upload fail");
return NULL;
}
@ -526,13 +591,26 @@ ASUpload(const ParseeConfig *c, Stream *from, unsigned int size)
}
HttpRequestSendHeaders(ctx);
StreamCopy(from, HttpClientStream(ctx));
for (i = 0; i < size; i++)
{
int ch = StreamGetc(from);
if (ch == EOF)
{
break;
}
StreamPutc(HttpClientStream(ctx), ch);
}
HttpRequestSend(ctx);
reply = JsonDecode(HttpClientStream(ctx));
ret = StrDuplicate(
JsonValueAsString(HashMapGet(reply, "content_uri"))
);
if (!ret)
{
JsonEncode(reply, StreamStdout(), JSON_PRETTY);
StreamFlush(StreamStdout());
Log(LOG_INFO, "Less obvious upload fail");
}
HttpClientContextFree(ctx);
JsonFree(reply);
Free(size_str);
@ -632,3 +710,92 @@ ASType(const ParseeConfig *c, char *user, char *room, bool status)
HttpClientContextFree(ctx);
Free(user);
}
HashMap *
ASGetUserConfig(const ParseeConfig *c, char *user, char *key)
{
HttpClientContext *ctx = NULL;
HashMap *json;
char *path;
if (!c || !key)
{
return NULL;
}
if (!user)
{
char *raw = StrConcat(4,
"@", c->sender_localpart,
":", c->homeserver_host
);
user = HttpUrlEncode(raw);
Free(raw);
}
else
{
user = HttpUrlEncode(user);
}
path = StrConcat(7,
"/_matrix/client/v3/user/",
user, "/account_data/", key, "?",
"user_id=", user
);
ctx = ParseeCreateRequest(c, HTTP_GET, path);
Free(path);
ASAuthenticateRequest(c, ctx);
HttpRequestSendHeaders(ctx);
HttpRequestSend(ctx);
json = JsonDecode(HttpClientStream(ctx));
HttpClientContextFree(ctx);
Free(user);
return json;
}
void
ASSetUserConfig(const ParseeConfig *c, char *user, char *key, HashMap *map)
{
HttpClientContext *ctx = NULL;
HashMap *json;
char *path;
if (!c || !key || !map)
{
JsonFree(map);
return;
}
if (!user)
{
char *raw = StrConcat(4,
"@", c->sender_localpart,
":", c->homeserver_host
);
user = HttpUrlEncode(raw);
Free(raw);
}
else
{
user = HttpUrlEncode(user);
}
path = StrConcat(7,
"/_matrix/client/v3/user/",
user, "/account_data/", key, "?",
"user_id=", user
);
ctx = ParseeCreateRequest(c, HTTP_PUT, path);
Free(path);
ASAuthenticateRequest(c, ctx);
ParseeSetRequestJSON(ctx, map);
HttpClientContextFree(ctx);
Free(user);
JsonFree(map);
return;
}

View file

@ -68,9 +68,10 @@ RouteHead(RouteRoomAck, arr, argp)
}
muc = ParseeDecodeLocalMUC(args->data->config, room);
if (!ParseeManageBan(args->data, muc, NULL))
if (ParseeManageBan(args->data, muc, NULL))
{
HttpResponseStatus(args->ctx, HTTP_METHOD_NOT_ALLOWED);
Log(LOG_INFO, "Nofly...");
response = MatrixCreateError(
"M_NOT_FOUND",
"XMPP MUC is banned from being accessed on this instance"

View file

@ -113,7 +113,7 @@ const static IoFunctions Functions = {
};
Stream *
StrStreamReader(char *buffer)
StrStreamReaderN(char *buffer, int n)
{
Io *raw_io;
ReaderCookie *cookie;
@ -124,7 +124,7 @@ StrStreamReader(char *buffer)
cookie = Malloc(sizeof(*cookie));
cookie->buffer = buffer;
cookie->length = strlen(buffer);
cookie->length = n ? n : strlen(buffer);
cookie->offset = 0;
raw_io = IoCreate(cookie, Functions);
return StreamIo(raw_io);

View file

@ -2,6 +2,7 @@
#include <pthread.h>
#include <string.h>
#include <ctype.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Base64.h>
@ -10,15 +11,21 @@
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h>
#include <StringStream.h>
#include <Matrix.h>
#include <XMPP.h>
#include <XML.h>
#include <AS.h>
/* TODO: Rewrite this avatar code.
* XEP-0084 sucks. */
#define IQ_ADVERT \
AdvertiseSimple("http://jabber.org/protocol/chatstates") \
AdvertiseSimple("http://jabber.org/protocol/caps") \
AdvertiseSimple("urn:xmpp:avatar:metadata+notify") \
AdvertiseSimple("urn:xmpp:avatar:metadata") \
AdvertiseSimple("urn:xmpp:avatar:data+notify") \
AdvertiseSimple("urn:xmpp:avatar:data") \
AdvertiseSimple("urn:xmpp:message-correct:0") \
AdvertiseSimple("urn:xmpp:reactions:0") \
AdvertiseSimple("urn:xmpp:styling:0") \
@ -290,8 +297,104 @@ typedef struct XMPPThread {
XMPPThreadInfo *info;
} XMPPThread;
/* TODO: Clean up all of this. We are currently separating DMs from MUCs,
* where we could unify all our code, and generalise everything. */
/* Manages an avatar metadata pubsub item */
static XMLElement *
CreateAvatarRequest(char *from, char *to, char *avatar_id)
{
XMLElement *iq_req, *pubsub, *items, *item;
char *id;
iq_req = XMLCreateTag("iq");
XMLAddAttr(iq_req, "from", from);
XMLAddAttr(iq_req, "to", to);
XMLAddAttr(iq_req, "id", (id = StrRandom(16)));
XMLAddAttr(iq_req, "type", "get");
pubsub = XMLCreateTag("pubsub");
XMLAddAttr(pubsub, "xmlns", "http://jabber.org/protocol/pubsub");
XMLAddChild(iq_req, pubsub);
items = XMLCreateTag("items");
XMLAddAttr(items, "node", "urn:xmpp:avatar:data");
XMLAddChild(pubsub, items);
item = XMLCreateTag("item");
XMLAddAttr(item, "id", avatar_id);
XMLAddChild(items, item);
Free(id);
return iq_req;
}
static XMLElement *
CreatePubsubRequest(char *from, char *to, char *node)
{
XMLElement *iq_req, *pubsub, *sub;
char *id;
iq_req = XMLCreateTag("iq");
XMLAddAttr(iq_req, "from", from);
XMLAddAttr(iq_req, "to", to);
XMLAddAttr(iq_req, "id", (id = StrRandom(16)));
XMLAddAttr(iq_req, "type", "set");
pubsub = XMLCreateTag("pubsub");
XMLAddAttr(pubsub, "xmlns", "http://jabber.org/protocol/pubsub");
XMLAddChild(iq_req, pubsub);
sub = XMLCreateTag("subscribe");
XMLAddAttr(sub, "node", node);
XMLAddAttr(sub, "jid", from);
XMLAddChild(pubsub, sub);
//Log(LOG_INFO, "Subscribed to %s's %s node", to, node);
Free(id);
return iq_req;
}
static void
ManageProfileItem(ParseeData *args, XMLElement *item, XMLElement *stanza, XMPPThread *thr)
{
XMPPComponent *jabber = args->jabber;
DbRef *avatars;
HashMap *json;
char *publisher = HashMapGet(item->attrs, "publisher");
char *id = HashMapGet(item->attrs, "id");
char *mxid;
avatars = DbLock(args->db, 1, "avatars");
if (!avatars)
{
avatars = DbCreate(args->db, 1, "avatars");
}
json = DbJson(avatars);
mxid = GrabString(json, 1, publisher);
if (mxid && StrEquals(mxid, id))
{
/* We have nothing to do. */
goto end;
}
/* We need to download the media to push it. Let's submit a pubsub request. */
{
char *from = HashMapGet(stanza->attrs, "to");
char *to = HashMapGet(stanza->attrs, "from");
char *url = HashMapGet(item->attrs, "url");
XMLElement *request = CreateAvatarRequest(from, to, id);
pthread_mutex_lock(&jabber->write_lock);
XMLEncode(jabber->stream, request);
StreamFlush(jabber->stream);
pthread_mutex_unlock(&jabber->write_lock);
XMLFreeElement(request);
}
end:
DbUnlock(args->db, avatars);
}
static bool
MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
{
@ -300,6 +403,7 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
XMLElement *reactions = NULL;
XMLElement *body = NULL;
XMLElement *data = NULL;
XMLElement *event = NULL;
char *to, *room, *from, *from_matrix, *decode_from;
char *chat_id = NULL, *mroom_id = NULL;
@ -312,6 +416,28 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
from = HashMapGet(stanza->attrs, "from");
event = XMLookForTKV(stanza, "event",
"xmlns", "http://jabber.org/protocol/pubsub#event"
);
if (event)
{
size_t i;
XMLElement *items =
XMLookForTKV(event, "items", "node", "urn:xmpp:avatar:metadata");
if (items)
{
for (i = 0; i < ArraySize(items->children); i++)
{
ManageProfileItem(
args,
ArrayGet(items->children, i),
stanza,
thr
);
}
}
}
#define CHAT_STATES "http://jabber.org/protocol/chatstates"
if (XMLookForTKV(stanza, "composing", "xmlns", CHAT_STATES))
{
@ -395,6 +521,21 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
chat = true;
}
{
char *parsee = StrConcat(2, "parsee@", args->config->component_host);
XMLElement *ps = CreatePubsubRequest(
parsee, decode_from, "urn:xmpp:avatar:metadata"
);
pthread_mutex_lock(&jabber->write_lock);
XMLEncode(jabber->stream, ps);
StreamFlush(jabber->stream);
pthread_mutex_unlock(&jabber->write_lock);
XMLFreeElement(ps);
Free(parsee);
}
pthread_mutex_lock(&thr->info->chk_lock);
if (ParseeVerifyAllStanza(args, stanza) && !replaced)
{
@ -581,10 +722,158 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza)
pthread_mutex_lock(&jabber->write_lock);
XMLEncode(jabber->stream, iq_reply);
StreamFlush(jabber->stream);
pthread_mutex_unlock(&jabber->write_lock);
XMLFreeElement(iq_reply);
}
static char *
TrimBase64(char *b64)
{
char *ret, *tmp;
ret = NULL;
while (*b64)
{
char ch[2] = { *b64, 0 };
if (isspace(*b64))
{
b64++;
continue;
}
tmp = ret;
ret = StrConcat(2, ret, ch);
Free(tmp);
b64++;
}
return ret;
}
static void
IQResult(ParseeData *args, XMLElement *stanza)
{
XMPPComponent *jabber = args->jabber;
XMLElement *vcard = XMLookForTKV(stanza, "vCard", "xmlns", "vcard-temp");
XMLElement *event = XMLookForTKV(stanza, "pubsub",
"xmlns", "http://jabber.org/protocol/pubsub"
);
if (event)
{
size_t i;
XMLElement *retrieve =
XMLookForTKV(event, "items", "node", "urn:xmpp:avatar:data");
if (retrieve)
{
for (i = 0; i < ArraySize(retrieve->children); i++)
{
XMLElement *item =
ArrayGet(retrieve->children, i);
XMLElement *avatar_data = XMLookForTKV(
item, "data", "xmlns", "urn:xmpp:avatar:data"
);
XMLElement *data = ArrayGet(avatar_data->children, 0);
char *id = HashMapGet(item->attrs, "id");
char *from = HashMapGet(stanza->attrs, "from");
char *base64;
unsigned char *bdata;
size_t length, b64len;
Stream *datastream;
char *mxc, *from_matrix, *jid;
DbRef *avatars;
HashMap *json;
if (!data || !data->data)
{
return;
}
avatars = DbLock(args->db, 1, "avatars");
if (!avatars)
{
avatars = DbCreate(args->db, 1, "avatars");
}
json = DbJson(avatars);
if (StrEquals(GrabString(json, 1, from), id))
{
DbUnlock(args->db, avatars);
return;
}
base64 = TrimBase64(data->data);
b64len = base64 ? strlen(base64) : 0;
length = Base64DecodedSize(base64, b64len);
/* TODO: Bound checks! */
bdata = Base64Decode(base64, b64len);
datastream = StrStreamReaderN(bdata, length);
mxc = ASUpload(args->config, datastream, length);
jid = ParseeLookupJID(from);
from_matrix = ParseeEncodeJID(args->config, jid, false);
ASSetAvatar(args->config, from_matrix, mxc);
JsonValueFree(JsonSet(
json, JsonValueString(id),
1, from)
);
DbUnlock(args->db, avatars);
Free(mxc);
Free(jid);
Free(bdata);
Free(from_matrix);
Free(base64);
StreamClose(datastream);
}
}
}
if (vcard)
{
XMLElement *photo = XMLookForUnique(vcard, "PHOTO");
XMLElement *nickname = XMLookForUnique(vcard, "NICKNAME");
if (nickname)
{
XMLElement *data = ArrayGet(nickname->children, 0);
}
if (photo)
{
XMLElement *binval = XMLookForUnique(photo, "BINVAL");
XMLElement *data = ArrayGet(binval->children, 0);
char *base64;
unsigned char *bdata;
size_t length, b64len;
Stream *datastream;
char *mxc, *from_matrix, *jid;
if (!data || !data->data)
{
return;
}
base64 = data->data;
b64len = base64 ? strlen(base64) : 0;
length = Base64DecodedSize(base64, b64len);
bdata = Base64Decode(base64, b64len);
datastream = StrStreamReaderN(bdata, length);
mxc = ASUpload(args->config, datastream, length);
jid = ParseeLookupJID(HashMapGet(stanza->attrs, "from"));
from_matrix = ParseeEncodeJID(args->config, jid, false);
ASSetAvatar(args->config, from_matrix, mxc);
/* TODO: Check if already set. */
Free(bdata);
StreamClose(datastream);
Free(from_matrix);
Free(jid);
Free(mxc);
}
}
}
static void
IQGet(ParseeData *args, XMLElement *stanza)
{
@ -622,6 +911,7 @@ IQGet(ParseeData *args, XMLElement *stanza)
pthread_mutex_lock(&jabber->write_lock);
XMLEncode(jabber->stream, iq_reply);
StreamFlush(jabber->stream);
pthread_mutex_unlock(&jabber->write_lock);
XMLFreeElement(iq_reply);
}
@ -634,6 +924,11 @@ IQGet(ParseeData *args, XMLElement *stanza)
}
}
static void
IQError(ParseeData *args, XMLElement *stanza)
{
/* TODO */
}
#undef DISCO
static void
IQStanza(ParseeData *args, XMLElement *stanza)
@ -651,9 +946,30 @@ IQStanza(ParseeData *args, XMLElement *stanza)
while (0)
OnType(get, IQGet);
OnType(error, IQError);
OnType(result, IQResult);
#undef OnType
}
static XMLElement *
CreateVCardRequest(char *from, char *to)
{
XMLElement *vcard_request, *vcard;
char *id;
vcard_request = XMLCreateTag("iq");
XMLAddAttr(vcard_request, "from", from);
XMLAddAttr(vcard_request, "to", to);
XMLAddAttr(vcard_request, "id", (id = StrRandom(16)));
XMLAddAttr(vcard_request, "type", "get");
vcard = XMLCreateTag("vCard");
XMLAddAttr(vcard, "xmlns", "vcard-temp");
XMLAddChild(vcard_request, vcard);
Free(id);
return vcard_request;
}
static void
PresenceStanza(ParseeData *args, XMLElement *stanza)
{
@ -664,35 +980,95 @@ PresenceStanza(ParseeData *args, XMLElement *stanza)
if ((user_info = XMLookForTKV(stanza, "x", "xmlns", MUC_USER_NS)))
{
XMLElement *item = XMLookForUnique(user_info, "item");
XMPPComponent *jabber = args->jabber;
char *jid = item ? HashMapGet(item->attrs, "jid") : NULL;
char *from, *best = jid ? jid : oid;
char *type = HashMapGet(stanza->attrs, "type");
if (StrEquals(type, "unavailable"))
{
/* TODO: Treat as a ban if the role is outcast */
char *room = ParseeGetBridgedRoom(args, stanza);
char *decode_from = ParseeLookupJID(oid);
char *from_matrix = ParseeDecodeMXID(decode_from);
char *affiliation = HashMapGet(item->attrs, "affiliation");
if (!from_matrix || *from_matrix != '@')
{
Free(from_matrix);
from_matrix = ParseeEncodeJID(args->config, oid, true);
}
if (StrEquals(affiliation, "outcast"))
{
ASBan(args->config, room, from_matrix);
}
else
{
ASKick(args->config, room, from_matrix);
}
Free(decode_from);
Free(from_matrix);
Free(room);
}
if (jid)
{
ParseePushJIDTable(oid, jid);
}
from = StrConcat(2, "parsee@", args->config->component_host);
Free(from);
}
else if (vc)
{
XMLElement *photo = XMLookForUnique(vc, "photo");
XMLElement *p_dat = photo ? ArrayGet(photo->children, 0) : NULL;
char *room_id = NULL, *chat_id = NULL;
XMLElement *vcard_request;
XMPPComponent *jabber = args->jabber;
char *from;
DbRef *avatars;
HashMap *json;
char *avatar_id;
if (!p_dat)
{
return;
}
chat_id = ParseeGetFromMUCID(args, oid);
room_id = ParseeGetRoomID(args, chat_id);
if (!room_id)
avatars = DbLock(args->db, 1, "avatars");
if (!avatars)
{
Free(chat_id);
Free(room_id);
avatars = DbCreate(args->db, 1, "avatars");
}
/* TODO: Get the media, and shove it to the room */
Free(chat_id);
Free(room_id);
json = DbJson(avatars);
avatar_id = GrabString(json, 1, oid);
if (StrEquals(avatar_id, p_dat->data))
{
DbUnlock(args->db, avatars);
return;
}
JsonValueFree(JsonSet(
json, JsonValueString(p_dat->data),
1, oid)
);
DbUnlock(args->db, avatars);
from = StrConcat(2, "parsee@", args->config->component_host);
vcard_request = CreateVCardRequest(
from, HashMapGet(stanza->attrs, "from")
);
pthread_mutex_lock(&jabber->write_lock);
XMLEncode(jabber->stream, vcard_request);
StreamFlush(jabber->stream);
pthread_mutex_unlock(&jabber->write_lock);
XMLFreeElement(vcard_request);
Free(from);
}
#undef MUC_USER_NS
}
@ -728,13 +1104,12 @@ XMPPDispatcher(void *argp)
XMLElement *stanza = RetrieveStanza(thread);
if (!stanza)
{
UtilSleepMillis(10);
UtilSleepMillis(1);
continue;
}
if (StrEquals(stanza->name, "presence"))
{
/* TODO: Manage presence */
PresenceStanza(args, stanza);
XMLFreeElement(stanza);
continue;
@ -770,7 +1145,8 @@ ParseeXMPPThread(void *argp)
XMLElement *stanza = NULL;
XMPPThreadInfo info;
pthread_mutex_t stanzas_lock = PTHREAD_MUTEX_INITIALIZER;
size_t i;
size_t i, j = 0;
/* Initialise the FIFO */
info.stanzas = ArrayCreate();
@ -795,6 +1171,7 @@ ParseeXMPPThread(void *argp)
pthread_create(thr, NULL, XMPPDispatcher, &info.dispatchers[i]);
}
while (true)
{
char *to, *room, *from, *from_matrix;
@ -829,7 +1206,6 @@ ParseeXMPPThread(void *argp)
}
PushStanza(&info, stanza);
//XMLFreeElement(stanza);
}
info.running = false;

View file

@ -28,6 +28,7 @@ extern void ASJoin(const ParseeConfig *, char *, char *);
/* Bans from a room a specific user */
extern void ASBan(const ParseeConfig *, char *, char *);
extern void ASKick(const ParseeConfig *, char *, char *);
/* Invites from a room a specific user */
extern void ASInvite(const ParseeConfig *, char *, char *);
@ -53,6 +54,7 @@ extern char * ASCreateRoom(const ParseeConfig *c, char *by, char *alias);
/* Sets a user's displayname */
extern void ASSetName(const ParseeConfig *c, char *user, char *name);
extern void ASSetAvatar(const ParseeConfig *c, char *user, char *mxc);
/* Returns the user's name in a room, or a copy of the MXID itself, to be
* Free'd. */
@ -63,4 +65,7 @@ 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);
extern HashMap * ASGetUserConfig(const ParseeConfig *c, char *user, char *key);
extern void ASSetUserConfig(const ParseeConfig *c, char *u, char *key, HashMap *map);
#endif

View file

@ -8,6 +8,7 @@
extern Stream * StrStreamWriter(char **buffer);
/* Creates a string stream reader. The referenced buffer may be everywhere. */
extern Stream * StrStreamReader(char *buffer);
extern Stream * StrStreamReaderN(char *buffer, int n);
#define StrStreamReader(buf) StrStreamReaderN(buf, 0)
#endif