ref: 38fbed97371f4f970a542945883e7d68762a2184
author: Sigrid Haflínudóttir <ftrvxmtrx@gmail.com>
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\""",
+ "\x06\''",
+ "\x04<<",
+ "\x04>>",
+ "\x05&&",
+};
+
+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)
+wernher@paperclip.gov private chat (roster, jid)
+.EE
+.PP
+Assuming you previously joined a MUC
+.I pravda@cc.cpsu.su,
+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.