mirror of
https://forge.fsky.io/lda/Parsee.git
synced 2026-03-13 21:25:11 +00:00
1290 lines
36 KiB
C
1290 lines
36 KiB
C
#include <Parsee.h>
|
|
|
|
#include <pthread.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
#include <Cytoplasm/Memory.h>
|
|
#include <Cytoplasm/Base64.h>
|
|
#include <Cytoplasm/Util.h>
|
|
#include <Cytoplasm/Log.h>
|
|
#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:data+notify") \
|
|
AdvertiseSimple("urn:xmpp:avatar:metadata") \
|
|
AdvertiseSimple("urn:xmpp:message-correct:0") \
|
|
AdvertiseSimple("urn:xmpp:avatar:data") \
|
|
AdvertiseSimple("urn:xmpp:chat-markers:0") \
|
|
AdvertiseSimple("urn:xmpp:reactions:0") \
|
|
AdvertiseSimple("urn:xmpp:styling:0") \
|
|
AdvertiseSimple("urn:xmpp:receipts") \
|
|
AdvertiseSimple("urn:xmpp:reply:0") \
|
|
AdvertiseSimple("jabber:x:oob") \
|
|
AdvertiseSimple("vcard-temp") \
|
|
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((char *) cata) : 0;
|
|
size_t bl = catb ? strlen((char *) 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 = (unsigned char *) ida->category;
|
|
unsigned char *catb = (unsigned char *) 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((const char *) 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 char *
|
|
ParseeGetBridgedRoom(ParseeData *data, XMLElement *stanza)
|
|
{
|
|
char *to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to"));
|
|
char *from = (HashMapGet(stanza->attrs, "from"));
|
|
char *chat_id = ParseeGetFromMUCID(data, from);
|
|
char *mroom_id = ParseeGetRoomID(data, chat_id);
|
|
char *ret;
|
|
|
|
Free(chat_id);
|
|
if (mroom_id)
|
|
{
|
|
Free(to);
|
|
return mroom_id;
|
|
}
|
|
|
|
/* Not a MUC, find the DMd room. */
|
|
ret = ParseeFindDMRoom(data, to, from);
|
|
Free(to);
|
|
return ret;
|
|
}
|
|
static char *
|
|
ParseeGetEventFromID(ParseeData *data, XMLElement *stanza, char *id)
|
|
{
|
|
char *from = (HashMapGet(stanza->attrs, "from"));
|
|
char *chat_id = ParseeGetFromMUCID(data, from);
|
|
char *ret = ParseeEventFromSID(data, chat_id, id);
|
|
char *mroom_id = ParseeGetBridgedRoom(data, stanza);
|
|
|
|
if (!ret)
|
|
{
|
|
Free(ret);
|
|
ret = ParseeEventFromID(data, chat_id, id);
|
|
}
|
|
|
|
Free(chat_id);
|
|
if (ret)
|
|
{
|
|
Free(mroom_id);
|
|
return ret;
|
|
}
|
|
ret = ParseeDMEventFromID(data, mroom_id, id);
|
|
Free(mroom_id);
|
|
|
|
return ret;
|
|
}
|
|
static char *
|
|
ParseeGetReactedEvent(ParseeData *data, XMLElement *stanza)
|
|
{
|
|
XMLElement *reactions = XMLookForTKV(stanza,
|
|
"reactions", "xmlns", "urn:xmpp:reactions:0"
|
|
);
|
|
char *reacted_id = reactions ? HashMapGet(reactions->attrs, "id") : NULL;
|
|
|
|
return ParseeGetEventFromID(data, stanza, reacted_id);
|
|
}
|
|
|
|
static void
|
|
ParseePushAllStanza(ParseeData *args, XMLElement *stanza, char *event)
|
|
{
|
|
char *xmpp_from = HashMapGet(stanza->attrs, "from");
|
|
char *mroom_id = ParseeGetBridgedRoom(args, stanza);
|
|
char *chat_id = ParseeGetFromMUCID(args, xmpp_from);
|
|
|
|
char *id_str = HashMapGet(stanza->attrs, "id");
|
|
char *s_id_str = XMPPGetStanzaID(stanza);
|
|
|
|
if (!chat_id)
|
|
{
|
|
ParseePushDMStanza(
|
|
args, mroom_id, s_id_str,
|
|
id_str, event, xmpp_from
|
|
);
|
|
}
|
|
ParseePushStanza(args, chat_id, s_id_str, id_str, event, xmpp_from);
|
|
Free(mroom_id);
|
|
Free(chat_id);
|
|
}
|
|
|
|
static bool
|
|
ParseeVerifyAllStanza(ParseeData *args, XMLElement *stanza)
|
|
{
|
|
char *to, *room;
|
|
char *from = HashMapGet(stanza->attrs, "from");
|
|
char *id = HashMapGet(stanza->attrs, "id");
|
|
char *chat_id = ParseeGetFromMUCID(args, from);
|
|
bool ret;
|
|
|
|
if (chat_id)
|
|
{
|
|
char *s_id_str = XMPPGetStanzaID(stanza);
|
|
ret = ParseeVerifyStanza(args, chat_id, s_id_str);
|
|
|
|
Free(chat_id);
|
|
return ret;
|
|
}
|
|
to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to"));
|
|
room = ParseeFindDMRoom(args, to, from);
|
|
ret = ParseeVerifyDMStanza(args, chat_id, id);
|
|
|
|
Free(room);
|
|
Free(to);
|
|
|
|
return ret;
|
|
}
|
|
struct XMPPThread;
|
|
typedef struct XMPPThreadInfo {
|
|
/* A FIFO of stanzas */
|
|
Array *stanzas;
|
|
pthread_mutex_t lock;
|
|
|
|
ParseeData *args;
|
|
XMPPComponent *jabber;
|
|
|
|
struct XMPPThread *dispatchers;
|
|
size_t available_dispatchers;
|
|
|
|
bool running;
|
|
pthread_mutex_t chk_lock;
|
|
} XMPPThreadInfo;
|
|
typedef struct XMPPThread {
|
|
pthread_t thr;
|
|
XMPPThreadInfo *info;
|
|
} XMPPThread;
|
|
|
|
/* 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);
|
|
|
|
(void) url; /* TODO */
|
|
}
|
|
|
|
end:
|
|
DbUnlock(args->db, avatars);
|
|
}
|
|
|
|
static bool
|
|
MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
|
{
|
|
XMPPComponent *jabber = args->jabber;
|
|
|
|
XMLElement *reactions = NULL;
|
|
XMLElement *body = NULL;
|
|
XMLElement *data = NULL;
|
|
XMLElement *event = NULL;
|
|
|
|
char *to, *room, *from, *from_matrix, *decode_from;
|
|
char *mroom_id = NULL;
|
|
size_t i;
|
|
|
|
to = NULL;
|
|
from = NULL;
|
|
decode_from = NULL;
|
|
from_matrix = NULL;
|
|
|
|
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))
|
|
{
|
|
decode_from = ParseeLookupJID(from);
|
|
from_matrix = ParseeEncodeJID(args->config, decode_from, true);
|
|
mroom_id = ParseeGetBridgedRoom(args, stanza);
|
|
|
|
ASType(args->config, from_matrix, mroom_id, true);
|
|
|
|
Free(decode_from);
|
|
Free(from_matrix);
|
|
Free(mroom_id);
|
|
mroom_id = NULL;
|
|
decode_from = NULL;
|
|
from_matrix = NULL;
|
|
}
|
|
if (XMLookForTKV(stanza, "active", "xmlns", CHAT_STATES))
|
|
{
|
|
char *latest = NULL;
|
|
decode_from = ParseeLookupJID(from);
|
|
from_matrix = ParseeEncodeJID(args->config, decode_from, true);
|
|
mroom_id = ParseeGetBridgedRoom(args, stanza);
|
|
|
|
latest = ParseeLookupHead(mroom_id);
|
|
|
|
ASType(args->config, from_matrix, mroom_id, false);
|
|
ASPresence(args->config, from_matrix, mroom_id, latest);
|
|
|
|
Free(decode_from);
|
|
Free(from_matrix);
|
|
Free(latest);
|
|
Free(mroom_id);
|
|
mroom_id = NULL;
|
|
decode_from = NULL;
|
|
from_matrix = NULL;
|
|
}
|
|
if (XMLookForTKV(stanza, "paused", "xmlns", CHAT_STATES) ||
|
|
XMLookForTKV(stanza, "received", "xmlns", "urn:xmpp:receipts") ||
|
|
XMLookForTKV(stanza, "displayed", "xmlns", "urn:xmpp:chat-markers:0"))
|
|
{
|
|
/* TODO: Use stanza ID if possible */
|
|
char *latest = NULL;
|
|
decode_from = ParseeLookupJID(from);
|
|
from_matrix = ParseeEncodeJID(args->config, decode_from, true);
|
|
mroom_id = ParseeGetBridgedRoom(args, stanza);
|
|
|
|
latest = ParseeLookupHead(mroom_id);
|
|
|
|
ASPresence(args->config, from_matrix, mroom_id, latest);
|
|
|
|
Free(decode_from);
|
|
Free(from_matrix);
|
|
Free(latest);
|
|
Free(mroom_id);
|
|
mroom_id = NULL;
|
|
decode_from = NULL;
|
|
from_matrix = NULL;
|
|
|
|
}
|
|
#undef CHAT_STATES
|
|
body = XMLookForUnique(stanza, "body");
|
|
if (!body)
|
|
{
|
|
XMLFreeElement(stanza);
|
|
return false;
|
|
}
|
|
|
|
/* 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);
|
|
data = ArrayGet(body->children, 0);
|
|
|
|
/* TODO: THIS IS A HACK. THIS CODE IGNORES ALL MUC MESSAGES EVER
|
|
* SENT TO NON-PARSEE PUPPETS, AS A "FIX" TO THE MULTITHREADED
|
|
* ISSUE.
|
|
*
|
|
* I HATE THIS. I NEED TO FIND A BETTER WAY. */
|
|
if (!room)
|
|
{
|
|
if (strncmp(HashMapGet(stanza->attrs, "to"), "parsee@", 7))
|
|
{
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
mroom_id = ParseeGetBridgedRoom(args, stanza);
|
|
if (mroom_id && !XMPPIsParseeStanza(stanza))
|
|
{
|
|
char *res = ParseeGetResource(from);
|
|
char *encoded = ParseeEncodeJID(args->config, decode_from, false);
|
|
char *event_id = NULL;
|
|
char *replaced = XMPPGetReplacedID(stanza);
|
|
char *reply_to = XMPPGetReply(stanza);
|
|
bool chat = false;
|
|
|
|
if (StrEquals(HashMapGet(stanza->attrs, "type"), "chat"))
|
|
{
|
|
Free(encoded);
|
|
encoded = StrDuplicate(from_matrix);
|
|
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)
|
|
{
|
|
XMLElement *oob, *oob_data;
|
|
|
|
pthread_mutex_unlock(&thr->info->chk_lock);
|
|
ASRegisterUser(args->config, encoded);
|
|
if (!chat)
|
|
{
|
|
ASSetName(args->config, encoded, res);
|
|
}
|
|
ASInvite(args->config, mroom_id, encoded);
|
|
Free(ASJoin(args->config, mroom_id, encoded));
|
|
|
|
/* Check if it is a media link */
|
|
oob = XMLookForTKV(stanza, "x", "xmlns", "jabber:x:oob");
|
|
reactions = XMLookForTKV(stanza,
|
|
"reactions", "xmlns", "urn:xmpp:reactions:0"
|
|
);
|
|
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);
|
|
|
|
event_id = ASSend(
|
|
args->config, mroom_id, encoded,
|
|
"m.room.message", content
|
|
);
|
|
Free(mime);
|
|
Free(mxc);
|
|
}
|
|
else if (reactions)
|
|
{
|
|
Array *react_child = reactions->children;
|
|
size_t reacts = ArraySize(react_child);
|
|
event_id = ParseeGetReactedEvent(args, stanza);
|
|
for (i = 0; i < reacts; i++)
|
|
{
|
|
XMLElement *reaction, *react_data;
|
|
reaction = ArrayGet(react_child, i);
|
|
react_data = ArrayGet(reaction->children, 0);
|
|
|
|
/* TODO: We should manage removed reactions. */
|
|
Free(ASSend(
|
|
args->config, mroom_id, encoded,
|
|
"m.reaction",
|
|
MatrixCreateReact(event_id, react_data->data)
|
|
));
|
|
}
|
|
Free(event_id);
|
|
event_id = NULL;
|
|
}
|
|
else
|
|
{
|
|
/* TODO: Use HTML-formatted bodies, and respect the fallback
|
|
* trims the stanza provides us if possible. Element does
|
|
* not like raw bodies on replies too. Go figure. */
|
|
size_t off =
|
|
reply_to ? ParseeFindDatastart(data->data) : 0;
|
|
HashMap *ev = MatrixCreateMessage(data->data + off);
|
|
if (reply_to)
|
|
{
|
|
char *reply_id = ParseeGetEventFromID(args, stanza, reply_to);
|
|
MatrixSetReply(ev, reply_id);
|
|
Free(reply_id);
|
|
}
|
|
event_id = ASSend(
|
|
args->config, mroom_id, encoded,
|
|
"m.room.message", ev
|
|
);
|
|
}
|
|
pthread_mutex_lock(&thr->info->chk_lock);
|
|
ParseePushAllStanza(args, stanza, event_id);
|
|
Free(event_id);
|
|
pthread_mutex_unlock(&thr->info->chk_lock);
|
|
}
|
|
else if (replaced)
|
|
{
|
|
event_id = ParseeGetEventFromID(args, stanza, replaced);
|
|
Free(ASSend(
|
|
args->config, mroom_id, encoded,
|
|
"m.room.message", MatrixCreateReplace(event_id, data->data)
|
|
));
|
|
ParseePushAllStanza(args, stanza, event_id);
|
|
pthread_mutex_unlock(&thr->info->chk_lock);
|
|
|
|
Free(event_id);
|
|
}
|
|
else
|
|
{
|
|
pthread_mutex_unlock(&thr->info->chk_lock);
|
|
}
|
|
Free(res);
|
|
Free(encoded);
|
|
}
|
|
else
|
|
{
|
|
XMLElement *parsee = XMLookForUnique(stanza, "x-parsee");
|
|
XMLElement *event = XMLookForUnique(parsee, "event-id");
|
|
XMLElement *e_d = ArrayGet(event ? event->children : NULL, 0);
|
|
|
|
if (e_d)
|
|
{
|
|
ParseePushAllStanza(args, stanza, e_d->data);
|
|
}
|
|
}
|
|
end:
|
|
Free(mroom_id);
|
|
mroom_id = NULL;
|
|
Free(from_matrix);
|
|
Free(decode_from);
|
|
Free(room);
|
|
Free(to);
|
|
|
|
return true;
|
|
}
|
|
|
|
#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)
|
|
{
|
|
char *from, *to, *id;
|
|
XMLElement *iq_reply, *query;
|
|
|
|
from = HashMapGet(stanza->attrs, "from");
|
|
to = HashMapGet(stanza->attrs, "to");
|
|
id = HashMapGet(stanza->attrs, "id");
|
|
|
|
/* Generate an IQ reply with discovery information */
|
|
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 = 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);
|
|
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)
|
|
{
|
|
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;
|
|
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 for a size limit. */
|
|
bdata = (char *) 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);
|
|
/* TODO: Use the nickname for something.
|
|
* And the rest of the vCard, somewhere. */
|
|
(void) data;
|
|
}
|
|
if (photo)
|
|
{
|
|
XMLElement *binval = XMLookForUnique(photo, "BINVAL");
|
|
XMLElement *data = ArrayGet(binval->children, 0);
|
|
char *base64;
|
|
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)
|
|
{
|
|
XMPPComponent *jabber = args->jabber;
|
|
if (XMLookForTKV(stanza, "query", "xmlns", DISCO))
|
|
{
|
|
IQDiscoGet(args, jabber, stanza);
|
|
}
|
|
else if (XMLookForTKV(stanza, "query", "xmlns", "jabber:iq:version"))
|
|
{
|
|
XMLElement *iq_reply, *query;
|
|
XMLElement *name, *version;
|
|
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);
|
|
StreamFlush(jabber->stream);
|
|
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());
|
|
}
|
|
|
|
}
|
|
static void
|
|
IQError(ParseeData *args, XMLElement *stanza)
|
|
{
|
|
/* TODO */
|
|
}
|
|
#undef DISCO
|
|
static void
|
|
IQStanza(ParseeData *args, XMLElement *stanza)
|
|
{
|
|
char *type;
|
|
type = HashMapGet(stanza->attrs, "type");
|
|
#define OnType(ctyp, callback) do \
|
|
{ \
|
|
if (StrEquals(type, #ctyp)) \
|
|
{ \
|
|
callback(args, stanza); \
|
|
return; \
|
|
} \
|
|
} \
|
|
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)
|
|
{
|
|
#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 *from;
|
|
char *type = HashMapGet(stanza->attrs, "type");
|
|
|
|
if (StrEquals(type, "unavailable"))
|
|
{
|
|
/* TODO: Treat as a ban if the role is outcast.
|
|
* Modify the code to be more accurate later. */
|
|
char *room = ParseeGetBridgedRoom(args, stanza);
|
|
char *decode_from = ParseeLookupJID(oid);
|
|
char *from_matrix = ParseeDecodeMXID(decode_from);
|
|
char *affiliation = HashMapGet(item->attrs, "affiliation");
|
|
|
|
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;
|
|
XMLElement *vcard_request;
|
|
XMPPComponent *jabber = args->jabber;
|
|
char *from;
|
|
|
|
DbRef *avatars;
|
|
HashMap *json;
|
|
char *avatar_id;
|
|
|
|
if (!p_dat)
|
|
{
|
|
return;
|
|
}
|
|
|
|
avatars = DbLock(args->db, 1, "avatars");
|
|
if (!avatars)
|
|
{
|
|
avatars = DbCreate(args->db, 1, "avatars");
|
|
}
|
|
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
|
|
}
|
|
|
|
static XMLElement *
|
|
RetrieveStanza(XMPPThread *thread)
|
|
{
|
|
XMLElement *ret = NULL;
|
|
|
|
pthread_mutex_lock(&thread->info->lock);
|
|
ret = ArrayDelete(thread->info->stanzas, 0);
|
|
pthread_mutex_unlock(&thread->info->lock);
|
|
|
|
return ret;
|
|
}
|
|
static void
|
|
PushStanza(XMPPThreadInfo *info, XMLElement *stanza)
|
|
{
|
|
pthread_mutex_lock(&info->lock);
|
|
ArrayAdd(info->stanzas, stanza);
|
|
pthread_mutex_unlock(&info->lock);
|
|
}
|
|
|
|
static void *
|
|
XMPPDispatcher(void *argp)
|
|
{
|
|
XMPPThread *thread = argp;
|
|
ParseeData *args = thread->info->args;
|
|
|
|
while (thread->info->running)
|
|
{
|
|
XMLElement *stanza = RetrieveStanza(thread);
|
|
|
|
if (!stanza)
|
|
{
|
|
UtilSleepMillis(1);
|
|
continue;
|
|
}
|
|
|
|
if (StrEquals(stanza->name, "presence"))
|
|
{
|
|
PresenceStanza(args, stanza);
|
|
XMLFreeElement(stanza);
|
|
continue;
|
|
}
|
|
else if (StrEquals(stanza->name, "message"))
|
|
{
|
|
if (!MessageStanza(args, stanza, thread))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (StrEquals(stanza->name, "iq"))
|
|
{
|
|
IQStanza(args, stanza);
|
|
}
|
|
else
|
|
{
|
|
Log(LOG_WARNING, "Unknown stanza '%s':", stanza->name);
|
|
XMLEncode(StreamStdout(), stanza);
|
|
StreamPrintf(StreamStdout(), "\n");
|
|
StreamFlush(StreamStdout());
|
|
}
|
|
XMLFreeElement(stanza);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
typedef struct XMPPAwait {
|
|
pthread_mutex_t cond_lock;
|
|
pthread_cond_t condition;
|
|
|
|
XMLElement *stanza;
|
|
} XMPPAwait;
|
|
static pthread_mutex_t await_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
static HashMap *await_table = NULL;
|
|
|
|
void *
|
|
ParseeXMPPThread(void *argp)
|
|
{
|
|
ParseeData *args = argp;
|
|
XMPPComponent *jabber = args->jabber;
|
|
XMLElement *stanza = NULL;
|
|
XMPPThreadInfo info;
|
|
size_t i;
|
|
|
|
/* Initialise the await table */
|
|
await_table = HashMapCreate();
|
|
|
|
/* Initialise the FIFO */
|
|
info.stanzas = ArrayCreate();
|
|
pthread_mutex_init(&info.lock, NULL);
|
|
|
|
/* TODO: Make that configurable. */
|
|
info.available_dispatchers = 8;
|
|
info.dispatchers = Malloc(
|
|
sizeof(*info.dispatchers) * info.available_dispatchers
|
|
);
|
|
|
|
info.args = args;
|
|
info.jabber = jabber;
|
|
info.running = true;
|
|
pthread_mutex_init(&info.chk_lock, NULL);
|
|
|
|
for (i = 0; i < info.available_dispatchers; i++)
|
|
{
|
|
pthread_t *thr = &info.dispatchers[i].thr;
|
|
info.dispatchers[i].info = &info;
|
|
|
|
pthread_create(thr, NULL, XMPPDispatcher, &info.dispatchers[i]);
|
|
}
|
|
|
|
|
|
while (true)
|
|
{
|
|
char *id;
|
|
|
|
stanza = XMLDecode(jabber->stream, false);
|
|
if (!stanza)
|
|
{
|
|
break;
|
|
}
|
|
|
|
id = HashMapGet(stanza->attrs, "id");
|
|
if (id)
|
|
{
|
|
XMPPAwait *await;
|
|
/* Lock out the table to see if we're awaiting. */
|
|
pthread_mutex_lock(&await_lock);
|
|
if ((await = HashMapGet(await_table, id)))
|
|
{
|
|
await->stanza = stanza;
|
|
pthread_mutex_lock(&await->cond_lock);
|
|
pthread_cond_signal(&await->condition);
|
|
pthread_mutex_unlock(&await->cond_lock);
|
|
|
|
HashMapDelete(await_table, id);
|
|
|
|
pthread_mutex_unlock(&await_lock);
|
|
continue;
|
|
}
|
|
pthread_mutex_unlock(&await_lock);
|
|
}
|
|
|
|
PushStanza(&info, stanza);
|
|
}
|
|
|
|
info.running = false;
|
|
for (i = 0; i < info.available_dispatchers; i++)
|
|
{
|
|
pthread_t thr = info.dispatchers[i].thr;
|
|
pthread_join(thr, NULL);
|
|
}
|
|
Free(info.dispatchers);
|
|
|
|
for (i = 0; i < ArraySize(info.stanzas); i++)
|
|
{
|
|
XMLFreeElement(ArrayGet(info.stanzas, i));
|
|
}
|
|
ArrayFree(info.stanzas);
|
|
|
|
HashMapFree(await_table);
|
|
|
|
pthread_mutex_destroy(&info.lock);
|
|
return NULL;
|
|
}
|
|
XMLElement *
|
|
ParseeAwaitStanza(char *identifier)
|
|
{
|
|
XMPPAwait awa, *await;
|
|
XMLElement *stanza;
|
|
if (!identifier)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* TODO: Pthreads HATE me using Malloc here, so I'm abusing stackspace.
|
|
* Not *too much* of a problem, just a weird oddity. If anyone has a clue
|
|
* on why that happens (at least on ARM64 with a Pi4 running Debian), let
|
|
* me know! */
|
|
await = &awa;
|
|
pthread_mutex_lock(&await_lock);
|
|
|
|
pthread_cond_init(&await->condition, NULL);
|
|
pthread_mutex_init(&await->cond_lock, NULL);
|
|
await->stanza = NULL;
|
|
|
|
HashMapSet(await_table, identifier, await);
|
|
pthread_mutex_unlock(&await_lock);
|
|
|
|
pthread_mutex_lock(&await->cond_lock);
|
|
while (!await->stanza)
|
|
{
|
|
pthread_cond_wait(&await->condition, &await->cond_lock);
|
|
}
|
|
|
|
stanza = await->stanza;
|
|
pthread_mutex_lock(&await_lock);
|
|
pthread_mutex_unlock(&await_lock);
|
|
|
|
pthread_mutex_unlock(&await->cond_lock);
|
|
|
|
pthread_cond_destroy(&await->condition);
|
|
pthread_mutex_destroy(&await->cond_lock);
|
|
return stanza;
|
|
}
|