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;
+}