mirror of
https://forge.fsky.io/lda/Parsee.git
synced 2026-03-13 12:15:12 +00:00
659 lines
19 KiB
C
659 lines
19 KiB
C
#include "XMPPThread/internal.h"
|
|
|
|
#include <Cytoplasm/Memory.h>
|
|
#include <Cytoplasm/Base64.h>
|
|
#include <Cytoplasm/Str.h>
|
|
#include <Cytoplasm/Log.h>
|
|
|
|
#include <StringStream.h>
|
|
#include <Matrix.h>
|
|
#include <AS.h>
|
|
|
|
#include <sys/utsname.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
static XMLElement *
|
|
CreateTagWithText(const char *tn, char *text)
|
|
{
|
|
XMLElement *tag = XMLCreateTag((char *) tn);
|
|
XMLElement *tex = XMLCreateText(text);
|
|
|
|
XMLAddChild(tag, tex);
|
|
|
|
return tag;
|
|
}
|
|
|
|
char *
|
|
TrimBase64(char *b64)
|
|
{
|
|
char *ret, *tmp;
|
|
|
|
ret = NULL;
|
|
while (*b64)
|
|
{
|
|
char ch[2] = { *b64, 0 };
|
|
if (isspace((int) *b64))
|
|
{
|
|
b64++;
|
|
continue;
|
|
}
|
|
|
|
tmp = ret;
|
|
ret = StrConcat(2, ret, ch);
|
|
Free(tmp);
|
|
b64++;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static bool
|
|
AvatarGrab(ParseeData *data, char *mxc, char **mime, char **b64, size_t *len)
|
|
{
|
|
char *mimei = NULL, *outi = NULL, *b64i = NULL;
|
|
size_t sizei;
|
|
if (!data || !mxc || !mime || !b64 || !len)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!ASGrab(data->config, mxc, &mimei, &outi, &sizei))
|
|
{
|
|
Free(mimei);
|
|
Free(outi);
|
|
return false;
|
|
}
|
|
|
|
b64i = Base64Encode(outi, sizei);
|
|
Free(outi);
|
|
if (!b64i)
|
|
{
|
|
Free(mimei);
|
|
b64i = StrDuplicate(media_unknown); /* TODO: Different assets! */
|
|
mimei = StrDuplicate("image/png");
|
|
}
|
|
|
|
*mime = mimei;
|
|
*b64 = b64i;
|
|
*len = sizei;
|
|
return true;
|
|
}
|
|
|
|
XMLElement *
|
|
GenerateAvatarData(ParseeData *data, char *mxid)
|
|
{
|
|
char *mxc = NULL, *mime = NULL, *b64 = NULL;
|
|
XMLElement *elem = NULL, *type, *binval;
|
|
size_t len = 0;
|
|
if (!data || !mxid)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* TODO: Use the right room for the avatar! */
|
|
mxc = ASGetAvatar(data->config, NULL, mxid);
|
|
if (!mxc || !AvatarGrab(data, mxc, &mime, &b64, &len))
|
|
{
|
|
goto end;
|
|
}
|
|
|
|
elem = XMLCreateTag("PHOTO");
|
|
type = XMLCreateTag("TYPE");
|
|
binval = XMLCreateTag("BINVAL");
|
|
|
|
XMLAddChild(type, XMLCreateText(mime));
|
|
XMLAddChild(binval, XMLCreateText(b64));
|
|
|
|
XMLAddChild(elem, type);
|
|
XMLAddChild(elem, binval);
|
|
|
|
end:
|
|
Free(mime);
|
|
Free(mxc);
|
|
Free(b64);
|
|
return elem;
|
|
}
|
|
|
|
#define DISCO "http://jabber.org/protocol/disco#info"
|
|
static XMLElement *
|
|
IQGenerateQuery(IQFeatures *features)
|
|
{
|
|
XMLElement *query;
|
|
if (!features)
|
|
{
|
|
return NULL;
|
|
}
|
|
query = XMLCreateTag("query");
|
|
XMLAddAttr(query, "xmlns", DISCO);
|
|
{
|
|
XMLElement *feature;
|
|
size_t i;
|
|
for (i = 0; i < ArraySize(features->identity); i++)
|
|
{
|
|
XMPPIdentity *identity = ArrayGet(features->identity, i);
|
|
|
|
feature = XMLCreateTag("identity");
|
|
XMLAddAttr(feature, "category", identity->category);
|
|
XMLAddAttr(feature, "type", identity->type);
|
|
XMLAddAttr(feature, "name", identity->name);
|
|
|
|
XMLAddChild(query, feature);
|
|
}
|
|
for (i = 0; i < ArraySize(features->adverts); i++)
|
|
{
|
|
char *var = ArrayGet(features->adverts, i);
|
|
|
|
feature = XMLCreateTag("feature");
|
|
XMLAddAttr(feature, "var", var);
|
|
XMLAddChild(query, feature);
|
|
}
|
|
}
|
|
|
|
return query;
|
|
}
|
|
void
|
|
IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza)
|
|
{
|
|
char *from, *to, *id;
|
|
XMLElement *iq_reply, *query;
|
|
IQFeatures *features;
|
|
|
|
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);
|
|
|
|
features = LookupJIDFeatures(to);
|
|
query = IQGenerateQuery(features);
|
|
{
|
|
char *ver = XMPPGenerateVer(features);
|
|
char *node = StrConcat(3, REPOSITORY, "#", ver);
|
|
XMLAddAttr(query, "node", node);
|
|
|
|
Free(node);
|
|
Free(ver);
|
|
}
|
|
XMLAddChild(iq_reply, query);
|
|
FreeIQFeatures(features);
|
|
|
|
XMPPSendStanza(jabber, iq_reply, args->config->max_stanza_size);
|
|
XMLFreeElement(iq_reply);
|
|
|
|
(void) args;
|
|
}
|
|
|
|
void
|
|
IQResult(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
|
{
|
|
XMLElement *vcard = XMLookForTKV(stanza, "vCard", "xmlns", "vcard-temp");
|
|
|
|
/* TODO: Move this to PEP */
|
|
XMLElement *event = XMLookForTKV(stanza, "pubsub",
|
|
"xmlns", "http://jabber.org/protocol/pubsub"
|
|
);
|
|
|
|
(void) thr;
|
|
|
|
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;
|
|
}
|
|
JsonValueFree(JsonSet(
|
|
json, JsonValueString(id),
|
|
1, from
|
|
));
|
|
DbUnlock(args->db, avatars);
|
|
|
|
base64 = TrimBase64(data->data);
|
|
b64len = base64 ? strlen(base64) : 0;
|
|
length = Base64DecodedSize(base64, b64len);
|
|
|
|
bdata = (char *) Base64Decode(base64, b64len);
|
|
datastream = StrStreamReaderN(bdata, length);
|
|
|
|
/* XEP-0084 effectively assumes an image/png MIME. */
|
|
mxc = ASUpload(args->config, datastream, length, "image/png");
|
|
|
|
jid = ParseeLookupJID(from);
|
|
from_matrix = ParseeGetBridgedUser(args, stanza);
|
|
Log(LOG_DEBUG,
|
|
"Setting the avatar of '%s' to %s",
|
|
from_matrix, mxc
|
|
);
|
|
ASSetAvatar(args->config, from_matrix, mxc);
|
|
|
|
Free(mxc);
|
|
Free(jid);
|
|
Free(bdata);
|
|
Free(from_matrix);
|
|
Free(base64);
|
|
StreamClose(datastream);
|
|
}
|
|
|
|
}
|
|
}
|
|
if (vcard)
|
|
{
|
|
XMLElement *photo = XMLookForUnique(vcard, "PHOTO");
|
|
|
|
if (photo)
|
|
{
|
|
XMLElement *binval = XMLookForUnique(photo, "BINVAL");
|
|
XMLElement *type = XMLookForUnique(photo, "TYPE");
|
|
XMLElement *data =
|
|
binval ? ArrayGet(binval->children, 0) : NULL;
|
|
XMLElement *tdata = type ? ArrayGet(type->children, 0) : NULL;
|
|
char *base64;
|
|
char *bdata;
|
|
size_t length, b64len;
|
|
Stream *datastream;
|
|
char *mxc = NULL, *from_matrix = NULL, *jid = NULL;
|
|
char *room = NULL, *resource;
|
|
char *from = HashMapGet(stanza->attrs, "from");
|
|
if (!data || !data->data)
|
|
{
|
|
Log(LOG_ERR, "%s NOT FOUND", HashMapGet(stanza->attrs, "from"));
|
|
return;
|
|
}
|
|
|
|
/* Get the base64, decode it, and shove it in a nicely put
|
|
* MXC address */
|
|
base64 = TrimBase64(data->data);
|
|
b64len = base64 ? strlen(base64) : 0;
|
|
length = Base64DecodedSize(base64, b64len);
|
|
|
|
bdata = Base64Decode(base64, b64len);
|
|
datastream = StrStreamReaderN(bdata, length);
|
|
Log(LOG_DEBUG, "(%dB @ %p) = %p (%d)", b64len, base64, bdata, length);
|
|
mxc = ASUpload(
|
|
args->config,
|
|
datastream,
|
|
length,
|
|
tdata ? tdata->data : NULL
|
|
);
|
|
Free(bdata);
|
|
Free(base64);
|
|
StreamClose(datastream);
|
|
|
|
room = ParseeGetBridgedRoom(args, stanza);
|
|
jid = ParseeLookupJID(HashMapGet(stanza->attrs, "from"));
|
|
|
|
resource = ParseeTrimJID(from);
|
|
|
|
/* TODO: More reliable system for telling the difference appart */
|
|
if (jid && !StrEquals(from, resource))
|
|
{
|
|
char *oid = ParseeLookupOID(jid);
|
|
char *hsh = ParseeSHA256(oid);
|
|
bool scrambled =
|
|
oid && StrEquals(jid, HashMapGet(stanza->attrs, "from"));
|
|
from_matrix = scrambled
|
|
? ScrambleOID(args, oid)
|
|
: ParseeGetBridgedUser(args, stanza);
|
|
|
|
Log(LOG_DEBUG,
|
|
"Setting the vCard of '%s'(%s) to %s (%d B)",
|
|
from_matrix, jid, mxc,
|
|
length
|
|
);
|
|
Log(LOG_DEBUG,
|
|
"OID=%s[%s]",
|
|
oid, hsh
|
|
);
|
|
|
|
ASSetAvatar(args->config, from_matrix, mxc);
|
|
Free(oid);
|
|
Free(hsh);
|
|
}
|
|
else if (room)
|
|
{
|
|
char *mask = ParseeMXID(args);
|
|
HashMap *obj = HashMapCreate();
|
|
|
|
HashMapSet(obj, "url", JsonValueString(mxc));
|
|
ASSetState(
|
|
args->config,
|
|
room, "m.room.avatar", "",
|
|
mask, obj
|
|
);
|
|
Free(mask);
|
|
}
|
|
|
|
Free(from_matrix);
|
|
Free(resource);
|
|
Free(room);
|
|
Free(jid);
|
|
Free(mxc);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
IQIsCommandList(ParseeData *args, XMLElement *stanza)
|
|
{
|
|
char *parsee = NULL, *to;
|
|
XMLElement *query = XMLookForTKV(
|
|
stanza, "query", "xmlns",
|
|
"http://jabber.org/protocol/disco#items"
|
|
);
|
|
bool ret = false;
|
|
|
|
if (!query ||
|
|
!StrEquals(HashMapGet(query->attrs, "node"),
|
|
"http://jabber.org/protocol/commands"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
parsee = ParseeJID(args);
|
|
to = HashMapGet(stanza->attrs, "to");
|
|
ret = StrEquals(to, parsee) || StrEquals(to, args->config->component_host);
|
|
Free(parsee);
|
|
|
|
return ret;
|
|
}
|
|
static bool
|
|
IsInMUC(ParseeData *data, char *jid)
|
|
{
|
|
char *trimmed = ParseeTrimJID(jid);
|
|
char *chat_id = ParseeGetFromMUCID(data, trimmed);
|
|
|
|
Free(trimmed);
|
|
Free(chat_id);
|
|
return !!chat_id;
|
|
}
|
|
void
|
|
IQGet(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
|
{
|
|
XMPPComponent *jabber = args->jabber;
|
|
XMLElement *pubsub;
|
|
char *from = HashMapGet(stanza->attrs, "from");
|
|
char *to = HashMapGet(stanza->attrs, "to");
|
|
char *id = HashMapGet(stanza->attrs, "id");
|
|
|
|
(void) thr;
|
|
|
|
if (IQIsCommandList(args, stanza))
|
|
{
|
|
XMLElement *iq_reply = XMLCreateTag("iq");
|
|
char *trimmed = ParseeTrimJID(from);
|
|
|
|
XMLAddAttr(iq_reply, "type", "result");
|
|
XMLAddAttr(iq_reply, "from", to);
|
|
XMLAddAttr(iq_reply, "to", from);
|
|
XMLAddAttr(iq_reply, "id", id);
|
|
{
|
|
XMLElement *q = XMLCreateTag("query");
|
|
char *parsee_muc_jid = StrConcat(3, trimmed, "/", "parsee");
|
|
XMLAddAttr(q, "xmlns", "http://jabber.org/protocol/disco#items");
|
|
XMLAddAttr(q, "node", "http://jabber.org/protocol/commands");
|
|
XMPPShoveCommandList(thr->info->m,
|
|
IsInMUC(args, from) ? parsee_muc_jid : to,
|
|
q, stanza
|
|
);
|
|
XMLAddChild(iq_reply, q);
|
|
Free(parsee_muc_jid);
|
|
}
|
|
XMPPSendStanza(jabber, iq_reply, args->config->max_stanza_size);
|
|
XMLFreeElement(iq_reply);
|
|
Free(trimmed);
|
|
}
|
|
else if (XMLookForTKV(stanza, "vCard", "xmlns", "vcard-temp"))
|
|
{
|
|
char *to_matrix = ParseeGetBridgedUser(args, stanza);
|
|
char *name = ASGetName(args->config, NULL, to_matrix);
|
|
XMLElement *iqVCard;
|
|
Log(LOG_DEBUG, "vCard information GET for %s (%s)", to, to_matrix);
|
|
|
|
if (!strncmp(to, "parsee@", 7))
|
|
{
|
|
iqVCard = XMLCreateTag("iq");
|
|
XMLAddAttr(iqVCard, "from", to);
|
|
XMLAddAttr(iqVCard, "to", from);
|
|
XMLAddAttr(iqVCard, "id", id);
|
|
XMLAddAttr(iqVCard, "type", "result");
|
|
{
|
|
XMLElement *vCard = XMLCreateTag("vCard");
|
|
XMLAddAttr(vCard, "xmlns", "vcard-temp");
|
|
{
|
|
XMLElement *fn = CreateTagWithText(
|
|
"FN", "Parsee Mizuhashi"
|
|
);
|
|
XMLElement *nick = CreateTagWithText(
|
|
"NICKNAME", "Parsee"
|
|
);
|
|
XMLElement *url = CreateTagWithText(
|
|
"URL", REPOSITORY
|
|
);
|
|
/* TODO: Maybe abstract the vCard code. */
|
|
XMLAddChild(vCard, nick);
|
|
XMLAddChild(vCard, url);
|
|
XMLAddChild(vCard, fn);
|
|
}
|
|
XMLAddChild(iqVCard, vCard);
|
|
}
|
|
|
|
XMPPSendStanza(jabber, iqVCard, args->config->max_stanza_size);
|
|
XMLFreeElement(iqVCard);
|
|
Free(to_matrix);
|
|
Free(name);
|
|
return;
|
|
}
|
|
|
|
Free(to_matrix);
|
|
to_matrix = ParseeDecodeMXID(to);
|
|
|
|
iqVCard = XMLCreateTag("iq");
|
|
XMLAddAttr(iqVCard, "from", to);
|
|
XMLAddAttr(iqVCard, "to", from);
|
|
XMLAddAttr(iqVCard, "id", id);
|
|
XMLAddAttr(iqVCard, "type", "result");
|
|
{
|
|
XMLElement *vCard = XMLCreateTag("vCard");
|
|
char *mto_link = ParseeGenerateMTO(to_matrix);
|
|
XMLAddAttr(vCard, "xmlns", "vcard-temp");
|
|
{
|
|
XMLAddChild(vCard, GenerateAvatarData(args, to_matrix));
|
|
|
|
Free(mto_link);
|
|
}
|
|
XMLAddChild(iqVCard, vCard);
|
|
}
|
|
|
|
XMPPSendStanza(jabber, iqVCard, args->config->max_stanza_size);
|
|
XMLFreeElement(iqVCard);
|
|
Free(to_matrix);
|
|
Free(name);
|
|
}
|
|
#define PS "http://jabber.org/protocol/pubsub"
|
|
else if ((pubsub = XMLookForTKV(stanza, "pubsub", "xmlns", PS)))
|
|
{
|
|
/* TODO: Pass this through the PEP manager */
|
|
XMLElement *a_items = XMLookForTKV(pubsub,
|
|
"items", "node", "urn:xmpp:avatar:data"
|
|
);
|
|
if (a_items)
|
|
{
|
|
/* Do, without regret, start shoving an avatar out the bus.
|
|
* NOTE: I explicitely choose to not do any manipulation
|
|
* because messing with random user images is inherently a
|
|
* risk I do *not* want to take. */
|
|
char *to_matrix = ParseeDecodeMXID(to);
|
|
char *avatar = ASGetAvatar(args->config, NULL, to_matrix);
|
|
char *mime = NULL;
|
|
char *b64 = NULL;
|
|
size_t len = 0;
|
|
XMLElement *reply;
|
|
|
|
AvatarGrab(args, avatar, &mime, &b64, &len);
|
|
|
|
Log(LOG_DEBUG, "IQ-GET: PUBSUB AVATAR OF=%s", to_matrix);
|
|
/* Strike back with a response */
|
|
reply = XMLCreateTag("iq");
|
|
XMLAddAttr(reply, "type", "result");
|
|
XMLAddAttr(reply, "to", from);
|
|
XMLAddAttr(reply, "from", to);
|
|
XMLAddAttr(reply, "id", HashMapGet(stanza->attrs, "id"));
|
|
{
|
|
XMLElement *ps = XMLCreateTag("pubsub");
|
|
XMLElement *items = XMLCreateTag("items");
|
|
XMLAddAttr(ps, "xmlns", PS);
|
|
XMLAddAttr(items, "node", "urn:xmpp:avatar:data");
|
|
{
|
|
XMLElement *item = XMLCreateTag("item");
|
|
XMLElement *data = XMLCreateTag("data");
|
|
XMLAddAttr(item, "id", "TODO");
|
|
XMLAddAttr(data, "xmlns", "urn:xmpp:avatar:data");
|
|
|
|
XMLAddChild(data, XMLCreateText(b64));
|
|
|
|
XMLAddChild(item, data);
|
|
XMLAddChild(items, item);
|
|
}
|
|
XMLAddChild(ps, items);
|
|
XMLAddChild(reply, ps);
|
|
}
|
|
|
|
XMPPSendStanza(jabber, reply, args->config->max_stanza_size);
|
|
XMLFreeElement(reply);
|
|
|
|
Free(to_matrix);
|
|
Free(avatar);
|
|
Free(mime);
|
|
Free(b64);
|
|
}
|
|
}
|
|
#undef PS
|
|
else 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, *os;
|
|
|
|
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");
|
|
{
|
|
struct utsname info;
|
|
name = XMLCreateTag("name");
|
|
version = XMLCreateTag("version");
|
|
os = XMLCreateTag("os");
|
|
uname(&info);
|
|
|
|
XMLAddChild(name, XMLCreateText(NAME));
|
|
XMLAddChild(version, XMLCreateText(VERSION "[" CODE "]"));
|
|
XMLAddChild(os, XMLCreateText(info.sysname));
|
|
}
|
|
XMLAddChild(query, name);
|
|
XMLAddChild(query, version);
|
|
XMLAddChild(query, os);
|
|
XMLAddChild(iq_reply, query);
|
|
|
|
XMPPSendStanza(jabber, iq_reply, args->config->max_stanza_size);
|
|
XMLFreeElement(iq_reply);
|
|
}
|
|
else
|
|
{
|
|
char *buf = NULL;
|
|
Stream *s = StrStreamWriter(&buf);
|
|
Log(LOG_WARNING, "Unknown I/Q received:");
|
|
XMLEncode(s, stanza);
|
|
StreamFlush(s);
|
|
StreamClose(s);
|
|
Log(LOG_WARNING, "%s", buf);
|
|
Free(buf);
|
|
}
|
|
|
|
}
|
|
void
|
|
IQError(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
|
{
|
|
/* TODO */
|
|
(void) args;
|
|
(void) stanza;
|
|
(void) thr;
|
|
}
|
|
void
|
|
IQSet(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
|
{
|
|
XMPPCommandManager *manager = thr->info->m;
|
|
XMPPManageCommand(manager, stanza, args);
|
|
}
|
|
#undef DISCO
|
|
|
|
void
|
|
IQStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
|
{
|
|
char *type = HashMapGet(stanza->attrs, "type");
|
|
|
|
if (PEPManagerHandle(thr->info->pep_manager, stanza))
|
|
{
|
|
return;
|
|
}
|
|
#define OnType(ctyp, callback) do \
|
|
{ \
|
|
if (StrEquals(type, #ctyp)) \
|
|
{ \
|
|
callback(args, stanza, thr); \
|
|
return; \
|
|
} \
|
|
} \
|
|
while (0)
|
|
|
|
OnType(get, IQGet);
|
|
OnType(set, IQSet);
|
|
OnType(error, IQError);
|
|
OnType(result, IQResult);
|
|
#undef OnType
|
|
}
|