]> Pileus Git - ~andy/lamechat/blobdiff - xmpp.c
Make logfile optional.
[~andy/lamechat] / xmpp.c
diff --git a/xmpp.c b/xmpp.c
index 3bcb846500209b2ecc53a7f44ada0d2f0631b135..374e6615d4c286c4525fdf49503b8ee24a4156a7 100644 (file)
--- a/xmpp.c
+++ b/xmpp.c
 #include <stdlib.h>
 #include <stdarg.h>
 #include <string.h>
+#include <ctype.h>
 #include <errno.h>
 #include <expat.h>
+#include <time.h>
 
 #include "util.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,
@@ -46,48 +51,77 @@ typedef enum {
        XMPP_RECV_SUCCESS,
        XMPP_SEND_BIND,
        XMPP_RECV_JID,
+       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 {
+       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;
 
-       const char     *room;
+       char           *room;
+       char           *name;
        int             join;
 } xmpp_channel_t;
 
 typedef struct {
        server_t        server;
+       xmpp_user_t     myself;
        xmpp_channel_t  system;
 
        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;
-       int             indent;
-
-       int             in_error;
+       buf_t           html;
+       int             level;
+       int             id;
+       char           *bind;
 
-       char            msg_jid[JID_LEN];
-       char            msg_usr[JID_LEN];
-       char            msg_srv[JID_LEN];
-       char            msg_res[JID_LEN];
-       char           *msg_from;
+       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;
+
+       int             in_html;
+       int             in_error;
 } xmpp_server_t;
 
 /* Helper functions */
@@ -100,6 +134,7 @@ static void srv_notice(xmpp_server_t *srv, const char *fmt, ...)
        vsnprintf(buf, sizeof(buf), fmt, ap);
        va_end(ap);
 
+       debug("xmpp: srv_notice: [%s]", buf);
        chat_recv(&srv->system.channel, NULL, buf);
 }
 
@@ -112,6 +147,8 @@ static void chan_notice(xmpp_channel_t *chan, const char *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);
 }
 
@@ -124,6 +161,8 @@ static void split_jid(const char *jid, char *usr, char *srv, char *res)
        if (srv) srv[0] = '\0';
        if (res) res[0] = '\0';
 
+       while (jid && *jid == ' ')
+               jid++;
        for (int i = 0; jid && jid[i]; i++) {
                switch (jid[i]) {
                        case '@':
@@ -140,12 +179,14 @@ static void split_jid(const char *jid, char *usr, char *srv, char *res)
                        ptr[pos] = '\0';
                }
        }
+       while (ptr && pos >= 1 && ptr[pos-1] == ' ')
+               ptr[--pos] = '\0';
 
        //debug("JID: '%s' usr=[%s] srv=[%s] res=[%s]",
        //              jid, usr, srv, res);
 }
 
-static xmpp_channel_t *find_dest(xmpp_server_t *srv,
+static xmpp_channel_t *find_channel(xmpp_server_t *srv,
                const char *jid, int is_muc)
 {
        static char jid_usr[JID_LEN];
@@ -153,14 +194,19 @@ static xmpp_channel_t *find_dest(xmpp_server_t *srv,
        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->host);
-
-       /* Server channels */
-       if (match(jid, srv->host))
-               return &srv->system;
+                is_muc     ? srv->muc : srv->srv);
 
        /* Find existing channels */
        for (channel_t *cur = channels; cur; cur = cur->next) {
@@ -172,11 +218,181 @@ static xmpp_channel_t *find_dest(xmpp_server_t *srv,
        }
 
        /* Create a new channel */
-       chan = (xmpp_channel_t *)add_channel(jid_usr, &srv->server);
+       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;
+               }
+       }
+
+       /* 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);
+
+       /* 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 int match_user(xmpp_user_t *usr, const char *ptrn)
+{
+       char tmp[32];
+
+       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;
+       }
+
+       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, "");
+               }
+       }
+}
+
+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 const char *find_attr(const char **attrs, const char *name)
 {
        for (int i = 0; attrs[i] && attrs[i+1]; i += 2)
@@ -185,15 +401,26 @@ static const char *find_attr(const char **attrs, const char *name)
        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,
+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, tag, attrs, NULL, reset(&srv->buf));
+       xmpp_run(srv, 0, tag, attrs, NULL, reset(&srv->buf));
 }
 
 static void on_data(void *_srv, const char *data, int len)
@@ -205,13 +432,13 @@ static void on_data(void *_srv, const char *data, int len)
 static void on_end(void *_srv, const char *tag)
 {
        xmpp_server_t *srv = _srv;
-       xmpp_run(srv, NULL, NULL, tag, reset(&srv->buf));
+       xmpp_run(srv, 0, NULL, NULL, tag, reset(&srv->buf));
 }
 
 static void on_send(void *_srv)
 {
        xmpp_server_t *srv = _srv;
-       xmpp_run(srv, NULL, NULL, NULL, NULL);
+       xmpp_run(srv, 0, NULL, NULL, NULL, NULL);
 }
 
 static void on_recv(void *_srv, char *buf, int len)
@@ -219,38 +446,43 @@ 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, NULL, NULL, NULL, NULL);
+       xmpp_run(srv, 0, NULL, NULL, NULL, NULL);
 }
 
-static void on_err(void *_srv, int errno)
+static void on_err(void *_srv, int err)
 {
        xmpp_server_t *srv = _srv;
-       xmpp_run(srv, NULL, NULL, NULL, NULL);
+       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,
+static void xmpp_run(xmpp_server_t *srv, int idle,
                const char *start, const char **attrs,
                const char *end, const char *data)
 {
-       xmpp_channel_t *chan = NULL;
-
        /* Debug print */
        if (data)
-               debug("%*s \"%s\"", srv->indent*4, "", data);
+               debug("%*s \"%s\"", srv->level*4, "", data);
        if (start) {
-               debug("%*s<%s>", srv->indent*4, "", 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->indent*4+8, "",
+                               srv->level*4+8, "",
                                attrs[i+0], attrs[i+1],
                                attrs[i+2] ? "" : ">");
                }
        }
        if (start)
-               srv->indent++;
+               srv->level++;
        if (end)
-               srv->indent--;
+               srv->level--;
 
        /* Connection Handling */
        if (srv->state == XMPP_CONNECT && !start && !end) {
@@ -258,43 +490,58 @@ static void xmpp_run(xmpp_server_t *srv,
                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("Error creating XML parser");
+                       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);
+               net_encrypt(&srv->net, srv->noverify ? NET_NOVERIFY : 0);
 
                if (!(XML_ParserReset(srv->expat, NULL)))
-                       error("Error resetting XML parser");
+                       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("Error resetting XML parser");
+                       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, " ");
+       }
+
        /* Stream Start */
        if (srv->state == XMPP_SEND_STREAM) {
                if (net_print(&srv->net,
@@ -306,7 +553,7 @@ static void xmpp_run(xmpp_server_t *srv,
                    " 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;
                }
@@ -359,8 +606,14 @@ static void xmpp_run(xmpp_server_t *srv,
                }
        }
        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;
                }
        }
@@ -384,69 +637,128 @@ static void xmpp_run(xmpp_server_t *srv,
                }
        }
        if (srv->state == XMPP_RECV_JID) {
-               if (match(start, "jid")) {
-                       debug("xmpp: jid -> presence");
+               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) {
-                       xmpp_channel_t *chan = (xmpp_channel_t*)cur;
-                       if (cur->server != &srv->server || !chan->join)
+                       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'>"
                                "<x xmlns='http://jabber.org/protocol/muc'/>"
                                "</presence>",
-                               srv->jid, chan->dest, srv->nick);
+                               srv->bind, chan->dest, srv->nick);
                }
+               debug("xmpp: join -> ready");
                srv->state = XMPP_READY;
        }
 
        /* Start message */
-       if (srv->state == XMPP_READY) {
-               srv->state = match(start, "iq")       ? XMPP_IN_IQ       :
-                            match(start, "message")  ? XMPP_IN_MESSAGE  :
-                            match(start, "presence") ? XMPP_IN_PRESENCE :
-                                                       XMPP_READY;
-               if (srv->state != XMPP_READY) {
-                       strncpy(srv->msg_jid, find_attr(attrs, "from"), JID_LEN);
-                       split_jid(srv->msg_jid, srv->msg_usr,
-                                 srv->msg_srv, srv->msg_res);
-
-                       if (match(srv->msg_srv, srv->muc)) {
-                               srv->msg_from = srv->msg_res[0] ? srv->msg_res : NULL;
-                               srv->msg_chan = find_dest(srv, srv->msg_jid, 1);
-                       } else {
-                               srv->msg_from = srv->msg_usr[0] ? srv->msg_usr : NULL;
-                               srv->msg_chan = find_dest(srv, srv->msg_jid, 0);
-                       }
-
-                       debug("xmpp: %s -- jid=[%s] from=[%s] chan=[%s]",
-                               start,
-                               srv->msg_jid, srv->msg_from,
-                               srv->msg_chan->channel.name);
-               }
+       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");
+
+               /* 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_IN_IQ ||
-           srv->state == XMPP_IN_MESSAGE ||
-           srv->state == XMPP_IN_PRESENCE) {
-               if (srv->msg_chan && srv->msg_chan != &srv->system)
-                       chan = srv->msg_chan;
+
+       /* 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;
        }
 
        /* Info/Queries */
        if (srv->state == XMPP_IN_IQ) {
-               if (match(start, "item") && chan) {
-                       static char res[JID_LEN];
-                       split_jid(find_attr(attrs, "jid"),
-                                 NULL, NULL, res);
-                       chan_notice(chan, "user: %s", res);
-               }
-               if (match(start, "item") && !chan) {
-                       srv_notice(srv, "item: [%s] %s",
-                                       find_attr(attrs, "jid"),
-                                       find_attr(attrs, "name"));
+               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",
@@ -468,64 +780,187 @@ static void xmpp_run(xmpp_server_t *srv,
                                find_attr(attrs, "type"));
                }
                if (match(end, "title")) {
-                       debug("xmpp: title -- jid=[%s]",
-                               end, srv->msg_jid);
+                       debug("xmpp: title -- chan=[%s]",
+                               end, chan->dest);
                        chan_notice(chan, "Title: %s", data);
                }
                if (match(end, "instructions")) {
-                       debug("xmpp: instructions -- jid=[%s]",
-                               end, srv->msg_jid);
+                       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) {
-                       debug("xmpp: body (%s) -- chan=[%s] jid=[%s] from=[%s]",
-                               srv->msg_from == srv->msg_usr ? "user" : "chat" ,
-                               srv->msg_chan->channel.name,
-                               srv->msg_jid, srv->msg_from);
-                       chat_recv(&chan->channel, srv->msg_from, 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) {
-               static char alias[JID_LEN];
-               const char *jid;
-               if (match(start, "item")) {
-                       if ((jid = find_attr(attrs, "jid")))
-                               strncpy(alias, jid, JID_LEN);
+               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") && chan) {
-                       if (alias[0])
+               if (match(end, "presence") && !srv->quiet) {
+                       if (alias)
                                chan_notice(chan, "%s (%s) entered room.",
-                                               srv->msg_from, alias);
-                       else
+                                               usr->user.name, alias->user.name);
+                       else if (usr)
                                chan_notice(chan, "%s entered room.",
-                                               srv->msg_from);
-                       alias[0] = '\0';
+                                               usr->user.name);
                }
        }
 
        /* End messages */
-       if (srv->state == XMPP_IN_IQ ||
-           srv->state == XMPP_IN_MESSAGE ||
-           srv->state == XMPP_IN_PRESENCE) {
-               if (match(end, "iq") ||
-                   match(end, "message") ||
-                   match(end, "presence")) {
+       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;
 
-                       srv->msg_jid[0] = '\0';
-                       srv->msg_usr[0] = '\0';
-                       srv->msg_srv[0] = '\0';
-                       srv->msg_res[0] = '\0';
-                       srv->msg_from   = NULL;
-                       srv->msg_chan   = NULL;
-               }
-       }
        /* Error handling */
        if (match(start, "stream:error"))
                srv->in_error = 1;
@@ -542,21 +977,48 @@ static void xmpp_run(xmpp_server_t *srv,
 /* XMPP functions */
 void xmpp_init(void)
 {
+        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;
 
                xmpp_server_t *srv = (xmpp_server_t*)cur;
-               srv->system.channel.server = &srv->server;
-               srv->system.channel.name   = srv->server.name;
-               srv_notice(srv, "XMPP Server: %s", srv->server.name);
+               split_jid(srv->jid, jid_usr, jid_srv, jid_res);
 
-               if (!srv->port)
-                       srv->port = 5222;
                if (!srv->jid)
                        error("jid is required");
-               if (srv->connect)
-                       xmpp_run(srv, NULL, NULL, NULL, NULL);
+                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)
@@ -564,6 +1026,8 @@ void xmpp_init(void)
 
                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",
@@ -571,16 +1035,6 @@ void xmpp_init(void)
        }
 }
 
-server_t *xmpp_server(void)
-{
-       return new0(xmpp_server_t);
-}
-
-channel_t *xmpp_channel(void)
-{
-       return new0(xmpp_channel_t);
-}
-
 void xmpp_config(server_t *server, channel_t *channel,
                  const char *group, const char *name,
                  const char *key, const char *value)
@@ -588,13 +1042,25 @@ void xmpp_config(server_t *server, channel_t *channel,
        xmpp_server_t  *srv  = (xmpp_server_t*)server;
        xmpp_channel_t *chan = (xmpp_channel_t*)channel;
 
-       if (server) {
-               if (match(key, "connect"))
+       if (match(group, "server")) {
+               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"))
@@ -605,61 +1071,114 @@ void xmpp_config(server_t *server, channel_t *channel,
                        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 (channel) {
-               if (match(key, "room"))
+       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_complete(channel_t *channel, const char *text)
+{
+       xmpp_server_t *srv = (xmpp_server_t*)channel->server;
+       const char *arg;
+
+       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 (text[0] == '/') {
+       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'/>"
                                "</iq>",
-                               srv->jid, arg ?: srv->host);
+                               srv->bind, arg ?: srv->srv);
                }
                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'/>"
                                "</iq>",
-                               srv->jid, arg ?: srv->host);
+                               srv->bind, arg ?: srv->srv);
                }
                else if (prefix(text, "/names", &arg)) {
                        if (arg)
-                               chan = find_dest(srv, arg, 1);
+                               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'>"
+                               "<iq id='names' type='get' from='%s' to='%s'>"
                                "<query xmlns='http://jabber.org/protocol/disco#items'/>"
                                "</iq>",
-                               srv->jid, chan->dest);
+                               srv->bind, chan->dest);
                }
                else if (prefix(text, "/join", &arg)) {
                        if (!arg) {
                                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'>"
                                "<x xmlns='http://jabber.org/protocol/muc'/>"
                                "</presence>",
-                               srv->jid, chan->dest, srv->nick);
-                       chan = find_dest(srv, arg, 1);
+                               srv->bind, chan->dest, srv->nick);
                        chan_notice(chan, "Room: %s", arg);
                }
                else if (prefix(text, "/config", &arg)) {
@@ -667,44 +1186,57 @@ void xmpp_send(channel_t *channel, const char *text)
                                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->jid, chan->dest);
+                               srv->bind, chan->dest);
                }
                else if (prefix(text, "/query", &arg)) {
                        if (!arg) {
                                chan_notice(chan, "usage: /query <user>");
                                return;
                        }
-                       chan = find_dest(srv, arg, 0);
+                       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 {
                        chan_notice(chan, "Unknown command %s", text);
                }
        } else {
                debug("message: [%s]", text);
+               if (text[0] == '/')
+                       text = &text[1];
                if (chan == &srv->system) {
                        chan_notice(chan, "Cannot send to server");
                }
-               else if (!chan->dest) {
-                       chan_notice(chan, "No destination for message");
-               }
-               else if (chan->room) {
+               else if (chan->muc) {
                        net_print(&srv->net,
-                               "<message id='chat' from='%s' to='%s' type='groupchat'>"
+                               "<message id='chat%d' from='%s' to='%s' type='groupchat'>"
                                "<body>%s</body>"
                                "</message>",
-                               srv->jid, chan->dest, text);
+                               srv->id++, srv->bind, chan->dest, text);
                } else {
                        net_print(&srv->net,
-                               "<message id='chat' from='%s' to='%s'>"
+                               "<message id='chat%d' from='%s' to='%s' type='chat'>"
                                "<body>%s</body>"
                                "</message>",
-                               srv->jid, chan->dest, text);
-                       chat_recv(channel, srv->nick, text);
+                               srv->id++, srv->bind, chan->dest, text);
+                       chat_recv(channel, &srv->myself.user, raw);
                }
        }
 }