#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
-#include <stdint.h>
#include <string.h>
+#include <ctype.h>
#include <errno.h>
-#include <unistd.h>
#include <expat.h>
#include <time.h>
-#include <sys/timerfd.h>
-
#include "util.h"
#include "conf.h"
#include "chat.h"
+#include "view.h"
#include "net.h"
/* Constants */
/* XMPP types */
typedef enum {
+ XMPP_DEAD,
XMPP_CONNECT,
XMPP_ENCRYPT,
XMPP_RESTART,
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;
char dest[JID_LEN];
int muc;
char *full_name;
+ char *mention_name;
} xmpp_user_t;
typedef struct {
const char *type;
int muc;
- const char *room;
+ char *room;
+ char *name;
int join;
} xmpp_channel_t;
const char *pass;
int quiet;
- int timer;
- poll_t poll;
+ 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;
+ int in_html;
int in_error;
} xmpp_server_t;
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 '@':
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);
debug("xmpp: added user: \"%s\"%s -> name=[%s] dest=[%s]",
jid, is_muc ? " (muc)" : "",
usr->user.name, usr->dest);
- if (!usr->muc)
- net_print(&srv->net,
- "<iq id='auto-vcard' type='get' from='%s' to='%s'>"
- "<vCard xmlns='vcard-temp'/>"
- "</iq>",
- srv->jid, 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)
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,
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, 0, NULL, NULL, NULL, NULL);
+ srv_notice(srv, "Server disconnected");
+ srv->state = XMPP_DEAD;
}
static void on_timer(void *_srv)
{
- uint64_t buf;
xmpp_server_t *srv = _srv;
- while (read(srv->timer, &buf, sizeof(buf)) > 0)
- xmpp_run(srv, 1, NULL, NULL, NULL, NULL);
+ xmpp_run(srv, 1, NULL, NULL, NULL, NULL);
}
/* XMPP State machine */
srv->net.recv = on_recv;
srv->net.err = on_err;
srv->net.data = srv;
- net_open(&srv->net, srv->host, srv->port);
- srv->timer = timerfd_create(CLOCK_MONOTONIC,
- TFD_NONBLOCK|TFD_CLOEXEC);
- if (srv->timer < 0)
- error("creating timer fd");
- struct timespec tspec = {srv->timeout, 0};
- struct itimerspec itspec = {tspec, tspec};
- timerfd_settime(srv->timer, 0, &itspec, NULL);
- poll_add(&srv->poll, srv->timer, on_timer, srv);
- poll_ctl(&srv->poll, 1, 0, 1);
+ 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);
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);
}
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);
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,
}
}
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;
}
}
}
}
if (srv->state == XMPP_RECV_JID) {
- if (match(start, "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_PRESENCE) {
if (net_print(&srv->net, "<presence/>")) {
- debug("xmpp: presence -> join");
+ 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;
}
}
xmpp_channel_t *chan = (xmpp_channel_t *)cur;
if (!chan->join)
continue;
- chan_notice(chan, "XMPP Channel: %s", chan->channel.name);
+ 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 && idle) {
- debug("xmpp: idle");
- net_print(&srv->net, " ");
- }
-
if (srv->state == XMPP_READY && start) {
const char *id = find_attr(attrs, "id");
const char *from = find_attr(attrs, "from");
} else if (chan->muc) {
const char *jid = find_attr(attrs, "jid");
xmpp_user_t *usr = find_user(srv, jid, 1);
- chan_notice(chan, "User: %s (%s)",
- usr->user.name, jid);
+ if (jid && usr)
+ chan_notice(chan, "User: %s (%s)",
+ usr->user.name, jid);
} else {
chan_notice(chan, "item: [%s] %s",
find_attr(attrs, "jid"),
}
if (usr) {
if (match(end, "FN")) {
+ strset(&chan->channel.name, data);
strset(&usr->user.name, data);
strset(&usr->full_name, data);
+ chat_update();
}
}
}
}
}
+ /* 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")) {
}
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)");
- if (srv->msg_body) {
- chat_recv(&chan->channel,
- &usr->user,
- srv->msg_body);
+ 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 (match(start, "item") && usr) {
const char *jid = find_attr(attrs, "jid");
xmpp_user_t *alias = find_user(srv, jid, 0);
- if (jid && alias)
+ 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)
srv->myself.user.server = &srv->server;
srv->myself.user.name = strcopy(srv->nick);
- if (srv->connect) {
+ 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);
}
}
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->srv);
+ 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->srv);
+ srv->bind, arg ?: srv->srv);
}
else if (prefix(text, "/names", &arg)) {
if (arg)
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_channel(srv, arg, 1);
+ srv->bind, chan->dest, srv->nick);
chan_notice(chan, "Room: %s", arg);
}
else if (prefix(text, "/config", &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->jid, chan->dest);
+ srv->bind, chan->dest);
}
else if (prefix(text, "/query", &arg)) {
if (!arg) {
"<iq id='vcard' type='get' from='%s' to='%s'>"
"<vCard xmlns='vcard-temp'/>"
"</iq>",
- srv->jid, chan->dest);
+ 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->muc) {
net_print(&srv->net,
"<message id='chat%d' from='%s' to='%s' type='groupchat'>"
"<body>%s</body>"
"</message>",
- srv->id++, srv->jid, chan->dest, text);
+ 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->jid, chan->dest, text);
- chat_recv(channel, &srv->myself.user, text);
+ srv->id++, srv->bind, chan->dest, text);
+ chat_recv(channel, &srv->myself.user, raw);
}
}
}