[ADD/WIP] Start having MUCwork half-decent

Still lots of things required(like using the users JIDs whenever
possible, otherwise dropping to occupant ID), images, replies, rich data
with HTML and whatever XMPP has, etc...
This commit is contained in:
LDA 2024-06-21 18:31:43 +02:00
commit a84ce05b9d
12 changed files with 329 additions and 40 deletions

View file

@ -251,3 +251,32 @@ ASCreateRoom(const ParseeConfig *conf, char *by, char *alias)
return id; return id;
} }
void
ASSetName(const ParseeConfig *conf, char *user, char *name)
{
HttpClientContext *ctx = NULL;
HashMap *json;
char *path;
if (!conf || !user || !name)
{
return;
}
user = HttpUrlEncode(user);
path = StrConcat(6,
"/_matrix/client/v3/profile/",
user, "/displayname", "?",
"user_id=", user
);
json = HashMapCreate();
HashMapSet(json, "displayname", JsonValueString(name));
ctx = ParseeCreateRequest(conf, HTTP_PUT, path);
Free(path);
ASAuthenticateRequest(conf, ctx);
ParseeSetRequestJSON(ctx, json);
HttpClientContextFree(ctx);
JsonFree(json);
Free(user);
}

View file

@ -45,3 +45,19 @@ MatrixCreateNameState(char *name)
return map; return map;
} }
HashMap *
MatrixCreateNickChange(char *nick)
{
HashMap *map;
if (!nick)
{
return NULL;
}
map = HashMapCreate();
HashMapSet(map, "displayname", JsonValueString(nick));
HashMapSet(map, "membership", JsonValueString("join"));
return map;
}

View file

@ -57,6 +57,7 @@ Main(void)
"%s - v%s (Cytoplasm %s)", "%s - v%s (Cytoplasm %s)",
NAME, VERSION, CytoplasmGetVersionStr() NAME, VERSION, CytoplasmGetVersionStr()
); );
Log(LOG_INFO, "=======================");
LogConfigIndent(LogConfigGlobal()); LogConfigIndent(LogConfigGlobal());
ParseeConfigLoad("parsee.json"); ParseeConfigLoad("parsee.json");
@ -102,6 +103,11 @@ Main(void)
goto end; goto end;
} }
Log(LOG_NOTICE, "Listening to MUCs...");
LogConfigIndent(LogConfigGlobal());
ParseeSendPresence(conf.handlerArgs);
LogConfigUnindent(LogConfigGlobal());
sigAction.sa_handler = SignalHandler; sigAction.sa_handler = SignalHandler;
sigfillset(&sigAction.sa_mask); sigfillset(&sigAction.sa_mask);
sigAction.sa_flags = SA_RESTART; sigAction.sa_flags = SA_RESTART;
@ -123,14 +129,13 @@ Main(void)
SIGACTION(SIGUSR1, &sigAction, NULL); SIGACTION(SIGUSR1, &sigAction, NULL);
#undef SIGACTION #undef SIGACTION
Log(LOG_INFO, "%d", XMPPQueryMUC(jabber, "rootard@muc.ari.lt", NULL));
server = HttpServerCreate(&conf); server = HttpServerCreate(&conf);
HttpServerStart(server); HttpServerStart(server);
LogConfigUnindent(LogConfigGlobal());
Log(LOG_INFO, "=======================");
HttpServerJoin(server); HttpServerJoin(server);
end: end:
LogConfigUnindent(LogConfigGlobal());
Log(LOG_INFO, "Exiting..."); Log(LOG_INFO, "Exiting...");
HttpServerFree(server); HttpServerFree(server);
ParseeConfigFree(); ParseeConfigFree();

View file

@ -46,12 +46,10 @@ ParseeMemberHandler(ParseeData *data, HashMap *event)
{ {
char *jid = ParseeEncodeMXID(state_key); char *jid = ParseeEncodeMXID(state_key);
chat_id = ParseeGetFromRoomID(data, room_id); chat_id = ParseeGetFromRoomID(data, room_id);
Log(LOG_INFO, "JID=%s", jid);
if (chat_id) if (chat_id)
{ {
char *muc = ParseeGetMUCID(data, chat_id); char *muc = ParseeGetMUCID(data, chat_id);
char *rev = StrConcat(2, muc, "/parsee"); char *rev = StrConcat(2, muc, "/parsee");
Log(LOG_INFO, "chat_id=%s muc=%s", chat_id, muc);
/* Make the user join the MUC */ /* Make the user join the MUC */
XMPPJoinMUC(data->jabber, jid, rev); XMPPJoinMUC(data->jabber, jid, rev);
Free(rev); Free(rev);
@ -72,13 +70,12 @@ ParseeMessageHandler(ParseeData *data, HashMap *event)
char *body = GrabString(event, 2, "content", "body"); char *body = GrabString(event, 2, "content", "body");
char *id = GrabString(event, 1, "room_id"); char *id = GrabString(event, 1, "room_id");
char *sender = GrabString(event, 1, "sender"); char *sender = GrabString(event, 1, "sender");
char *chat_id; char *chat_id, *muc_id, *jid;
bool direct = false; bool direct = false;
if (ParseeIsPuppet(data->config, sender)) if (ParseeIsPuppet(data->config, sender))
{ {
Log(LOG_INFO, "Do not bridge the puppet");
return; return;
} }
@ -92,7 +89,6 @@ ParseeMessageHandler(ParseeData *data, HashMap *event)
char *user = GrabString(json, 1, "xmpp_user"); char *user = GrabString(json, 1, "xmpp_user");
char *local = ParseeEncodeMXID(sender); char *local = ParseeEncodeMXID(sender);
Log(LOG_INFO, "Sending to %s on XMPP", user);
XMPPSendPlain(jabber, local, user, body, NULL); XMPPSendPlain(jabber, local, user, body, NULL);
Free(local); Free(local);
@ -101,12 +97,21 @@ ParseeMessageHandler(ParseeData *data, HashMap *event)
/* Try to find the chat ID */ /* Try to find the chat ID */
chat_id = ParseeGetFromRoomID(data, id); chat_id = ParseeGetFromRoomID(data, id);
muc_id = ParseeGetMUCID(data, chat_id);
if (!chat_id) if (!chat_id)
{ {
return; return;
} }
Log(LOG_INFO, "Chat ID=%s", chat_id); jid = ParseeEncodeMXID(sender);
{
char *rev = StrConcat(3, muc_id, "/", jid);
XMPPJoinMUC(jabber, jid, rev);
XMPPSendPlain(jabber, jid, muc_id, body, "groupchat");
Free(rev);
}
Free(chat_id); Free(chat_id);
Free(muc_id);
Free(jid);
} }
void void
@ -119,7 +124,6 @@ ParseeEventHandler(ParseeData *data, HashMap *event)
} }
event_type = GrabString(event, 1, "type"); event_type = GrabString(event, 1, "type");
Log(LOG_INFO, "E->%s", GrabString(event, 1, "sender"));
if (StrEquals(event_type, "m.room.member")) if (StrEquals(event_type, "m.room.member"))
{ {
ParseeMemberHandler(data, event); ParseeMemberHandler(data, event);

View file

@ -4,6 +4,7 @@
#include <Cytoplasm/Json.h> #include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h> #include <Cytoplasm/Sha.h>
#include <Cytoplasm/Log.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@ -37,7 +38,22 @@ ParseeIsPuppet(const ParseeConfig *conf, char *user)
* room. */ * room. */
return flag; return flag;
} }
bool
ParseeIsJabberPuppet(const ParseeConfig *config, char *jid)
{
char *resource;
bool ret;
if (!config || !jid)
{
return false;
}
resource = ParseeGetResource(jid);
ret = *resource == '%';
Free(resource);
return ret;
}
char * char *
ParseeDecodeJID(char *str, char term) ParseeDecodeJID(char *str, char term)
{ {
@ -139,7 +155,7 @@ ParseeDecodeLocalMUC(const ParseeConfig *c, char *alias)
} }
char * char *
ParseeEncodeJID(const ParseeConfig *c, char *jid) ParseeEncodeJID(const ParseeConfig *c, char *jid, bool trim)
{ {
char *ret, *tmp; char *ret, *tmp;
size_t i; size_t i;
@ -155,7 +171,7 @@ ParseeEncodeJID(const ParseeConfig *c, char *jid)
char cpy = jid[i]; char cpy = jid[i];
char cs[4] = { 0 }; char cs[4] = { 0 };
cs[0] = cpy; cs[0] = cpy;
if (*cs == '/') if (*cs == '/' && trim)
{ {
/* RID: Break everything and die. */ /* RID: Break everything and die. */
break; break;
@ -360,6 +376,24 @@ ParseeTrimJID(char *jid)
return ret; return ret;
} }
char * char *
ParseeGetResource(char *jid)
{
if (!jid)
{
return NULL;
}
while (*jid && *jid != '/')
{
jid++;
}
if (!*jid)
{
return NULL;
}
return StrDuplicate(jid + 1);
}
char *
ParseePushMUC(ParseeData *data, char *room_id, char *jid) ParseePushMUC(ParseeData *data, char *room_id, char *jid)
{ {
DbRef *ref; DbRef *ref;
@ -504,3 +538,97 @@ ParseeGetMUCID(ParseeData *data, char *chat_id)
DbUnlock(data->db, ref); DbUnlock(data->db, ref);
return ret; return ret;
} }
void
ParseePushStanza(ParseeData *data, char *chat_id, char *stanza_id)
{
DbRef *ref;
HashMap *j;
HashMap *stanzas;
bool new_stanzas = false;
if (!data || !chat_id || !stanza_id)
{
return;
}
ref = DbLock(data->db, 2, "chats", chat_id);
j = DbJson(ref);
if (!ref)
{
return;
}
stanzas = JsonValueAsObject(HashMapGet(j, "stanzas"));
if (!stanzas)
{
stanzas = HashMapCreate();
new_stanzas = true;
}
JsonValueFree(HashMapSet(stanzas, stanza_id, JsonValueNull()));
if (new_stanzas)
{
HashMapSet(j, "stanzas", JsonValueObject(stanzas));
}
DbUnlock(data->db, ref);
}
bool
ParseeVerifyStanza(ParseeData *data, char *chat_id, char *stanza_id)
{
DbRef *ref = NULL;
HashMap *j = NULL;
HashMap *stanzas = NULL;
bool ret = true;
if (!data || !chat_id || !stanza_id)
{
return true;
}
ref = DbLock(data->db, 2, "chats", chat_id);
j = DbJson(ref);
if (!ref)
{
goto end;
}
stanzas = JsonValueAsObject(HashMapGet(j, "stanzas"));
if (!stanzas)
{
goto end;
}
ret = !HashMapGet(stanzas, stanza_id);
end:
DbUnlock(data->db, ref);
return ret;
}
void
ParseeSendPresence(ParseeData *data)
{
DbRef *ref;
HashMap *j, *mucs;
JsonValue *val;
char *chatid = NULL, *muc;
if (!data)
{
return;
}
ref = DbLock(data->db, 1, "chats");
j = DbJson(ref);
mucs = JsonValueAsObject(HashMapGet(j, "mucs"));
while (HashMapIterate(mucs, &muc, (void **) &val))
{
char *rev = StrConcat(2, muc, "/parsee");
/* Make a fake user join the MUC */
Log(LOG_NOTICE, "Sending presence to %s", rev);
XMPPJoinMUC(data->jabber, "parsee", rev);
Free(rev);
}
DbUnlock(data->db, ref);
}

View file

@ -598,6 +598,47 @@ XMLCreateEmptyElem(XMLexer *lexer, HashMap *attrs)
return event; return event;
} }
static char *
XMLDecodeString(char *s)
{
char *ret = NULL, *tmp;
char c, cs[2] = { 0 };
while (*s)
{
cs[0] = *s;
if (!strncmp(s, "&apos;", 6))
{
cs[0] = '\'';
s += 6;
}
else if (!strncmp(s, "&lt;", 4))
{
cs[0] = '<';
s += 4;
}
else if (!strncmp(s, "&gt;", 4))
{
cs[0] = '>';
s += 4;
}
else if (!strncmp(s, "&amp;", 5))
{
cs[0] = '&';
s += 5;
}
else
{
s++;
}
tmp = ret;
ret = StrConcat(2, ret, cs);
Free(tmp);
}
return ret;
}
XMLEvent * XMLEvent *
XMLCreateData(XMLexer *lexer) XMLCreateData(XMLexer *lexer)
{ {
@ -609,13 +650,14 @@ XMLCreateData(XMLexer *lexer)
StrDuplicate(ArrayGet(lexer->data.elements, elements - 1)) : StrDuplicate(ArrayGet(lexer->data.elements, elements - 1)) :
NULL; NULL;
event->attrs = NULL; event->attrs = NULL;
event->data = lexer->data.str; event->data = XMLDecodeString(lexer->data.str);
/* TODO */ /* TODO */
event->line = 0; event->line = 0;
event->col = 0; event->col = 0;
event->offset = 0; event->offset = 0;
Free(lexer->data.str);
lexer->data.str = NULL; lexer->data.str = NULL;
return event; return event;

View file

@ -33,10 +33,40 @@ XMPPSendPlain(XMPPComponent *comp, char *fr, char *to, char *msg, char *type)
Free(from); Free(from);
} }
void void
XMPPSendMUC(XMPPComponent *comp, char *fr, char *as, char *to, char *msg, char *type)
{
XMLElement *message, *body, *data;
char *from, *id;
if (!comp || !fr || !to || !as || !msg)
{
return;
}
message = XMLCreateTag("message");
XMLAddAttr(message, "from",
(from = StrConcat(5, fr, "@", comp->host, "/", as)));
XMLAddAttr(message, "to", to);
XMLAddAttr(message, "type", type);
XMLAddAttr(message, "id", (id = StrRandom(8)));
body = XMLCreateTag("body");
data = XMLCreateText(msg);
XMLAddChild(message, body);
XMLAddChild(body, data);
XMLEncode(comp->stream, message);
StreamFlush(comp->stream);
XMLFreeElement(message);
Free(from);
Free(id);
}
void
XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc) XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc)
{ {
XMLElement *presence, *x; XMLElement *presence, *x;
char *from; char *from, *id;
if (!comp || !fr || !muc) if (!comp || !fr || !muc)
{ {
return; return;
@ -46,19 +76,17 @@ XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc)
x = XMLCreateTag("x"); x = XMLCreateTag("x");
XMLAddAttr(presence, "from", (from = StrConcat(3, fr, "@", comp->host))); XMLAddAttr(presence, "from", (from = StrConcat(3, fr, "@", comp->host)));
XMLAddAttr(presence, "to", muc); XMLAddAttr(presence, "to", muc);
XMLAddAttr(presence, "id", (id = StrRandom(8)));
XMLAddAttr(x, "xmlns", "http://jabber.org/protocol/muc"); XMLAddAttr(x, "xmlns", "http://jabber.org/protocol/muc");
XMLAddChild(presence, x); XMLAddChild(presence, x);
/* A*/
XMLEncode(comp->stream, presence); XMLEncode(comp->stream, presence);
Log(LOG_INFO, "Flushing...");
/* B */
StreamFlush(comp->stream); StreamFlush(comp->stream);
XMLFreeElement(presence); XMLFreeElement(presence);
Free(from); Free(from);
/* How is that leaking */ Free(id);
} }
void void
XMPPKillThread(XMPPComponent *jabber, char *killer) XMPPKillThread(XMPPComponent *jabber, char *killer)

View file

@ -27,7 +27,6 @@ ParseeWakeupThread(void)
static void static void
ParseeDMHandler(char *room, char *from, XMLElement *data, const ParseeConfig *c) ParseeDMHandler(char *room, char *from, XMLElement *data, const ParseeConfig *c)
{ {
Log(LOG_INFO, "Trying to send %s %s", room, from);
ASSend( ASSend(
c, room, from, c, room, from,
"m.room.message", MatrixCreateMessage(data->data) "m.room.message", MatrixCreateMessage(data->data)
@ -42,9 +41,9 @@ ParseeXMPPThread(void *argp)
XMLElement *stanza = NULL; XMLElement *stanza = NULL;
while (true) while (true)
{ {
/* TODO: pthread cancelation points */
XMLElement *body = NULL; XMLElement *body = NULL;
XMLElement *data = 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;
@ -59,7 +58,7 @@ ParseeXMPPThread(void *argp)
XMLFreeElement(stanza); XMLFreeElement(stanza);
continue; continue;
} }
if (StrEquals(stanza->name, "message")) else if (StrEquals(stanza->name, "message"))
{ {
size_t i; size_t i;
if (XMPPIsKiller(stanza)) if (XMPPIsKiller(stanza))
@ -69,7 +68,6 @@ ParseeXMPPThread(void *argp)
from = HashMapGet(stanza->attrs, "from"); from = HashMapGet(stanza->attrs, "from");
if (!strncmp(from, killer, strlen(killer))) if (!strncmp(from, killer, strlen(killer)))
{ {
Log(LOG_INFO, "Dropping thread...");
XMLFreeElement(stanza); XMLFreeElement(stanza);
break; break;
} }
@ -82,40 +80,50 @@ ParseeXMPPThread(void *argp)
continue; continue;
} }
} }
for (i = 0; i < ArraySize(stanza->children); i++) body = XMLookForUnique(stanza, "body");
{
XMLElement *child = ArrayGet(stanza->children, i);
if (StrEquals(child->name, "body"))
{
body = child;
break;
}
}
if (!body) if (!body)
{ {
XMLFreeElement(stanza); XMLFreeElement(stanza);
continue; continue;
} }
stanza_id = XMLookForUnique(stanza, "stanza-id");
/* TODO: Check the type */
to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to")); to = ParseeDecodeMXID(HashMapGet(stanza->attrs, "to"));
from = HashMapGet(stanza->attrs, "from"); from = HashMapGet(stanza->attrs, "from");
from_matrix = ParseeEncodeJID(args->config, from); from_matrix = ParseeEncodeJID(args->config, from, true);
room = ParseeFindDMRoom(args, to, from); room = ParseeFindDMRoom(args, to, from);
data = ArrayGet(body->children, 0); data = ArrayGet(body->children, 0);
/* TODO: Consider having rich messages */ /* TODO: Consider having rich messages, and move that out
* to separate functions */
chat_id = ParseeGetFromMUCID(args, from); chat_id = ParseeGetFromMUCID(args, from);
mroom_id = ParseeGetRoomID(args, chat_id); mroom_id = ParseeGetRoomID(args, chat_id);
if (room) if (room)
{ {
ParseeDMHandler(room, from_matrix, data, args->config); ParseeDMHandler(room, from_matrix, data, args->config);
} }
if (mroom_id) if (mroom_id && !ParseeIsJabberPuppet(args->config, from))
{ {
Log(LOG_INFO, "Send to %s", mroom_id); char *res = ParseeGetResource(from);
/* TODO: Grab username, create a puppet, and go from char *encoded = ParseeEncodeJID(args->config, from, false);
* there */ 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);
ASJoin(args->config, mroom_id, encoded);
ASSetName(args->config, encoded, res);
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(chat_id);
Free(mroom_id); Free(mroom_id);
@ -123,6 +131,13 @@ ParseeXMPPThread(void *argp)
Free(room); Free(room);
Free(to); Free(to);
} }
else
{
Log(LOG_WARNING, "Unknown stanza '%s':", stanza->name);
XMLEncode(StreamStdout(), stanza);
StreamPrintf(StreamStdout(), "\n");
StreamFlush(StreamStdout());
}
XMLFreeElement(stanza); XMLFreeElement(stanza);
} }

View file

@ -36,4 +36,7 @@ extern void ASSetState(const ParseeConfig *conf, char *id, char *type, char *key
/* Creates a room, with a masquerade user as its creator. This function /* Creates a room, with a masquerade user as its creator. This function
* returns it's ID if it exists. */ * returns it's ID if it exists. */
extern char * ASCreateRoom(const ParseeConfig *c, char *by, char *alias); 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);
#endif #endif

View file

@ -14,4 +14,7 @@ extern HashMap * MatrixCreateMessage(char *body);
/* 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);
/* Creates a join membership with a specific nickname */
extern HashMap * MatrixCreateNickChange(char *nick);
#endif #endif

View file

@ -73,11 +73,14 @@ extern void ParseeEventHandler(ParseeData *, HashMap *);
/* Verifies if a user is a Parsee puppet user. */ /* Verifies if a user is a Parsee puppet user. */
extern bool ParseeIsPuppet(const ParseeConfig *, char *); extern bool ParseeIsPuppet(const ParseeConfig *, char *);
/* Verifies if a user is a Parsee puppet user on XMPP. */
extern bool ParseeIsJabberPuppet(const ParseeConfig *, char *);
/* Decodes a local JID for a user into a string. */ /* Decodes a local JID for a user into a string. */
extern char * ParseeDecodeLocalJID(const ParseeConfig *, char *); extern char * ParseeDecodeLocalJID(const ParseeConfig *, char *);
/* Encodes a JID into a Parsee localpart */ /* Encodes a JID into a Parsee localpart */
extern char * ParseeEncodeJID(const ParseeConfig *, char *); extern char * ParseeEncodeJID(const ParseeConfig *, char *, bool);
/* Gets the localpart of a MXID */ /* Gets the localpart of a MXID */
extern char * ParseeGetLocal(char *); extern char * ParseeGetLocal(char *);
@ -111,6 +114,9 @@ extern void ParseePushDMRoom(ParseeData *, char *mxid, char *jid, char *r);
/* Trims the component from a JID */ /* Trims the component from a JID */
extern char * ParseeTrimJID(char *jid); extern char * ParseeTrimJID(char *jid);
/* Gets the component from a JID */
extern char * ParseeGetResource(char *jid);
/* Decodes a room alias into a MUC JID */ /* Decodes a room alias into a MUC JID */
extern char * ParseeDecodeLocalMUC(const ParseeConfig *c, char *alias); extern char * ParseeDecodeLocalMUC(const ParseeConfig *c, char *alias);
@ -132,4 +138,13 @@ extern char * ParseeGetRoomID(ParseeData *, char *chat_id);
/* Finds the MUC JID from a chat ID */ /* Finds the MUC JID from a chat ID */
extern char * ParseeGetMUCID(ParseeData *, char *chat_id); extern char * ParseeGetMUCID(ParseeData *, char *chat_id);
/* Pushes a stanza ID to a chat ID */
extern void ParseePushStanza(ParseeData *, char *chat_id, char *stanza_id);
/* Checks if a stanza is not duplicated in a chat ID */
extern bool ParseeVerifyStanza(ParseeData *, char *chat_id, char *stanza_id);
/* Sends presence requests for every MUC around as a fake JID */
extern void ParseeSendPresence(ParseeData *);
#endif #endif

View file

@ -24,6 +24,7 @@ extern void XMPPJoinMUC(XMPPComponent *comp, char *fr, char *muc);
/* TODO: XMPP stuff, I don't fucking know, I'm not a Jabbernerd. */ /* TODO: XMPP stuff, I don't fucking know, I'm not a Jabbernerd. */
extern void XMPPSendPlain(XMPPComponent *c, char *f, char *t, char *m, char *type); extern void XMPPSendPlain(XMPPComponent *c, char *f, char *t, char *m, char *type);
extern void XMPPSendMUC(XMPPComponent *comp, char *fr, char *as, char *to, char *msg, char *type);
/* Closes a raw component stream. */ /* Closes a raw component stream. */
extern void XMPPEndCompStream(XMPPComponent *stream); extern void XMPPEndCompStream(XMPPComponent *stream);