shithub: ircs

Download patch

ref: c5df4bd968e0c643de952eac7287497094334b62
author: kemal <[email protected]>
date: Sun Oct 3 04:14:42 EDT 2021

init

--- /dev/null
+++ b/dat.h
@@ -1,0 +1,50 @@
+enum {
+	Bufsize = 512,
+};
+
+typedef struct Ircmsg Ircmsg;
+typedef struct Ircpref Ircpref;
+typedef void (*Ircfmt)(Ircmsg *, char *, int);
+typedef struct List List;
+typedef struct Trie Trie;
+typedef struct Triewalk Triewalk;
+
+struct Ircpref {
+	char	*nick;
+	char	*user;
+	char	*host;
+	char	prebuf[128];
+};
+
+struct Ircmsg {
+	char	*pre;
+	char	*cmd;
+	char	*par[15];
+	char	*trail;
+	int	npar;
+	char	buf[Bufsize];
+	Ircpref;
+};
+
+struct List {
+	void	*val;
+	List	*next;
+};
+
+struct Trie {
+	Rune	rune;
+	Trie	*parent;
+	Trie	*child;
+	Trie	*next;
+	void	*value;
+};
+
+struct Triewalk {
+	Trie	*root;
+	Trie	*curr;
+	Rune	*key;
+	int	keysize;
+	int	depth;
+};
+
+extern char *logdir;
--- /dev/null
+++ b/fns.h
@@ -1,0 +1,20 @@
+int	ircparse(Ircmsg *, char *);
+void	ircprint(Ircmsg *);
+int	ircischan(char *);
+int	ircfmttime(char *, int, long, int *);
+void	ircfmtjoin(Ircmsg *, char *, int);
+void	ircfmtnick(Ircmsg *, char *, int);
+void	ircfmtquit(Ircmsg *, char *, int);
+void	ircfmtpart(Ircmsg *, char *, int);
+void	ircfmtpriv(Ircmsg *, char *, int);
+void	ircfmtnumeric(Ircmsg *, char *, int);
+void	listfree(List **);
+int	listadd(List **, void *);
+int	listdel(List **, void *);
+Trie*	triealloc(void);
+void	triefree(Trie *);
+void	triewalk(Trie *, Triewalk *, Rune *, int);
+void*	trienext(Triewalk *);
+void*	trieadd(Trie *, Rune *, void *);
+void*	triedel(Trie *, Rune *);
+void*	trieget(Trie *, Rune *);
--- /dev/null
+++ b/irc.c
@@ -1,0 +1,100 @@
+#include <u.h>
+#include <libc.h>
+#include "dat.h"
+
+static int
+ircprefix(Ircmsg *irc)
+{
+	char *p;
+
+	irc->nick = nil;
+	irc->user = nil;
+	irc->host = nil;
+	irc->prebuf[0] = 0;
+	
+	if(irc->pre == nil)
+		return -1;
+
+	snprint(irc->prebuf, sizeof(irc->prebuf), "%s", irc->pre);
+
+	irc->nick = irc->prebuf;
+
+	for(p = irc->prebuf; *p != 0; p++){
+		if(*p == '@'){
+			*p = 0;
+			irc->host = p+1;
+			break;
+		}
+		if(irc->user == nil && *p == '!'){
+			*p = 0;
+			irc->user = p+1;
+		}
+	}
+	return 0;
+}
+
+int
+ircparse(Ircmsg *irc, char *msg)
+{
+	int i;
+	char *p;
+
+	snprint(irc->buf, sizeof(irc->buf), "%s", msg);
+
+	irc->pre = nil;
+	irc->cmd = nil;
+	for(i = 0; i < 15; i++)
+		irc->par[i] = nil;
+	irc->trail = nil;
+	irc->npar = 0;
+
+	i = 0;
+	p = irc->buf;
+
+	if(*p == ':')
+		irc->pre = ++p;
+	else
+		irc->cmd = p;
+
+	for(; *p != 0; p++){
+		if(*p == ' '){
+			*p = 0;
+			if(irc->cmd == nil)
+				irc->cmd = p+1;
+			else {
+				if(i == 14 || *(p+1) == ':'){
+					if(*(p+1) == ':')
+						irc->trail = p+2;
+					else
+						irc->trail = p+1;
+					irc->par[i++] = irc->trail;
+					break;
+				}
+				irc->par[i++] = p+1;
+			}
+		}
+	}
+	assert(irc->trail == nil || irc->trail == irc->par[i-1]);
+	irc->npar = i;
+	ircprefix(irc);
+	return 0;
+}
+
+void
+ircprint(Ircmsg *irc)
+{
+	int i;
+
+	fprint(2, "pre=%s cmd=%s", irc->pre, irc->cmd);
+
+	for(i = 0; i < irc->npar; i++)
+		fprint(2, " par[%d]=%s", i, irc->par[i]);
+
+	fprint(2, " trail=%s\n", irc->trail != nil ? "y" : "n");
+}
+
+int
+ircischan(char *target)
+{
+	return target != nil && target[0] == '#';
+}
--- /dev/null
+++ b/ircfmt.c
@@ -1,0 +1,140 @@
+#include <u.h>
+#include <libc.h>
+#include "dat.h"
+#include "fns.h"
+
+static char *wday[] = {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri",
+	"Sat",
+};
+static char *mon[] = {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+};
+
+int
+ircfmttime(char *buf, int bufsize, long time, int *mday)
+{
+	Tm *t;
+	int n;
+
+	n = 0;
+	t = localtime(time);
+
+	if(t->mday != *mday){
+		*mday = t->mday;
+		n += snprint(buf, bufsize,
+			"-- %s, %02d %s %d --\n",
+			wday[t->wday], t->mday, mon[t->mon],
+			t->year + 1900);
+		bufsize -= n;
+		buf += n;
+	}
+	n += snprint(buf, bufsize,
+		"%02d:%02d:%02d ",
+		t->hour, t->min, t->sec);
+	return n;
+}
+
+void
+ircfmtjoin(Ircmsg *irc, char *buf, int bufsize)
+{
+	if(irc->pre != nil){
+		if(logdir != nil)
+			snprint(buf, bufsize,
+				"JOIN (%s)",
+				irc->nick);
+		else
+			snprint(buf, bufsize,
+				"JOIN (%s)\t%s",
+				irc->nick, irc->par[0]);
+	} else
+		snprint(buf, bufsize,
+			"JOIN %s",
+			irc->par[0]);
+}
+
+void
+ircfmtnick(Ircmsg *irc, char *buf, int bufsize)
+{
+	if(irc->pre != nil){
+		snprint(buf, bufsize,
+			"NICK %s → %s",
+			irc->nick, irc->par[0]);
+	} else
+		snprint(buf, bufsize,
+			"NICK %s",
+			irc->par[0]);
+}
+
+void
+ircfmtquit(Ircmsg *irc, char *buf, int bufsize)
+{
+	if(irc->pre != nil){
+		snprint(buf, bufsize,
+			"QUIT (%s)\t%s",
+			irc->nick, irc->trail);
+	} else
+		snprint(buf, bufsize,
+			"QUIT\t%s",
+			irc->trail);
+}
+
+void
+ircfmtpart(Ircmsg *irc, char *buf, int bufsize)
+{
+	if(irc->pre != nil){
+		if(logdir != nil)
+			snprint(buf, bufsize,
+				"PART (%s)",
+				irc->nick);
+		else
+			snprint(buf, bufsize,
+				"PART (%s)\t%s",
+				irc->nick, irc->par[0]);
+	} else
+		snprint(buf, bufsize,
+			"PART %s",
+			irc->par[0]);
+}
+
+void
+ircfmtpriv(Ircmsg *irc, char *buf, int bufsize)
+{
+	if(irc->pre != nil){
+		if(logdir != nil && ircischan(irc->par[0]))
+			snprint(buf, bufsize,
+				"%s → %s",
+				irc->nick, irc->trail);
+		else
+			snprint(buf, bufsize,
+				"(%s) %s → %s",
+				irc->par[0], irc->nick, irc->trail);
+	} else {
+		if(logdir != nil && ircischan(irc->par[0]))
+			snprint(buf, bufsize,
+				"⇐ %s",
+				irc->trail);
+		else
+			snprint(buf, bufsize,
+				"(%s) ⇐ %s",
+				irc->par[0], irc->trail);
+	}
+}
+
+void
+ircfmtnumeric(Ircmsg *irc, char *buf, int bufsize)
+{
+	int i, n;
+
+	*buf = 0;
+	n = snprint(buf, bufsize, irc->cmd);
+	bufsize -= n;
+	buf += n;
+
+	for(i = 1; i < irc->npar; i++){
+		n = snprint(buf, bufsize, " %s", irc->par[i]);
+		bufsize -= n;
+		buf += n;
+	}
+}
--- /dev/null
+++ b/ircs.man
@@ -1,0 +1,141 @@
+.TH IRCS 1
+.SH NAME
+ircs, ircx \- internet relay chat client and ui
+.SH SYNOPSIS
+ircs [ -deprTU ] [ -s srvname ] [ -f file ] nick[!user] addr
+
+ircx [ -eprTU ] [ -s srvname ] [ -f file ] [ -t target ] [ -b
+lines ] [ nick[!user] addr ]
+
+irctime [ file ... ]
+
+.SH DESCRIPTION
+Ircs is a persistent, logging IRC client inspired by the classic
+Plan 9 IRC client irc7.  Its usage is similar to irc7's "ircsrv":
+it makes a connection to an IRC server, posts its command pipe in
+srv(3), and logs incoming and outgoing messages to a log
+directory, or to a given file.
+
+Ircs dials the IRC server at addr and uses nick as the IRC
+nickname.  The addr is a network address understood by dial(2).
+The default port number is 6667 unless the -e option (enable TLS
+encryption) is given, in which case the default port is 6697.
+The username user can be specified by separating the nick and the
+user with an exclamation mark ('!').  If no user is given the
+nick is used also as the username.
+
+The user's (raw) IRC commands are read via a command pipe which
+by default is posted to service file /srv/ircs.  The log is
+written by default to files under /tmp/ircs directory; server and
+private messages are written to the main log file /tmp/ircs/log,
+and messages related to channels are written to files named
+/tmp/ircs/#channel, where "#channel" is the name of the channel.
+
+The -s option changes the service file to /srv/srvname and the
+log directory to /tmp/srvname.  The -f option causes all logs to
+be written to a given file instead of a directory.
+
+All messages are logged apart from PING/PONG; when a PING is
+received the modification time of the (main) log file is set to
+the current time.
+
+When logging to a directory, ircs attempts to keep track of all
+users (i.e.  nicks) on joined channels in order to log incoming
+NICK/QUIT messages to related channel log files.  When ircs
+cannot determine the channel(s) a particular user is on, or if
+the option -U (disable user tracking) is given, incoming
+NICK/QUIT messages are logged to the main log file.
+
+The -d option causes some debug info to be printed to standard
+error.
+
+The -p option causes a password to be sent to the server.  The
+password is read via the authentication agent factotum(4) which
+must be mounted in the same namespace.  The -p option can only be
+used with the -e (use TLS) option.
+
+By default the logs are formatted so that they can easily be
+processed with Plan 9 tools, like cat(1) or grep(1).  The log
+lines are preceded with timestamps in a human-readable format.
+The -r option disables this formatting and enables raw logging
+with timestamps in the Unix time format.  The -T option disables
+timestamps altogether.
+
+Normally a user need not deal with ircs directly, but can use the
+ircx wrapper script, whose usage is nearly identical to ircs.
+
+Ircx is a simple rc(1) user interface to ircs, similar to irc7's
+client program "irc".  It makes use of the service provided by an
+ircs instance, and provides an interface for starting and
+stopping ircs and for sending and displaying messages.  Unlike in
+irc7, where the most functionality is in the client part, in ircs
+the main functionality is intended to be in the service part.
+
+With nick and addr arguments ircx attempts to start an ircs
+instance; if the ircs starts successfully, ircx then runs the
+user interface: the related log file is printed to standard
+output as it accumulates, and user's commands are read from
+standard input.
+
+Without nick and addr arguments ircx assumes that the ircs
+instance is already running, and only the user interface is run.
+Ircx can be exited by pressing <del>; the /h command prints the
+list of known commands.
+
+The -t option sets the target (channel or nick) to which the
+input commands apply.  If the target is a channel (the first
+character is '#'), the channel is joined.  The -b option sets the
+number of lines that are printed from the end of the log file
+when ircx starts.
+
+An ircs instance is defined by its service name (and log file if
+-f option is used).  Like ircs, ircx uses by default the service
+file /srv/ircs and log directory /tmp/ircs; the service name
+"ircs" can be changed by the -s option.  When running multiple
+ircs instances, like when connecting to multiple IRC networks,
+the -s and (optionally) -f options should be used to
+differentiate between the instances.
+
+Irctime copies the given files (standard input by default) to its
+standard output, converting the default human-readable ircs
+timestamps to ISO 8601 format.
+
+.SH EXAMPLES
+Connect to freenode using nick and username "foo":
+.IP
+.EX
+ircx foo chat.freenode.net
+.EE
+.PP
+Join the channel "#cat-v" assuming ircs is already running:
+.IP
+.EX
+ircx -t '#cat-v'
+.EE
+.PP
+Follow server and private messages assuming ircs is already
+running:
+.IP
+.EX
+ircx
+.EE
+.PP
+Check what ircs instances are running:
+.IP
+.EX
+ps -a | grep ircs
+.EE
+
+.SH SOURCE
+
+http://www.plan9.fi/src/ircs.tgz
+
+.SH SEE ALSO
+
+https://tools.ietf.org/html/rfc2812
+
+.SH BUGS
+
+The -t (set target) option does not affect the output of ircx
+when the -f (set log file) option is used; ircx prints out all
+messages from the log file regardless of the target.
--- /dev/null
+++ b/irctime
@@ -1,0 +1,27 @@
+#!/bin/rc
+# irctime: convert ircs timestamps to ISO 8601 format
+awk '
+BEGIN {
+	mon["Jan"] = "01"
+	mon["Feb"] = "02"
+	mon["Mar"] = "03"
+	mon["Apr"] = "04"
+	mon["May"] = "05"
+	mon["Jun"] = "06"
+	mon["Jul"] = "07"
+	mon["Aug"] = "08"
+	mon["Sep"] = "09"
+	mon["Oct"] = "10"
+	mon["Nov"] = "11"
+	mon["Dec"] = "12"
+}
+/^-- / {
+	dd = $3
+	mm = mon[$4]
+	yyyy = $5
+	next
+}
+{
+	print yyyy "-" mm "-" dd "T" $0
+}
+' $*
--- /dev/null
+++ b/ircx
@@ -1,0 +1,317 @@
+#!/bin/rc
+# ircx: ircs ui
+rfork e
+
+flagfmt='e,p,r,T,U,s srvname,f file,t target,b lines'
+args='[nick[!user] addr]'
+
+srv=ircs
+mainlog=()
+log=()
+opt=()
+
+lines=()
+target=()
+offset=()
+
+help='<del>, /q       quit ircx (leaves ircs running)
+/h              print this help
+/j #channel     join #channel and set it as target
+/n nick         change nick
+/N              list nicks on channel target
+/p [message]    part from channel target
+/Q [message]    quit (stops ircs)
+/r message      send raw message to server
+/t target       set target (channel or nick)
+/T [topic]      get [set] topic of channel target
+/w mask         send who query
+/W mask         send whois query
+/x              create new ircx window
+[/ ]message     send message to target'
+
+fn usage{
+	aux/usage
+	exit usage
+}
+fn fatal{
+	echo $* >[1=2]
+	exit $"*
+}
+fn isnum{
+	echo $1 | grep -s '^[0-9]+$'
+}
+fn clear{
+	awk 'BEGIN {
+		for(i = 0; i < '$1'; i++)
+			printf "\x08"
+	}'
+}
+fn runtail{
+	{	tail -$2
+		while(sleep 1) cat
+	} <$1 &
+	tailpid=$apid
+}
+fn oldlabel{
+	if(! ~ $#oldlabel 0)
+		label $oldlabel
+}
+fn killtail{
+	@{
+		echo kill >/proc/$tailpid/note
+	} &
+}
+fn saveoffset{
+	if(~ $#offset 1 && test -r $log)
+		wc -l <$log | tr -d ' ' >$offset
+}
+fn shutdown{
+	saveoffset
+	killtail
+	oldlabel
+	exit 0
+}
+fn sighup sigint sigterm{
+	shutdown
+}
+fn send{
+	if(test -e /srv/$srv)
+		echo $* >>/srv/$srv
+	if not{
+		echo /srv/$srv not found '(ircs stopped?)'
+		shutdown
+	}
+}
+fn settarget{
+	if(~ $1 ''){
+		target=()
+		label $mainlog
+	}
+	if not{
+		target=$1
+		label $target
+	}
+}
+fn newtarget{
+	if(~ $#flagf 1)
+		settarget $newtarget
+	if not{
+		if(~ $newtarget '#'*){
+			f=/tmp/$srv/$newtarget
+			sleep 1
+		}
+		if not{
+			settarget $newtarget
+			f=$mainlog
+		}
+		if(! ~ $f $log){
+			if(test -r $f){
+				settarget $newtarget
+				saveoffset
+				offset=()
+				killtail
+				log=$f
+				echo $log
+				runtail $log 10
+			}
+			if not
+				echo can''''t access $f
+		}
+	}
+	newtarget=()
+}
+fn lines{
+	if(~ $#offset 1 && test -r $offset && test -r $log){
+		n₀=`{cat $offset}
+		n₁=`{wc -l <$log}
+		δn=`{echo $n₁ - $n₀ + 10 | bc}
+		echo $δn
+	}
+	if not
+		echo 10
+}
+
+if(! ifs=() eval `{aux/getflags $*} || ! ~ $#* 0 2)
+	usage
+
+if(~ $#flagb 1){
+	if(! isnum $flagb)
+		usage
+	lines=$flagb
+}
+if(~ $#flagt 1)
+	target=$flagt
+
+if(~ $#flags 1){
+	if(~ $flags '')
+		fatal empty srvname
+	srv=$flags
+	opt=($opt -s $srv)
+}
+if(~ $#flagf 1){
+	mainlog=$flagf
+	opt=($opt -f $mainlog)
+}
+if not{
+	mainlog=/tmp/$srv/log
+	if(~ $#lines 0){
+		offset=/tmp/$srv/offsets/log
+		mkdir -p /tmp/$srv/offsets
+	}
+}
+if(~ $#flage 1) opt=($opt -e)
+if(~ $#flagp 1) opt=($opt -p)
+if(~ $#flagr 1) opt=($opt -r)
+if(~ $#flagT 1) opt=($opt -T)
+if(~ $#flagU 1) opt=($opt -U)
+
+switch($#*){
+case 0
+	for(i in /srv/$srv $mainlog)
+		if(! test -e $i)
+			fatal $i not found
+case 2
+	if(test -e /srv/$srv)
+		fatal ircs already running
+	nick=$1
+	addr=$2
+	ircs $opt $nick $addr
+	stat=$status
+	if(! ~ $stat '')
+		exit $stat
+case *
+	usage
+}
+
+oldlabel=`{cat /dev/label}
+oifs=$ifs
+log=$mainlog
+
+if(~ $#target 1){
+	label $target
+	if(~ $target '#'*){
+		send JOIN $target
+		if(~ $#flagf 0){
+			log=/tmp/$srv/$target
+			if(~ $#lines 0)
+				offset=/tmp/$srv/offsets/$target
+			if(! test -e $log)
+				sleep 1
+		}
+	}
+}
+if not
+	label $log
+
+if(~ $#lines 0)
+	lines=`{lines}
+
+echo $log -$lines
+runtail $log $lines
+
+while(ifs=() line=`{read | tr -d \xa}){
+	ifs=$oifs
+	clear `{echo $line | wc -r}
+
+	if(! ~ $line ''){
+		msg=()
+		*=`{echo $line}
+		cmd=$1
+		shift
+
+		switch($cmd){
+		case /h
+			echo $help
+		case /j
+			if(~ $#* 0)
+				echo no channel
+			if not{
+				msg='JOIN '^$"*
+				newtarget=$1
+			}
+		case /t
+			if(~ $#* 0)
+				echo no target
+			if not
+				newtarget=$1
+		case /N
+			if(! ~ $target '#'*)
+				echo no channel
+			if not
+				msg='NAMES '^$target
+		case /T
+			if(! ~ $target '#'*)
+				echo no channel
+			if not{
+				msg='TOPIC '^$target
+				if(! ~ $#* 0)
+					msg=$msg^' :'^$"*
+			}
+		case /p
+			if(! ~ $target '#'*)
+				echo no channel
+			if not{
+				msg='PART '^$target
+				if(! ~ $#* 0)
+					msg=$msg^' :'^$"*
+				newtarget=''
+			}
+		case /q
+			shutdown
+		case /Q
+			if(~ $#* 0)
+				send QUIT
+			if not
+				send 'QUIT :'^$"*
+			sleep 1
+			shutdown
+		case /r
+			if(~ $#* 0)
+				echo no message
+			if not{
+				ifs=()
+				msg=`{echo -n $line | sed 's!^[ 	]*/r[ 	]+!!'}
+				ifs=$oifs
+			}
+		case /n
+			if(~ $#* 0)
+				echo no nick
+			if not
+				msg='NICK '^$1
+		case /w
+			if(~ $#* 0)
+				echo no mask
+			if not
+				msg='WHO '^$"*
+		case /W
+			if(~ $#* 0)
+				echo no mask
+			if not
+				msg='WHOIS '^$"*
+		case /x
+			window -m ircx $opt
+		case /
+			if(~ $#target 0)
+				echo no target
+			if not{
+				ifs=()
+				line=`{echo -n $line | sed 's!^[ 	]*/[ 	]*!!'}
+				ifs=$oifs				
+				msg='PRIVMSG '^$target^' :'^$line
+			}
+		case /*
+			echo unknown command
+		case *
+			if(~ $#target 0)
+				echo no target
+			if not
+				msg='PRIVMSG '^$target^' :'^$line
+		}
+		if(! ~ $#msg 0)
+			send $msg
+
+		if(~ $#newtarget 1)
+			newtarget
+	}
+}
+shutdown
--- /dev/null
+++ b/list.c
@@ -1,0 +1,60 @@
+#include <u.h>
+#include <libc.h>
+#include "dat.h"
+
+int listallocs;
+
+void
+listfree(List **lp)
+{
+	List *l, *n;
+
+	for(l = *lp; l != nil; l = n){
+		n = l->next;
+		free(l);
+		listallocs--;
+	}
+	*lp = nil;
+}
+
+int
+listadd(List **lp, void *val)
+{
+	List *l;
+
+	for(l = *lp; l != nil; l = l->next)
+		if(l->val == val)
+			break;
+	if(l != nil)
+		return 0;
+	l = mallocz(sizeof(List), 1);
+	if(l == nil)
+		sysfatal("mallocz: %r");
+	listallocs++;
+	l->val = val;
+	l->next = *lp;
+	*lp = l;
+	return 1;
+}
+
+int
+listdel(List **lp, void *val)
+{
+	List *l, *prev;
+
+	prev = nil;
+	for(l = *lp; l != nil; l = l->next){
+		if(l->val == val)
+			break;
+		prev = l;
+	}
+	if(l == nil)
+		return 0;
+	if(prev == nil)
+		*lp = l->next;
+	else
+		prev->next = l->next;
+	free(l);
+	listallocs--;
+	return 1;
+}
--- /dev/null
+++ b/main.c
@@ -1,0 +1,1049 @@
+#include <u.h>
+#include <libc.h>
+#include <auth.h>
+#include <libsec.h>
+#include <thread.h>
+#include "dat.h"
+#include "fns.h"
+
+enum {
+	Stacksize = 2 * 8192,
+	Chanmsgs = 10,
+};
+
+enum {
+	Init = 0,
+	Ok,
+	Fail,
+	Quit,
+	Connend,
+	Connerr,
+	Pipeend,
+	Pipeerr,
+	Reconn,
+	Reconnok,
+};
+
+typedef struct Log Log;
+typedef struct Ch Ch;
+typedef struct User User;
+
+struct Log {
+	int	fd;
+	int	mday;
+};
+
+struct Ch {
+	char	name[64];
+	Log	log;
+	List	*users;
+};
+
+struct User {
+	char	nick[32];
+	List	*channels;
+};
+
+int mainstacksize = Stacksize;
+char *logdir;
+
+static char *service;
+
+static char *post;
+static char *file;
+static char *mynick;
+static char *user;
+static char *addr;
+
+static int cmdfd = -1;
+static int ircfd = -1;
+
+static int state = Init;
+
+static Channel *inic;
+static Channel *ctlc;
+
+static Ioproc *outio;
+static Ioproc *logio;
+
+static TLSconn tls;
+
+static int debug;
+static int timestamps = 1;
+static int usetls;
+static int passwd;
+static int rawlog;
+static int userdb = 1;
+
+static Log mainlog;
+static Trie *channels;
+static Trie *users;
+
+static void *
+emalloc(ulong n)
+{
+	void *p;
+
+	p = mallocz(n, 1);
+	if(p == nil)
+		sysfatal("mallocz: %r");
+	return p;
+}
+
+static char *
+estrdup(char *s)
+{
+	s = strdup(s);
+	if(s == nil)
+		sysfatal("strdup: %r");
+	return s;
+}
+
+static int
+eiowrite(Ioproc *io, int fd, void *buf, long n)
+{
+	if(iowrite(io, fd, buf, n) != n){
+		perror("iowrite");
+		return -1;
+	}
+	return 0;
+}
+
+static long
+_iosetname(va_list *arg)
+{
+	char *name;
+
+	name = va_arg(*arg, char*);
+	threadsetname(name);
+	return 0;
+}
+
+static void
+iosetname(Ioproc *io, char *name, ...)
+{
+	va_list arg;
+	char buf[Bufsize];
+
+	va_start(arg, name);
+	vsnprint(buf, sizeof(buf), name, arg);
+	va_end(arg);
+	iocall(io, _iosetname, buf);
+}
+
+static Ch *
+challoc(char *name)
+{
+	Ch *c;
+	char buf[128];
+
+	c = emalloc(sizeof(Ch));
+	snprint(c->name, sizeof(c->name), "%s", name);
+	if(logdir != nil){		
+		snprint(buf, sizeof(buf), "%s/%s", logdir, name);
+		c->log.fd = create(buf, OWRITE, 0600 | DMAPPEND);
+		if(c->log.fd < 0)
+			sysfatal("create: %r");
+		c->log.mday = 0;
+	} else
+		c->log.fd = 0;
+	c->users = nil;
+	return c;
+}
+
+static Ch *
+chget(char *name)
+{
+	Ch *c;
+	Rune key[64];
+
+	runesnprint(key, nelem(key), "%s", name);
+	c = trieget(channels, key);
+	if(c == nil){
+		c = challoc(name);
+		trieadd(channels, key, c);
+	}
+	return c;
+}
+
+static void
+chfree(Ch *c)
+{
+	Rune key[64];
+
+	runesnprint(key, nelem(key), "%s", c->name);
+	triedel(channels, key);
+	listfree(&c->users);
+	if(c->log.fd > 0)
+		close(c->log.fd);
+	free(c);
+}
+
+static User *
+useralloc(char *nick)
+{
+	User *u;
+
+	u = emalloc(sizeof(User));
+	snprint(u->nick, sizeof(u->nick), "%s", nick);
+	u->channels = nil;
+	return u;
+}
+
+static User *
+userget(char *nick)
+{
+	User *u;
+	Rune key[32];
+
+	runesnprint(key, nelem(key), "%s", nick);
+	u = trieget(users, key);
+	if(u == nil){
+		u = useralloc(nick);
+		trieadd(users, key, u);
+	}
+	return u;
+}
+
+static void
+userfree(User *u)
+{
+	Rune key[32];
+
+	runesnprint(key, nelem(key), "%s", u->nick);
+	triedel(users, key);
+	listfree(&u->channels);
+	free(u);
+}
+
+static void
+usernick(User *u, char *newnick)
+{
+	Rune key[32];
+
+	runesnprint(key, nelem(key), "%s", u->nick);
+	triedel(users, key);
+	snprint(u->nick, sizeof(u->nick), "%s", newnick);
+	runesnprint(key, nelem(key), "%s", u->nick);
+	trieadd(users, key, u);
+}
+
+static void
+ircsend(char *msg)
+{
+	long len;
+
+	len = strlen(msg);
+	if(debug) fprint(2, "ircsend: %s", msg);
+	if(len > 0){
+		eiowrite(outio, ircfd, msg, len);
+		if(msg[len-1] != '\n'){
+			if(debug) fprint(2, "\n");
+			eiowrite(outio, ircfd, "\r\n", 2);
+		}
+	}
+}
+
+static void
+logsend(char *msg, long time, Log *log)
+{
+	long len, n;
+	char buf[Bufsize];
+
+	len = strlen(msg);
+	if(len > 0 && log->fd > 0){
+		if(timestamps){
+			if(rawlog)
+				n = snprint(buf, sizeof(buf),
+					"%ld ", time);
+			else
+				n = ircfmttime(buf, sizeof(buf),
+					time, &log->mday);
+			eiowrite(logio, log->fd, buf, n);
+		}
+		n = len;
+		eiowrite(logio, log->fd, msg, n);
+		if(msg[n-1] != '\n')
+			eiowrite(logio, log->fd, "\n", 1);
+	}
+}
+
+static void
+loginfo(char *msg)
+{
+	Triewalk w;
+	Rune key[64];
+	Ch *c;
+	long t;
+
+	t = time(0);
+	logsend(msg, t, &mainlog);
+	if(logdir != nil){
+		triewalk(channels, &w, key, nelem(key));
+		while((c = trienext(&w)) != nil)
+			logsend(msg, t, &c->log);
+	}
+}
+
+static void
+joinmsg(Ircmsg *irc, char *msg, long time)
+{
+	Ch *c;
+	Log *log;
+	User *u;
+	char *channel, *m, buf[Bufsize];
+	
+	channel = irc->par[0];
+
+	if(rawlog)
+		m = msg;
+	else {
+		ircfmtjoin(irc, buf, sizeof(buf));
+		m = buf;
+	}
+	if(logdir != nil){
+		c = chget(channel);
+		log = &c->log;
+
+		if(userdb && irc->pre != nil){
+			u = userget(irc->nick);
+			listadd(&c->users, u);
+			listadd(&u->channels, c);
+		}
+	} else {
+		log = &mainlog;
+
+		/* for rejoin() */
+		if(irc->pre != nil && strcmp(irc->nick, mynick) == 0)
+			chget(channel);
+	}
+	logsend(m, time, log);
+}
+
+static void
+freeusers(Ch *c)
+{
+	List *i;
+	User *u;
+
+	for(i = c->users; i != nil; i = i->next){
+		u = i->val;
+		listdel(&u->channels, c);
+		if(u->channels == nil)
+			userfree(u);
+	}
+	listfree(&c->users);
+}
+
+static void
+partmsg(Ircmsg *irc, char *msg, long time)
+{
+	Ch *c;
+	User *u;
+	char *channel, *m, buf[Bufsize];
+
+	channel = irc->par[0];
+
+	if(rawlog)
+		m = msg;
+	else {
+		ircfmtpart(irc, buf, sizeof(buf));
+		m = buf;
+	}
+	if(logdir != nil){
+		c = chget(channel);
+
+		if(userdb && irc->pre != nil){
+			u = userget(irc->nick);
+			listdel(&c->users, u);
+			listdel(&u->channels, c);
+			if(u->channels == nil)
+				userfree(u);
+		}
+		logsend(m, time, &c->log);
+
+		if(irc->pre != nil && strcmp(irc->nick, mynick) == 0){
+			freeusers(c);
+			chfree(c);
+		}
+	} else {
+		logsend(m, time, &mainlog);
+
+		/* for rejoin() */
+		if(irc->pre != nil && strcmp(irc->nick, mynick) == 0){
+			c = chget(channel);
+			freeusers(c);
+			chfree(c);
+		}
+	}
+}
+
+static void
+quitmsg(Ircmsg *irc, char *msg, long time)
+{
+	User *u;
+	List *i;
+	Ch *c;
+	char *m, buf[Bufsize];
+
+	if(rawlog)
+		m = msg;
+	else {
+		ircfmtquit(irc, buf, sizeof(buf));
+		m = buf;
+	}
+	if(logdir != nil && userdb && irc->pre != nil){
+		u = userget(irc->nick);
+
+		for(i = u->channels; i != nil; i = i->next){
+			c = i->val;
+			logsend(m, time, &c->log);
+			listdel(&c->users, u);
+		}
+		if(u->channels == nil)
+			logsend(m, time, &mainlog);
+
+		userfree(u);
+	} else
+		logsend(m, time, &mainlog);
+}
+
+static void
+nickmsg(Ircmsg *irc, char *msg, long time)
+{
+	User *u;
+	List *i;
+	Ch *c;
+	char *newnick, *m, buf[Bufsize];
+
+	newnick = irc->par[0];
+
+	if(rawlog)
+		m = msg;
+	else {
+		ircfmtnick(irc, buf, sizeof(buf));
+		m = buf;
+	}
+	if(logdir != nil && userdb && irc->pre != nil){
+		u = userget(irc->nick);
+
+		for(i = u->channels; i != nil; i = i->next){
+			c = i->val;
+			logsend(m, time, &c->log);
+		}
+		if(u->channels == nil)
+			logsend(m, time, &mainlog);
+
+		usernick(u, newnick);
+	} else
+		logsend(m, time, &mainlog);
+
+	if(irc->pre != nil && strcmp(irc->nick, mynick) == 0){
+		free(mynick);
+		mynick = estrdup(newnick);
+	}
+}
+
+static void
+namreply(Ircmsg *irc, char *msg, long time)
+{
+	Ch *c;
+	Log *log;
+	User *u;
+	char *channel, *nick, *m, *a[128], buf[Bufsize];
+	int i, n;
+
+	if(rawlog)
+		m = msg;
+	else {
+		ircfmtnumeric(irc, buf, sizeof(buf));
+		m = buf;
+	}
+	if(logdir != nil){
+		channel = irc->par[2];
+		c = chget(channel);
+		log = &c->log;
+
+		if(userdb){
+			n = getfields(irc->trail, a, nelem(a), 1, " ");
+			for(i = 0; i < n; i++){
+				nick = a[i];
+				if(*nick == '@' || *nick == '+'){
+					nick++;
+					if(*nick == 0)
+						continue;
+				}
+				u = userget(nick);
+				listadd(&c->users, u);
+				listadd(&u->channels, c);
+			}
+		}
+	} else
+		log = &mainlog;
+
+	logsend(m, time, log);
+}
+
+static void
+srvmsg(char *msg, long time)
+{
+	Ircmsg irc;
+	Ircfmt fmt;
+	Ch *c;
+	Log *log;
+	char *target, *m, buf[Bufsize];
+
+	ircparse(&irc, msg);
+	if(debug)
+		ircprint(&irc);
+
+	fmt = nil;
+	target = nil;
+
+	if(strcmp(irc.cmd, "PRIVMSG") == 0){
+		fmt = ircfmtpriv;
+		target = irc.par[0];
+	} else if(strcmp(irc.cmd, "JOIN") == 0){
+		joinmsg(&irc, msg, time);
+		return;
+	} else if(strcmp(irc.cmd, "QUIT") == 0){
+		quitmsg(&irc, msg, time);
+		return;
+	} else if(strcmp(irc.cmd, "PART") == 0){
+		partmsg(&irc, msg, time);
+		return;
+	} else if(strcmp(irc.cmd, "NICK") == 0){
+		nickmsg(&irc, msg, time);
+		return;
+	} else if(strcmp(irc.cmd, "MODE") == 0 ||
+		strcmp(irc.cmd, "KICK") == 0 ||
+		strcmp(irc.cmd, "TOPIC") == 0){
+		target = irc.par[0];
+	} else if(strcmp(irc.cmd, "001") == 0){
+		/* welcome */
+		sendul(ctlc, Ok);
+	} else if(strcmp(irc.cmd, "433") == 0){
+		/* nick in use */
+		sendul(ctlc, Fail);
+	} else if(strcmp(irc.cmd, "332") == 0 ||
+		strcmp(irc.cmd, "333") == 0 ||
+		strcmp(irc.cmd, "366") == 0){
+		/* 332 = RPL_TOPIC */
+		/* 333 = RPL_TOPICWHOTIME */
+		/* 366 = RPL_ENDOFNAMES */
+		target = irc.par[1];
+	} else if(strcmp(irc.cmd, "353") == 0){
+		/* 353 = RPL_NAMREPLY */
+		namreply(&irc, msg, time);
+		return;
+	}
+
+	if(irc.cmd[0] >= '0' && irc.cmd[0] <= '9')
+		fmt = ircfmtnumeric;
+
+	if(logdir != nil && ircischan(target)){
+		c = chget(target);
+		log = &c->log;
+	} else
+		log = &mainlog;
+
+	if(rawlog || fmt == nil)
+		m = msg;
+	else {
+		fmt(&irc, buf, sizeof(buf));
+		m = buf;
+	}		
+	logsend(m, time, log);
+}
+
+static void
+usrmsg(char *msg, long time)
+{
+	Ircmsg irc;
+	Ircfmt fmt;
+	Ch *c;
+	Log *log;
+	char *target, *m, buf[Bufsize];
+
+	ircparse(&irc, msg);
+	if(debug)
+		ircprint(&irc);
+
+	fmt = nil;
+	target = nil;
+
+	if(strcmp(irc.cmd, "PRIVMSG") == 0){
+		fmt = ircfmtpriv;
+		target = irc.par[0];
+	} else if(strcmp(irc.cmd, "QUIT") == 0){
+		state = Quit;
+	}
+	if(logdir != nil && ircischan(target)){
+		c = chget(target);
+		log = &c->log;
+	} else
+		log = &mainlog;
+
+	if(rawlog || fmt == nil)
+		m = msg;
+	else {
+		fmt(&irc, buf, sizeof(buf));
+		m = buf;
+	}
+	logsend(m, time, log);
+	ircsend(msg);
+}
+
+static void
+touch(int fd, long time)
+{
+	Dir d;
+
+	nulldir(&d);
+	d.mtime = time;
+	if(dirfwstat(fd, &d) < 0)
+		perror("dirfwstat");
+}
+
+static void
+ircin(void *v)
+{
+	Ioproc *io;
+	char buf[Bufsize], *e, *p;
+	long n, t;
+
+	io = v;
+	threadsetname("ircin");
+	iosetname(io, "%s net reader", service);
+
+	p = buf;
+	e = buf + sizeof(buf);
+
+	while((n = ioread(io, ircfd, p, e - p)) > 0){
+		t = time(0);
+		p += n;
+		while((p > buf) && (e = memchr(buf, '\n', p - buf))){
+			if((e > buf) && (e[-1] == '\r'))
+				e[-1] = 0;
+			*e++ = 0;
+			if(strncmp(buf, "PING", 4) == 0){
+				buf[1] = 'O';
+				ircsend(buf);
+				touch(mainlog.fd, t);
+			} else
+				srvmsg(buf, t);
+			p -= e - buf;
+			if(p > buf)
+				memmove(buf, e, p - buf);
+		}
+		e = buf + sizeof(buf);
+	}
+	if(n < 0){
+		perror("ircin: ioread");
+		loginfo("ircs: connection error");
+		sendul(ctlc, Connerr);
+	}
+	if(n == 0){
+		loginfo("ircs: connection eof");
+		sendul(ctlc, Connend);
+	}
+	closeioproc(io);
+	threadexits(nil);
+}
+
+static void
+cmdin(void *v)
+{
+	Ioproc *io;
+	char buf[Bufsize];
+	long n, t;
+
+	io = v;
+	threadsetname("cmdin");
+	iosetname(io, "%s cmd reader", service);
+
+	while((n = ioread(io, cmdfd, buf, sizeof(buf)-1)) > 0){
+		t = time(0);
+		buf[n] = 0;
+		if(debug) fprint(2, "cmdin: %s", buf);
+		if(strncmp(buf, "quit", 4) == 0){
+			sendul(ctlc, Quit);
+			break;
+		}
+		if(buf[n-1] == '\n')
+			buf[n-1] = 0;
+		if(n > 1 && buf[n-2] == '\r')
+			buf[n-2] = 0;
+		usrmsg(buf, t);
+	}
+	if(n < 0){
+		perror("cmdin: ioread");
+		loginfo("ircs: command pipe error");
+		sendul(ctlc, Pipeerr);
+	}
+	if(n == 0){
+		loginfo("ircs: command pipe eof");
+		sendul(ctlc, Pipeend);
+	}
+	closeioproc(io);
+	threadexits(nil);
+}
+
+static void
+cleanup(void)
+{
+	if(access(post, AEXIST) == 0)
+		if(remove(post) < 0)
+			perror("remove");
+}
+
+static int
+note(void *, char *msg)
+{
+	fprint(2, "%d: received note: %s\n", getpid(), msg);
+	cleanup();
+	threadexitsall("note");
+	return 0;
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-deprTU] [-s srvname] [-f file] nick[!user] addr\n", argv0);
+	threadexits("usage");
+}
+
+static int
+connect(void)
+{
+	UserPasswd *u;
+	char buf[Bufsize];
+	long t;
+	int fd;
+
+	u = nil;
+	if(passwd && usetls){
+		u = auth_getuserpasswd(auth_getkey, "proto=pass service=irc server=%q user=%q", addr, mynick);
+		if(u == nil){
+			perror("auth_getuserpasswd");
+			return -1;
+		}
+	}
+	if(ircfd >= 0)
+		close(ircfd);
+
+	ircfd = dial(netmkaddr(addr, "tcp", usetls ? "6697" : "6667"), nil, nil, nil);
+	if(ircfd < 0){
+		perror("dial");
+		free(u);
+		return -1;
+	}
+	if(usetls){
+		memset(&tls, 0, sizeof(tls));
+		fd = tlsClient(ircfd, &tls);
+		if(fd < 0){
+			perror("tlsClient");
+			close(ircfd);
+			free(u);
+			return -1;
+		}
+		ircfd = fd;
+	}
+	t = time(0);
+
+	if(u != nil){
+		snprint(buf, sizeof(buf), "PASS");
+		logsend(buf, t, &mainlog);
+		snprint(buf+4, sizeof(buf)-4, " %s", u->passwd);
+		ircsend(buf);
+		free(u);
+	}
+	snprint(buf, sizeof(buf), "USER %s foo bar :<nil>", user);
+	logsend(buf, t, &mainlog);
+	ircsend(buf);
+
+	snprint(buf, sizeof(buf), "NICK %s", mynick);
+	logsend(buf, t, &mainlog);
+	ircsend(buf);
+
+	return 0;
+}
+
+static void
+freeallusers(void)
+{
+	Triewalk w;
+	Rune key[64];
+	Ch *c;
+
+	triewalk(channels, &w, key, nelem(key));
+	while((c = trienext(&w)) != nil)
+		freeusers(c);
+}
+
+static void
+rejoin(void)
+{
+	Triewalk w;
+	Rune key[64];
+	Ch *c;
+	long t;
+	char buf[Bufsize];
+
+	t = time(0);
+	triewalk(channels, &w, key, nelem(key));
+	while((c = trienext(&w)) != nil){
+		snprint(buf, sizeof(buf), "JOIN %s", c->name);
+		logsend(buf, t, &mainlog);
+		ircsend(buf);
+	}
+}
+
+static void
+reconnproc(void *)
+{
+	int c, i;
+	long sec[] = { 5, 30, 60, 300, 900 };
+
+	threadsetname("%s reconnect", service);
+	c = 0;
+	i = 0;
+	while(connect() < 0){
+		c++;
+		threadsetname("%s reconnect attempts=%d",
+			service, c);
+		sleep(sec[i] * 1000);
+		if(i < nelem(sec)-1)
+			i++;
+	}
+	sendul(ctlc, Reconnok);
+	threadexits(nil);
+}
+
+static void
+mainproc(void *)
+{
+	ulong msg;
+
+	threadnotify(note, 1);
+	threadsetname("%s %s %s %s", mynick, addr, post,
+		logdir != nil ? logdir : file);
+
+	outio = ioproc();
+	logio = ioproc();
+
+	iosetname(outio, "%s net writer", service);
+	iosetname(logio, "%s log writer", service);
+
+	if(connect() < 0){
+		sendul(inic, Fail);
+		threadexits("no");
+	}
+	threadcreate(cmdin, ioproc(), Stacksize);
+	threadcreate(ircin, ioproc(), Stacksize);
+
+	for(;;){
+		msg = recvul(ctlc);
+		if(state == Init)
+			sendul(inic, msg);
+		switch(msg){
+		case Ok:
+			state = Ok;
+			break;
+		case Connend:
+		case Connerr:
+			if(state == Quit)
+				goto done;
+			if(state != Init){
+				state = Reconn;
+				if(userdb)
+					freeallusers();
+				loginfo("ircs: reconnecting");
+				proccreate(reconnproc, nil, Stacksize);
+			}
+			break;
+		case Reconnok:
+			state = Reconnok;
+			rejoin();
+			threadcreate(ircin, ioproc(), Stacksize);
+			break;
+		case Pipeend:
+		case Pipeerr:
+		case Fail:
+		case Quit:
+			if(state != Init)
+				goto done;
+			break;
+		default:
+			assert(0);
+		}
+	}
+done:
+	cleanup();
+	threadexitsall(nil);
+}
+
+static void
+postpipe(void)
+{
+	int fd, p[2];
+
+	assert(service != nil && *service != 0);
+
+	post = smprint("/srv/%s", service);
+	if(post == nil)
+		sysfatal("smprint: %r");
+
+	fd = create(post, OWRITE, 0600);
+	if(fd < 0)
+		sysfatal("create: %r");
+
+	if(pipe(p) < 0)
+		sysfatal("pipe: %r");
+
+	if(fprint(fd, "%d", p[1]) < 0)
+		sysfatal("can't post: %r");
+
+	close(fd);
+	close(p[1]);
+
+	if(dup(p[0], 0) < 0)
+		sysfatal("dup: %r");
+
+	close(p[0]);
+	cmdfd = 0;
+}
+
+static void
+initlog(void)
+{
+	int fd;
+
+	if(file == nil){
+		assert(service != nil && *service != 0);
+
+		logdir = smprint("/tmp/%s", service);
+		if(logdir == nil)
+			sysfatal("smprint: %r");
+
+		if(access(logdir, AEXIST) < 0){
+			fd = create(logdir, OREAD, DMDIR | 0700);
+			if(fd < 0)
+				sysfatal("create: %r");
+			close(fd);
+		}
+		file = smprint("%s/log", logdir);
+		if(file == nil)
+			sysfatal("smprint: %r");
+	}
+	fd = create(file, OWRITE, 0600 | DMAPPEND);
+	if(fd < 0)
+		sysfatal("create: %r");
+
+	if(dup(fd, 1) < 0)
+		sysfatal("dup: %r");
+
+	close(fd);
+	mainlog.fd = 1;
+	mainlog.mday = 0;
+}
+
+static void
+setnickuser(char *nickuser)
+{
+	char *p;
+
+	if(p = strchr(nickuser, '!')){
+		*p = 0;
+		if(*(++p) != 0)
+			user = estrdup(p);
+		else
+			user = estrdup(nickuser);
+	} else
+		user = estrdup(nickuser);
+
+	if(*nickuser != 0)
+		mynick = estrdup(nickuser);
+	else
+		sysfatal("empty nick");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	ARGBEGIN{
+	case 'd':
+		debug++;
+		break;
+	case 'e':
+		usetls++;
+		break;
+	case 'f':
+		file = EARGF(usage());
+		break;
+	case 'p':
+		passwd++;
+		break;
+	case 'r':
+		rawlog++;
+		break;
+	case 's':
+		service = EARGF(usage());
+		break;
+	case 'T':
+		timestamps = 0;
+		break;
+	case 'U':
+		userdb = 0;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 2)
+		usage();
+
+	if(passwd && !usetls){
+		fprint(2, "-p requires -e, exiting\n");
+		threadexits("passwd");
+	}
+	if(file != nil)
+		userdb = 0;
+
+	if(service == nil)
+		service = estrdup("ircs");
+	else if(*service == 0)
+		sysfatal("empty srvname");
+
+	setnickuser(argv[0]);
+	addr = argv[1];
+
+	fprint(2, "initializing, please wait\n");
+
+	initlog();
+	postpipe();
+	threadnotify(note, 1);
+
+	inic = chancreate(sizeof(ulong), Chanmsgs);	
+	ctlc = chancreate(sizeof(ulong), Chanmsgs);
+
+	if(inic == nil || ctlc == nil)
+		sysfatal("chancreate");
+
+	channels = triealloc();
+	users = triealloc();
+
+	procrfork(mainproc, nil, Stacksize, RFNOTEG);
+
+	switch(recvul(inic)){
+	case Ok:
+		break;
+	default:
+		fprint(2, "init failed, exiting\n");
+		cleanup();
+		threadexitsall("no");
+	}
+	fprint(2, "init ok\n");
+	chanclose(inic);
+	threadexits(nil);
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,19 @@
+</$objtype/mkfile
+
+BIN=$home/bin/$objtype
+TARG=ircs
+OFILES=\
+	irc.$O\
+	ircfmt.$O\
+	list.$O\
+	main.$O\
+	trie.$O\
+
+HFILES=\
+	dat.h\
+	fns.h\
+
+</sys/src/cmd/mkone
+
+install:V:	$BIN/$TARG
+	cp ircx irctime nojqpn $home/bin/rc/
--- /dev/null
+++ b/nojqpn
@@ -1,0 +1,3 @@
+#!/bin/rc
+# nojqpn: filter out JOIN/QUIT/PART/NICK
+grep -v '^[0-9:]+ (JOIN|QUIT|PART|NICK)' $*
--- /dev/null
+++ b/trie.c
@@ -1,0 +1,232 @@
+#include <u.h>
+#include <libc.h>
+#include "dat.h"
+
+int trieallocs;
+int triedebug;
+
+#define dprint	if(triedebug) print
+
+Trie *
+triealloc(void)
+{
+	Trie *t;
+
+	t = mallocz(sizeof(Trie), 1);
+	if(t == nil)
+		sysfatal("mallocz: %r");
+	trieallocs++;
+	return t;
+}
+
+void
+triefree(Trie *t)
+{
+	Trie *c, *n;
+
+	for(c = t->child; c != nil; c = n){
+		n = c->next;
+		triefree(c);
+	}
+	free(t);
+	trieallocs--;
+}
+
+void
+triewalk(Trie *t, Triewalk *w, Rune *key, int keysize)
+{
+	w->root = t;
+	w->curr = nil;
+	w->key = key;
+	w->keysize = keysize;
+	w->depth = 0;
+}
+
+void *
+trienext(Triewalk *w)
+{
+	Trie *t;
+	int d;
+
+	if(w->curr == nil){
+		w->curr = w->root;
+		w->depth = 0;
+		w->key[0] = 0;
+		if(w->root->value != nil)
+			return w->root->value;
+	}
+	t = w->curr;
+	d = w->depth;
+	do{
+		if(t->child != nil && d < w->keysize-1){
+			t = t->child;
+			d++;
+		}
+		else if(t->next != nil)
+			t = t->next;
+		else {
+			while(t->next == nil && t != w->root){
+				t = t->parent;
+				d--;
+			}
+			if(t->next != nil)
+				t = t->next;
+			else {
+				assert(t == w->root);
+				w->curr = nil;
+				return nil;
+			}				
+		}
+		w->key[d-1] = t->rune;
+	} while(t->value == nil);
+
+	w->key[d] = 0;
+	w->curr = t;
+	w->depth = d;
+	return t->value;
+}
+
+static Trie *
+getchild(Trie *t, Rune r)
+{
+	dprint("  getchild %C: ", r);
+
+	for(t = t->child; t != nil; t = t->next)
+		if(t->rune == r)
+			break;
+
+	dprint("%s\n", t != nil ? "found" : "not found");
+	return t;
+}
+
+static Trie *
+addchild(Trie *t, Rune r)
+{
+	Trie *c, *n, *prev;
+
+	dprint("  addchild %C\n", r);
+
+	c = triealloc();
+	c->rune = r;
+	c->parent = t;
+
+	prev = nil;
+	for(n = t->child; n != nil; n = n->next){
+		if((prev == nil || prev->rune < r) && r < n->rune)
+			break;
+		assert(r > n->rune);
+		prev = n;
+	}
+	if(prev == nil)
+		t->child = c;
+	else
+		prev->next = c;
+	c->next = n;
+	return c;
+}
+
+static int
+delchild(Trie *t, Rune r)
+{
+	Trie *c, *prev;
+
+	dprint("  delchild %C\n", r);
+
+	prev = nil;
+	for(c = t->child; c != nil; c = c->next){
+		if(c->rune == r)
+			break;
+		prev = c;
+	}
+	if(c == nil)
+		return 0;
+	if(c->child != nil)
+		return 0;
+	if(prev == nil)
+		t->child = c->next;
+	else
+		prev->next = c->next;
+	triefree(c);
+	return 1;
+}
+
+void *
+trieadd(Trie *t, Rune *key, void *val)
+{
+	Trie *c;
+	void *v;
+
+	if(val == nil)
+		return nil;
+
+	while(*key){
+		c = getchild(t, *key);
+		if(c == nil)
+			break;
+		t = c;
+		key++;
+	}
+	while(*key)
+		t = addchild(t, *key++);
+
+	v = t->value;
+	t->value = val;
+	return v;	
+}
+
+static Trie *
+find(Trie *t, Rune *key)
+{
+	Trie *c;
+
+	while(*key){
+		c = getchild(t, *key);
+		if(c == nil)
+			break;
+		t = c;
+		key++;
+	}
+	if(*key)
+		return nil;
+	return t;
+}
+
+void *
+triedel(Trie *t, Rune *key)
+{
+	Rune *r;
+	void *v;
+
+	t = find(t, key);
+	if(t == nil)
+		return nil;
+
+	/* only non-leaves and root can have empty value */
+	if(t->value == nil){
+		assert(t->child != nil || t->parent == nil);
+		return nil;
+	}
+	v = t->value;
+	t->value = nil;
+
+	if(t->child != nil || t->parent == nil)
+		return v;
+
+	r = runestrchr(key, 0);
+
+	while(t->child == nil && t->value == nil && --r >= key){
+		t = t->parent;
+		assert(t != nil);
+		assert(delchild(t, *r));
+	}
+	return v;
+}
+
+void *
+trieget(Trie *t, Rune *key)
+{
+	t = find(t, key);
+	if(t == nil)
+		return nil;
+	return t->value;	
+}