#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
+#include <ctype.h>
#include <errno.h>
-#include <fcntl.h>
-#include <netdb.h>
#include <expat.h>
+#include <time.h>
#include "util.h"
-#include "chat.h"
#include "conf.h"
+#include "chat.h"
+#include "view.h"
#include "net.h"
/* Constants */
+#define ID_LEN 256
#define AUTH_LEN 512
+#define JID_LEN 256
/* XMPP types */
typedef enum {
XMPP_DEAD,
+ XMPP_CONNECT,
+ XMPP_ENCRYPT,
+ XMPP_RESTART,
XMPP_SEND_STREAM,
XMPP_RECV_FEATURES,
XMPP_SEND_STARTTLS,
XMPP_RECV_SUCCESS,
XMPP_SEND_BIND,
XMPP_RECV_JID,
- XMPP_PRESENCE,
- XMPP_ENCRYPT,
- XMPP_RESTART,
+ XMPP_SEND_SESSION,
+ XMPP_SEND_PRESENCE,
+ XMPP_SEND_ROSTER,
+ XMPP_SEND_ROOMS,
+ XMPP_SEND_JOIN,
XMPP_READY,
+ XMPP_IN_IQ,
+ XMPP_IN_VCARD,
+ XMPP_IN_ROSTER,
+ XMPP_IN_ROOMS,
+ XMPP_IN_MESSAGE,
+ XMPP_IN_PRESENCE,
} xmpp_state_t;
-typedef struct xmpp_server_t {
- const char *name;
- const char *protocol;
- int connect;
- const char *host;
- int port;
- const char *muc;
- const char *nick;
- const char *jid;
- const char *user;
- const char *pass;
-
- net_t net;
- XML_Parser expat;
- xmpp_state_t state;
- buf_t buf;
- int indent;
-
- int in_error;
-
- struct xmpp_server_t *next;
-} xmpp_server_t;
+typedef struct {
+ user_t user;
+ char dest[JID_LEN];
+ int muc;
+ char *full_name;
+ char *mention_name;
+} xmpp_user_t;
+
+typedef struct {
+ channel_t channel;
+ char dest[JID_LEN];
+ const char *type;
+ int muc;
-typedef struct xmpp_channel_t {
- const char *name;
- const char *channel;
- const char *server;
- int join;
- struct xmpp_channel_t *next;
+ char *room;
+ char *name;
+ int join;
} xmpp_channel_t;
-/* Local data */
-static xmpp_server_t *servers;
-static xmpp_channel_t *channels;
+typedef struct {
+ server_t server;
+ xmpp_user_t myself;
+ xmpp_channel_t system;
-/* Local functions */
-const char *find_attr(const char **attrs, const char *name)
-{
- for (int i = 0; attrs[i] && attrs[i+1]; i += 2)
- if (match(attrs[i+0], name))
- return attrs[i+1];
- return NULL;
-}
+ int connect;
+ int timeout;
+ int noverify;
+ const char *host;
+ int port;
+ const char *srv;
+ const char *muc;
+ const char *nick;
+ const char *jid;
+ const char *user;
+ const char *pass;
+ int quiet;
+
+ idle_t idle;
+
+ net_t net;
+ XML_Parser expat;
+ xmpp_state_t state;
+ buf_t buf;
+ buf_t html;
+ int level;
+ int id;
+ char *bind;
+
+ char msg_id[ID_LEN];
+ xmpp_channel_t *msg_chan;
+ xmpp_user_t *msg_usr;
+ char *msg_body;
+ char *msg_html;
+ stamp_t msg_stamp;
-static xmpp_server_t *find_server(const char *name, int create)
+ int in_html;
+ int in_error;
+} xmpp_server_t;
+
+/* Helper functions */
+static void srv_notice(xmpp_server_t *srv, const char *fmt, ...)
{
- xmpp_server_t *cur = NULL, *last = NULL;
- for (cur = servers; cur; last = cur, cur = cur->next)
- if (match(cur->name, name))
- break;
- if (!cur && create) {
- cur = new0(xmpp_server_t);
- cur->name = get_name(name);
- if (last)
- last->next = cur;
- else
- servers = cur;
- }
- return cur;
+ static char buf[1024];
+
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+
+ debug("xmpp: srv_notice: [%s]", buf);
+ chat_recv(&srv->system.channel, NULL, buf);
}
-static xmpp_channel_t *find_channel(const char *name, int create)
+static void chan_notice(xmpp_channel_t *chan, const char *fmt, ...)
{
- xmpp_channel_t *cur = NULL, *last = NULL;
- for (cur = channels; cur; last = cur, cur = cur->next)
- if (match(cur->name, name))
- break;
- if (!cur && create) {
- cur = new0(xmpp_channel_t);
- cur->name = get_name(name);
- if (last)
- last->next = cur;
- else
- channels = cur;
- }
- return cur;
+ static char buf[1024];
+
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+
+ debug("xmpp: chan_notice -- %s [%s]",
+ chan->channel.name, buf);
+ chat_recv(&chan->channel, NULL, buf);
}
-static void on_start(void *_srv, const char *tag, const char **attrs)
+static void split_jid(const char *jid, char *usr, char *srv, char *res)
{
- xmpp_server_t *srv = _srv;
+ char *ptr = usr;
+ int pos = 0;
- /* Debug print */
- debug("%*s<%s>",
- srv->indent*4, "", tag);
- for (int i = 0; attrs[i] && attrs[i+1]; i += 2) {
- debug("%*s%s=\"%s\"%s",
- srv->indent*4+8, "",
- attrs[i+0], attrs[i+1],
- attrs[i+2] ? "" : ">");
- }
- srv->indent++;
+ if (usr) usr[0] = '\0';
+ if (srv) srv[0] = '\0';
+ if (res) res[0] = '\0';
- /* Start TLS */
- if (srv->state == XMPP_RECV_FEATURES) {
- if (match(tag, "starttls")) {
- debug("xmpp: features -> starttls");
- srv->state = XMPP_SEND_STARTTLS;
- }
- if (match(tag, "mechanisms")) {
- debug("xmpp: features -> auth");
- srv->state = XMPP_SEND_AUTH;
+ while (jid && *jid == ' ')
+ jid++;
+ for (int i = 0; jid && jid[i]; i++) {
+ switch (jid[i]) {
+ case '@':
+ ptr = srv;
+ pos = 0;
+ continue;
+ case '/':
+ ptr = res;
+ pos = 0;
+ continue;
}
- if (match(tag, "bind")) {
- debug("xmpp: features -> bind");
- srv->state = XMPP_SEND_BIND;
- }
- }
- if (srv->state == XMPP_RECV_PROCEED) {
- if (match(tag, "proceed")) {
- debug("xmpp: proceed -> encrypt");
- srv->state = XMPP_ENCRYPT;
+ if (ptr && (pos+1) < JID_LEN) {
+ ptr[pos++] = jid[i];
+ ptr[pos] = '\0';
}
}
+ while (ptr && pos >= 1 && ptr[pos-1] == ' ')
+ ptr[--pos] = '\0';
- /* Authentication */
- if (srv->state == XMPP_RECV_SUCCESS) {
- if (match(tag, "success")) {
- debug("xmpp: success -> restart");
- srv->state = XMPP_RESTART;
- }
+ //debug("JID: '%s' usr=[%s] srv=[%s] res=[%s]",
+ // jid, usr, srv, res);
+}
+
+static xmpp_channel_t *find_channel(xmpp_server_t *srv,
+ const char *jid, int is_muc)
+{
+ static char jid_usr[JID_LEN];
+ static char jid_srv[JID_LEN];
+ static char dest[JID_LEN];
+ xmpp_channel_t *chan;
+
+ /* Server channels */
+ if (!jid || match(jid, srv->srv))
+ return &srv->system;
+
+ /* Parse JID and check for MUC */
+ split_jid(jid, jid_usr, jid_srv, NULL);
+ if (match(jid_srv, srv->muc))
+ is_muc = 1;
+
+ /* Find resource-less JID */
+ snprintf(dest, JID_LEN, "%s@%s", jid_usr,
+ jid_srv[0] ? jid_srv :
+ is_muc ? srv->muc : srv->srv);
+
+ /* Find existing channels */
+ for (channel_t *cur = channels; cur; cur = cur->next) {
+ if (cur->server != &srv->server)
+ continue;
+ chan = (xmpp_channel_t *)cur;
+ if (match(chan->dest, dest))
+ return chan;
}
- if (srv->state == XMPP_RECV_JID) {
- if (match(tag, "jid")) {
- debug("xmpp: jid -> presence");
- srv->state = XMPP_PRESENCE;
+
+ /* Create a new channel */
+ chan = new0(xmpp_channel_t);
+ chan->muc = is_muc;
+ chan->type = is_muc ? "muc" : "usr";
+ chan->channel.server = &srv->server;
+ chan->channel.name = strcopy(jid_usr);
+ strncpy(chan->dest, dest, JID_LEN);
+ add_channel(&chan->channel);
+ return chan;
+}
+
+static xmpp_user_t *find_user(xmpp_server_t *srv,
+ const char *jid, int is_muc)
+{
+ static char jid_usr[JID_LEN];
+ static char jid_srv[JID_LEN];
+ static char jid_res[JID_LEN];
+ static char dest[JID_LEN];
+ xmpp_user_t *usr;
+
+ /* Server channels */
+ if (!jid || match(jid, srv->srv))
+ return NULL;
+
+ /* Parse JID and check for MUC */
+ split_jid(jid, jid_usr, jid_srv, jid_res);
+ if (match(jid_srv, srv->muc))
+ is_muc = 1;
+
+ /* Channel notices have no resource */
+ if (is_muc && !jid_res[0])
+ return NULL;
+
+ /* Ignore resources for real users */
+ if (is_muc)
+ snprintf(dest, JID_LEN, "%s@%s/%s",
+ jid_usr,
+ jid_srv[0] ? jid_srv : srv->muc,
+ jid_res);
+ else
+ snprintf(dest, JID_LEN, "%s@%s",
+ jid_usr,
+ jid_srv[0] ? jid_srv : srv->srv);
+
+ /* Find existing users */
+ for (user_t *cur = users; cur; cur = cur->next) {
+ if (cur->server != &srv->server)
+ continue;
+ usr = (xmpp_user_t *)cur;
+ if (match(usr->dest, dest)) {
+ debug("xmpp: found user: \"%s\" -> "
+ "name=[%s] dest=[%s] alias=[%s]",
+ jid, usr->user.name, usr->dest,
+ usr->user.alias ? usr->user.alias->name : "(none)");
+ return usr;
}
}
- /* Info queries */
- if (srv->state == XMPP_READY) {
- if (match(tag, "item"))
- chat_notice(NULL, NULL, "item: [%s] %s",
- find_attr(attrs, "jid"),
- find_attr(attrs, "name"));
- if (match(tag, "identity"))
- chat_notice(NULL, NULL, "identity: %s",
- find_attr(attrs, "name"));
- if (match(tag, "feature"))
- chat_notice(NULL, NULL, "feature: %s",
- find_attr(attrs, "var"));
+ /* Create a new user */
+ usr = new0(xmpp_user_t);
+ usr->user.server = &srv->server;
+ if (is_muc) {
+ usr->user.name = strcopy(jid_res);
+ usr->full_name = strcopy(jid_res);
+ } else {
+ usr->user.name = strcopy(jid_usr);
}
+ usr->muc = is_muc;
+ strncpy(usr->dest, dest, JID_LEN);
+ add_user(&usr->user);
- /* Error handling */
- if (match(tag, "stream:error"))
- srv->in_error = 1;
+ /* Send vcard probe */
+ debug("xmpp: added user: \"%s\"%s -> name=[%s] dest=[%s]",
+ jid, is_muc ? " (muc)" : "",
+ usr->user.name, usr->dest);
+
+ return usr;
}
-static void on_end(void *_srv, const char *tag)
+static int match_user(xmpp_user_t *usr, const char *ptrn)
{
- xmpp_server_t *srv = _srv;
- const char *data = srv->buf.data;
+ char tmp[32];
- /* Debug print */
- if (srv->buf.len) {
- debug("%*s \"%s\"",
- srv->indent*4, "",
- (char*)srv->buf.data);
- srv->buf.len = 0;
+ const char *user = usr->dest;
+ const char *full = usr->full_name;
+ const char *nick = usr->mention_name;
+
+ /* Match all users */
+ if (!ptrn || !ptrn[0])
+ return 1;
+
+ /* Match user id and mention name */
+ if (user && strcasestr(user, ptrn) == user)
+ return 1;
+ if (nick && strcasestr(nick, ptrn) == nick)
+ return 1;
+
+ /* Full name matching */
+ if (!full || !full[0])
+ return 0;
+
+ /* Match first name */
+ if (strcasestr(full, ptrn) == full)
+ return 1;
+
+ /* Match last name */
+ snprintf(tmp, sizeof(tmp), " %s", ptrn);
+ if (strcasestr(full, tmp))
+ return 1;
+ snprintf(tmp, sizeof(tmp), "-%s", ptrn);
+ if (strcasestr(full, tmp))
+ return 1;
+
+ /* Match first initial last name */
+ if (tolower(full[0]) == tolower(ptrn[0])) {
+ snprintf(tmp, sizeof(tmp), " %s", &ptrn[1]);
+ if (strcasestr(full, tmp))
+ return 1;
+ snprintf(tmp, sizeof(tmp), "-%s", &ptrn[1]);
+ if (strcasestr(full, tmp))
+ return 1;
}
- srv->indent--;
- /* Receive messages */
- if (srv->state == XMPP_READY) {
- if (match(tag, "body")) {
- debug("xmpp: jid -> ready");
- chat_recv("#test", NULL, data);
+ return 0;
+}
+
+static int match_channel(xmpp_channel_t *chan, const char *ptrn)
+{
+ const char *dest = chan->dest;
+ const char *room = chan->room;
+
+ /* Match all users */
+ if (!ptrn || !ptrn[0])
+ return 1;
+
+ /* Match dest and channel name */
+ if (dest && strcasestr(dest, ptrn))
+ return 1;
+ if (room && strcasestr(room, ptrn))
+ return 1;
+
+ return 0;
+}
+
+static void complete_xmpp_user(xmpp_server_t *srv, const char *prefix, int mention)
+{
+ for (user_t *cur = users; cur; cur = cur->next) {
+ if (cur->server != &srv->server)
+ continue;
+ xmpp_user_t *usr = (xmpp_user_t *)cur;
+ if (usr->muc)
+ continue;
+ if (match_user(usr, prefix)) {
+ if (!mention)
+ complete_item(prefix, usr->dest, usr->full_name);
+ else if (usr->mention_name)
+ complete_item(prefix, usr->mention_name, usr->full_name);
+ else
+ complete_item(prefix, usr->full_name, "");
}
}
+}
- /* Error handling */
- if (match(tag, "stream:error"))
- srv->in_error = 0;
- if (srv->in_error) {
- if (match(tag, "text")) {
- debug("xmpp: error: %s", data);
- chat_notice(NULL, NULL, "error: %s", data);
- }
+static void complete_xmpp_channel(xmpp_server_t *srv, const char *prefix)
+{
+ for (channel_t *cur = channels; cur; cur = cur->next) {
+ if (cur->server != &srv->server)
+ continue;
+ xmpp_channel_t *chan = (xmpp_channel_t *)cur;
+ if (!chan->muc)
+ continue;
+ if (match_channel(chan, prefix))
+ complete_item(prefix, chan->dest, chan->name ?: chan->room);
}
}
-static void on_data(void *_srv, const char *data, int len)
+static const char *find_attr(const char **attrs, const char *name)
+{
+ for (int i = 0; attrs[i] && attrs[i+1]; i += 2)
+ if (match(attrs[i+0], name))
+ return attrs[i+1];
+ return NULL;
+}
+
+static void lookup_user(xmpp_server_t *srv, xmpp_user_t *usr)
+{
+ if (usr->full_name)
+ return;
+ net_print(&srv->net,
+ "<iq id='auto-vcard' type='get' from='%s' to='%s'>"
+ "<vCard xmlns='vcard-temp'/>"
+ "</iq>",
+ srv->bind, usr->dest);
+}
+
+/* Callback functions */
+static void xmpp_run(xmpp_server_t *srv, int idle,
+ const char *start, const char **attrs,
+ const char *end, const char *data);
+
+static void on_start(void *_srv, const char *tag, const char **attrs)
{
xmpp_server_t *srv = _srv;
+ xmpp_run(srv, 0, tag, attrs, NULL, reset(&srv->buf));
+}
- /* Debug print */
+static void on_data(void *_srv, const char *data, int len)
+{
+ xmpp_server_t *srv = _srv;
append(&srv->buf, data, len);
}
-static void on_recv(void *_srv, char *buf, int len)
+static void on_end(void *_srv, const char *tag)
{
- static char plain[AUTH_LEN];
- static char coded[AUTH_LEN];
- const char *resource;
+ xmpp_server_t *srv = _srv;
+ xmpp_run(srv, 0, NULL, NULL, tag, reset(&srv->buf));
+}
+static void on_send(void *_srv)
+{
xmpp_server_t *srv = _srv;
+ xmpp_run(srv, 0, NULL, NULL, NULL, NULL);
+}
- /* Parse input */
+static void on_recv(void *_srv, char *buf, int len)
+{
+ xmpp_server_t *srv = _srv;
if (len > 0)
XML_Parse(srv->expat, buf, len, 0);
+ xmpp_run(srv, 0, NULL, NULL, NULL, NULL);
+}
+
+static void on_err(void *_srv, int err)
+{
+ xmpp_server_t *srv = _srv;
+ srv_notice(srv, "Server disconnected");
+ srv->state = XMPP_DEAD;
+}
+
+static void on_timer(void *_srv)
+{
+ xmpp_server_t *srv = _srv;
+ xmpp_run(srv, 1, NULL, NULL, NULL, NULL);
+}
+
+/* XMPP State machine */
+static void xmpp_run(xmpp_server_t *srv, int idle,
+ const char *start, const char **attrs,
+ const char *end, const char *data)
+{
+ /* Debug print */
+ if (data)
+ debug("%*s \"%s\"", srv->level*4, "", data);
+ if (start) {
+ debug("%*s<%s>", srv->level*4, "", start);
+ for (int i = 0; attrs[i] && attrs[i+1]; i += 2) {
+ debug("%*s%s=\"%s\"%s",
+ srv->level*4+8, "",
+ attrs[i+0], attrs[i+1],
+ attrs[i+2] ? "" : ">");
+ }
+ }
+ if (start)
+ srv->level++;
+ if (end)
+ srv->level--;
+
+ /* Connection Handling */
+ if (srv->state == XMPP_CONNECT && !start && !end) {
+ srv->net.send = on_send;
+ srv->net.recv = on_recv;
+ srv->net.err = on_err;
+ srv->net.data = srv;
+
+ srv->idle.timer = on_timer;
+ srv->idle.data = srv;
+
+ net_open(&srv->net, srv->host, srv->port);
+ idle_add(&srv->idle);
+ idle_set(&srv->idle, srv->timeout, srv->timeout);
+
+ if (!(srv->expat = XML_ParserCreate(NULL)))
+ error("creating XML parser");
+ XML_SetUserData(srv->expat, srv);
+ XML_SetStartElementHandler(srv->expat, on_start);
+ XML_SetEndElementHandler(srv->expat, on_end);
+ XML_SetCharacterDataHandler(srv->expat, on_data);
+
+ debug("xmpp: connect -> stream");
+ srv->level = 0;
+ srv->state = XMPP_SEND_STREAM;
+ }
+ if (srv->state == XMPP_ENCRYPT && !start && !end) {
+ net_encrypt(&srv->net, srv->noverify ? NET_NOVERIFY : 0);
+
+ if (!(XML_ParserReset(srv->expat, NULL)))
+ error("resetting XML parser");
+ XML_SetUserData(srv->expat, srv);
+ XML_SetStartElementHandler(srv->expat, on_start);
+ XML_SetEndElementHandler(srv->expat, on_end);
+ XML_SetCharacterDataHandler(srv->expat, on_data);
+
+ debug("xmpp: encrypt -> stream");
+ srv->level = 0;
+ srv->state = XMPP_SEND_STREAM;
+ }
+ if (srv->state == XMPP_RESTART && !start && !end) {
+ if (!(XML_ParserReset(srv->expat, NULL)))
+ error("resetting XML parser");
+ XML_SetUserData(srv->expat, srv);
+ XML_SetStartElementHandler(srv->expat, on_start);
+ XML_SetEndElementHandler(srv->expat, on_end);
+ XML_SetCharacterDataHandler(srv->expat, on_data);
+
+ debug("xmpp: restart -> stream");
+ srv->level = 0;
+ srv->state = XMPP_SEND_STREAM;
+ }
+
+ /* Idle handling */
+ if (srv->state > XMPP_CONNECT && idle) {
+ debug("xmpp: idle");
+ net_print(&srv->net, " ");
+ }
- /* State machine */
-output:
+ /* Stream Start */
if (srv->state == XMPP_SEND_STREAM) {
if (net_print(&srv->net,
"<?xml version='1.0'?>"
" xml:lang='en'"
" xmlns='jabber:client'"
" xmlns:stream='http://etherx.jabber.org/streams'>",
- srv->jid, srv->host)) {
+ srv->jid, srv->srv)) {
debug("xmpp: stream -> features");
srv->state = XMPP_RECV_FEATURES;
}
}
+ if (srv->state == XMPP_RECV_FEATURES) {
+ if (match(start, "starttls")) {
+ debug("xmpp: features -> starttls");
+ srv->state = XMPP_SEND_STARTTLS;
+ }
+ if (match(start, "mechanisms")) {
+ debug("xmpp: features -> auth");
+ srv->state = XMPP_SEND_AUTH;
+ }
+ if (match(start, "bind")) {
+ debug("xmpp: features -> bind");
+ srv->state = XMPP_SEND_BIND;
+ }
+ }
+
+ /* Start TLS */
if (srv->state == XMPP_SEND_STARTTLS) {
if (net_print(&srv->net,
"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")) {
srv->state = XMPP_RECV_PROCEED;
}
}
+ if (srv->state == XMPP_RECV_PROCEED) {
+ if (match(start, "proceed")) {
+ debug("xmpp: proceed -> encrypt");
+ srv->state = XMPP_ENCRYPT;
+ }
+ }
+
+ /* Authentication */
if (srv->state == XMPP_SEND_AUTH) {
+ static char plain[AUTH_LEN];
+ static char coded[AUTH_LEN];
+ int len;
len = snprintf(plain, AUTH_LEN, "%s%c%s%c%s",
srv->user, '\0', srv->user, '\0', srv->pass);
len = base64(plain, len, coded, AUTH_LEN);
srv->state = XMPP_RECV_SUCCESS;
}
}
+ if (srv->state == XMPP_RECV_SUCCESS) {
+ if (match(start, "failure")) {
+ debug("xmpp: success -> dead");
+ srv_notice(srv, "Authentication failure");
+ srv->state = XMPP_DEAD;
+ }
+ if (match(start, "success")) {
+ debug("xmpp: success -> restart");
+ srv_notice(srv, "Authentication success");
+ srv->state = XMPP_RESTART;
+ }
+ }
+
+ /* Binding */
if (srv->state == XMPP_SEND_BIND) {
- resource = srv->jid;
+ const char *resource = srv->jid;
while (*resource && *resource != '/')
resource++;
while (*resource && *resource == '/')
"<resource>%s</resource>"
"</bind>"
"</iq>",
- resource)) {
+ resource)) {
debug("xmpp: bind -> jid");
srv->state = XMPP_RECV_JID;
}
}
- if (srv->state == XMPP_PRESENCE) {
- for (xmpp_channel_t *chan = channels; chan; chan = chan->next) {
+ if (srv->state == XMPP_RECV_JID) {
+ if (match(end, "jid")) {
+ debug("xmpp: jid -> session");
+ strset(&srv->bind, data ?: srv->jid);
+ srv->state = XMPP_SEND_SESSION;
+ }
+ }
+ if (srv->state == XMPP_SEND_SESSION) {
+ if (net_print(&srv->net,
+ "<iq id='session' type='set' to='%s'>"
+ "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>"
+ "</iq>",
+ srv->srv)) {
+ debug("xmpp: session -> presence");
+ srv->state = XMPP_SEND_PRESENCE;
+ }
+ }
+ if (srv->state == XMPP_SEND_PRESENCE) {
+ if (net_print(&srv->net, "<presence/>")) {
+ debug("xmpp: presence -> roster");
+ srv->state = XMPP_SEND_ROSTER;
+ }
+ }
+ if (srv->state == XMPP_SEND_ROSTER) {
+ if (net_print(&srv->net,
+ "<iq id='roster' type='get'>"
+ "<query xmlns='jabber:iq:roster'/>"
+ "</iq>")) {
+ debug("xmpp: roster -> rooms");
+ srv->state = XMPP_SEND_ROOMS;
+ }
+ }
+ if (srv->state == XMPP_SEND_ROOMS) {
+ if (net_print(&srv->net,
+ "<iq id='rooms' type='get' to='%s'>"
+ "<query xmlns='http://jabber.org/protocol/disco#items'/>"
+ "</iq>",
+ srv->muc)) {
+ debug("xmpp: rooms -> join");
+ srv->state = XMPP_SEND_JOIN;
+ }
+ }
+ if (srv->state == XMPP_SEND_JOIN) {
+ for (channel_t *cur = channels; cur; cur = cur->next) {
+ if (cur->server != &srv->server)
+ continue;
+ xmpp_channel_t *chan = (xmpp_channel_t *)cur;
if (!chan->join)
continue;
+ if (!srv->quiet)
+ chan_notice(chan, "XMPP Channel: %s", chan->channel.name);
net_print(&srv->net,
- "<presence id='join' from='%s' to='%s@%s/%s'>"
+ "<presence id='join' from='%s' to='%s/%s'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"</presence>",
- srv->jid, chan->channel, srv->muc, srv->nick);
+ srv->bind, chan->dest, srv->nick);
}
+ debug("xmpp: join -> ready");
srv->state = XMPP_READY;
}
- if (srv->state == XMPP_ENCRYPT) {
- /* Encrypt connection */
- net_encrypt(&srv->net);
- /* Reset Expat */
- if (!(XML_ParserReset(srv->expat, NULL)))
- error("Error resetting XML parser");
- XML_SetUserData(srv->expat, srv);
- XML_SetStartElementHandler(srv->expat, on_start);
- XML_SetEndElementHandler(srv->expat, on_end);
- XML_SetCharacterDataHandler(srv->expat, on_data);
+ /* Start message */
+ if (srv->state == XMPP_READY && start) {
+ const char *id = find_attr(attrs, "id");
+ const char *from = find_attr(attrs, "from");
+ const char *type = find_attr(attrs, "type");
+ int is_muc = match(type, "groupchat");
- /* Reset server */
- debug("xmpp: encrypt -> stream");
- srv->state = XMPP_SEND_STREAM;
- goto output;
+ /* Ignore presence errors (federated remote timeout, etc) */
+ if (match(type, "error"))
+ return;
+
+ strncpy(srv->msg_id, id ?: "", ID_LEN);
+
+ srv->msg_chan = find_channel(srv, from, is_muc);
+ srv->msg_usr = find_user(srv, from, is_muc);
+
+ if (match(start, "iq"))
+ srv->state = XMPP_IN_IQ;
+ if (match(start, "message"))
+ srv->state = XMPP_IN_MESSAGE;
+ if (match(start, "presence"))
+ srv->state = XMPP_IN_PRESENCE;
+
+ if (srv->state != XMPP_READY)
+ debug("xmpp: ready -> in_%s -- "
+ "from=[%s] -> chan=[%s:%s] user=[%s]",
+ start, from,
+ srv->msg_chan->type,
+ srv->msg_chan->channel.name,
+ srv->msg_usr ? srv->msg_usr->user.name : "(none)");
}
- if (srv->state == XMPP_RESTART) {
- /* Reset Expat */
- if (!(XML_ParserReset(srv->expat, NULL)))
- error("Error resetting XML parser");
- XML_SetUserData(srv->expat, srv);
- XML_SetStartElementHandler(srv->expat, on_start);
- XML_SetEndElementHandler(srv->expat, on_end);
- XML_SetCharacterDataHandler(srv->expat, on_data);
- /* Reset server */
- debug("xmpp: restart -> stream");
- srv->state = XMPP_SEND_STREAM;
- goto output;
+ /* Shorthand Message Data */
+ xmpp_channel_t *chan = NULL;
+ xmpp_user_t *usr = NULL;
+ xmpp_user_t *alias = NULL;
+
+ if (srv->state > XMPP_READY) {
+ chan = srv->msg_chan;
+ usr = srv->msg_usr;
+ if (usr)
+ alias = (xmpp_user_t*)usr->user.alias;
}
-}
-static void xmpp_connect(xmpp_server_t *srv)
-{
- /* Net connect */
- srv->net.recv = on_recv;
- srv->net.data = srv;
- net_open(&srv->net, srv->host, srv->port);
-
- /* Setup Expat */
- if (!(srv->expat = XML_ParserCreate(NULL)))
- error("Error creating XML parser");
- XML_SetUserData(srv->expat, srv);
- XML_SetStartElementHandler(srv->expat, on_start);
- XML_SetEndElementHandler(srv->expat, on_end);
- XML_SetCharacterDataHandler(srv->expat, on_data);
-
- /* Setup server */
- srv->state = XMPP_SEND_STREAM;
+ /* Info/Queries */
+ if (srv->state == XMPP_IN_IQ) {
+ if (match(start, "item")) {
+ if (chan == &srv->system) {
+ srv_notice(srv, "item: [%s] %s",
+ find_attr(attrs, "jid"),
+ find_attr(attrs, "name"));
+ } else if (chan->muc) {
+ const char *jid = find_attr(attrs, "jid");
+ xmpp_user_t *usr = find_user(srv, jid, 1);
+ if (jid && usr)
+ chan_notice(chan, "User: %s (%s)",
+ usr->user.name, jid);
+ } else {
+ chan_notice(chan, "item: [%s] %s",
+ find_attr(attrs, "jid"),
+ find_attr(attrs, "name"));
+ }
+ }
+ if (match(start, "identity")) {
+ srv_notice(srv, "identity: %s",
+ find_attr(attrs, "name"));
+ }
+ if (match(start, "feature")) {
+ srv_notice(srv, "feature: %s",
+ find_attr(attrs, "var"));
+ }
+ if (match(start, "field")) {
+ debug("xmpp: %s -- type=[%s] label=[%s]", end,
+ find_attr(attrs, "type"),
+ find_attr(attrs, "label"));
+ if (!find_attr(attrs, "label"))
+ return;
+ chan_notice(chan, "%-36s -- %s (%s)",
+ find_attr(attrs, "var"),
+ find_attr(attrs, "label"),
+ find_attr(attrs, "type"));
+ }
+ if (match(end, "title")) {
+ debug("xmpp: title -- chan=[%s]",
+ end, chan->dest);
+ chan_notice(chan, "Title: %s", data);
+ }
+ if (match(end, "instructions")) {
+ debug("xmpp: instructions -- chan=[%s]",
+ end, chan->dest);
+ chan_notice(chan, "%s", data);
+ }
+ }
+
+ /* vCards */
+ if (srv->state == XMPP_IN_IQ) {
+ if (match(start, "vCard")) {
+ if (!match(srv->msg_id, "auto-vcard"))
+ chan_notice(chan, "Begin vCard (%s)",
+ usr ? usr->user.name :
+ chan->channel.name);
+ srv->state = XMPP_IN_VCARD;
+ }
+ }
+ if (srv->state == XMPP_IN_VCARD) {
+ if (end && srv->level == 3) {
+ if (!match(srv->msg_id, "auto-vcard") &&
+ !match(end, "BINVAL"))
+ chan_notice(chan, " %s -> %s", end, data ?: "...");
+ }
+ if (usr) {
+ if (match(end, "FN")) {
+ strset(&chan->channel.name, data);
+ strset(&usr->user.name, data);
+ strset(&usr->full_name, data);
+ chat_update();
+ }
+ }
+ }
+ if (srv->state == XMPP_IN_VCARD) {
+ if (match(end, "vCard")) {
+ if (!match(srv->msg_id, "auto-vcard"))
+ chan_notice(chan, "End vCard");
+ srv->state = XMPP_IN_IQ;
+ }
+ }
+
+ /* Rosters */
+ if (srv->state == XMPP_IN_IQ) {
+ if (match(srv->msg_id, "roster"))
+ srv->state = XMPP_IN_ROSTER;
+ }
+ if (srv->state == XMPP_IN_ROSTER) {
+ if (match(start, "item")) {
+ // Todo: cleanup name setting
+ const char *jid = find_attr(attrs, "jid");
+ const char *fname = find_attr(attrs, "name");
+ const char *mname = find_attr(attrs, "mention_name");
+ xmpp_user_t *usr = find_user(srv, jid, 0);
+ xmpp_channel_t *chan = find_channel(srv, jid, 0);
+ if (usr) {
+ strset(&usr->full_name, fname);
+ strset(&usr->user.name, fname);
+ strset(&usr->mention_name, mname);
+ }
+ if (chan) {
+ strset(&chan->channel.name, fname);
+ }
+ }
+ }
+ if (srv->state == XMPP_IN_ROSTER) {
+ if (match(end, "iq"))
+ srv->state = XMPP_IN_IQ;
+ }
+
+ /* Rooms */
+ if (srv->state == XMPP_IN_IQ) {
+ if (match(srv->msg_id, "rooms"))
+ srv->state = XMPP_IN_ROOMS;
+ }
+ if (srv->state == XMPP_IN_ROOMS) {
+ if (match(start, "item")) {
+ const char *jid = find_attr(attrs, "jid");
+ xmpp_channel_t *chan = find_channel(srv, jid, 1);
+ if (jid && chan) {
+ strset(&chan->room,
+ find_attr(attrs, "name"));
+ strset(&chan->name,
+ find_attr(attrs, "name"));
+ }
+ }
+ }
+ if (srv->state == XMPP_IN_ROOMS) {
+ if (match(end, "iq"))
+ srv->state = XMPP_IN_IQ;
+ }
+
+ /* Messages */
+ if (srv->state == XMPP_IN_MESSAGE) {
+ if (match(start, "delay")) {
+ const char *ts = find_attr(attrs, "stamp");
+ if (ts) {
+ struct tm tm = {};
+ strptime(ts, "%Y-%m-%dT%H:%M:%S", &tm);
+ srv->msg_stamp = timegm(&tm);
+ }
+ }
+ if (match(end, "subject")) {
+ strset(&chan->channel.topic, data);
+ if (!srv->quiet)
+ chan_notice(chan, "Topic: %s", data);
+ }
+ if (match(end, "body") && data) {
+ strset(&srv->msg_body, data);
+ }
+ if (match(start, "html")) {
+ srv->in_html = 1;
+ }
+ if (srv->in_html && data) {
+ append(&srv->html, data, strlen(data));
+ }
+ if (match(end, "html")) {
+ strset(&srv->msg_html, reset(&srv->html));
+ srv->in_html = 0;
+ }
+ if (match(end, "message")) {
+ debug("xmpp: body (%s) -- chan=[%s] from=[%s]",
+ chan->type,
+ chan->channel.name,
+ usr ? usr->user.name : "(none)");
+ char *content = srv->msg_body;
+ if (srv->msg_html)
+ content = despace(srv->msg_html);
+ if (usr && !usr->muc)
+ lookup_user(srv, usr);
+ if (content) {
+ chat_recv(&chan->channel, &usr->user, content);
+ message_t *msg = &messages[history-1];
+ msg->when = srv->msg_stamp ?: msg->when;
+ }
+ srv->msg_stamp = 0;
+ strset(&srv->msg_body, NULL);
+ strset(&srv->msg_html, NULL);
+ }
+
+ }
+
+ /* Presence */
+ if (srv->state == XMPP_IN_PRESENCE) {
+ if (match(start, "item") && usr) {
+ const char *jid = find_attr(attrs, "jid");
+ xmpp_user_t *alias = find_user(srv, jid, 0);
+ if (alias)
+ usr->user.alias = &alias->user;
+ if (alias && !alias->full_name && usr->full_name) {
+ strset(&alias->user.name, usr->full_name);
+ strset(&alias->full_name, usr->full_name);
+ }
+ xmpp_channel_t *chan = find_channel(srv, jid, 0);
+ if (alias && alias->full_name && chan) {
+ strset(&chan->channel.name, alias->full_name);
+ }
+ }
+ if (match(end, "presence") && !srv->quiet) {
+ if (alias)
+ chan_notice(chan, "%s (%s) entered room.",
+ usr->user.name, alias->user.name);
+ else if (usr)
+ chan_notice(chan, "%s entered room.",
+ usr->user.name);
+ }
+ }
+
+ /* End messages */
+ if (srv->state == XMPP_IN_IQ)
+ if (match(end, "iq"))
+ srv->state = XMPP_READY;
+ if (srv->state == XMPP_IN_MESSAGE)
+ if (match(end, "message"))
+ srv->state = XMPP_READY;
+ if (srv->state == XMPP_IN_PRESENCE)
+ if (match(end, "presence"))
+ srv->state = XMPP_READY;
+
+ /* Error handling */
+ if (match(start, "stream:error"))
+ srv->in_error = 1;
+ if (match(end, "stream:error"))
+ srv->in_error = 0;
+ if (srv->in_error) {
+ if (match(end, "text")) {
+ debug("xmpp: error: %s", data);
+ srv_notice(srv, "error: %s", data);
+ }
+ }
}
/* XMPP functions */
void xmpp_init(void)
{
- for (xmpp_server_t *cur = servers; cur; cur = cur->next) {
- if (!match(cur->protocol, "xmpp"))
+ static char jid_usr[JID_LEN];
+ static char jid_srv[JID_LEN];
+ static char jid_res[JID_LEN];
+
+ for (server_t *cur = servers; cur; cur = cur->next) {
+ if (cur->protocol != XMPP)
continue;
- if (!cur->port)
- cur->port = 5222;
- if (!cur->jid)
+
+ xmpp_server_t *srv = (xmpp_server_t*)cur;
+ split_jid(srv->jid, jid_usr, jid_srv, jid_res);
+
+ if (!srv->jid)
error("jid is required");
- if (cur->connect)
- xmpp_connect(cur);
+ if (!srv->host)
+ srv->host = strcopy(jid_srv);
+ if (!srv->port)
+ srv->port = 5222;
+ if (!srv->muc)
+ srv->muc = strcopy(srv->host);
+ if (!srv->srv)
+ srv->srv = strcopy(srv->host);
+ if (!srv->user)
+ srv->user = strcopy(jid_usr);
+ if (!srv->nick)
+ srv->nick = strcopy(jid_usr);
+ if (!srv->timeout)
+ srv->timeout = 60;
+
+ srv->system.type = "sys";
+ srv->system.channel.server = &srv->server;
+ srv->system.channel.name = strcopy(srv->server.name);
+
+ strncpy(srv->myself.dest, srv->jid, JID_LEN);
+ srv->myself.user.server = &srv->server;
+ srv->myself.user.name = strcopy(srv->nick);
+
+ if (srv->connect && !srv->quiet)
+ srv_notice(srv, "XMPP Server: %s", srv->server.name);
+ if (srv->connect) {
+ srv->state = XMPP_CONNECT;
+ xmpp_run(srv, 0, NULL, NULL, NULL, NULL);
+ }
+ }
+ for (channel_t *cur = channels; cur; cur = cur->next) {
+ if (cur->server->protocol != XMPP)
+ continue;
+
+ xmpp_channel_t *chan = (xmpp_channel_t*)cur;
+ xmpp_server_t *srv = (xmpp_server_t*)cur->server;
+ chan->muc = 1;
+ chan->type = "muc";
+ if (!chan->room)
+ chan->room = strcopy(cur->name);
+ snprintf(chan->dest, JID_LEN, "%s@%s",
+ chan->room, srv->muc);
}
}
-void xmpp_config(const char *group, const char *name, const char *key, const char *value)
+void xmpp_config(server_t *server, channel_t *channel,
+ const char *group, const char *name,
+ const char *key, const char *value)
{
- xmpp_server_t *srv;
- xmpp_channel_t *chan;
+ xmpp_server_t *srv = (xmpp_server_t*)server;
+ xmpp_channel_t *chan = (xmpp_channel_t*)channel;
if (match(group, "server")) {
- srv = find_server(name, 1);
- if (match(key, "protocol") &&
- match(value, "xmpp"))
- srv->protocol = get_string(value);
- if (match(srv->protocol, "xmpp")) {
- if (match(key, "connect"))
- srv->connect = get_bool(value);
- else if (match(key, "host"))
- srv->host = get_string(value);
- else if (match(key, "port"))
- srv->port = get_number(value);
- else if (match(key, "muc"))
- srv->muc = get_string(value);
- else if (match(key, "nick"))
- srv->nick = get_string(value);
- else if (match(key, "jid"))
- srv->jid = get_string(value);
- else if (match(key, "user"))
- srv->user = get_string(value);
- else if (match(key, "pass"))
- srv->pass = get_string(value);
- }
- } else if (match(group, "channel")) {
- chan = find_channel(name, 1);
- if (match(key, "server") &&
- find_server(value, 0))
- chan->server = get_string(value);
- if (chan->server) {
- if (match(key, "channel"))
- chan->channel = get_string(value);
- else if (match(key, "join"))
- chan->join = get_bool(value);
+ if (match(key, "protocol")) {
+ xmpp_server_t *srv = new0(xmpp_server_t);
+ srv->server.protocol = XMPP;
+ srv->server.name = strcopy(get_name(name));
+ add_server(&srv->server);
+ }
+ else if (match(key, "connect"))
+ srv->connect = get_bool(value);
+ else if (match(key, "timeout"))
+ srv->timeout = get_number(value);
+ else if (match(key, "noverify"))
+ srv->noverify = get_bool(value);
+ else if (match(key, "host"))
+ srv->host = get_string(value);
+ else if (match(key, "port"))
+ srv->port = get_number(value);
+ else if (match(key, "srv"))
+ srv->srv = get_string(value);
+ else if (match(key, "muc"))
+ srv->muc = get_string(value);
+ else if (match(key, "nick"))
+ srv->nick = get_string(value);
+ else if (match(key, "jid"))
+ srv->jid = get_string(value);
+ else if (match(key, "user"))
+ srv->user = get_string(value);
+ else if (match(key, "pass"))
+ srv->pass = get_string(value);
+ else if (match(key, "quiet"))
+ srv->quiet = get_bool(value);
+ }
+ if (match(group, "channel")) {
+ if (match(key, "server")) {
+ xmpp_channel_t *chan = new0(xmpp_channel_t);
+ chan->channel.server = &srv->server;
+ chan->channel.name = strcopy(get_name(name));
+ add_channel(&chan->channel);
}
+ else if (match(key, "room"))
+ chan->room = get_string(value);
+ else if (match(key, "join"))
+ chan->join = get_bool(value);
+ }
+ if (match(group, "autojoin")) {
+ xmpp_channel_t *chan = new0(xmpp_channel_t);
+ chan->channel.server = &srv->server;
+ chan->channel.name = strcopy(key);
+ chan->room = get_string(value);
+ chan->join = 1;
+ add_channel(&chan->channel);
}
}
-void xmpp_send(const char *channel, const char *msg)
+void xmpp_complete(channel_t *channel, const char *text)
{
+ xmpp_server_t *srv = (xmpp_server_t*)channel->server;
const char *arg;
- xmpp_channel_t *chan = find_channel(channel, 0);
- if (!chan)
- return;
- xmpp_server_t *srv = find_server(chan->server, 0);
- if (!srv || !srv->protocol)
- return;
+ if (suffix(text, "@", &arg)) {
+ complete_xmpp_user(srv, arg, 1);
+ }
+ else if (prefix(text, "/join ", &arg)) {
+ complete_xmpp_channel(srv, arg);
+ }
+ else if (prefix(text, "/query ", &arg)) {
+ complete_xmpp_user(srv, arg, 0);
+ }
+ else if (prefix(text, "/vcard ", &arg)) {
+ complete_xmpp_user(srv, arg, 0);
+ }
+ else {
+ complete_args(text,
+ "/items ", "Query XMPP server 'items'",
+ "/info ", "Query XMPP server 'info'",
+ "/names ", "Query XMPP channel users",
+ "/join", "Join XMPP (muc) room",
+ "/config ", "Configure XMPP (muc) room",
+ "/query ", "Open XMPP user chat",
+ "/vcard ", "Send XMPP vCard query",
+ NULL);
+ }
+}
+
+void xmpp_send(channel_t *channel, const char *text)
+{
+ static char buf[4096];
+
+ xmpp_channel_t *chan = (xmpp_channel_t*)channel;
+ xmpp_server_t *srv = (xmpp_server_t*)channel->server;
+ const char *arg;
+
+ /* Escape HTML */
+ const char *raw = text;
+ escape(buf, text, sizeof(buf));
+ text = buf;
/* Handle commands */
- if (msg[0] == '/') {
- if (prefix(msg, "/items", &arg)) {
+ if (text[0] == '/' && text[1] != '/') {
+ if (prefix(text, "/items", &arg)) {
net_print(&srv->net,
"<iq id='items' type='get' from='%s' to='%s'>"
- "<query xmlns='http://jabber.org/protocol/disco#items' />"
+ "<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>",
- srv->jid, arg ?: srv->host);
+ srv->bind, arg ?: srv->srv);
}
- else if (prefix(msg, "/info", &arg)) {
+ else if (prefix(text, "/info", &arg)) {
net_print(&srv->net,
"<iq id='info' type='get' from='%s' to='%s'>"
- "<query xmlns='http://jabber.org/protocol/disco#info' />"
+ "<query xmlns='http://jabber.org/protocol/disco#info'/>"
"</iq>",
- srv->jid, arg ?: srv->host);
+ srv->bind, arg ?: srv->srv);
}
- else if (prefix(msg, "/list", &arg)) {
+ else if (prefix(text, "/names", &arg)) {
+ if (arg)
+ chan = find_channel(srv, arg, 1);
+ if (chan == &srv->system) {
+ chan_notice(chan, "Cannot get names from server");
+ return;
+ }
net_print(&srv->net,
- "<iq id='list' type='get' from='%s' to='%s'>"
- "<query xmlns='http://jabber.org/protocol/disco#items' />"
+ "<iq id='names' type='get' from='%s' to='%s'>"
+ "<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>",
- srv->jid, arg ?: srv->muc);
+ srv->bind, chan->dest);
}
- else if (prefix(msg, "/join", &arg)) {
+ else if (prefix(text, "/join", &arg)) {
if (!arg) {
- chat_notice(NULL, NULL, "usage: /join <channel>");
+ chan_notice(chan, "usage: /join <channel>");
return;
}
+ chan = find_channel(srv, arg, 1);
net_print(&srv->net,
- "<presence id='join' from='%s' to='%s@%s/%s'>"
+ "<presence id='join' from='%s' to='%s/%s'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"</presence>",
- srv->jid, arg, srv->muc, srv->nick);
+ srv->bind, chan->dest, srv->nick);
+ chan_notice(chan, "Room: %s", arg);
+ }
+ else if (prefix(text, "/config", &arg)) {
+ if (arg) {
+ chan_notice(chan, "Unimplemented: /config <arg>");
+ return;
+ }
+ if (chan == &srv->system) {
+ chan_notice(chan, "Cannot get config from server");
+ return;
+ }
+ net_print(&srv->net,
+ "<iq id='config' type='get' from='%s' to='%s'>"
+ "<query xmlns='http://jabber.org/protocol/muc#owner'/>"
+ "</iq>",
+ srv->bind, chan->dest);
+ }
+ else if (prefix(text, "/query", &arg)) {
+ if (!arg) {
+ chan_notice(chan, "usage: /query <user>");
+ return;
+ }
+ chan = find_channel(srv, arg, 0);
+ chan_notice(chan, "User: %s", arg);
+ }
+ else if (prefix(text, "/vcard", &arg)) {
+ if (arg)
+ chan = find_channel(srv, arg, 0);
+ if (chan)
+ net_print(&srv->net,
+ "<iq id='vcard' type='get' from='%s' to='%s'>"
+ "<vCard xmlns='vcard-temp'/>"
+ "</iq>",
+ srv->bind, chan->dest);
}
else {
- debug("unknown: [%s]", msg);
- chat_notice(NULL, NULL,
- "unknown command %s", msg);
+ chan_notice(chan, "Unknown command %s", text);
}
} else {
- debug("message: [%s]", msg);
- net_print(&srv->net,
- "<message to='%s'><body>%s</body></message>",
- "andy@pileus.org", msg);
+ debug("message: [%s]", text);
+ if (text[0] == '/')
+ text = &text[1];
+ if (chan == &srv->system) {
+ chan_notice(chan, "Cannot send to server");
+ }
+ else if (chan->muc) {
+ net_print(&srv->net,
+ "<message id='chat%d' from='%s' to='%s' type='groupchat'>"
+ "<body>%s</body>"
+ "</message>",
+ srv->id++, srv->bind, chan->dest, text);
+ } else {
+ net_print(&srv->net,
+ "<message id='chat%d' from='%s' to='%s' type='chat'>"
+ "<body>%s</body>"
+ "</message>",
+ srv->id++, srv->bind, chan->dest, text);
+ chat_recv(channel, &srv->myself.user, raw);
+ }
}
}