mirror of
https://forge.fsky.io/lda/Parsee.git
synced 2026-03-13 21:35:10 +00:00
[ADD/WIP] XMPP->Matrix avatar, start bridging bans
I still hate XEP-0084.
This commit is contained in:
parent
a3bef5c0c1
commit
1f658ece76
9 changed files with 587 additions and 27 deletions
4
Makefile
4
Makefile
|
|
@ -19,8 +19,8 @@ SOURCE=src
|
||||||
OBJECT=build
|
OBJECT=build
|
||||||
INCLUDES=src/include
|
INCLUDES=src/include
|
||||||
CC=cc
|
CC=cc
|
||||||
CFLAGS=-I$(INCLUDES) -I$(CYTO_INC) -DNAME="\"$(NAME)\"" -DVERSION="\"$(VERSION)\"" -DREPOSITORY=\"$(REPOSITORY)\" -g -ggdb
|
CFLAGS=-I$(INCLUDES) -I$(CYTO_INC) -DNAME="\"$(NAME)\"" -DVERSION="\"$(VERSION)\"" -DREPOSITORY=\"$(REPOSITORY)\" -O3
|
||||||
LDFLAGS=-L $(CYTO_LIB) -lCytoplasm -Wl,--export-dynamic
|
LDFLAGS=-L $(CYTO_LIB) -lCytoplasm -Wl,--export-dynamic -O3
|
||||||
BINARY=parsee
|
BINARY=parsee
|
||||||
# ============================ Compilation =================================
|
# ============================ Compilation =================================
|
||||||
SRC_FILES:=$(shell find $(SOURCE) -name '*.c')
|
SRC_FILES:=$(shell find $(SOURCE) -name '*.c')
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ TODO
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
## TODOS
|
## TODOS
|
||||||
|
- PROPER FUCKING AVATARS
|
||||||
|
XEP-0084 IS THE WORST PIECE OF SHIT KNOWN TO MAN. If any Jabberbros want to
|
||||||
|
look at terrible code/XML and suggest things to have *proper* avatar support,
|
||||||
|
I'm all in.
|
||||||
- Look at XEPS-TBD.TXT for XEPs to be done
|
- Look at XEPS-TBD.TXT for XEPs to be done
|
||||||
- Achievements
|
- Achievements
|
||||||
### Why?
|
### Why?
|
||||||
|
|
|
||||||
14
XEPS-TBD.TXT
14
XEPS-TBD.TXT
|
|
@ -25,14 +25,20 @@ For future XEPs:
|
||||||
- https://xmpp.org/extensions/xep-0080.html
|
- https://xmpp.org/extensions/xep-0080.html
|
||||||
Doxxing people over two protocols is great!
|
Doxxing people over two protocols is great!
|
||||||
|
|
||||||
- https://xmpp.org/extensions/xep-0084.html
|
|
||||||
Avatar support would be extremely useful, if just a QoL improvment.
|
|
||||||
Matrix and XMPP both have support for these.
|
|
||||||
|
|
||||||
- https://xmpp.org/extensions/xep-0449.html
|
- https://xmpp.org/extensions/xep-0449.html
|
||||||
Stickers are great. Matrix and XMPP somewhat has support for them, so
|
Stickers are great. Matrix and XMPP somewhat has support for them, so
|
||||||
might be a nice-to-have, and also to push over XMPP support.
|
might be a nice-to-have, and also to push over XMPP support.
|
||||||
|
|
||||||
|
ON STANDBY BECAUSE THESE HAVE BEEN TERRIBLE TO DEAL WITH AND WHO KEEPS WRITING
|
||||||
|
THESE I WANT TO SEND THEM A NICE, BRIGHT GIFT:
|
||||||
|
(x) https://xmpp.org/extensions/xep-0084.html
|
||||||
|
Avatar support would be extremely useful, if just a QoL improvment.
|
||||||
|
Matrix and XMPP both have support for these.
|
||||||
|
|
||||||
|
XEP-0084 is a pain in the ass to implement and seems generally just
|
||||||
|
unreliable, however.
|
||||||
|
|
||||||
|
|
||||||
Not XEPs, but ideas that _needs_ to be added:
|
Not XEPs, but ideas that _needs_ to be added:
|
||||||
- "GIVE THE PUPPETS APPROPRIATE PLS/ROLES" - Hydro/t4d
|
- "GIVE THE PUPPETS APPROPRIATE PLS/ROLES" - Hydro/t4d
|
||||||
"also it [Bifrost] doesn't respect voice either"
|
"also it [Bifrost] doesn't respect voice either"
|
||||||
|
|
|
||||||
171
src/AS.c
171
src/AS.c
|
|
@ -188,6 +188,41 @@ ASBan(const ParseeConfig *conf, char *id, char *banned)
|
||||||
JsonFree(json);
|
JsonFree(json);
|
||||||
}
|
}
|
||||||
void
|
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)
|
ASJoin(const ParseeConfig *conf, char *id, char *masquerade)
|
||||||
{
|
{
|
||||||
HttpClientContext *ctx = NULL;
|
HttpClientContext *ctx = NULL;
|
||||||
|
|
@ -331,6 +366,35 @@ ASCreateRoom(const ParseeConfig *conf, char *by, char *alias)
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
void
|
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)
|
ASSetName(const ParseeConfig *conf, char *user, char *name)
|
||||||
{
|
{
|
||||||
HttpClientContext *ctx = NULL;
|
HttpClientContext *ctx = NULL;
|
||||||
|
|
@ -510,6 +574,7 @@ ASUpload(const ParseeConfig *c, Stream *from, unsigned int size)
|
||||||
HashMap *reply;
|
HashMap *reply;
|
||||||
if (!c || !from)
|
if (!c || !from)
|
||||||
{
|
{
|
||||||
|
Log(LOG_INFO, "Obvious upload fail");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -526,13 +591,26 @@ ASUpload(const ParseeConfig *c, Stream *from, unsigned int size)
|
||||||
}
|
}
|
||||||
HttpRequestSendHeaders(ctx);
|
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);
|
HttpRequestSend(ctx);
|
||||||
|
|
||||||
reply = JsonDecode(HttpClientStream(ctx));
|
reply = JsonDecode(HttpClientStream(ctx));
|
||||||
ret = StrDuplicate(
|
ret = StrDuplicate(
|
||||||
JsonValueAsString(HashMapGet(reply, "content_uri"))
|
JsonValueAsString(HashMapGet(reply, "content_uri"))
|
||||||
);
|
);
|
||||||
|
if (!ret)
|
||||||
|
{
|
||||||
|
JsonEncode(reply, StreamStdout(), JSON_PRETTY);
|
||||||
|
StreamFlush(StreamStdout());
|
||||||
|
Log(LOG_INFO, "Less obvious upload fail");
|
||||||
|
}
|
||||||
HttpClientContextFree(ctx);
|
HttpClientContextFree(ctx);
|
||||||
JsonFree(reply);
|
JsonFree(reply);
|
||||||
Free(size_str);
|
Free(size_str);
|
||||||
|
|
@ -632,3 +710,92 @@ ASType(const ParseeConfig *c, char *user, char *room, bool status)
|
||||||
HttpClientContextFree(ctx);
|
HttpClientContextFree(ctx);
|
||||||
Free(user);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,10 @@ RouteHead(RouteRoomAck, arr, argp)
|
||||||
}
|
}
|
||||||
|
|
||||||
muc = ParseeDecodeLocalMUC(args->data->config, room);
|
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);
|
HttpResponseStatus(args->ctx, HTTP_METHOD_NOT_ALLOWED);
|
||||||
|
Log(LOG_INFO, "Nofly...");
|
||||||
response = MatrixCreateError(
|
response = MatrixCreateError(
|
||||||
"M_NOT_FOUND",
|
"M_NOT_FOUND",
|
||||||
"XMPP MUC is banned from being accessed on this instance"
|
"XMPP MUC is banned from being accessed on this instance"
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ const static IoFunctions Functions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
Stream *
|
Stream *
|
||||||
StrStreamReader(char *buffer)
|
StrStreamReaderN(char *buffer, int n)
|
||||||
{
|
{
|
||||||
Io *raw_io;
|
Io *raw_io;
|
||||||
ReaderCookie *cookie;
|
ReaderCookie *cookie;
|
||||||
|
|
@ -124,7 +124,7 @@ StrStreamReader(char *buffer)
|
||||||
|
|
||||||
cookie = Malloc(sizeof(*cookie));
|
cookie = Malloc(sizeof(*cookie));
|
||||||
cookie->buffer = buffer;
|
cookie->buffer = buffer;
|
||||||
cookie->length = strlen(buffer);
|
cookie->length = n ? n : strlen(buffer);
|
||||||
cookie->offset = 0;
|
cookie->offset = 0;
|
||||||
raw_io = IoCreate(cookie, Functions);
|
raw_io = IoCreate(cookie, Functions);
|
||||||
return StreamIo(raw_io);
|
return StreamIo(raw_io);
|
||||||
|
|
|
||||||
408
src/XMPPThread.c
408
src/XMPPThread.c
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
#include <Cytoplasm/Memory.h>
|
#include <Cytoplasm/Memory.h>
|
||||||
#include <Cytoplasm/Base64.h>
|
#include <Cytoplasm/Base64.h>
|
||||||
|
|
@ -10,15 +11,21 @@
|
||||||
#include <Cytoplasm/Str.h>
|
#include <Cytoplasm/Str.h>
|
||||||
#include <Cytoplasm/Sha.h>
|
#include <Cytoplasm/Sha.h>
|
||||||
|
|
||||||
|
#include <StringStream.h>
|
||||||
#include <Matrix.h>
|
#include <Matrix.h>
|
||||||
#include <XMPP.h>
|
#include <XMPP.h>
|
||||||
#include <XML.h>
|
#include <XML.h>
|
||||||
#include <AS.h>
|
#include <AS.h>
|
||||||
|
|
||||||
|
/* TODO: Rewrite this avatar code.
|
||||||
|
* XEP-0084 sucks. */
|
||||||
#define IQ_ADVERT \
|
#define IQ_ADVERT \
|
||||||
AdvertiseSimple("http://jabber.org/protocol/chatstates") \
|
AdvertiseSimple("http://jabber.org/protocol/chatstates") \
|
||||||
AdvertiseSimple("http://jabber.org/protocol/caps") \
|
AdvertiseSimple("http://jabber.org/protocol/caps") \
|
||||||
AdvertiseSimple("urn:xmpp:avatar:metadata+notify") \
|
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:message-correct:0") \
|
||||||
AdvertiseSimple("urn:xmpp:reactions:0") \
|
AdvertiseSimple("urn:xmpp:reactions:0") \
|
||||||
AdvertiseSimple("urn:xmpp:styling:0") \
|
AdvertiseSimple("urn:xmpp:styling:0") \
|
||||||
|
|
@ -290,8 +297,104 @@ typedef struct XMPPThread {
|
||||||
XMPPThreadInfo *info;
|
XMPPThreadInfo *info;
|
||||||
} XMPPThread;
|
} XMPPThread;
|
||||||
|
|
||||||
/* TODO: Clean up all of this. We are currently separating DMs from MUCs,
|
/* Manages an avatar metadata pubsub item */
|
||||||
* where we could unify all our code, and generalise everything. */
|
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
|
static bool
|
||||||
MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
||||||
{
|
{
|
||||||
|
|
@ -300,6 +403,7 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
||||||
XMLElement *reactions = NULL;
|
XMLElement *reactions = NULL;
|
||||||
XMLElement *body = NULL;
|
XMLElement *body = NULL;
|
||||||
XMLElement *data = NULL;
|
XMLElement *data = NULL;
|
||||||
|
XMLElement *event = NULL;
|
||||||
|
|
||||||
char *to, *room, *from, *from_matrix, *decode_from;
|
char *to, *room, *from, *from_matrix, *decode_from;
|
||||||
char *chat_id = NULL, *mroom_id = NULL;
|
char *chat_id = NULL, *mroom_id = NULL;
|
||||||
|
|
@ -312,6 +416,28 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
||||||
|
|
||||||
from = HashMapGet(stanza->attrs, "from");
|
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"
|
#define CHAT_STATES "http://jabber.org/protocol/chatstates"
|
||||||
if (XMLookForTKV(stanza, "composing", "xmlns", CHAT_STATES))
|
if (XMLookForTKV(stanza, "composing", "xmlns", CHAT_STATES))
|
||||||
{
|
{
|
||||||
|
|
@ -395,6 +521,21 @@ MessageStanza(ParseeData *args, XMLElement *stanza, XMPPThread *thr)
|
||||||
chat = true;
|
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);
|
pthread_mutex_lock(&thr->info->chk_lock);
|
||||||
if (ParseeVerifyAllStanza(args, stanza) && !replaced)
|
if (ParseeVerifyAllStanza(args, stanza) && !replaced)
|
||||||
{
|
{
|
||||||
|
|
@ -581,10 +722,158 @@ IQDiscoGet(ParseeData *args, XMPPComponent *jabber, XMLElement *stanza)
|
||||||
|
|
||||||
pthread_mutex_lock(&jabber->write_lock);
|
pthread_mutex_lock(&jabber->write_lock);
|
||||||
XMLEncode(jabber->stream, iq_reply);
|
XMLEncode(jabber->stream, iq_reply);
|
||||||
|
StreamFlush(jabber->stream);
|
||||||
pthread_mutex_unlock(&jabber->write_lock);
|
pthread_mutex_unlock(&jabber->write_lock);
|
||||||
|
|
||||||
XMLFreeElement(iq_reply);
|
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
|
static void
|
||||||
IQGet(ParseeData *args, XMLElement *stanza)
|
IQGet(ParseeData *args, XMLElement *stanza)
|
||||||
{
|
{
|
||||||
|
|
@ -622,6 +911,7 @@ IQGet(ParseeData *args, XMLElement *stanza)
|
||||||
|
|
||||||
pthread_mutex_lock(&jabber->write_lock);
|
pthread_mutex_lock(&jabber->write_lock);
|
||||||
XMLEncode(jabber->stream, iq_reply);
|
XMLEncode(jabber->stream, iq_reply);
|
||||||
|
StreamFlush(jabber->stream);
|
||||||
pthread_mutex_unlock(&jabber->write_lock);
|
pthread_mutex_unlock(&jabber->write_lock);
|
||||||
XMLFreeElement(iq_reply);
|
XMLFreeElement(iq_reply);
|
||||||
}
|
}
|
||||||
|
|
@ -634,6 +924,11 @@ IQGet(ParseeData *args, XMLElement *stanza)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
static void
|
||||||
|
IQError(ParseeData *args, XMLElement *stanza)
|
||||||
|
{
|
||||||
|
/* TODO */
|
||||||
|
}
|
||||||
#undef DISCO
|
#undef DISCO
|
||||||
static void
|
static void
|
||||||
IQStanza(ParseeData *args, XMLElement *stanza)
|
IQStanza(ParseeData *args, XMLElement *stanza)
|
||||||
|
|
@ -651,9 +946,30 @@ IQStanza(ParseeData *args, XMLElement *stanza)
|
||||||
while (0)
|
while (0)
|
||||||
|
|
||||||
OnType(get, IQGet);
|
OnType(get, IQGet);
|
||||||
|
OnType(error, IQError);
|
||||||
|
OnType(result, IQResult);
|
||||||
#undef OnType
|
#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
|
static void
|
||||||
PresenceStanza(ParseeData *args, XMLElement *stanza)
|
PresenceStanza(ParseeData *args, XMLElement *stanza)
|
||||||
{
|
{
|
||||||
|
|
@ -664,35 +980,95 @@ PresenceStanza(ParseeData *args, XMLElement *stanza)
|
||||||
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");
|
||||||
|
XMPPComponent *jabber = args->jabber;
|
||||||
char *jid = item ? HashMapGet(item->attrs, "jid") : NULL;
|
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)
|
if (jid)
|
||||||
{
|
{
|
||||||
ParseePushJIDTable(oid, jid);
|
ParseePushJIDTable(oid, jid);
|
||||||
}
|
}
|
||||||
|
from = StrConcat(2, "parsee@", args->config->component_host);
|
||||||
|
Free(from);
|
||||||
}
|
}
|
||||||
else if (vc)
|
else if (vc)
|
||||||
{
|
{
|
||||||
XMLElement *photo = XMLookForUnique(vc, "photo");
|
XMLElement *photo = XMLookForUnique(vc, "photo");
|
||||||
XMLElement *p_dat = photo ? ArrayGet(photo->children, 0) : NULL;
|
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)
|
if (!p_dat)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chat_id = ParseeGetFromMUCID(args, oid);
|
avatars = DbLock(args->db, 1, "avatars");
|
||||||
room_id = ParseeGetRoomID(args, chat_id);
|
if (!avatars)
|
||||||
|
|
||||||
if (!room_id)
|
|
||||||
{
|
{
|
||||||
Free(chat_id);
|
avatars = DbCreate(args->db, 1, "avatars");
|
||||||
Free(room_id);
|
|
||||||
}
|
}
|
||||||
/* TODO: Get the media, and shove it to the room */
|
json = DbJson(avatars);
|
||||||
Free(chat_id);
|
|
||||||
Free(room_id);
|
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
|
#undef MUC_USER_NS
|
||||||
}
|
}
|
||||||
|
|
@ -728,13 +1104,12 @@ XMPPDispatcher(void *argp)
|
||||||
XMLElement *stanza = RetrieveStanza(thread);
|
XMLElement *stanza = RetrieveStanza(thread);
|
||||||
if (!stanza)
|
if (!stanza)
|
||||||
{
|
{
|
||||||
UtilSleepMillis(10);
|
UtilSleepMillis(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StrEquals(stanza->name, "presence"))
|
if (StrEquals(stanza->name, "presence"))
|
||||||
{
|
{
|
||||||
/* TODO: Manage presence */
|
|
||||||
PresenceStanza(args, stanza);
|
PresenceStanza(args, stanza);
|
||||||
XMLFreeElement(stanza);
|
XMLFreeElement(stanza);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -770,7 +1145,8 @@ ParseeXMPPThread(void *argp)
|
||||||
XMLElement *stanza = NULL;
|
XMLElement *stanza = NULL;
|
||||||
XMPPThreadInfo info;
|
XMPPThreadInfo info;
|
||||||
pthread_mutex_t stanzas_lock = PTHREAD_MUTEX_INITIALIZER;
|
pthread_mutex_t stanzas_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
size_t i;
|
size_t i, j = 0;
|
||||||
|
|
||||||
|
|
||||||
/* Initialise the FIFO */
|
/* Initialise the FIFO */
|
||||||
info.stanzas = ArrayCreate();
|
info.stanzas = ArrayCreate();
|
||||||
|
|
@ -795,6 +1171,7 @@ ParseeXMPPThread(void *argp)
|
||||||
pthread_create(thr, NULL, XMPPDispatcher, &info.dispatchers[i]);
|
pthread_create(thr, NULL, XMPPDispatcher, &info.dispatchers[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
char *to, *room, *from, *from_matrix;
|
char *to, *room, *from, *from_matrix;
|
||||||
|
|
@ -829,7 +1206,6 @@ ParseeXMPPThread(void *argp)
|
||||||
}
|
}
|
||||||
|
|
||||||
PushStanza(&info, stanza);
|
PushStanza(&info, stanza);
|
||||||
//XMLFreeElement(stanza);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info.running = false;
|
info.running = false;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ extern void ASJoin(const ParseeConfig *, char *, char *);
|
||||||
|
|
||||||
/* Bans from a room a specific user */
|
/* Bans from a room a specific user */
|
||||||
extern void ASBan(const ParseeConfig *, char *, char *);
|
extern void ASBan(const ParseeConfig *, char *, char *);
|
||||||
|
extern void ASKick(const ParseeConfig *, char *, char *);
|
||||||
|
|
||||||
/* Invites from a room a specific user */
|
/* Invites from a room a specific user */
|
||||||
extern void ASInvite(const ParseeConfig *, char *, char *);
|
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 */
|
/* Sets a user's displayname */
|
||||||
extern void ASSetName(const ParseeConfig *c, char *user, char *name);
|
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
|
/* Returns the user's name in a room, or a copy of the MXID itself, to be
|
||||||
* Free'd. */
|
* 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. */
|
/* Reuploads a HTTP URL to Matrix, with an optional MIME type returned. */
|
||||||
extern char * ASReupload(const ParseeConfig *c, char *from, char **mime);
|
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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
extern Stream * StrStreamWriter(char **buffer);
|
extern Stream * StrStreamWriter(char **buffer);
|
||||||
|
|
||||||
/* Creates a string stream reader. The referenced buffer may be everywhere. */
|
/* 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
|
#endif
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue