shithub: xmpp

Download patch

ref: 38fbed97371f4f970a542945883e7d68762a2184
author: Sigrid Haflínudóttir <[email protected]>
date: Mon Mar 16 12:59:12 EDT 2020

update for latest 9front; squash everything

--- /dev/null
+++ b/.gitignore
@@ -1,0 +1,2 @@
+[a0125678vqki].*
+*.[o0125678vqki]
--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,1 @@
+Public domain.
--- /dev/null
+++ b/conn.c
@@ -1,0 +1,449 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <libsec.h>
+#include <ndb.h>
+#include "xml.h"
+#include "xmpp.h"
+
+enum
+{
+	Ascramsha1 = 1<<0,
+	Adigestmd5 = 1<<1,
+	Aplain     = 1<<2,
+};
+
+static Xelem*
+expect(Biobuf *b, int flags, char *name)
+{
+	Xelem *x;
+	int err;
+
+	x = xmlread(b, flags, &err);
+	if(x != nil && strcmp(x->n, name) != 0){
+		werrstr("expected %q, got %q", name, x->n);
+		xmlprint(x, 2);
+		xmlfree(x);
+		x = nil;
+	}
+	return x;
+}
+
+static int
+authscramsha1(int fd, Biobuf *b, char *user, char *passwd)
+{
+	uchar cnonce[33], h[3][SHA1dlen], cp[SHA1dlen], svsig[SHA1dlen];
+	char cnonce64[45], *snonce64, *salt64, *salt, *s, *ni, *svfst;
+	int i, j, numiter, pwdlen, slen;
+	Xelem *x;
+
+	svfst = nil;
+	genrandom(cnonce, sizeof(cnonce));
+	if(enc64(cnonce64, sizeof(cnonce64), cnonce, sizeof(cnonce)) < 0)
+		return -1;
+
+	/* client first message */
+	s = smprint("n,,n=%s,r=%s", user, cnonce64);
+	fprint(fd,
+		"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
+		" mechanism='SCRAM-SHA-1'>%.*[</auth>",
+		(int)strlen(s), s);
+	free(s);
+	s = nil;
+
+	/* server first message */
+	if((x = expect(b, 0, "challenge")) == nil || x->v == nil)
+		goto error;
+	slen = strlen(x->v);
+	svfst = malloc(slen);
+	if((slen = dec64((uchar*)svfst, slen, x->v, slen)) < 0)
+		goto error;
+	svfst[slen] = 0;
+	s = strdup(svfst);
+	if(s[0] != 'r' || s[1] != '=' || strncmp(cnonce64, s+2, strlen(cnonce64)) != 0)
+		goto error;
+	snonce64 = s + 2;
+	salt64 = strstr(snonce64, ",s=");
+	ni = salt64 != nil ? strstr(salt64+3, ",i=") : nil;
+	if(!snonce64[0] || salt64 == nil || ni == nil)
+		goto error;
+	*salt64 = 0;
+	salt64 += 3;
+	*ni = 0;
+	ni += 3;
+	numiter = atoi(ni);
+	if(!salt64[0] || numiter < 1)
+		goto error;
+
+	/* decode salt */
+	slen = strlen(salt64);
+	salt = malloc(slen+4);
+	if((slen = dec64((uchar*)salt, slen, salt64, slen)) < 0){
+		free(salt);
+		goto error;
+	}
+
+	/* calc salted password in h[2] */
+	salt[slen+0] = 0;
+	salt[slen+1] = 0;
+	salt[slen+2] = 0;
+	salt[slen+3] = 1;
+	pwdlen = strlen(passwd);
+	hmac_sha1((uchar*)salt, slen+4, (uchar*)passwd, pwdlen, h[0], nil);
+	free(salt);
+	memcpy(h[2], h[0], SHA1dlen);
+	for(i = 1; i < numiter; i++){
+		hmac_sha1(h[0], SHA1dlen, (uchar*)passwd, pwdlen, h[1], nil);
+		for(j = 0; j < SHA1dlen; j++)
+			h[2][j] ^= h[1][j];
+		memcpy(h[0], h[1], SHA1dlen);
+	}
+
+	/* client (h[0]), server (h[1]) and stored (h[2]) keys */
+	hmac_sha1((uchar*)"Client Key", 10, h[2], SHA1dlen, h[0], nil);
+	hmac_sha1((uchar*)"Server Key", 10, h[2], SHA1dlen, h[1], nil);
+	sha1(h[0], SHA1dlen, h[2], nil);
+
+	/* auth message */
+	snonce64 = strdup(snonce64);
+	free(s);
+	s = smprint("n=%s,r=%s,%s,c=biws,r=%s", user, cnonce64, svfst, snonce64);
+	free(svfst);
+	svfst = nil;
+	xmlfree(x);
+
+	/* client and server signatures */
+	hmac_sha1((uchar*)s, strlen(s), h[2], SHA1dlen, cp, nil);
+	hmac_sha1((uchar*)s, strlen(s), h[1], SHA1dlen, svsig, nil);
+
+	/* client proof */
+	for(i = 0; i < SHA1dlen; i++)
+		cp[i] = h[0][i] ^ cp[i];
+
+	free(s);
+	s = smprint("c=biws,r=%s,p=%.*[", snonce64, SHA1dlen, cp);
+	fprint(fd,
+		"<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"
+		"%.*[</response>",
+		(int)strlen(s), s);
+	free(s);
+	s = nil;
+	free(snonce64);
+
+	if((x = expect(b, 0, "success")) == nil || x->v == nil)
+		goto error;
+	slen = strlen(x->v);
+	s = malloc(slen);
+	if((slen = dec64((uchar*)s, slen, x->v, slen)) < 0)
+		goto error;
+	s[slen] = 0;
+	svfst = smprint("v=%.*[", SHA1dlen, (uchar*)svsig);
+	if(strcmp(s, svfst) != 0){
+		werrstr("server signature doesn't match");
+		goto error;
+	}
+	xmlfree(x);
+	free(s);
+	free(svfst);
+	return 0;
+
+error:
+	werrstr("authscramsha1: %r");
+	free(s);
+	free(svfst);
+	xmlfree(x);
+	return -1;
+}
+
+static DigestState*
+md5fmt(char *out, DigestState *st, char *fmt, ...)
+{
+	va_list arg;
+	char *s;
+
+	va_start(arg, fmt);
+	s = vsmprint(fmt, arg);
+	st = md5((uchar*)s, strlen(s), (uchar*)out, st);
+	free(s);
+	return st;
+}
+
+static int
+authmd5(int fd, Biobuf *b, char *user, char *passwd)
+{
+	Xelem *x;
+	int chsz;
+	char *ch, *realm, *nonce, *s;
+	char ha1[MD5dlen], ha2[MD5dlen], res[MD5dlen], cnonce[MD5dlen];
+	DigestState *dgst;
+
+	fprint(fd,
+		"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
+		" mechanism='DIGEST-MD5'/>");
+	if((x = expect(b, 0, "challenge")) == nil || x->v == nil)
+		return -1;
+	chsz = strlen(x->v)/4*3;
+	ch = malloc(chsz + 1);
+	chsz = dec64((uchar*)ch, chsz, x->v, strlen(x->v));
+	xmlfree(x);
+	if(chsz < 0)
+		return -1;
+
+	realm = strstr(ch, "realm=");
+	nonce = strstr(ch, "nonce=");
+	if(realm != nil && (s = strchr(realm+7, '"')) != nil){
+		*s = 0;
+		realm += 7;
+	}
+	if(nonce != nil && (s = strchr(nonce+7, '"')) != nil){
+		*s = 0;
+		nonce += 7;
+	}else if(nonce == nil){
+		werrstr("nil nonce");
+		free(ch);
+		return -1;
+	}
+
+	genrandom((uchar*)cnonce, MD5dlen);
+
+	if(realm == nil)
+		realm = mydomain;
+
+	/* ha1 = md5(md5(user:realm:passwd):nonce:cnonce:jid) */
+	md5fmt(ha1, nil, "%s:%s:%s", user, realm, passwd);
+	dgst = md5((uchar*)ha1, MD5dlen, nil, nil);
+	md5fmt(ha1, dgst, ":%s:%.*lH:%s", nonce, MD5dlen, cnonce, myjid);
+
+	/* ha2 = md5(method:digesturi) */
+	md5fmt(ha2, nil, "AUTHENTICATE:xmpp/%s", mydomain);
+
+	/* response = md5(ha1:nonce:nc:cnonce:qop:ha2) */
+	md5fmt(res, nil, "%.*lH:%s:00000001:%.*lH:auth:%.*lH",
+		MD5dlen, ha1, nonce, MD5dlen, cnonce, MD5dlen, ha2);
+
+	s = smprint("username=\"%s\",realm=\"%s\",nonce=\"%s\","
+		"cnonce=\"%.*lH\",nc=00000001,qop=auth,digest-uri=\"xmpp/%s\","
+		"response=%.*lH,charset=utf-8,authzid=\"%s\"",
+		user, realm, nonce, MD5dlen, cnonce, mydomain, MD5dlen, res, myjid);
+	fprint(fd,
+		"<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"
+		"%.*[</response>",
+		(int)strlen(s), s);
+	free(s);
+	free(ch);
+
+	if((x = expect(b, 0, "challenge")) != nil){
+		xmlfree(x);
+		fprint(fd, "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
+		if((x = expect(b, 0, "success")) != nil){
+			xmlfree(x);
+			return 0;
+		}
+	}
+	return -1;
+}
+
+static int
+authplain(int fd, Biobuf *b, char *user, char *passwd)
+{
+	int len;
+	char *p;
+	Xelem *x;
+
+	p = smprint("%c%s%c%s", 0, user, 0, passwd);
+	len = 1+strlen(user)+1+strlen(passwd);
+	fprint(fd,
+		"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
+		" mechanism='PLAIN'>"
+		"%.*[</auth>",
+		len, p);
+	free(p);
+	if((x = expect(b, 0, "success")) != nil){
+		xmlfree(x);
+		return 0;
+	}
+	return -1;
+}
+
+static int
+streamstart(int fd)
+{
+	return fprint(fd,
+		"<?xml version='1.0'?>"
+		"<stream:stream"
+		" to='%Ӽ'"
+		" xmlns='jabber:client'"
+		" xmlns:stream='http://etherx.jabber.org/streams'"
+		" version='1.0'>",
+		mydomain);
+}
+
+static int
+login(int fd, Biobuf *b, char *user, char *passwd)
+{
+	Xelem *x, *y;
+	Thumbprint *th;
+	TLSconn tls;
+	int auth, r, oldfd, err;
+	uchar hash[SHA1dlen];
+
+	x = nil;
+	if((th = initThumbprints("/sys/lib/tls/xmpp", nil, "x509")) == nil)
+		return -1;
+	if(Binit(b, fd, OREAD) != 0)
+		return -1;
+	streamstart(fd);
+	xmlfree(xmlread(b, Xmlstartonly, &err));
+	if(err != 0 || (x = expect(b, 0, "stream:features")) == nil)
+		goto error;
+	if(debug > 1)
+		xmlprint(x, 2);
+
+	/* require TLS */
+	if(xmlget(x->ch, "starttls") == nil){
+		werrstr("tls not supported");
+		goto error;
+	}
+	xmlfree(x);
+	fprint(fd, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
+	if((x = expect(b, 0, "proceed")) == nil)
+		goto error;
+	xmlfree(x);
+	x = nil;
+	Bterm(b);
+	memset(&tls, 0, sizeof(tls));
+	oldfd = fd;
+	fd = tlsClient(fd, &tls);
+	close(oldfd);
+	if(th != nil){
+		if(tls.cert == nil || tls.certlen < 1){
+			werrstr("no server cert");
+			goto error;
+		}
+		sha1(tls.cert, tls.certlen, hash, nil);
+		if(!okThumbprint(hash, SHA1dlen, th)){
+			werrstr("unknown thumbprint: %.*H", SHA1dlen, hash);
+			goto error;
+		}
+		freeThumbprints(th);
+	}
+	free(tls.sessionID);
+	free(tls.cert);
+	if(Binit(b, fd, OREAD) != 0)
+		return -1;
+	if(fd < 0)
+		goto error;
+
+	streamstart(fd);
+	xmlfree(xmlread(b, Xmlstartonly, &err));
+	if(err != 0 || (x = expect(b, 0, "stream:features")) == nil)
+		goto error;
+	if(debug > 1)
+		xmlprint(x, 2);
+	auth = 0;
+	if((y = xmlget(x->ch, "mechanisms")) != nil){
+		if(debug > 0)
+			fprint(2, "auth methods:");
+		for(y = y->ch; y != nil; y = y->next){
+			if(debug > 0)
+				fprint(2, " %s", y->v);
+			if(strcmp(y->v, "SCRAM-SHA-1") == 0)
+				auth |= Ascramsha1;
+			else if(strcmp(y->v, "DIGEST-MD5") == 0)
+				auth |= Adigestmd5;
+			else if(strcmp(y->v, "PLAIN") == 0 && plainallow)
+				auth |= Aplain;
+		}
+		if(debug > 0)
+			fprint(2, "\n");
+	}
+	xmlfree(x);
+	x = nil;
+
+	if(auth & Ascramsha1)
+		r = authscramsha1(fd, b, user, passwd);
+	else if(auth & Adigestmd5)
+		r = authmd5(fd, b, user, passwd);
+	else if(auth & Aplain)
+		r = authplain(fd, b, user, passwd);
+	else{
+		werrstr("no supported auth methods");
+		goto error;
+	}
+
+	if(r != 0)
+		goto error;
+
+	streamstart(fd);
+	xmlfree(xmlread(b, Xmlstartonly, &err));
+	if(err != 0)
+		goto error;
+	xmlfree(xmlread(b, Xmlstartonly, &err));
+	if(err != 0)
+		goto error;
+	xmlfree(xmlread(b, 0, &err));
+	if(err != 0)
+		goto error;
+
+	if(myresource == nil){
+		fprint(fd,
+			"<iq type='set' id='xml sucks'>"
+			"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>");
+	}else{
+		fprint(fd,
+			"<iq type='set' id='xml sucks'>"
+			"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>"
+			"<resource>%Ӽ</resource>"
+			"</bind></iq>",
+			myresource);
+	}
+	xmlfree(xmlread(b, 0, &err));
+	if(err != 0)
+		goto error;
+
+	fprint(fd,
+		"<iq type='set' id='xmpp sucks'>"
+		"<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>");
+	xmlfree(xmlread(b, 0, &err));
+	if(err == 0)
+		return fd;
+
+error:
+	werrstr("login: %r");
+	xmlfree(x);
+	Bterm(b);
+	return -1;
+}
+
+int
+connect(Biobuf *b, char *user, char *passwd)
+{
+	int fd, clfd;
+	Ndbtuple *srv, *s, *targ, *port;
+	char *p;
+
+	p = smprint("_xmpp-client._tcp.%s", server);
+	srv = dnsquery(nil, p, "srv");
+	free(p);
+	for(s = srv, fd = -1; s != nil && fd < 0; s = s->entry){
+		if(strcmp(s->attr, "dom") != 0)
+			continue;
+
+		targ = ndbfindattr(s, s->line, "target");
+		port = ndbfindattr(s, s->line, "port");
+		if(targ == nil || port == nil)
+			continue;
+
+		fd = dial(netmkaddr(targ->val, "tcp", port->val), nil, nil, &clfd);
+	}
+	ndbfree(srv);
+
+	if(fd < 0){
+		fd = dial(netmkaddr(server, "tcp", "5222"), nil, nil, &clfd);
+		if(fd < 0)
+			return -1;
+	}
+	write(clfd, "keepalive", 9);
+	close(clfd);
+	return login(fd, b, user, passwd);
+}
--- /dev/null
+++ b/misc.c
@@ -1,0 +1,162 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ctype.h>
+
+extern Biobuf kbin;
+
+char*
+strtime(void)
+{
+	static char buf[6];
+	Tm *tm;
+
+	tm = localtime(time(nil));
+	sprint(buf, "%02d:%02d", tm->hour, tm->min);
+	return buf;
+}
+
+char*
+strenttime(char **tzo)
+{
+	static char buf[32];
+	Tm *tm;
+	int n;
+	long t;
+
+	t = time(nil);
+	tm = gmtime(t);
+	n = sprint(buf, "%04d-%02d-%02dT%02d:%02d:%02dZ",
+		tm->year+1900,
+		tm->mon+1,
+		tm->mday,
+		tm->hour,
+		tm->min,
+		tm->sec);
+	*tzo = &buf[n+1];
+	tm = localtime(t);
+	sprint(*tzo, "%+03d:%02d", tm->tzoff/3600, abs(tm->tzoff/60)%60);
+	return buf;
+}
+
+void
+cleaninput(int n)
+{
+	static char d[] = {8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8};
+
+	while(n > 0)
+		n -= write(1, d, sizeof(d) > n ? n : sizeof(d));
+}
+
+void
+setlabel(char *label, char *prsuffix)
+{
+	static char *oldlabel;
+	int fd;
+
+	if(oldlabel == nil){
+		oldlabel = mallocz(64, 1);
+		fd = open("/dev/label", OREAD);
+		read(fd, oldlabel, 63);
+		close(fd);
+	}
+
+	fd = open("/dev/label", OWRITE);
+	if(label != nil){
+		if(label[0] == 0){
+			fprint(fd, "xmpp");
+			print("no target\n");
+		}else{
+			fprint(fd, "%s", label);
+			if(prsuffix != nil && prsuffix[0] != 0)
+				print("←→ %s [%s]\n", label, prsuffix);
+			else
+				print("←→ %s\n", label);
+		}
+	}else
+		fprint(fd, "%s", oldlabel);
+	close(fd);
+}
+
+char*
+strstamp(char *v)
+{
+	static char buf[32];
+	Tm utc, *tm;
+	int i, len;
+
+	len = strlen(v);
+	for(i = 0; i < len && (isdigit(v[i]) || v[i] == '.') ; i++);
+	if(i == len){ /* 1456830231.000345 this is Slack being a bag of shit */
+		tm = localtime(atol(v));
+		sprint(buf, "%02d:%02d", tm->hour, tm->min);
+		return buf;
+	}
+
+	if(len < 17) /* can't parse, just use the current time */
+		return strtime();
+
+	/* 20130327T16:28:58 http://xmpp.org/extensions/xep-0091.html
+	 * or
+	 * 2016-02-25T10:00:15.400Z http://xmpp.org/extensions/xep-0203.html
+	 */
+	for(i = 0; i < len; i++)
+		if(isdigit(v[i]))
+			v[i] -= '0';
+
+	memset(&utc, 0, sizeof(utc));
+	utc.year = v[0]*1000+v[1]*100+v[2]*10+v[3] - 1900;
+	if(v[4] == '-' && v[7] == '-'){
+		utc.mon = v[5]*10+v[6] - 1;
+		utc.mday = v[8]*10+v[9];
+		i = 2;
+	}else{
+		utc.mon = v[4]*10+v[5] - 1;
+		utc.mday = v[6]*10+v[7];
+		i = 0;
+	}
+	utc.hour = v[9+i]*10+v[10+i];
+	utc.min = v[12+i]*10+v[13+i];
+	utc.zone[0] = 'G';
+	utc.zone[1] = 'M';
+	utc.zone[2] = 'T';
+
+	tm = localtime(tm2sec(&utc));
+	sprint(buf, "%04d/%02d/%02d %02d:%02d",
+		tm->year+1900,
+		tm->mon+1,
+		tm->mday,
+		tm->hour,
+		tm->min);
+
+	memmove(&utc, tm, sizeof(utc));
+	tm = localtime(time(nil));
+	if(tm->year != utc.year || tm->mon != utc.mon || tm->mday != utc.mday)
+		return buf;
+
+	return buf+11;
+}
+
+char *
+readlines(void)
+{
+	char *str, *s;
+	int len, catlen, total;
+
+	str = strdup("");
+	len = total = 0;
+	for(; s = Brdstr(&kbin, '\n', 0);){
+		catlen = strlen(s);
+		total += utflen(s);
+		str = realloc(str, len + catlen + 1);
+		memcpy(&str[len], s, catlen + 1);
+		free(s);
+		len += catlen;
+		if(len >= 2 && str[len-2] == '.' && (len < 3 || str[len-3] == '\n')){
+			str[len - (len > 2 ? 3 : 2)] = 0;
+			break;
+		}
+	}
+	cleaninput(total);
+	return str;
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,31 @@
+</$objtype/mkfile
+MAN=/sys/man/1
+
+TARG=\
+	xmpp\
+
+BIN=/$objtype/bin
+
+OFILES=\
+	conn.$O\
+	misc.$O\
+	muc.$O\
+	rost.$O\
+	targ.$O\
+	xml.$O\
+	xmpp.$O\
+
+HFILES=\
+	xml.h\
+	xmpp.h\
+
+UPDATE=\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+	mkfile\
+
+default:V:	all
+
+</sys/src/cmd/mkmany
+
+install: xmpp.man
--- /dev/null
+++ b/muc.c
@@ -1,0 +1,570 @@
+#include <u.h>
+#include <libc.h>
+#include "xml.h"
+#include "xmpp.h"
+
+typedef struct Bookmark Bookmark;
+struct Bookmark
+{
+	char *jid;
+};
+
+char *affs[] = {
+	[Anone]    "none",
+	[Aowner]   "owner",
+	[Aadmin]   "admin",
+	[Amember]  "member",
+	[Aoutcast] "outcast",
+};
+
+static char *roles[] = {
+	[Rnone]  "none",
+	[Rmoder] "moderator",
+	[Rpart]  "participant",
+	[Rvisit] "visitor",
+};
+
+static Bookmark *bookmarks;
+static int numbookmarks, mucwidth;
+
+static int
+addnick(Target *room, char *nick, char *jid, char *show, Target **ent)
+{
+	Target *t;
+	int i, width;
+
+	for(i = 0; i < numtargets; i++){
+		int r;
+		t = targets[i];
+		if(t->type != Emucent || t->mucent.room != room)
+			continue;
+		r = strcmp(t->name, nick);
+		if(r == 0){
+			*ent = t;
+			return 0;
+		}
+		if(r > 0)
+			break;
+	}
+	t = addtarget(Emucent, nick);
+	t->jid = smprint("%s/%s", room->jid, nick);
+	t->mucent.jid = (jid == nil) ? nil : strdup(jid);
+	t->mucent.show = (show == nil) ? nil : strdup(show);
+	t->mucent.room = room;
+	if((width = utflen(nick)) > room->muc.width)
+		room->muc.width = width;
+	*ent = t;
+	room->muc.numents++;
+	return 1;
+}
+
+static int
+rmnick(Target *room, char *nick)
+{
+	Target *t;
+	int i, width, removed;
+
+	removed = 0;
+	room->muc.width = 0;
+	for(i = 0; i < numtargets; i++){
+		t = targets[i];
+		if(t->type != Emucent || t->mucent.room != room)
+			continue;
+		if(strcmp(t->name, nick) == 0){
+			rmtarget(t);
+			removed = 1;
+		}else if((width = utflen(t->name)) > room->muc.width)
+			room->muc.width = width;
+	}
+	if(removed)
+		room->muc.numents--;
+	return removed;
+}
+
+static int
+setaffrole(Target *t, Xelem *item)
+{
+	Xattr *aff, *role;
+	int changed, n, i;
+
+	if(item == nil)
+		return 0;
+	changed = 0;
+	role = xmlgetattr(item->a, "role");
+	aff = xmlgetattr(item->a, "affiliation");
+	if(role != nil){
+		n = Rnone;
+		for(i = 0; i < nelem(roles); i++){
+			if(strcmp(roles[i], role->v) == 0){
+				n = i;
+				break;
+			}
+		}
+		changed |= (t->mucent.role != n);
+		t->mucent.role = n;
+	}
+	if(aff != nil){
+		n = Anone;
+		for(i = 0; i < nelem(affs); i++){
+			if(strcmp(affs[i], aff->v) == 0){
+				n = i;
+				break;
+			}
+		}
+		changed |= (t->mucent.aff != n);
+		t->mucent.aff = n;
+	}
+
+	return changed;
+}
+
+static int
+addbookmark(char *jid)
+{
+	Bookmark *b;
+	int i;
+
+	for(i = 0; i < numbookmarks; i++)
+		if(strcmp(bookmarks[i].jid, jid) == 0)
+			return 0;
+
+	numbookmarks++;
+	bookmarks = realloc(bookmarks, sizeof(*b)*numbookmarks);
+	b = &bookmarks[numbookmarks-1];
+	b->jid = strdup(jid);
+	return 1;
+}
+
+static int
+rmbookmark(char *jid)
+{
+	int i;
+
+	for(i = 0; i < numbookmarks; i++){
+		if(strcmp(bookmarks[i].jid, jid) == 0){
+			numbookmarks--;
+			memcpy(&bookmarks[i], &bookmarks[i+1], sizeof(bookmarks[0])*(numbookmarks-i));
+			return 1;
+		}
+	}
+	return 0;
+}
+
+void
+mucpresence(Xelem *xml, Target *room, Xattr *from)
+{
+	char *s, didwhat[32];
+	Xattr *type;
+	Xelem *msg, *item, *x, *ch, *show;
+	int changed;
+
+	if((s = strchr(from->v, '/')) == nil)
+		return;
+	s++;
+
+	type = xmlgetattr(xml->a, "type");
+	msg = xmlget(xml->ch, "status");
+	x = item = nil;
+	show = xmlget(xml->ch, "show");
+	for(ch = xml->ch; ch != nil; ch = ch->next){
+		if(item == nil && (x = xmlget(ch, "x")) != nil)
+			item = xmlget(x->ch, "item");
+	}
+	if(x == nil)
+		return;
+
+	if(type != nil && strcmp(type->v, "unavailable") == 0){
+		Xelem *xstatus;
+		Xattr *code;
+
+		strcpy(didwhat, "left");
+		changed = rmnick(room, s);
+		xstatus = xmlget(x->ch, "status");
+		code = (xstatus != nil) ? xmlgetattr(xstatus->a, "code") : nil;
+		if(code != nil && strcmp(code->v, "303") == 0){
+			Xattr *nick;
+			nick = xmlgetattr(item->a, "nick");
+			if(nick != nil){
+				print("[%s] (%s) %s changed nick to %s\n",
+					strtime(), room->name, s, nick->v);
+			}
+			return;
+		}
+	}else{
+		Target *t;
+		Xattr *jid;
+		char *j, *sh;
+
+		jid = xmlgetattr(item->a, "jid");
+		j = (jid != nil && jid->v != nil) ? jid->v : nil;
+		sh = (show != nil && show->v != nil) ? show->v : nil;
+
+		strcpy(didwhat, "joined");
+		changed = addnick(room, s, j, sh, &t);
+		if(setaffrole(t, item) && !changed)
+			snprint(didwhat, sizeof(didwhat), "role=%s affiliation=%s",
+				affs[t->mucent.aff], roles[t->mucent.role]);
+	}
+	if(!changed || nopresence)
+		return;
+	print("[%s] (%s) %s %s", strtime(), room->name, s, didwhat);
+	if(msg != nil)
+		print(" (%s)", msg->v);
+	print("\n");
+}
+
+int
+mucbookmarks(Xelem *e, int fd)
+{
+	Xelem *x;
+
+	for(x = e->ch; x != nil; x = x->next){
+		char *argv[2];
+		Xattr *a;
+
+		if(strcmp(x->n, "conference") != 0)
+			continue;
+		if((a = xmlgetattr(x->a, "autojoin")) == nil)
+			continue;
+		if(strcmp(a->v, "1") != 0 && strcmp(a->v, "true") != 0)
+			continue;
+		argv[0] = "j";
+		a = xmlgetattr(x->a, "jid");
+		argv[1] = a->v;
+		if(cmdjoin(fd, 2, argv) < 0)
+			return -1;
+		addbookmark(a->v);
+	}
+
+	return 0;
+}
+
+int
+cmdaff(int fd, int argc, char **argv)
+{
+	Target *t, *room;
+	char *targ, *aff, *slash, *jid;
+	int i, nlen, res;
+
+	room = nil;
+	res = 0;
+	if(argc < 3){
+		if(argc < 2 && curr >= 0 && targets[curr]->type == Emuc)
+			room = targets[curr];
+		else{
+			targ = argv[1];
+			nlen = strlen(targ);
+			for(i = 0; i < numtargets; i++){
+				t = targets[i];
+				if(t->type == Emuc && targmatches(t, targ, nlen)){
+					room = t;
+					break;
+				}
+			}
+		}
+		for(i = Anone+1; room != nil && i < nelem(affs) && res >= 0; i++){
+			res = fprint(fd,
+				"<iq to='%Ӽ' type='get' id='afflist'>"
+				"<query xmlns='http://jabber.org/protocol/muc#admin'>"
+				"<item affiliation='%Ӽ'/>"
+				"</query></iq>",
+				room->jid,
+				affs[i]);
+		}
+		return res;
+	}
+
+	targ = argv[1];
+	aff = argv[2];
+	slash = strchr(targ, '/');
+	jid = nil;
+
+	if(curr >= 0 && targets[curr]->type == Emuc && slash == nil){
+		room = targets[curr];
+		if(strchr(targ, '@') != nil)
+			jid = targ;
+		else{
+			nlen = strlen(targ);
+			for(i = 0; i < numtargets; i++){
+				t = targets[i];
+				if(t->type == Emucent && t->mucent.room == room && targmatches(t, targ, nlen)){
+					jid = t->mucent.jid;
+					break;
+				}
+			}
+		}
+	}else if(slash != nil){
+		int rlen;
+		rlen = slash - targ;
+		slash++;
+		nlen = strlen(slash);
+		jid = (strchr(slash, '@') != nil) ? slash : nil;
+		for(i = 0; i < numtargets; i++){
+			t = targets[i];
+			if(t->type == Emucent && targmatches(t->mucent.room, targ, rlen) && targmatches(t, slash, nlen)){
+				room = t->mucent.room;
+				jid = t->mucent.jid;
+				break;
+			}
+			if(t->type == Emuc && targmatches(t, targ, rlen) && jid != nil){
+				room = t;
+				break;
+			}
+		}
+	}
+
+	if(room != nil && jid != nil){
+		res = fprint(fd,
+			"<iq to='%Ӽ' type='set'>"
+			"<query xmlns='http://jabber.org/protocol/muc#admin'>"
+			"<item affiliation='%Ӽ' jid='%Ӽ'/>"
+			"</query></iq>",
+			room->jid,
+			aff,
+			jid);
+	}else
+		print("no such target: %q\n", targ);
+
+	return res;
+}
+
+int
+cmdbookmark(int fd, int /*argc*/, char **argv)
+{
+	int (*f)(char *jid);
+	int i, res;
+
+	if(argv[0][1] == 0){
+		int i;
+		for(i = 0; i < numbookmarks; i++)
+			print("  %s\n", bookmarks[i].jid);
+		print("%d bookmark(s)\n", numbookmarks);
+		return 0;
+	}
+
+	if(argv[0][1] == '+')
+		f = addbookmark;
+	else if(argv[0][1] == '-')
+		f = rmbookmark;
+	else
+		return 0;
+	if(targets[curr]->type != Emuc || !f(targets[curr]->jid))
+		return 0;
+
+	res = fprint(fd,
+		"<iq type='set' id='takethat'>"
+		"<query xmlns='jabber:iq:private'>"
+		"<storage xmlns='storage:bookmarks'>");
+	for(i = 0; i < numbookmarks && res >= 0; i++)
+		res = fprint(fd, "<conference autojoin='1' jid='%Ӽ'/>", bookmarks[i].jid);
+	return res < 0 ? res : fprint(fd, "</storage></query></iq>");
+}
+
+int
+cmdjoin(int fd, int argc, char **argv)
+{
+	Target *t;
+	char *room, *rnick, *s;
+	int i, width, num;
+
+	if(argc < 2){
+		for(i = num = 0; i < numtargets; i++){
+			t = targets[i];
+			if(t->type == Emuc){
+				print("  %*s  %d\n", -mucwidth, t->jid, t->muc.numents);
+				num++;
+			}
+		}
+		print("%d muc(s)\n", num);
+		return 0;
+	}
+
+	room = argv[1];
+	if(rnick = strchr(room, '/'))
+		*rnick++ = 0;
+	else
+		rnick = mynick;
+
+	if(fprint(fd,
+		"<presence to='%Ӽ/%Ӽ'>"
+		"<x xmlns='http://jabber.org/protocol/muc'>",
+		room, rnick) < 0)
+		return -1;
+	if(argc > 2 && fprint(fd, "<password>%Ӽ</password>", argv[2]) < 0)
+		return -1;
+	if(nohistory && fprint(fd, "<history maxchars='0'/>") < 0)
+		return -1;
+	if(fprint(fd, "</x></presence>") < 0)
+		return -1;
+
+	for(i = 0; i < numtargets; i++)
+		if(strcmp(targets[i]->name, room) == 0)
+			return 0;
+
+	t = addtarget(Emuc, room);
+	t->jid = strdup(room);
+	if(s = strchr(t->name, '@'))
+		*s = 0;
+	mucwidth = 0;
+	for(i = 0; i < numtargets; i++){
+		t = targets[i];
+		if(t->type == Emuc && (width = utflen(t->jid)) > mucwidth)
+			mucwidth = width;
+	}
+	return 0;
+}
+
+static Target *
+findmuc(char *name)
+{
+	int tlen, i;
+	Target *t;
+
+	t = nil;
+	tlen = strlen(name);
+	for(i = 0; i < numtargets; i++, t = nil){
+		t = targets[i];
+		if(t->type == Emuc && targmatches(t, name, tlen))
+			break;
+	}
+	if(t == nil)
+		print("no such muc: %q\n", name);
+	return t;
+}
+
+int
+cmdpart(int fd, int argc, char **argv)
+{
+	Target *t;
+	int i, width;
+
+	t = nil;
+	if(argc >= 2)
+		t = findmuc(argv[1]);
+	else if(curr >= 0 && targets[curr]->type == Emuc)
+		t = targets[curr];
+	if(t == nil)
+		return 0;
+
+	/* free private chats */
+	if(fprint(fd,
+		"<presence from='%Ӽ' to='%Ӽ' type='unavailable'></presence>",
+		myjid, t->jid) < 0)
+		return -1;
+
+	mucwidth = 0;
+	for(i = 0; i < numtargets; i++){
+		if(targets[i]->type == Emucent && targets[i]->mucent.room == t)
+			rmtarget(targets[i]);
+		if(t->type == Emuc && (width = utflen(t->jid)) > mucwidth)
+			mucwidth = width;
+	}
+
+	print("left %s\n", t->name);
+	rmtarget(t);
+	return 0;
+}
+
+int
+cmdsubj(int fd, int argc, char **argv)
+{
+	Target *t;
+	char *subj;
+	int res;
+
+	t = nil;
+	if(argc >= 2)
+		t = findmuc(argv[1]);
+	else if(curr >= 0 && targets[curr]->type == Emuc)
+		t = targets[curr];
+	if(t == nil)
+		return 0;
+
+	if(argv[0][0] == 's'){
+		print("subject for %s:\n%s\n", t->name, (t->muc.subj == nil) ? "" : t->muc.subj);
+		return 0;
+	}
+
+	subj = readlines();
+	res = fprint(fd,
+		"<message to='%Ӽ' type='%Ӽ'><subject>%Ӽ</subject></message>",
+		t->jid,
+		enttypes[t->type],
+		subj);
+	free(subj);
+	return res;
+}
+
+int
+cmdwho(int, int argc, char **argv)
+{
+	Target *room, *t;
+	char *show;
+	int i, num;
+
+	room = nil;
+	if(argc < 2){
+		if(curr < 0 || targets[curr]->type != Emuc)
+			return 0;
+		room = targets[curr];
+	}else{
+		int tlen;
+		tlen = strlen(argv[1]);
+		for(i = 0; i < numtargets; i++){
+			t = targets[i];
+			if(t->type == Emuc && targmatches(t, argv[1], tlen)){
+				room = t;
+				break;
+			}
+		}
+		if(room == nil){
+			print("no such target: %q\n", argv[1]);
+			return 0;
+		}
+	}
+
+	num = 0;
+	print("(%s):\n", room->jid);
+	for(i = 0; i < numtargets; i++){
+		t = targets[i];
+		if(t->type == Emucent && t->mucent.room == room){
+			show = t->mucent.show;
+			if(argv[0][0] == 'w' && show != nil){
+				if(strcmp(show, "away") == 0 || strcmp(show, "xa") == 0)
+					continue;
+			}
+
+			print("  %*s  ", -room->muc.width, t->name);
+			if(argv[0][0] == 'W')
+				print(" %-7s ", show != nil ? show : "");
+			print("%-11s %-7s  %s\n",
+				roles[t->mucent.role],
+				affs[t->mucent.aff],
+				t->mucent.jid != nil ? t->mucent.jid : "");
+			num++;
+		}
+	}
+	print("%d user(s)\n", num);
+	return 0;
+}
+
+int
+cmdnick(int fd, int argc, char **argv)
+{
+	char *nick, *p;
+	int i, res;
+
+	if(argc < 2 || curr < 0 || targets[curr]->type != Emuc)
+		return 0;
+	nick = strdup(argv[1]);
+	for(i = 2; i < argc; i++){
+		p = nick;
+		nick = smprint("%s %s", nick, argv[i]);
+		free(p);
+	}
+	res = fprint(fd,
+		"<presence from='%Ӽ' to='%Ӽ/%Ӽ'></presence>",
+		myjid, targets[curr]->jid, nick);
+	free(nick);
+	return res;
+}
--- /dev/null
+++ b/rost.c
@@ -1,0 +1,287 @@
+#include <u.h>
+#include <libc.h>
+#include "xml.h"
+#include "xmpp.h"
+
+enum
+{
+	Snone = 0,
+	Sto,
+	Sfrom,
+	Sboth,
+};
+
+char *subscr2str[] =
+{
+	[Snone] "none",
+	[Sto]   "to",
+	[Sfrom] "from",
+	[Sboth] "both",
+};
+
+char *subscr2fmt[] =
+{
+	[Snone] "[  ]",
+	[Sto]   "[← ]",
+	[Sfrom] "[ →]",
+	[Sboth] "[←→]",
+};
+
+static char **asked;
+static int numasked, rostwidth;
+
+int
+rostupdate(Xelem *x, int fd)
+{
+	Xattr *n, *s, *j;
+	Target *t;
+	int i, width;
+
+	if(fprint(fd, "<presence/>") < 0)
+		return -1;
+
+	for(x = x->ch->ch; x != nil; x = x->next){
+		n = xmlgetattr(x->a, "name");
+		s = xmlgetattr(x->a, "subscription");
+		j = xmlgetattr(x->a, "jid");
+		if(j == nil || s == nil)
+			continue;
+		if(n == nil)
+			n = j;
+
+		for(i = 0, t = nil; i < numtargets; i++, t = nil){
+			t = targets[i];
+			if(t->type == Erost && strcmp(t->jid, j->v) == 0)
+				break;
+		}
+		if(t == nil){
+			t = addtarget(Erost, n->v);
+			t->jid = strdup(j->v);
+		}else{
+			if(strcmp(s->v, "remove") == 0){
+				print("%t removed from roster\n", t);
+				rmtarget(t);
+				continue;
+			}
+			free(t->name);
+			t->name = strdup(n->v);
+		}
+		if((width = utflen(t->name)) > rostwidth)
+			rostwidth = width;
+		for(i = 0; i < nelem(subscr2str); i++){
+			if(strcmp(s->v, subscr2str[i]) == 0){
+				t->rost.subscr = i;
+				break;
+			}
+		}
+	}
+
+	return 0;
+}
+
+int
+rostsubscr(char *from, char *type, int fd)
+{
+	Target *t;
+	int i;
+
+	if(strcmp(type, "subscribe") == 0){
+		for(i = 0; i < numtargets; i++){
+			t = targets[i];
+			if(t->type == Erost && (t->rost.flags & Fasked) && strncmp(t->jid, from, strlen(t->jid)) == 0){
+				fprint(fd, "<presence to='%Ӽ' type='subscribed'/>", from);
+				return 1;
+			}
+		}
+		for(i = 0; i < numasked; i++)
+			if(strcmp(asked[i], from) == 0)
+				return 1;
+		print("%s asks for a subscription\n", from);
+		numasked++;
+		asked = realloc(asked, numasked*sizeof(asked[0]));
+		asked[numasked-1] = strdup(from);
+		return 1;
+	}else if(strcmp(type, "subscribed") == 0){
+		for(i = 0; i < numtargets; i++){
+			t = targets[i];
+			if(t->type == Erost && strncmp(t->jid, from, strlen(t->jid)) == 0){
+				t->rost.flags |= Fasked;
+				print("%s has approved subscription\n", from);
+				fprint(fd, "<presence to='%Ӽ' type='subscribe'/>", from); /* ack */
+				return 1;
+			}
+		}
+	}
+
+	return 0;
+}
+
+void
+rostpresence(Xelem *x, Target *t)
+{
+	Xelem *show;
+	Xattr *type;
+	char *didwhat, *s;
+	int online;
+
+	didwhat = nil;
+	type = xmlgetattr(x->a, "type");
+	show = xmlget(x->ch, "show");
+	online = 1;
+	if(type != nil){
+		if(strcmp(type->v, "unavailable") == 0)
+			online = 0;
+		else{
+			if(strcmp(type->v, "unsubscribed") == 0)
+				print("%t cancelled subscription\n", t);
+			else if(strcmp(type->v, "unsubscribe") == 0)
+				print("%t unsubscribed\n", t);
+			return;
+		}
+	}
+
+	if(!online && (t->rost.flags & Fonline)){
+		didwhat = "went offline";
+		t->rost.flags &= ~Fonline;
+	}else if(online && (t->rost.flags & Fonline) == 0){
+		didwhat = "is online";
+		t->rost.flags |= Fonline;
+	}
+
+	if(show == nil)
+		s = "";
+	else
+		s = show->v;
+	if(t->rost.show != nil && strcmp(s, t->rost.show) == 0)
+		s = nil;
+	else{
+		free(t->rost.show);
+		t->rost.show = strdup(s);
+	}
+
+	if(nopresence || didwhat == nil)
+		return;
+	print("[%s] %s %s", strtime(), t->name, didwhat);
+	if(s != nil && s[0] != 0)
+		print(" (%s)", s);
+	print("\n");
+}
+
+static int
+cmdrostadd(int fd, char *jid, char *name)
+{
+	int i;
+
+	for(i = 0; i < numasked; i++){
+		if(strncmp(asked[i], jid, strlen(jid)) == 0){
+			if(fprint(fd, "<presence to='%Ӽ' type='subscribed'/>", asked[i]) < 0)
+				return -1;
+			jid = asked[i];
+			break;
+		}
+	}
+
+	if(fprint(fd, "<iq type='set'><query xmlns='jabber:iq:roster'><item jid='%Ӽ'", jid) < 0)
+		return -1;
+	if(name != nil && fprint(fd, " name='%Ӽ'", name) < 0)
+		return -1;
+	if(fprint(fd, "/></query></iq><presence to='%Ӽ' type='subscribe'/>", jid) < 0)
+		return -1;
+	print("asking %s for a subscription\n", jid);
+
+	if(i < numasked){
+		free(asked[i]);
+		numasked--;
+		memcpy(&asked[i], &asked[i+1], sizeof(asked[0])*(numasked-i));
+	}
+	return 0;
+}
+
+static int
+cmdrostrm(int fd, char *jid)
+{
+	Target *t;
+	char *a, *b;
+	int i, alen, blen, res;
+
+	for(i = 0; i < numasked; i++){
+		if(strncmp(asked[i], jid, strlen(jid)) == 0){
+			if(fprint(fd, "<presence to='%Ӽ' type='unsubscribed'/>", asked[i]) < 0)
+				return -1;
+			free(asked[i]);
+			numasked--;
+			memcpy(&asked[i], &asked[i+1], sizeof(asked[0])*(numasked-i));
+			break;
+		}
+	}
+
+	a = jid;
+	b = strchr(a, '/');
+	if(b == nil){
+		blen = 0;
+		alen = strlen(a);
+	}else{
+		b++;
+		blen = strlen(b);
+		alen = b-1 - a;
+	}
+
+	res = 0;
+	for(i = 0; i < numtargets; i++){
+		t = targets[i];
+		if(t->type == Erost && targmatches(t, a, alen) && (b == nil || strncmp(t->jid, b, blen) == 0)){
+			res = fprint(fd,
+				"<iq type='set'>"
+				"<query xmlns='jabber:iq:roster'>"
+				"<item jid='%Ӽ' subscription='remove'/>"
+				"</query></iq>",
+				t->jid);
+			if(res > 0)
+				res = fprint(fd, "<presence to='%Ӽ' type='unsubscribe'/>", t->jid);
+			break;
+		}
+	}
+	return res;
+}
+
+static int
+cmdrostshow(int extra)
+{
+	int i, num;
+
+	for(i = num = 0; i < numtargets; i++){
+		Target *t;
+		t = targets[i];
+		if(t->type != Erost)
+			continue;
+		if((t->rost.flags & Fonline) == 0 && !extra)
+			continue;
+		print("  %s %*s", subscr2fmt[t->rost.subscr], -rostwidth, t->name);
+		if(t->rost.show != nil && t->rost.show[0] != 0)
+			print("  [%s]", t->rost.show);
+		else
+		if((t->rost.flags & Fonline) == 0)
+			print("  [offline]");
+		print("\n");
+		if(extra && strcmp(t->name, t->jid) != 0)
+			print("    %s\n", t->jid);
+		num++;
+	}
+	print("%d user(s) online\n", num);
+	return 0;
+}
+
+int
+cmdroster(int fd, int argc, char **argv)
+{
+	int op;
+
+	op = argv[0][1];
+	if(op == 0)
+		return cmdrostshow(argv[0][0] == 'R');
+	else if(op == '+' && argc > 1)
+		return cmdrostadd(fd, argv[1], (argc > 2 ? argv[2] : nil));
+	else if(op == '-' && argc > 1)
+		return cmdrostrm(fd, argv[1]);
+	return 0;
+}
--- /dev/null
+++ b/targ.c
@@ -1,0 +1,169 @@
+#include <u.h>
+#include <libc.h>
+#include "xml.h"
+#include "xmpp.h"
+
+Target **targets;
+int curr, numtargets;
+
+int
+targmatches(Target *t, char *s, int n)
+{
+	return	(t->name != nil && strncmp(t->name, s, n) == 0) ||
+			(t->jid  != nil && strncmp(t->jid,  s, n) == 0);
+}
+
+static int
+cmptarg(void *a, void *b)
+{
+	Target *ta, *tb;
+	int r;
+
+	ta = *(void**)a;
+	tb = *(void**)b;
+	if(ta->name != nil && tb->name != nil)
+		r = strcmp(ta->name, tb->name);
+	else
+		r = strcmp(ta->jid, tb->jid);
+	if(r == 0 && ta->type == Erost && tb->type == Erost)
+		return (ta->rost.flags & Fonline) ? -1 : 1;
+	return r;
+}
+
+static void
+sorttargets(void)
+{
+	Target *t;
+
+	t = (curr >= 0) ? targets[curr] : nil;
+	qsort(targets, numtargets, sizeof(t), cmptarg);
+	if(t != nil){
+		for(curr = 0; curr < numtargets; curr++)
+			if(targets[curr] == t)
+				break;
+	}
+}
+
+Target*
+addtarget(int type, char *name)
+{
+	Target *t;
+
+	numtargets++;
+	if(numtargets == 1 || (numtargets & (numtargets-1)) == 0)
+		targets = realloc(targets, sizeof(t)*numtargets*2);
+	if(targets == nil)
+		sysfatal("%r");
+	t = targets[numtargets-1] = mallocz(sizeof(*t), 1);
+	if(t == nil)
+		sysfatal("%r");
+	t->type = type;
+	t->name = strdup(name);
+	sorttargets();
+	return t;
+}
+
+void
+rmtarget(Target *t)
+{
+	int i;
+
+	if(curr >= 0 && targets[curr] == t){
+		setlabel("", nil);
+		curr = -1;
+	}
+	free(t->name);
+	free(t->jid);
+
+	if(t->type == Erost)
+		free(t->rost.show);
+	else if(t->type == Emucent){
+		free(t->mucent.jid);
+		free(t->mucent.show);
+	}else if(t->type == Emuc)
+		free(t->muc.subj);
+
+	free(t);
+	for(i = 0; i < numtargets; i++){
+		if(targets[i] == t){
+			numtargets--;
+			memcpy(&targets[i], &targets[i+1], sizeof(t)*(numtargets-i));
+			if(curr > i)
+				curr--;
+			t = nil;
+			break;
+		}
+	}
+	if(t != nil)
+		sysfatal("rmtarget: not found");
+}
+
+int
+cmdtarget(int, int argc, char **argv)
+{
+	Target *t;
+	char *a, *b;
+	int i, cycle, alen, blen;
+
+	if(argc < 2){
+		curr = -1;
+		setlabel("", nil);
+		return 0;
+	}
+
+	a = argv[1];
+	b = strchr(a, '/');
+	if(b == nil){
+		blen = 0;
+		alen = strlen(a);
+	}else{
+		b++;
+		blen = strlen(b);
+		alen = b-1 - a;
+	}
+
+	for(cycle = 0, i = curr+1; cycle < 2; cycle++){
+		for(; i < numtargets; i++){
+			char *s;
+			t = targets[i];
+			if(b == nil && t->type == Emuc && targmatches(t, a, alen)){
+				setlabel(t->jid, nil);
+				curr = i;
+				return 0;
+			}
+			if(t->type == Erost && targmatches(t, a, alen) && (b == nil || strncmp(t->jid, b, blen) == 0)){
+				s = smprint("%t", t);
+				setlabel(s, (t->rost.flags & Fonline) ? t->rost.show : "offline");
+				free(s);
+				curr = i;
+				return 0;
+			}
+			if(t->type == Emucent && b != nil && targmatches(t->mucent.room, a, alen) && strncmp(t->name, b, blen) == 0){
+				s = smprint("%t", t);
+				setlabel(s, nil);
+				free(s);
+				curr = i;
+				return 0;
+			}
+		}
+		i = 0;
+	}
+
+	print("no such target: %q\n", a);
+	return 0;
+}
+
+int
+targetfmt(Fmt *f)
+{
+	Target *t;
+
+	t = va_arg(f->args, Target*);
+	if(t->type == Erost && curr >= 0 && targets[curr] == t)
+		return fmtprint(f, "%s", t->name);
+	if(t->type == Emucent)
+		return fmtprint(f, "%s/%s", t->mucent.room->name, t->name);
+	if(t->jid == nil)
+		return fmtprint(f, "%s", t->name);
+	return fmtprint(f, "%s/%s", t->name, t->jid);
+}
--- /dev/null
+++ b/xml.c
@@ -1,0 +1,343 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "xml.h"
+
+static char *escmap[] =
+{
+	"\x06\"&quot;",
+	"\x06\'&apos;",
+	"\x04<&lt;",
+	"\x04>&gt;",
+	"\x05&&amp;",
+};
+
+enum
+{
+	Xmlvalue = 2,
+};
+
+static char*
+unxml(char *orig)
+{
+	char *s;
+	Rune r;
+	int i, n, rsz;
+
+	n = 0;
+	for(s = orig; *s != 0; s += rsz, n += rsz){
+		if(*s == '\r'){
+			if(s[1] == '\n'){
+				n--;
+				rsz = 1;
+				continue;
+			}
+			*s = '\n';
+		}
+
+		rsz = chartorune(&r, s);
+		memmove(orig+n, s, rsz);
+		if(*s == '&'){
+			for(i = 0; i < nelem(escmap); i++){
+				if(strncmp(s, &escmap[i][2], escmap[i][0]) == 0){
+					orig[n] = escmap[i][1];
+					s += escmap[i][0] - 1;
+					break;
+				}
+			}
+		}
+	}
+
+	orig[n] = 0;
+	return orig;
+}
+
+static void
+xmlprint_(Xelem *x, int fd, int off)
+{
+	Xattr *a;
+
+	for(; x != nil; x = x->next){
+		fprint(fd, "%*c%q", off, ' ', x->n);
+		if(x->v != nil)
+			fprint(fd, "=%q", x->v);
+		for(a = x->a; a != nil; a = a->next)
+			fprint(fd, " %q=%q", a->n, a->v);
+		fprint(fd, "\n");
+		off += 4;
+		xmlprint_(x->ch, fd, off);
+		off -= 4;
+	}
+}
+
+static Xattr*
+xmlattr(char *s, int *err)
+{
+	Xattr *a, *attrs;
+	char *p;
+
+	attrs = nil;
+
+	for(; *s;){
+		a = mallocz(sizeof(*a), 1);
+		a->n = s;
+		for(; *s && *s != '='; s++);
+		if(*s != '='){
+			werrstr("xml sucks (%d)", *s);
+			goto error;
+		}
+		*s++ = 0;
+		if(*s != '\'' && *s != '\"'){
+			werrstr("xml is complicated (%d)", *s);
+			goto error;
+		}
+		a->v = s+1;
+		s = utfrune(a->v, *s);
+		if(s == nil){
+			werrstr("xml is broken");
+			goto error;
+		}
+		*s++ = 0;
+		a->next = attrs;
+		a->n = unxml(a->n);
+		a->v = unxml(a->v);
+		attrs = a;
+		if(*s == ' ')
+			s++;
+		if((p = strchr(a->n, ':')) != nil && strncmp(p, ":zdef", 5) == 0)
+			*p = 0;
+	}
+
+	return attrs;
+error:
+	*err = 1;
+	free(a);
+	for(; attrs != nil; attrs = a){
+		a = attrs->next;
+		free(attrs);
+	}
+	return nil;
+}
+
+static Xelem*
+xmlread_(Biobufhdr *h, Xelem *par, int flags, int *err)
+{
+	char *s, *t;
+	Xelem *x, *ch;
+	int r, closed, len;
+
+	x = nil;
+
+	for(;;){
+		r = Bgetrune(h);
+		if(r < 0){
+			werrstr("xmlread: %r");
+			goto error;
+		}
+		if(r == '<')
+			break;
+		if(isspacerune(r))
+			continue;
+		if(flags & Xmlvalue && par != nil){
+			Bungetrune(h);
+			if((s = Brdstr(h, '<', 1)) == nil){
+				werrstr("xmlread: %r");
+				goto error;
+			}
+			par->v = unxml(s);
+			if((s = Brdstr(h, '>', 1)) == nil){
+				free(par->v);
+				par->v = nil;
+				werrstr("xmlread: %r");
+			}
+			free(s);
+			return nil;
+		}
+		werrstr("xmlread: unexpected rune (%C)", r);
+		goto error;
+	}
+
+	s = Brdstr(h, '>', 1);
+	if(s == nil){
+		werrstr("xmlread: %r");
+		goto error;
+	}
+	if(s[0] == '/'){
+		free(s);
+		return nil;
+	}
+	if(s[0] == '?'){
+		free(s);
+		return xmlread_(h, par, flags, err);
+	}
+
+	x = mallocz(sizeof(*x), 1);
+	x->priv = s;
+	x->n = s;
+
+	if(strncmp(x->n, "zdef", 4) == 0){
+		if((x->n = strchr(x->n, ':')) == nil){
+			werrstr("xmlread: zdef without ':'");
+			goto error;
+		}
+		x->n += 1;
+	}
+
+	len = strlen(s);
+	if(s[len-1] == '/' || s[len-1] == '?'){
+		closed = 1;
+		s[len-1] = 0;
+	}else
+		closed = flags & Xmlstartonly;
+
+	for(; *s && *s != ' '; s++);
+	if(*s){
+		*s++ = 0;
+		x->a = xmlattr(s, err);
+	}
+
+	if(strcmp(x->n, "html") == 0){
+		for(len = 0;; len += r){
+			s = Brdstr(h, '>', 0);
+			if(s == nil){
+				werrstr("xmlread: %r");
+				goto error;
+			}
+
+			r = strlen(s);
+			x->v = realloc(x->v, len + r + 1);
+			if(x->v == nil){
+				werrstr("xmlread: %r");
+				goto error;
+			}
+			strcpy(x->v+len, s);
+			free(s);
+			t = strstr(x->v+len, "</html>");
+			if(t != nil){
+				*t = 0;
+				return x;
+			}
+		}
+	}
+
+	if(!closed){
+		for(;;){
+			flags = Xmlvalue;
+			ch = xmlread_(h, x, flags, err);
+			if(ch == nil)
+				break;
+			ch->next = x->ch;
+			x->ch = ch;
+		}
+	}
+
+	if(!*err)
+		return x;
+
+error:
+	*err = 2;
+	xmlfree(x);
+	return nil;
+}
+
+Xelem*
+xmlread(Biobuf *b, int flags, int *err)
+{
+	*err = 0;
+	return xmlread_(b, nil, flags & Xmlstartonly, err);
+}
+
+void
+xmlfree(Xelem *x)
+{
+	Xattr *a, *ta;
+	Xelem *n, *n2;
+
+	if(x == nil)
+		return;
+
+	xmlfree(x->ch);
+	free(x->v);
+	x->ch = nil;
+	x->v = nil;
+	free(x->priv);
+	for(a = x->a; a != nil; a = ta){
+		ta = a->next;
+		free(a);
+	}
+
+	for(n = x->next; n != nil; n = n2){
+		n2 = n->next;
+		n->next = nil;
+		xmlfree(n);
+	}
+
+	free(x);
+}
+
+Xelem*
+xmlget(Xelem *x, char *name)
+{
+	for(; x != nil; x = x->next)
+		if(strcmp(x->n, name) == 0)
+			return x;
+	return nil;
+}
+
+Xattr*
+xmlgetattr(Xattr *a, char *name)
+{
+	for(; a != nil; a = a->next)
+		if(strcmp(a->n, name) == 0)
+			return a;
+	return nil;
+}
+
+void
+xmlprint(Xelem *x, int fd)
+{
+	xmlprint_(x, fd, 0);
+}
+
+int
+xmlstrfmt(Fmt *f)
+{
+	char *s, *orig, *new;
+	int i, sz, n;
+
+	orig = va_arg(f->args, char*);
+	for(s = orig; *s; s++){
+		for(i = 0; i < nelem(escmap); i++){
+			if(escmap[i][1] == *s)
+				goto escape;
+		}
+	}
+	return fmtprint(f, "%s", orig);
+
+escape:
+	n = s-orig;
+	sz = n*2 + 1;
+	new = malloc(sz);
+	memcpy(new, orig, n);
+
+	for(; *s; s++, n++){
+		if(sz <= n+6){
+			sz = (n+6)*2;
+			new = realloc(new, sz);
+		}
+		new[n] = *s;
+
+		for(i = 0; i < nelem(escmap); i++){
+			if(escmap[i][1] == *s){
+				memcpy(new+n, &escmap[i][2], escmap[i][0]);
+				n += escmap[i][0] - 1;
+				break;
+			}
+		}
+	}
+
+	new[n] = 0;
+	n = fmtprint(f, "%s", new);
+	free(new);
+	return n;
+}
--- /dev/null
+++ b/xml.h
@@ -1,0 +1,35 @@
+typedef struct Xelem Xelem;
+typedef struct Xattr Xattr;
+
+struct Xelem
+{
+	char  *n;
+	char  *v;
+	Xattr *a;
+	Xelem *ch;
+	Xelem *next;
+	void  *priv;
+};
+
+struct Xattr
+{
+	char  *n;
+	char  *v;
+	Xattr *next;
+};
+
+enum
+{
+	Xmlstartonly = 1,
+};
+
+typedef struct Biobuf Biobuf;
+
+Xelem *xmlread(Biobuf *b, int flags, int *err);
+void xmlfree(Xelem *xml);
+void xmlprint(Xelem *x, int fd);
+Xelem *xmlget(Xelem *x, char *n);
+Xattr *xmlgetattr(Xattr *a, char *n);
+
+#pragma varargck type "Ӽ" char*
+int xmlstrfmt(Fmt *f);
--- /dev/null
+++ b/xmpp.c
@@ -1,0 +1,515 @@
+#include <u.h>
+#include <libc.h>
+#include <auth.h>
+#include <bio.h>
+#include <thread.h>
+#include "xml.h"
+#include "xmpp.h"
+
+/* commands are at xmpp.c:/^handle */
+
+char *enttypes[] =
+{
+	[Emuc]    "groupchat",
+	[Emucent] "chat",
+	[Erost]   "chat",
+};
+
+static int pfd;
+static Biobuf pb;
+static char lastid[32];
+
+int debug, nopresence, nohistory, plainallow;
+Biobuf kbin;
+char *server, *mydomain, *myjid, *mynick, *myresource;
+static QLock prlock;
+
+static void
+inerror(Xelem *x)
+{
+	Xattr *from, *type;
+	Xelem *err;
+
+	from = xmlgetattr(x->a, "from");
+	err = xmlget(x->ch, "error");
+	type = err == nil ? nil : xmlgetattr(err->a, "type");
+	print("[%s] (%s, error) %s %s",
+		strtime(), (from == nil) ? mydomain : from->v, x->n, type == nil ? "" : type->v);
+	err = err == nil ? nil : err->ch;
+	if(err != nil)
+		print(": %s", (err->v == nil) ? err->n : err->v);
+	print("\n");
+}
+
+static void
+inmsg(Xelem *x)
+{
+	Xattr *type, *from, *stamp, *to;
+	Xelem *body, *delay, *subj;
+	char *s, *nick, *bodyv, tmp[64];
+	Target *t, *room;
+	int i;
+
+	type = xmlgetattr(x->a, "type");
+	from = xmlgetattr(x->a, "from");
+	body = xmlget(x->ch, "body");
+	subj = xmlget(x->ch, "subject");
+	/* ignore "composing..." messages */
+	if(body == nil && subj == nil)
+		return;
+
+	to = xmlgetattr(x->a, "to");
+	if(to != nil && strncmp(to->v, myjid, strlen(myjid)) != 0)
+		return;
+
+	if((delay = xmlget(x->ch, "delay")) == nil)
+		delay = xmlget(x->ch, "x");
+	if((stamp = delay ? xmlgetattr(delay->a, "stamp") : nil) == nil)
+		stamp = xmlgetattr(x->a, "ts");
+	bodyv = (body == nil) ? nil : ((body->v == nil) ? "" : body->v);
+
+	/*
+	 * there is no difference between mucpriv and raw jid
+	 * try to find the target
+	 */
+	t = room = nil;
+	for(i = 0; i < numtargets; i++, t = nil){
+		t = targets[i];
+		if(t->type == Emuc && strncmp(t->jid, from->v, strlen(t->jid)) == 0)
+			room = t;
+		else if((t->type == Emucent && strcmp(t->jid, from->v) == 0) ||
+				(t->type == Erost && strncmp(t->jid, from->v, strlen(t->jid)) == 0)){
+			break;
+		}
+	}
+
+	if(subj != nil && room != nil){
+		free(room->muc.subj);
+		room->muc.subj = strdup((subj->v == nil) ? "" : subj->v);
+		return;
+	}
+	if(bodyv == nil)
+		return;
+
+	print("[%s] ", (stamp != nil) ? strstamp(stamp->v) : strtime());
+	if(t == nil && room == nil)
+		nick = from->v;
+	else if(t != nil && t->type == Erost){
+		snprint(tmp, sizeof(tmp), "%t", t);
+		nick = tmp;
+	}else{
+		/* extract nick and muc */
+		if(nick = strchr(from->v, '/'))
+			nick++;
+		if(s = strchr(from->v, '@'))
+			*s = 0;
+		print("(%s", from->v);
+		if(type != nil && strcmp(type->v, enttypes[Emucent]) == 0)
+			print(", private) ");
+		else
+			print(") ");
+	}
+
+	if(nick == nil)
+		print("%s\n", bodyv);
+	else if(strncmp(bodyv, "/me ", 4) == 0)
+		print("→ %s %s\n", nick, bodyv+4);
+	else
+		print("%s → %s\n", nick, bodyv);
+}
+
+static int
+iniq(Xelem *x, int fd)
+{
+	Xelem *e;
+	Xattr *a, *from, *id;
+	int isget, isset;
+
+	a = xmlgetattr(x->a, "type");
+	if(a == nil || x->ch == nil)
+		return 0;
+	id = xmlgetattr(x->a, "id");
+
+	if(strcmp(a->v, "result") == 0){
+		if(x->ch != nil && (e = xmlget(x->ch->ch, "storage")) != nil){
+			/* autojoin bookmarked MUCs http://xmpp.org/extensions/xep-0048.html */
+			a = xmlgetattr(e->a, "xmlns");
+			if(strcmp(a->v, "storage:bookmarks") != 0)
+				return 0;
+			return mucbookmarks(e, fd);
+		}
+		if(x->ch != nil && x->ch->ch != nil && strcmp(x->ch->n, "bind") == 0){
+			if(strcmp(x->ch->ch->n, "jid") == 0){
+				free(myjid);
+				myjid = strdup(x->ch->ch->v);
+			}
+		}
+		if(id != nil && strcmp(id->v, "afflist") == 0){
+			int width, len, num;
+			Xattr *aff, *jid;
+			for(e = x->ch->ch, width = 0; e != nil; e = e->next){
+				if((jid = xmlgetattr(e->a, "jid")) != nil && (len = strlen(jid->v)) > width)
+					width = len;
+			}
+			for(e = x->ch->ch, num = 0; e != nil; e = e->next, num++){
+				if((jid = xmlgetattr(e->a, "jid")) != nil && (aff = xmlgetattr(e->a, "affiliation")) != nil)
+					print("  %*s  %-8s\n", -width, jid->v, aff->v);
+			}
+			print("%d jid(s)\n", num);
+			return 0;
+		}else if(id != nil && strcmp(id->v, "gimme0") == 0){
+			/* bookmarks http://xmpp.org/extensions/xep-0048.html */
+			if(fprint(pfd,
+				"<iq type='get' from='%Ӽ' id='gimme1'>"
+				"<query xmlns='jabber:iq:private'>"
+				"<storage xmlns='storage:bookmarks'/>"
+				"</query></iq>",
+				myjid) < 0)
+				return -1;
+
+			/* ask for roster */
+			if(fprint(pfd,
+				"<iq type='get' from='%Ӽ' id='gimme2'>"
+				"<query xmlns='jabber:iq:roster'/></iq>",
+				myjid) < 0)
+				return -1;
+		}
+	}
+
+	/* incoming queries */
+	isget = strcmp(a->v, "get") == 0;
+	isset = strcmp(a->v, "set") == 0;
+	if(!isget && !isset && strcmp(a->v, "result") != 0)
+		return 0;
+	from = xmlgetattr(x->a, "from");
+	if(isget && (from == nil || id == nil))
+		return 0;
+
+	if(e = xmlget(x->ch, "query")){
+		a = xmlgetattr(e->a, "xmlns");
+		if(a != nil && isget &&strcmp(a->v, "jabber:iq:version") == 0){
+			/* software version http://xmpp.org/extensions/xep-0092.html */
+			return fprint(fd,
+				"<iq type='result' to='%Ӽ' id='%Ӽ'>"
+				"<query xmlns='jabber:iq:version'>"
+				"<name>xmpp</name>"
+				"<version>9front edition</version>"
+				"<os>Plan 9</os>"
+				"</query></iq>",
+				from->v, id->v);
+		}
+		if(a != nil && isget && strcmp(a->v, "http://jabber.org/protocol/disco#info") == 0){
+			/* service discovery http://xmpp.org/extensions/xep-0030.html */
+			return fprint(fd,
+				"<iq type='result' to='%Ӽ' id='%Ӽ'>"
+				"<query xmlns='http://jabber.org/protocol/disco#info'>"
+				"<feature var='http://jabber.org/protocol/disco#info'/>"
+				"<feature var='jabber:iq:version'/>"
+				"<feature var='urn:xmpp:time'/>"
+				"<feature var='urn:xmpp:ping'/>"
+				"<feature var='http://jabber.org/protocol/muc'/>"
+				"</query></iq>",
+				from->v, id->v);
+		}
+		if(a != nil && !isget && strcmp(a->v, "jabber:iq:roster") == 0)
+			return rostupdate(x, fd);
+	}else if(isget && (e = xmlget(x->ch, "time")) != nil){
+		a = xmlgetattr(e->a, "xmlns");
+		if(a != nil && strcmp(a->v, "urn:xmpp:time") == 0){
+			/* entity time http://xmpp.org/extensions/xep-0202.html */
+			char *utc, *tzo;
+
+			utc = strenttime(&tzo);
+			return fprint(fd,
+				"<iq type='result' to='%Ӽ' id='%Ӽ'>"
+				"<time xmlns='urn:xmpp:time'>"
+				"<utc>%Ӽ</utc><tzo>%Ӽ</tzo>"
+				"</time></iq>",
+				from->v, id->v, utc, tzo);
+		}
+	}else if(isget && (e = xmlget(x->ch, "ping")) != nil){
+		a = xmlgetattr(e->a, "xmlns");
+		if(a != nil && strcmp(a->v, "urn:xmpp:ping") == 0){
+			/* ping http://xmpp.org/extensions/xep-0199.html */
+			return fprint(fd,
+				"<iq type='result' to='%Ӽ' id='%Ӽ'/>",
+				from->v, id->v);
+		}
+	}
+
+	if(isget)
+		return fprint(fd,
+			"<iq type='error' to='%Ӽ' id='%Ӽ'>"
+			"<error type='cancel'>"
+			"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
+			"</error></iq>",
+			from->v, id->v);
+	return 0;
+}
+
+static void
+inpresence(Xelem *x, int fd)
+{
+	int i, found;
+	Target *t;
+	Xattr *from, *type;
+
+	from = xmlgetattr(x->a, "from");
+	type = xmlgetattr(x->a, "type");
+	if(type != nil && rostsubscr(from->v, type->v, fd))
+		return;
+	found = 0;
+	for(i = 0; i < numtargets; i++){
+		t = targets[i];
+		if(t->type == Erost && strncmp(t->jid, from->v, strlen(t->jid)) == 0){
+			rostpresence(x, t);
+			found = 1;
+			break;
+		}
+		if(t->type == Emucent && strcmp(t->jid, from->v) == 0){
+			mucpresence(x, t->mucent.room, from);
+			found = 1;
+		}
+		if(t->type == Emuc && strncmp(t->jid, from->v, strlen(t->jid)) == 0){
+			mucpresence(x, t, from);
+			found = 1;
+		}
+	}
+
+	if(!found && debug > 0)
+		fprint(2, "presence from unknown target: %q\n", from->v);
+}
+
+static void
+reader(void *pd)
+{
+	Xelem *x;
+	Xattr *type, *id;
+	int err;
+
+	USED(pd);
+	threadsetname("reader");
+
+	fprint(pfd,
+		"<iq type='get' from='%Ӽ' to='%Ӽ' id='gimme0'>"
+		"<query xmlns='http://jabber.org/protocol/disco#info'/>"
+		"</iq>",
+		myjid, server);
+
+	for(err = 0; err == 0;){
+		if((x = xmlread(&pb, 0, &err)) == nil)
+			continue;
+
+		qlock(&prlock);
+		if((id = xmlgetattr(x->a, "id")) != nil && strcmp(id->v, lastid) == 0){
+			xmlprint(x, 2);
+			lastid[0] = 0;
+		}else if(debug > 1){
+			if(strcmp(x->n, "presence") != 0 || debug > 2)
+				xmlprint(x, 2);
+		}
+
+		type = xmlgetattr(x->a, "type");
+		if(type != nil && strcmp(type->v, "error") == 0)
+			inerror(x);
+		else if(strcmp(x->n, "message") == 0)
+			inmsg(x);
+		else if(strcmp(x->n, "presence") == 0)
+			inpresence(x, pfd);
+		else if(strcmp(x->n, "iq") == 0)
+			iniq(x, pfd);
+		xmlfree(x);
+		qunlock(&prlock);
+	}
+	fprint(2, "%r\n");
+	threadexitsall(nil);
+}
+
+static int
+cmdmsg(int fd, int, char **)
+{
+	char *s;
+	int res;
+
+	if(curr < 0)
+		return 0;
+
+	s = readlines();
+	res = fprint(fd,
+		"<message to='%Ӽ' type='%Ӽ'><body>%Ӽ</body></message>",
+		targets[curr]->jid,
+		enttypes[targets[curr]->type],
+		s);
+	free(s);
+	return res;
+}
+
+static int
+handle(int fd, char *s)
+{
+	typedef int (*cmdf)(int, int, char **);
+	char *ps, *pe, *argv[3];
+	static cmdf cmds[256] = {
+		['a'] cmdaff,      /*  muc.c:/^cmdaff      */
+		['b'] cmdbookmark, /*  muc.c:/^cmdbookmark */
+		['j'] cmdjoin,     /*  muc.c:/^cmdjoin     */
+		['m'] cmdmsg,      /* xmpp.c:/^cmdmsg      */
+		['n'] cmdnick,     /*  muc.c:/^cmdnick     */
+		['p'] cmdpart,     /*  muc.c:/^cmdpart     */
+		['r'] cmdroster,   /* rost.c:/^cmdroster   */
+		['R'] cmdroster,   /* rost.c:/^cmdroster   */
+		['s'] cmdsubj,     /*  muc.c:/^cmdsubj     */
+		['S'] cmdsubj,     /*  muc.c:/^cmdsubj     */
+		['t'] cmdtarget,   /* targ.c:/^cmdtarget   */
+		['w'] cmdwho,      /*  muc.c:/^cmdwho      */
+		['W'] cmdwho,      /*  muc.c:/^cmdwho      */
+	};
+	int argc;
+
+	cleaninput(utflen(s)+1);
+	if(*s == '/' && *(++s) != '/'){
+		if(*s == 'q'){
+			for(s++; (*s == ' ' || *s == '\t'); s++);
+			lastid[0] = 0;
+			if((ps = utfutf(s, "id='")) != nil && (pe = utfrune(ps+4, '\'')) != nil){
+				ps += 4;
+				if(sizeof(lastid) > pe-ps)
+					strncpy(lastid, ps, pe-ps);
+			}
+			return fprint(fd, "%s", s);
+		}else if(*s == 'm' && s[1] == 'e'){
+			s--;
+		}else if(cmds[*s] != nil){
+			argc = tokenize(s, argv, nelem(argv));
+			return cmds[*s](fd, argc, argv);
+		}else{
+			s--;
+			print("unknown cmd %q\n", s);
+			return 0;
+		}
+	}
+
+	if(curr < 0)
+		return 0;
+
+	return fprint(fd,
+		"<message to='%Ӽ' type='%Ӽ'><body>%Ӽ</body></message>",
+		targets[curr]->jid, enttypes[targets[curr]->type], s);
+}
+
+static void
+writer(void *pd)
+{
+	char *s;
+	int err;
+
+	USED(pd);
+	threadsetname("writer");
+
+	Binit(&kbin, 0, OREAD);
+	for(err = 0; (s = Brdstr(&kbin, '\n', 1)) != nil && err >= 0;){
+		qlock(&prlock);
+		if(s[0] != 0)
+			err = handle(pfd, s);
+		free(s);
+		qunlock(&prlock);
+	}
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: xmpp [-n nick] [-r resource] [-p] [-y] jid\n");
+	threadexits("usage");
+}
+
+static int
+die(void *, char *)
+{
+	setlabel(nil, nil);
+	return 0;
+}
+
+static void
+pblethal(char *m)
+{
+	threadexitsall(m);
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	UserPasswd *up;
+	char *user;
+
+	debug = 0;
+	plainallow = 0;
+	nopresence = 1;
+	nohistory = 1;
+	myjid = nil;
+	mynick = getuser();
+	myresource = nil;
+	curr = -1;
+
+	ARGBEGIN{
+	case 'd':
+		debug++;
+		break;
+	case 'n':
+		mynick = EARGF(usage());
+		break;
+	case 'p':
+		nopresence = 0;
+		break;
+	case 'r':
+		myresource = EARGF(usage());
+		break;
+	case 'y':
+		plainallow = 1;
+		break;
+	case 'h':
+		nohistory = 0;
+		break;
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+	myjid = strdup(argv[0]);
+
+	quotefmtinstall();
+	fmtinstall('H', encodefmt);
+	fmtinstall('[', encodefmt);
+	fmtinstall('t', targetfmt);
+	fmtinstall(L'Ӽ', xmlstrfmt);
+	user = strdup(myjid);
+	server = strrchr(user, '@');
+	if(server == nil)
+		sysfatal("invalid jid: %q", user);
+	*server++ = 0;
+	server = strdup(server);
+	mydomain = strrchr(user, '@');
+	if(mydomain == nil)
+		mydomain = server;
+	else
+		mydomain = strdup(mydomain);
+	srand(time(nil));
+
+	up = auth_getuserpasswd(auth_getkey, "proto=pass service=xmpp server=%s user=%s", server, user);
+	if(up == nil)
+		sysfatal("no password: %r");
+
+	if((pfd = connect(&pb, up->user, up->passwd)) < 0)
+		sysfatal("connect: %r");
+	memset(up->passwd, 0, strlen(up->passwd));
+	free(up);
+	free(user);
+
+	setlabel("", nil);
+
+	Blethal(&pb, pblethal);
+	threadnotify(die, 1);
+	proccreate((void*)reader, nil, 8*1024);
+	writer(nil);
+	fprint(2, "%r\n");
+	threadexitsall(nil);
+}
--- /dev/null
+++ b/xmpp.h
@@ -1,0 +1,95 @@
+typedef struct Target Target;
+struct Target
+{
+	int  type;
+	char *name;
+	char *jid;
+	union
+	{
+		struct
+		{
+			int  width;
+			int  numents;
+			char *subj;
+		}muc;
+		struct
+		{
+			Target *room;
+			int    aff;
+			int    role;
+			char   *jid;
+			char   *show;
+		}mucent;
+		struct
+		{
+			char *show;
+			int  subscr;
+			int  flags;
+		}rost;
+	};
+};
+
+#pragma varargck type "t" Target*
+
+enum
+{
+	Anone = 0,
+	Aowner,
+	Aadmin,
+	Amember,
+	Aoutcast,
+
+	Rnone = 0,
+	Rmoder,
+	Rpart,
+	Rvisit,
+
+	Emuc = 0,
+	Emucent,
+	Erost,
+
+	Fonline = 1<<0,
+	Fasked = 1<<1,
+};
+
+extern char *server, *mydomain, *myjid, *mynick, *myresource;
+extern int debug, nopresence, nohistory, plainallow;
+extern Target **targets;
+extern int curr, numtargets;
+extern char *enttypes[];
+
+/* conn.c */
+int connect(Biobuf *b, char *user, char *passwd);
+
+/* muc.c */
+void mucpresence(Xelem *xml, Target *room, Xattr *from);
+int mucbookmarks(Xelem *e, int fd);
+int cmdaff(int fd, int argc, char **argv);
+int cmdbookmark(int fd, int argc, char **argv);
+int cmdjoin(int fd, int argc, char **argv);
+int cmdpart(int fd, int argc, char **argv);
+int cmdwho(int, int argc, char **argv);
+int cmdnick(int fd, int argc, char **argv);
+int cmdsubj(int fd, int argc, char **argv);
+
+/* rost.c */
+int rostupdate(Xelem *x, int fd);
+int rostsubscr(char *from, char *type, int fd);
+void rostpresence(Xelem *x, Target *t);
+int cmdroster(int fd, int argc, char **argv);
+
+/* misc.c */
+char *strtime(void);
+char *strenttime(char **tzo);
+void cleaninput(int n);
+void setlabel(char *label, char *prsuffix);
+char *strstamp(char *v);
+char *readlines(void);
+
+/* targ.c */
+int targmatches(Target *t, char *s, int n);
+Target *addtarget(int type, char *name);
+void rmtarget(Target *t);
+int cmdtarget(int, int argc, char **argv);
+#pragma varargck type "t" Target*
+int targetfmt(Fmt *f);
--- /dev/null
+++ b/xmpp.man
@@ -1,0 +1,153 @@
+.TH XMPP 1
+.SH NAME
+xmpp \- XMPP client
+.SH SYNOPSIS
+.B xmpp
+[
+.I -n nick
+]
+[
+.I -r resource
+]
+[
+.I -p
+]
+[
+.I -h
+]
+[
+.I -y
+]
+.I jid | jid@server
+.SH DESCRIPTION
+.I xmpp
+is a simple XMPP client.
+.PP
+It supports multi-user chat (MUC).
+Server-side bookmarks are used to join specific MUCs
+automatically on connect. TLS is required, SCRAM-SHA-1,
+DIGEST-MD5 and PLAIN authentication methods are supported.
+The latter is enabled using
+.I -y
+option. Thumbprints of trusted servers are expected to be in
+.I /sys/lib/tls/xmpp,
+see
+.IR thumbprint(6) .
+.PP
+.I -p
+enables "joined"/"left" messages, which are disabled by default.
+.PP
+.I -h
+allows MUCs to send out "discussion history".
+.PP
+The command language understood by
+.I xmpp
+is as follows:
+.EX
+/j [jid[/nick] [passwd]]
+.EE
+	join MUC
+.EX
+/p [target]
+.EE
+	part MUC
+.EX
+/q data
+.EE
+	send raw data to the server
+.EX
+/t target
+.EE
+	cycle through matching targets
+.EX
+/w [target]
+.EE
+	get list of active nicks in the MUC
+.EX
+/W [target]
+	get list of all nicks in the MUC
+.EE
+.EX
+/b
+.EE
+	list bookmarked MUCs
+.EX
+/b+
+.EE
+	bookmark current MUC
+.EX
+/b-
+.EE
+	remove current MUC from bookmarks
+.EX
+/a target affiliation
+.EE
+	set affiliation (for a target in MUC)
+.EX
+/a [room]
+.EE
+	get affiliations for a room (current one if no args given)
+.EX
+/me ...
+.EE
+	"/me"-style message
+.EX
+/m
+.EE
+	start multiline message (end with a dot on a single line)
+.EX
+/n nick
+.EE
+	set nick for current MUC
+.EX
+/r
+.EE
+	show roster (list online contacts)
+.EX
+/R
+.EE
+	show roster (show offline and jids)
+.EX
+/r+ (jid|target) [name]
+.EE
+	subscribe/approve, add to roster
+.EX
+/r- target
+.EE
+	unsubscribe/decline, remove from roster
+.EX
+/s [room]
+.EE
+	show current subject
+.EX
+/S [room]
+.EE
+	start new subject (end with a dot on a single line)
+.PP
+Affiliations are: none, owner, admin, member, outcast.
+.PP
+The target can be specified as follows:
+.EX
+room@domain            a specific room
+room@domain/nick       private chat (MUC)
+Wernher                private chat (roster, name)
[email protected]  private chat (roster, jid)
+.EE
+.PP
+Assuming you previously joined a MUC
+.I [email protected],
+you can quickly switch to private chat with
+.I kgbvax
+as follows:
+.EX
+/t pravda/kgbvax
+/t pr/kgb
+/t p/k
+.EE
+.PP
+.SH SOURCE
+https://github.com/ftrvxmtrx/xmpp
+.SH BUGS
+Of course.
+.SH NOTES
+XMPP sucks. XML sucks.