[ADD/WIP] One-way chatstates, capabilities

No avatarwerk today because avatars are an absolute sore.
This commit is contained in:
LDA 2024-07-02 02:21:31 +02:00
commit 6b0f08c49e
10 changed files with 349 additions and 24 deletions

View file

@ -32,3 +32,10 @@ TODO
## DOCS ## DOCS
TODO TODO
## TODOS
- Look at XEPS-TBD.TXT for XEPs to be done
- Achievements
### Why?
> "[...] and it [BoVeX] has an achievement system, because another thing I don't like is when software will not acknowledge that you've reached an obscure error state, essentially outsmarting it."
- Tom Murphy VII

View file

@ -1,6 +1,8 @@
XEPs current supported are in src/XMPPThread.c, at the IQ disco advertising. XEPs current supported are in src/XMPPThread.c, at the IQ disco advertising.
Somewhat implemented XEPs: Somewhat implemented XEPs:
~ https://xmpp.org/extensions/xep-0085.html
Only XMPP->Matrix
~ https://xmpp.org/extensions/xep-0444.html ~ https://xmpp.org/extensions/xep-0444.html
This allows reactions, which Matrix also has support to. The two This allows reactions, which Matrix also has support to. The two
systems don't seem *too* restrictive on one-another (unlike some systems don't seem *too* restrictive on one-another (unlike some

View file

@ -602,3 +602,33 @@ ASReupload(const ParseeConfig *c, char *from, char **mime)
UriFree(uri); UriFree(uri);
return ret; return ret;
} }
void
ASType(const ParseeConfig *c, char *user, char *room, bool status)
{
HttpClientContext *ctx = NULL;
HashMap *json;
char *path, *full;
if (!c || !user || !room)
{
return;
}
user = HttpUrlEncode(user);
path = StrConcat(6,
"/_matrix/client/v3/rooms/",
room, "/typing/", user,
"?user_id=", user
);
json = HashMapCreate();
HashMapSet(json, "typing", JsonValueBoolean(status));
HashMapSet(json, "timeout", JsonValueBoolean(1 MINUTES));
ctx = ParseeCreateRequest(c, HTTP_PUT, path);
Free(path);
ASAuthenticateRequest(c, ctx);
ParseeSetRequestJSON(ctx, json);
JsonFree(json);
HttpClientContextFree(ctx);
Free(user);
}

View file

@ -107,9 +107,9 @@ ParseeCleanup(void *datp)
} \ } \
while (0) while (0)
CleanupField(stanza, 1 HOURS); CleanupField(stanza, 3 HOURS);
CleanupField(event, 1 HOURS); CleanupField(event, 3 HOURS);
CleanupField(id, 1 HOURS); CleanupField(id, 3 HOURS);
DbUnlock(data->db, ref); DbUnlock(data->db, ref);
} }

View file

@ -46,6 +46,7 @@ XMPPQueryMUC(XMPPComponent *jabber, char *muc, MUCInfo *out)
/* Except an IQ reply */ /* Except an IQ reply */
iq_query = XMLDecode(jabber->stream, false); iq_query = XMLDecode(jabber->stream, false);
/* TODO: I've spotted presence requests spawning there. */
if (!iq_query || !StrEquals(iq_query->name, "iq")) if (!iq_query || !StrEquals(iq_query->name, "iq"))
{ {
XMLFreeElement(iq_query); XMLFreeElement(iq_query);

View file

@ -175,6 +175,7 @@ XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc)
XMLAddAttr(x, "xmlns", "http://jabber.org/protocol/muc"); XMLAddAttr(x, "xmlns", "http://jabber.org/protocol/muc");
XMLAddChild(presence, x); XMLAddChild(presence, x);
XMPPAnnotatePresence(presence);
XMLEncode(comp->stream, presence); XMLEncode(comp->stream, presence);
StreamFlush(comp->stream); StreamFlush(comp->stream);
@ -279,3 +280,23 @@ XMPPGetReply(XMLElement *elem)
return HashMapGet(rep->attrs, "id"); return HashMapGet(rep->attrs, "id");
} }
void
XMPPAnnotatePresence(XMLElement *presence)
{
XMLElement *c;
char *ver;
if (!presence)
{
return;
}
ver = XMPPGenerateVer();
c = XMLCreateTag("c");
XMLAddAttr(c, "xmlns", "http://jabber.org/protocol/caps");
XMLAddAttr(c, "hash", "sha-1");
XMLAddAttr(c, "node", REPOSITORY);
XMLAddAttr(c, "ver", ver);
Free(ver);
XMLAddChild(presence, c);
}

View file

@ -4,8 +4,10 @@
#include <string.h> #include <string.h>
#include <Cytoplasm/Memory.h> #include <Cytoplasm/Memory.h>
#include <Cytoplasm/Base64.h>
#include <Cytoplasm/Log.h> #include <Cytoplasm/Log.h>
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h>
#include <Matrix.h> #include <Matrix.h>
#include <XMPP.h> #include <XMPP.h>
@ -13,15 +15,142 @@
#include <AS.h> #include <AS.h>
#define IQ_ADVERT \ #define IQ_ADVERT \
AdvertiseSimple("http://jabber.org/protocol/caps") \
AdvertiseSimple("http://jabber.org/protocol/chatstates") \ AdvertiseSimple("http://jabber.org/protocol/chatstates") \
AdvertiseSimple("urn:xmpp:message-correct:0") \ AdvertiseSimple("urn:xmpp:message-correct:0") \
AdvertiseSimple("urn:xmpp:reactions:0") \ AdvertiseSimple("urn:xmpp:reactions:0") \
AdvertiseSimple("urn:xmpp:styling:0") \ AdvertiseSimple("urn:xmpp:styling:0") \
AdvertiseSimple("urn:xmpp:reply:0") \ AdvertiseSimple("urn:xmpp:reply:0") \
AdvertiseSimple("jabber:x:oob") \ AdvertiseSimple("jabber:x:oob") \
AdvertiseSimple("vcard-temp") \
AdvertiseSimple("urn:xmpp:avatar:metadata+notify") \
AdvertiseSimple("jabber:iq:version") \
AdvertiseSimple("urn:parsee:x-parsee:0") \ AdvertiseSimple("urn:parsee:x-parsee:0") \
AdvertiseSimple("urn:parsee:jealousy: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(cata) : 0;
size_t bl = catb ? strlen(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 = ida->category;
unsigned char *catb = 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(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 pthread_mutex_t cond_var_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t cond_var_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER; static pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER;
@ -42,6 +171,8 @@ ParseeDMHandler(char *room, char *from, XMLElement *data, const ParseeConfig *c)
)); ));
} }
/* TODO: Clean up all of this. We are currently separating DMs from MUCs,
* where we could unify all our code, and generalise everything. */
static bool static bool
MessageStanza(ParseeData *args, XMLElement *stanza) MessageStanza(ParseeData *args, XMLElement *stanza)
{ {
@ -54,6 +185,39 @@ MessageStanza(ParseeData *args, XMLElement *stanza)
char *to, *room, *from, *from_matrix, *decode_from; char *to, *room, *from, *from_matrix, *decode_from;
char *chat_id, *mroom_id; char *chat_id, *mroom_id;
size_t i; size_t i;
from = HashMapGet(stanza->attrs, "from");
#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);
chat_id = ParseeGetFromMUCID(args, from);
mroom_id = ParseeGetRoomID(args, chat_id);
ASType(args->config, from_matrix, mroom_id, true);
Free(decode_from);
Free(from_matrix);
Free(mroom_id);
Free(chat_id);
}
else if (XMLookForTKV(stanza, "active", "xmlns", CHAT_STATES))
{
decode_from = ParseeLookupJID(from);
from_matrix = ParseeEncodeJID(args->config, decode_from, true);
chat_id = ParseeGetFromMUCID(args, from);
mroom_id = ParseeGetRoomID(args, chat_id);
ASType(args->config, from_matrix, mroom_id, false);
Free(decode_from);
Free(from_matrix);
Free(mroom_id);
Free(chat_id);
}
#undef CHAT_STATES
body = XMLookForUnique(stanza, "body"); body = XMLookForUnique(stanza, "body");
if (!body) if (!body)
{ {
@ -61,14 +225,13 @@ MessageStanza(ParseeData *args, XMLElement *stanza)
return false; return false;
} }
to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to"));
from = HashMapGet(stanza->attrs, "from");
/* TODO: On semi-anonymous MUCs, it might be preferable to use a /* 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 * form of the occupant ID as the base, as it is more unique, and
* less prone to trigger the character limit on Matrix. * less prone to trigger the character limit on Matrix.
* *
* See: https://xmpp.org/extensions/xep-0421.html */ * See: https://xmpp.org/extensions/xep-0421.html */
to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to"));
decode_from = ParseeLookupJID(from); decode_from = ParseeLookupJID(from);
from_matrix = ParseeEncodeJID(args->config, decode_from, true); from_matrix = ParseeEncodeJID(args->config, decode_from, true);
room = ParseeFindDMRoom(args, to, from); room = ParseeFindDMRoom(args, to, from);
@ -205,6 +368,39 @@ MessageStanza(ParseeData *args, XMLElement *stanza)
} }
#define DISCO "http://jabber.org/protocol/disco#info" #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 static void
IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza) IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza)
{ {
@ -221,25 +417,17 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza)
XMLAddAttr(iq_reply, "from", to); XMLAddAttr(iq_reply, "from", to);
XMLAddAttr(iq_reply, "type", "result"); XMLAddAttr(iq_reply, "type", "result");
XMLAddAttr(iq_reply, "id", id); XMLAddAttr(iq_reply, "id", id);
{
query = XMLCreateTag("query");
XMLAddAttr(query, "xmlns", DISCO);
{
XMLElement *feature;
#define AdvertiseSimple(f) do \
{ \
feature = XMLCreateTag("feature"); \
XMLAddAttr(feature, "var", f); \
XMLAddChild(query, feature); \
} \
while (0);
/* TODO: Advertise more things */ query = IQGenerateQuery();
IQ_ADVERT {
#undef AdvertiseSimple char *ver = XMPPGenerateVer();
char *node = StrConcat(3, REPOSITORY, "#", ver);
XMLAddAttr(query, "node", node);
Free(node);
Free(ver);
} }
XMLAddChild(iq_reply, query); XMLAddChild(iq_reply, query);
}
pthread_mutex_lock(&jabber->write_lock); pthread_mutex_lock(&jabber->write_lock);
XMLEncode(jabber->stream, iq_reply); XMLEncode(jabber->stream, iq_reply);
@ -255,6 +443,45 @@ IQGet(ParseeData *args, XMLElement *stanza)
{ {
IQDiscoGet(args, jabber, stanza); IQDiscoGet(args, jabber, stanza);
} }
else if (XMLookForTKV(stanza, "query", "xmlns", "jabber:iq:version"))
{
XMLElement *iq_reply, *query;
XMLElement *name, *version, *val;
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);
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());
}
} }
#undef DISCO #undef DISCO
@ -282,18 +509,41 @@ PresenceStanza(ParseeData *args, XMLElement *stanza)
{ {
#define MUC_USER_NS "http://jabber.org/protocol/muc#user" #define MUC_USER_NS "http://jabber.org/protocol/muc#user"
XMLElement *user_info; 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))) if ((user_info = XMLookForTKV(stanza, "x", "xmlns", MUC_USER_NS)))
{ {
XMLElement *item = XMLookForUnique(user_info, "item"); XMLElement *item = XMLookForUnique(user_info, "item");
char *jid = item ? HashMapGet(item->attrs, "jid") : NULL; char *jid = item ? HashMapGet(item->attrs, "jid") : NULL;
char *oid = HashMapGet(stanza->attrs, "from");
if (jid) if (jid)
{ {
ParseePushJIDTable(oid, jid); ParseePushJIDTable(oid, jid);
} }
} }
else if (vc)
{
XMLElement *photo = XMLookForUnique(vc, "photo");
XMLElement *p_dat = photo ? ArrayGet(photo->children, 0) : NULL;
char *room_id = NULL, *chat_id = NULL;
if (!p_dat)
{
return;
}
chat_id = ParseeGetFromMUCID(args, oid);
room_id = ParseeGetRoomID(args, chat_id);
if (!room_id)
{
Free(chat_id);
Free(room_id);
}
/* TODO: Get the media, and shove it to the room */
Free(chat_id);
Free(room_id);
}
#undef MUC_USER_NS #undef MUC_USER_NS
} }

View file

@ -38,6 +38,7 @@ extern HashMap * ASFind(const ParseeConfig *, char *, char *);
/* Sends a message event with a specific type and body. /* Sends a message event with a specific type and body.
* Said body is freed during the function's execution. */ * Said body is freed during the function's execution. */
extern char * ASSend(const ParseeConfig *, char *, char *, char *, HashMap *); extern char * ASSend(const ParseeConfig *, char *, char *, char *, HashMap *);
extern void ASType(const ParseeConfig *, char *, char *, bool);
/* Sets a state event with a specific type and body */ /* Sets a state event with a specific type and body */
extern void ASSetState(const ParseeConfig *conf, char *id, char *type, char *key, char *mask, HashMap *event); extern void ASSetState(const ParseeConfig *conf, char *id, char *type, char *key, char *mask, HashMap *event);

8
src/include/Bot.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef PARSEE_BOT_H
#define PARSEE_BOT_H
#include <Cytoplasm/HashMap.h>
#define BotInitialise() profile =
#endif

View file

@ -73,4 +73,9 @@ extern char * XMPPGetReplacedID(XMLElement *);
/* Get the replied-to stanza ID, if existent. */ /* Get the replied-to stanza ID, if existent. */
extern char * XMPPGetReply(XMLElement *elem); extern char * XMPPGetReply(XMLElement *elem);
/* Generate the B64-encoded SHA-256 hash for the 'ver' field in caps. */
extern char * XMPPGenerateVer(void);
/* Annotates a presence with https://xmpp.org/extensions/xep-0115.html */
extern void XMPPAnnotatePresence(XMLElement *presence);
#endif #endif