www.gnu.org/software/src-highlite
www.lorenzobettini.it
www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd

/*
 * name:        ircbot
 * date:        08-12-2003
 * version:     0.1
 * license:     public domain
 * website:     http://www.xs4all.nl/~mechiel/projects/ircbot/
 * author:      mechiel lukkien
 * e-mail:      mechiel@xs4all.nl or mechiel@ueber.net
 * compile:     cc -g -Wall -pthread -o ircbot ircbot.c
 * xlint:       lint -aabchruH -lposix ircbot.c
 * usage:       ./ircbot nick host port listenhost listenport nickfile >> logfile
 * see also:    rfc2810, rfc2811, rfc2812 and rfc2813
 * tested on:   OpenBSD 3.3 (extensively), FreeBSD 4.7 and some Linux
 *
 *
 * Note that you also need a program/script `botcmd' in the same directory as
 * in which you executed ircbot.
 *
 * This ircbot is very simplistic:  For example, it will not reconnect when its
 * connection disappears, and keeps only a minimal of state (e.g. it does know
 * it own nick, but it doesn't know which channels it joined).  There is
 * virtually nothing to configure, but you can always modify the C code.
 *
 * When you start an instance of ircbot, it connects to host:port (as given on
 * the command line) and logs in as nick.  If nick is already in use, probably
 * nothing useful will happen.  If all goes well the bot will just stay
 * connected, replying to ping messages, but not joining channels or doing
 * anything on its own.  Ircbot also reads nickfile (also specified on the
 * command line) and keeps it in memory.  The file contains nicks that are
 * allowed to execute commands, one on each line.  All communications are
 * printed to stdout, you may want to log it.  Warnings, diagnostics and errors
 * are printed to stderr.  Ircbot also listens on the TCP port
 * listenhost:listenport, a connection to this port is called a control
 * connection, since by this means you control ircbot.  One can send the
 * following commands over the control connection (one command per line,
 * trailing \r\n or \n is stripped):
 *
 * - `#channel howdy folks!' sends `howdy folks!' to #channel
 * - `%user howdy user!' sends `howdy user!' to user
 * - `;raw-irc-command' sends raw-irc-command to ircd.  e.g. `;JOIN #channel'.
 * - `flush' will clear the queue for outgoing irc messages
 *    (except important messages like ping replies)
 * - `readnicks' causes nickfile to be reread
 *
 * You can also have ircbot execute commands by sending commands over irc.
 * Lines starting with a semicolon (;) are commands (most bots are triggered by
 * !, so yes, this is different).  Commands are accepted both in channels and
 * in private messages (of course, ircbot checks whether the nick that send the
 * command is allowed to execute commands).  Ircbot has no builtin commands.
 * For every command, ircbot forks, sets a number of environment variables (see
 * below), and executes ./botcmd (so botcmd must be present in the directory
 * ircbot was started) with the command as its only argument (but without the
 * leading semicolon).  In principle, a botcmd can do whatever it wants, but
 * the delivered botcmd script (a shell script for the rc shell
 * (http://www.star.le.ac.uk/~tjg/rc/) will execute the command and send the
 * output to the channel or user by means of a control connection.
 *
 * Environment variables set before executing ./botcmd (and thus available in
 * botcmd and whatever processes botcmd is creating):
 * - target	irc target to send the reply (e.g. #channel or user (actually,
 *              the default botcmd sets target to %$target if it doesn't start
 *              with a #. since the control connection needs a % prepended to
 *              send data to a user))
 * - nick	nick of the bot
 * - nickfile	file containing nicks allowed to execute commands
 * - fromuser	user sending the command
 * - bothost	host the bot listens on
 * - botport	port the bot listens on, on bothost
 * - botpid	pid of the bot
 * Also, when a command was given in a channel, fromchannel is set to that
 * channel.
 *
 * Hopefully, enough information has been provided to get you started with this
 * program.
 *
 *
 * Now for some comments on the implementation.  Ircbot is heavily
 * multithreaded, featuring the following threads:
 *
 * - ircreader, reading irc messages from the ircd
 * - ircwriter, writing irc messages to the ircd
 * - tcpacceptor, accepting incoming control connections
 * - tcpreader, one thread for each established control connection
 * - handlequeue, the main program that coordinates the data flow between the threads
 *
 * The main program/thread is the central part of ircbot.  It reads events from
 * an `inqueue', processes them, and possibly puts lines to be written to the
 * ircd in an `outqueue'.
 *
 * - Ircreader simply reads irc messages (read a line and parse it), and puts it
 *   into the event queue.
 * - Tcpacceptor does nothing with the queue, it only starts a tcpreader thread
 *   for each incoming control connection.
 * - Tcpreader reads and parses commands from the control channel and puts them
 *   into the event queue.
 * - Handlequeue reads an event, processes it and waits for the next event.  It
 *   will never block (except when waiting for incoming events of course).
 *   Processing sometimes includes putting an irc message in the outqueue to be
 *   read by ircwriter.
 * - Ircwriter reads lines from the outqueue and writes them to the ircd.  It
 *   makes sure to not flood the ircd because that wouldn't be nice.  This is
 *   done by simply keeping at least NOFLOOD_DELAY miliseconds between every
 *   two messages.
 *
 * Synchronisation is done by the queue functions using a mutex (to make sure a
 * thread has exclusive access to the queue for a short period) and a non-empty
 * condition (to make sure threads are woken up as soon as the queue is
 * non-empty).
 * http://www.llnl.gov/computing/tutorials/workshops/workshop/pthreads/MAIN.html
 * has some nice examples.
 *
 * Possible future improvements:
 * - Currently, the control command `flush' removes everything from the queue
 *   (except ping replies of course).  It would be nice to only flush output to
 *   the channel/user by which the flush was executed.
 * - Thread-safety of standard library functions could be an issue.  I have not
 *   tried really hard to make sure it won't cause problems.
 * - Make the exit code indicate if the error was fatal or non-fatal.  With
 *   non-fatal meaning we can try to reconnect.
 * - Find some mechanism to kill off old processes that somehow went wrong
 *   (e.g. when someone starts something like vim).
 */


#define _GNU_SOURCE	/* to make it compile without warning on glibc systems */
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/resource.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <pthread.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>


#define nil	NULL
#define strequiv(s1, s2)	(strcasecmp(s1, s2) == 0)
#define streql(s1, s2)		(strcmp(s1, s2) == 0)


#define IRCBOT_VERSION		"ircbot, version 0.1, http://www.xs4all.nl/~mechiel/projects/ircbot/"
enum
{
	IRCMSG_MAXLEN = 512,            	/* maximum length of an irc (protocol) message */
	BBUFFERSIZE   = IRCMSG_MAXLEN+1,	/* size of the buffer for incoming irc data */
	SOCK_MAX      = 32,			/* maximum number of sockets to listen on */
	NICK_MAXLEN   = 128,			/* `should be more than enough' */
	NOFLOOD_DELAY = 700			/* milliseconds between the sending of two irc messages */
};


/* types of the data send to the event processing main loop */
enum
{
	INTYPEircmsg,		/* irc message from ircd */
	INTYPEraw,		/* raw message to be send to the ircd */
	INTYPEbuiltin		/* builtin command */
};

/* type of the data send to the thread that sends irc messages to the ircd (without flooding) */
enum
{
	OUTTYPEimmediate,	/* accompanied data should be send immediately (e.g. for PING) */
	OUTTYPEnormal		/* normal data */
};


typedef unsigned long long uvlong;
typedef long long vlong;
typedef struct Source Source;
typedef struct Msg Msg;
typedef struct Elem Elem;
typedef struct Queue Queue;
typedef struct Nick Nick;
typedef struct Nicks Nicks;
typedef struct Buf Buf;


/* irc source of the message */
struct Source {
	char   *nick, *server;	/* when server is non-nil, nick will contain the same value as server */
	char   *user;		/* user an irc message-line originated from, may be nil */
	char   *host;		/* host the user is on, may be nil */
};

/* irc messages as received from the ircd */
struct Msg {
	Source *src;		/* source of the message, may be nil */
	char   *cmd;		/* irc command, always non-nil */
	char  **params;		/* parameters for the command, may be nil */
	int	nparams;	/* number of parameters */
};

/* element of the event processing queues */
struct Elem {
	void   *data;		/* data part of the element */
	int	type;		/* type of data (INTYPE* for incoming, OUTTYPE* for outgoing) */
	Elem   *next;		/* next element in the list, may be nil for the last element */
};

/* event processing queue */
struct Queue {
	Elem   *first;		/* head of the queue, may be nil */
	pthread_mutex_t	mutex;	/* to control access to the queue */
	pthread_cond_t	nonempty;	/* signalled when something is put in the queue */
};

/* nick with permission to let the bot execute commands */
struct Nick {
	char   *name;
	Nick   *next;
};

/* list of nicks with permission */
struct Nicks {
	Nick   *first;
};

/* buffer with incoming data from the ircd */
struct Buf {
	char    s[BBUFFERSIZE];	/* data buffer */
	int	slen;		/* length of data in the buffer `s' */
	int	fd;		/* file descriptor to read from when `s' is empty */
	int	eof;		/* whether or not we have seen eof on `fd' */
};


void	handlequeue(char *, const char *, const char *, const char *, Nicks *);	/* main event processing loop */
void   *Fircreader(void *);			/* thread: reads and parses lines from ircd, then puts them in incoming queue */
void   *Fircwriter(void *);			/* thread: writes lines in outgoing queue to ircd */
void   *Ftcpacceptor(void *);			/* thread: accepts incoming tcp control connections (and creates an Ftcpreader for it) */
void   *Ftcpreader(void *);			/* thread: reads data from tcp control connection, data is put in the outgoing queue */
int	tcpopen(const char *, const char *);	/* opens connection to ircd */
void    tcpbind(const char *, int []);		/* binds to localhost tcp ports for accepting incoming control connections */

Msg    *msg_parse(char *);			/* parses a line received from the irc connection, Msg is allocated */
Msg    *msg_new(void);				/* allocates and initializes a Msg */
void	msg_free(Msg *);			/* frees any memory allocated in Msg */
Source *source_parse(char *);			/* parse the source of the message (`prefix' in irc terms) */

Queue  *qnew(void);				/* allocate and initialize a queue */
void	qappend(Queue *, int, void *);		/* append message to the queue */
void	qprepend(Queue *, int, void *);		/* prepend message to the queue */
void   *qremove(Queue *, int *);		/* retrieve and remove first element from queue */
void	qappendf(Queue *, int, const char *, ...);	/* append printf-like string to the queue */
void	qprependf(Queue *, int, const char *, ...);	/* same as qappendf but prepend */
void	qflushout(void);			/* flushes all non-important lines in outgoing queue */

Nicks  *nnew(const char *);			/* initialize nicks from the specified file */
void    nfree(Nicks *);				/* free Nicks */
void    nadd(Nicks *, char *);			/* add a nick */
void    nremove(Nicks *, char *);		/* remove the nick */
int	nhas(Nicks *, const char *);		/* check if the nick is present (allowed) */
void	nprint(Nicks *);			/* print a list of nicks to stderr, for debugging */
void    nsave(Nicks *, const char *);		/* save the nicks to the specified file */

Buf    *bnew(int);				/* create new buffer for reading incoming data from ircd */
int	breadln(Buf *, char *, int);		/* read a line (an irc message) from the buffer, keeps the trailing newline */

void    *xmalloc(size_t s)		{ void *p; p = malloc(s); if (p == nil) err(2, "xmalloc"); return p; }
void    *xrealloc(void *p, size_t s)	{ p = realloc(p, s); if (p == nil) err(2, "xrealloc"); return p; }
char    *xstrdup(char *str)		{ char *p; p = malloc(strlen(str) + 1); if (p == nil) err(2, "xstrdup"); strcpy(p, str); return p; }


/*
 * these are the only global variables (accessed by more than one thread),
 * these are protected by a pthread mutex and condition.
 */
Queue *inq;
Queue *outq;


int
main(int argc, char *argv[])
{
	char   *listenhost, *listenport;
	pthread_t Tircreader, Tircwriter, Ttcpacceptor[SOCK_MAX];
	char   *host, *port;
	char   *nickfile;
	struct	passwd *pw;
	int	ircfd;
	int     bindfds[SOCK_MAX+1];
	int	i;
	char   *nick;
	char   *userinfo;
	Nicks  *nicks;


	/* to make it accept `--' and print warnings for options like it should */
	if (getopt(argc, argv, "") != -1) {
		fprintf(stderr, "usage: bot nick host port listenhost listenport nickfile\n");
		exit(1);
	}
	argc -= optind;
	argv += optind;

	if (argc != 6) {
		warnx("invalid number of arguments");
		fprintf(stderr, "usage: bot nick host port listenhost listenport nickfile\n");
		exit(1);
	}

	/*
	 * a copy of nick is needed because the nick may change, then the
	 * current nick is freed and a new one allocated
	 */
	nick = xstrdup(argv[0]);
	host = argv[1];
	port = argv[2];
	listenhost = argv[3];
	listenport = argv[4];
	nickfile = argv[5];

	/* find username and info to present to ircd */
	pw = getpwnam(getlogin());
	if (pw == nil)
		errx(1, "could not find passwd entry for %s", getlogin());

	/* read nicks that are allowed to execute commands */
	nicks = nnew(nickfile);

	/*
	 * allocate and initialize event queues.  one for incoming commands
	 * (from ircd or listenhost:listenport) and one for messages to be send
	 * to the ircd.
	 */
	inq = qnew();
	outq = qnew();

	/* commands to be executed need a clean environment */
	if (fcntl(STDIN_FILENO,  F_SETFD, 1) == -1 ||
	    fcntl(STDOUT_FILENO, F_SETFD, 1) == -1 ||
	    fcntl(STDERR_FILENO, F_SETFD, 1) == -1)
		err(1, "setting close-on-exec for stdin, stdout and stderr");

	/*
	 * connect to the ircd.  start listening for incoming commands.  on
	 * error, exit is called
	 */
	ircfd = tcpopen(host, port);
	tcpbind(listenport, bindfds);

	/* create the threads */
	pthread_create(&Tircreader, nil, Fircreader, &ircfd);
	pthread_create(&Tircwriter, nil, Fircwriter, &ircfd);
	for (i = 0; bindfds[i] != -1; ++i)
		pthread_create(&Ttcpacceptor[i], nil, Ftcpacceptor, &bindfds[i]);

	/* send the login commands to get the connection started */
	userinfo = pw->pw_gecos;
	if (streql(userinfo, ""))
		userinfo = "none";
	qappendf(outq, OUTTYPEimmediate, "NICK %s\r\n", nick);
	qappendf(outq, OUTTYPEimmediate, "USER %s 0 * :%s\r\n", pw->pw_name, userinfo);

	/* into message handling loop, this receives incoming matches and dispatches outgoing ones */
	handlequeue(nick, listenhost, listenport, nickfile, nicks);
	exit(0);	/* not reached */
}


void
handlequeue(char *nick, const char *listenhost, const char *listenport, const char *nickfile, Nicks *nicks)
{
	void   *data;
	int	type;
	Msg    *msg;
	char	botpidbuf[64];
	char   *target;
	char   *param;

	assert(snprintf(botpidbuf, sizeof botpidbuf, "%llu", (uvlong)getpid()) != -1);

	for (;;) {
		/*
		 * this call blocks (properly with mutexes and non-empty-queue
		 * condition) until something needs to be processed
		 */
		data = qremove(inq, &type);

		/* cleanup possible child processes, doesn't block */
		for (;;) {
			int	r;
			int	status;

			r = wait3(&status, WNOHANG, nil);
			if (r == 0 || (r == -1 && errno == ECHILD))
				break;		/* no children at all (0) or no exited children (-1) */
			if (r == -1) {
				warn("wait3");	/* error in wait3 */
				break;
			}
		}
		errno = 0;

		/* handle the data retrieved from the queue, based on the type of the data */
		switch (type) {
		case INTYPEircmsg:	/* incoming irc message from ircd */
			msg = (Msg *)data;

			/*
			 * ping message from ircd
			 *
			 * <<< PING :irc.snt.utwente.nl
			 * >>> PONG brein irc.snt.utwente.nl
			 */
			if (strequiv(msg->cmd, "PING") && msg->nparams >= 1)
				qprependf(outq, OUTTYPEimmediate, "PONG %s %s\r\n", nick, msg->params[0]);

			/*
			 * handle messages telling our nick changed
			 *
			 * >>> NICK newbrein
			 * <<< :brein!plan@tunnel4040.ipv6.xs4all.nl NICK :newbrein
			 */
			if (strequiv(msg->cmd, "NICK") && msg->nparams == 1 &&
			    msg->src != nil && msg->src->nick != nil && strequiv(msg->src->nick, nick)) {
				free(nick);
				nick = xstrdup(msg->params[0]);
			}

			/* private messages from nicks and with an argument may contain a command or a ctcp version */
			if (msg->src != nil && msg->src->nick != nil && strequiv(msg->cmd, "PRIVMSG") && msg->nparams >= 2) {

				param = msg->params[1];

				/*
				 * only accept commands from known people (both in a channel and in private)
				 * skip some common smileys
				 *
				 * <<< :Oksel!mechiel@tunnel4040.ipv6.xs4all.nl PRIVMSG #deadbeef :;help
				 * or
				 * <<< :Oksel!mechiel@tunnel4040.ipv6.xs4all.nl PRIVMSG brein :;wn whatsoever
				 */
				if (nhas(nicks, msg->src->nick) && param[0] == ';' && 
				    !(streql(param, ";)") || streql(param, ";-)") ||
				      streql(param, ";(") || streql(param, ";-("))) {

					target = msg->params[0];
					if (strequiv(msg->params[0], nick))	/* private message to bot, send response to nick, not channel */
						target = msg->src->nick;

					switch (fork()) {
					case 0:
						setenv("target", target, 1);
						setenv("nick", nick, 1);
						setenv("nickfile", nickfile, 1);
						setenv("fromuser", msg->src->nick, 1);
						if (target[0] == '#')
							setenv("fromchannel", target, 1);
						setenv("bothost", listenhost, 1);
						setenv("botport", listenport, 1);
						setenv("botpid", botpidbuf, 1);

						/* note: all fd's have close-on-exec set */
						execlp("./botcmd", "./botcmd", param + 1, (char *)nil);
						warn("execlp ./botcmd");
						break;
					case -1:
						warn("forking for execlp");
						break;
					}
				} else if (strequiv(param, "\001VERSION\001")) {
					/* handle a ctcp version request */
					qappendf(outq, OUTTYPEnormal, "NOTICE %s :\001VERSION %s\001\r\n", msg->src->nick, IRCBOT_VERSION);
				}
			}

			msg_free(msg); msg = nil;
			break;
		case INTYPEraw:		/* raw command to send to ircd */
			qappendf(outq, OUTTYPEnormal, "%s\r\n", (char *)data);
			free(data); data = nil;
			break;
		case INTYPEbuiltin:	/* built in commands */
			if (streql("flush", (char *)data)) {
				qflushout();
			} else if (streql("readnicks", (char *)data)) {
				nfree(nicks);
				nicks = nnew(nickfile);
			} else {
				warnx("unexpected builtin command read in handlequeue: %s", (char *)data);
			}
			free(data); data = nil;
			break;
		default:
			warnx("unknown message received in main, type=%d", type);
			break;
		}
	}
}


void *
Fircreader(void *ircfdp)
{
	char    buf[IRCMSG_MAXLEN+1];
	int	count;
	Buf    *bin;
	Msg    *msg;
	struct	timeval now;
	uvlong	msecs;

	bin = bnew(*(int *)ircfdp);
	for (;;) {
		/* read a message line from the ircd (including \r\n if present) */
		count = breadln(bin, buf, sizeof buf);
		if (count == 0)
			errx(1, "eof from ircd");
		if (count == -1)
			err(1, "reading from ircd");

		gettimeofday(&now, nil);
		msecs = (uvlong)now.tv_sec*1000 + (uvlong)now.tv_usec/1000;
		printf("<<< %llu %s", msecs, buf);
		fflush(stdout);
		msg = msg_parse(buf);
		if (msg == nil)
			continue;	/* message has been printed by msg_parse */

		/* insert the irc message into the event queue */
		if (strequiv(msg->cmd, "PING"))
			qprepend(inq, INTYPEircmsg, msg);
		else
			qappend(inq, INTYPEircmsg, msg);
	}
}


void *
Fircwriter(void *ircfdp)
{
	char   *line;
	int	type;
	uvlong	msecs;
	int	length, count;
	struct timeval	lastwrite = {0, 0};
	struct timeval  now;

	for (;;) {
		line = qremove(outq, &type);
		if (type == OUTTYPEimmediate) {
			length = strlen(line);
			count = write(*(int *)ircfdp, line, length);
			if (count != length)
				warnx("ircwriter: line not written entirely, length=%d count=%d", length, count);

			gettimeofday(&lastwrite, nil);
		} else {
			if (type != OUTTYPEnormal)
				warnx("ircwriter: unknown message, type=%d, writing as if type=OUTTYPEnormal", type);

			gettimeofday(&now, nil);
			msecs = (now.tv_sec-lastwrite.tv_sec)*1000 + (now.tv_usec-lastwrite.tv_usec)/1000;
			if (msecs < NOFLOOD_DELAY)
				usleep((NOFLOOD_DELAY-msecs) * 1000);

			length = strlen(line);
			count = write(*(int *)ircfdp, line, length);
			if (count != length)
				warnx("ircwriter: line not written entirely, length=%d count=%d", length, count);

			gettimeofday(&lastwrite, nil);
		}

		msecs = (uvlong)lastwrite.tv_sec*1000 + (uvlong)lastwrite.tv_usec/1000;
		printf(">>> %llu %s", msecs, line);
		fflush(stdout);
		free(line);
	}
}


void *
Ftcpacceptor(void *bindfdp)
{
	int	tcpfd;
	struct	sockaddr sa;
	socklen_t  salen;
	pthread_t *Ttcpreader;

	for (;;) {
		tcpfd = accept(*(int *)bindfdp, (struct sockaddr *)&sa, &salen);
		if (tcpfd < 0) {
			if (errno == EINTR)
				continue;
			err(1, "accept");
		}

		if (fcntl(tcpfd, F_SETFD, 1) == -1) {
			warn("setting close-on-exec flag for incoming connection");
			close(tcpfd);
			continue;
		}

		Ttcpreader = xmalloc(sizeof Ttcpreader[0]);
		pthread_create(Ttcpreader, nil, Ftcpreader, &tcpfd);
	}
}


void *
Ftcpreader(void *tcpfdp)
{
	char	buf[BBUFFERSIZE];
	int	count;
	Buf    *bin;
	char   *cp;
	char   *errstr;

	/*
	 * bnew and breadln were actually meant for reading irc messages with a
	 * maximum length of 512 characters.  the tcp connection has this limit
	 * too now, no problem though.
	 */
	bin = bnew(*(int *)tcpfdp);
	for (;;) {
		/* read a message line from the client (including \n if present) */
		count = breadln(bin, buf, sizeof buf);
		if (count == 0) {
			close(*(int *)tcpfdp);
			pthread_exit(nil);
		}
		if (count == -1) {
			warn("tcpreader: error reading from client");
			close(*(int *)tcpfdp);
			pthread_exit(nil);
		}
		if (buf[strlen(buf) - 1] == '\n') {
			buf[strlen(buf) - 1] = '\0';
			if (buf[strlen(buf) - 1] == '\r')	/* telnet(1) appends an \r, remove it */
				buf[strlen(buf) - 1] = '\0';
		}

		if (streql(buf, "flush") || streql(buf, "readnicks")) {
			qprepend(inq, INTYPEbuiltin, xstrdup(buf));
		} else if (buf[0] == ';') {
			qappend(inq, INTYPEraw, xstrdup(buf+1));
		} else if (buf[0] == '%') {
			cp = strchr(buf, ' ');
			if (cp == nil || cp == buf+1) {
				errstr = "no message to send to nick or empty nick\n";
				break;
			} else {
				*cp = '\0';
				qappendf(inq, INTYPEraw, "PRIVMSG %s :%s", buf+1, cp+1);
			}
		} else if (buf[0] == '#') {
			cp = strchr(buf, ' ');
			if (cp == nil || cp == buf+1) {
				errstr = "no message to send to channel or empty channel\n";
				break;
			} else {
				*cp = '\0';
				qappendf(inq, INTYPEraw, "PRIVMSG %s :%s", buf, cp+1);
			}
		} else {
			errstr = "no nick or channel specified\n";
			break;
		}
	}

	if (write(*(int *)tcpfdp, errstr, strlen(errstr)) != strlen(errstr))
		warnx("tcpreader: error string not written entirely to connection");
	close(*(int *)tcpfdp);
	pthread_exit(nil);
}


int
tcpopen(const char *host, const char *port)
{
	int	gaierrno;
	struct	addrinfo hints = { 0, PF_UNSPEC, SOCK_STREAM, 0, 0, nil, nil, nil };
	struct	addrinfo *res0, *res;
	char   *cause;
	int	save_errno;
	int	fd;

	gaierrno = getaddrinfo(host, port, &hints, &res0);
	if (gaierrno)
		errx(1, "getaddrinfo: %s", gai_strerror(gaierrno));

	fd = -1;
	for (res = res0; res != nil; res = res->ai_next) {
		fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); 
		if (fd < 0) {
			fd = -1;
			cause = "socket";
			continue;
		}

		if (connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
			save_errno = errno;
			(void)close(fd);
			errno = save_errno;

			fd = -1;
			cause = "connect";
			continue;
		}

		break;
	}
	freeaddrinfo(res0);

	if (fd == -1)
		err(1, "connecting to %s:%s: %s", host, port, cause);

	if (fcntl(fd, F_SETFD, 1) == -1)
		err(1, "setting close-on-exec flag for irc connection");

	return fd;
}


void
tcpbind(const char *port, int bindfds[SOCK_MAX+1])
{
	struct	addrinfo hints = { AI_PASSIVE, PF_UNSPEC, SOCK_STREAM, 0, 0, nil, nil, nil };
	struct	addrinfo *res, *res0;
	int	gaierrno, save_errno;
	int	fd, nsock;
	char   *cause;

	gaierrno = getaddrinfo(nil, port, &hints, &res0);
	if (gaierrno)
		errx(1, "getaddrinfo: %s", gai_strerror(gaierrno));

	cause = nil;
	nsock = 0;
	for (res = res0; res != nil && nsock < SOCK_MAX; res = res->ai_next) {
		fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (fd < 0) {
			cause = "socket";
			continue;
		}

		if (bind(fd, res->ai_addr, res->ai_addrlen) < 0 || fcntl(fd, F_SETFD, 1) == -1) {
			cause = "bind or fcntl";
			save_errno = errno;
			close(fd);
			errno = save_errno;

			continue;
		}
		(void)listen(fd, 5);
		bindfds[nsock++] = fd;
		bindfds[nsock] = -1;
	}
	if (nsock == 0)
		err(1, cause);

	freeaddrinfo(res0);
}


/*
 * see rfc2812 for a detailed description of irc messages
 *
 * simply put, an irc message looks like this:
 *
 * message = (:$prefix)? $command ($param)* crlf
 * prefix = servername | nickname ((!user)?@host)?
 * command = string | three-digit-number
 * param = string
 *
 * a parameter starting with a colon is special.  the parameter ends at
 * the end of the line, i.e. does not end with the next space.
 */
Msg *
msg_parse(char *str)
{
	int	stop;
	char   *cp;
	Msg    *msg;

	assert(str != nil);

	msg = msg_new();

	if (str[0] == ':') {
		++str;
		cp = strchr(str, ' ');
		if (cp == nil) {
			warnx("parsing: message contains prefix but no space (and thus no command)");
			goto error;
		}
		*cp = '\0';

		msg->src = source_parse(str);
		if (msg->src == nil)
			goto error;	/* diagnostics printed by source_parse */

		str = cp+1;
	}

	stop = 0;

	cp = strchr(str, ' ');
	if (cp == nil) {
		cp = strchr(str, '\r');
		if (cp == nil) {
			warnx("parsing: message doesn't contain carriage return (for end) or space (for parameter delimiter) after command");
			goto error;
		}
		stop = 1;
	}

	*cp = '\0';
	msg->cmd = xstrdup(str);
	str = cp+1;

	while (!stop) {
		if (str[0] == ':') {
			++str;
			stop = 1;
		}

                /* when we saw a colon and thus `stop' is set, don't
                 * look for a space but for the closing carriage return */
		cp = strchr(str, stop ? '\r' : ' ');
		if (cp == nil) {
			/* the last argument is not required to have a colon */
			cp = strchr(str, '\r');
			if (cp == nil) {
				warnx("parsing: no carriage return after arguments");
				goto error;
			}
			stop = 1;
		}
		*cp = '\0';
		msg->params = xrealloc(msg->params, sizeof msg->params[0] * (msg->nparams+1));
		msg->params[msg->nparams++] = xstrdup(str);

		str = cp+1;
	}

	/* debug code... */
	if (0) {
		int	i;

		if (msg->src != nil) {
			if (msg->src->host != nil) {
				fprintf(stderr, "src->nick=%s\n", msg->src->nick);
				if (msg->src->user != nil)
					fprintf(stderr, "src->user=%s\n", msg->src->user);
				else
					fprintf(stderr, "src->user=(nil)\n");
				fprintf(stderr, "src->host=%s\n", msg->src->host);
			} else {
				fprintf(stderr, "nick=server=%s\n", msg->src->nick);
			}
		} else {
			fprintf(stderr, "src=(nil)\n");
		}

		fprintf(stderr, "cmd=%s\n", msg->cmd);
		fprintf(stderr, "nparams=%d\n", msg->nparams);
		for (i = 0; i < msg->nparams; ++i)
			fprintf(stderr, "params[%d]=%s\n", i, msg->params[i]);
	}

	return (Msg *)msg;

error:
	msg_free((Msg *)msg); msg = nil;
	return nil;
}


Source *
source_parse(char *str)
{
	Source *source;
	char   *cp;

	assert(str != nil);

	source = xmalloc(sizeof source[0]);
	source->nick = nil;
	source->server = nil;
	source->user = nil;
	source->host = nil;

	cp = strchr(str, '@');
	if (cp == nil) {
		/* the source is either a servername or a nick without user/host part */
		source->nick = source->server = xstrdup(str);
		return source;
	}

	source->host = xstrdup(cp+1);
	*cp = '\0';	/* make location of @ end of string, thus str only contains nick with optionally !user */

	cp = strchr(str, '!');
	if (cp == nil) {
		source->nick = xstrdup(str);
		return source;
	}

	source->user = xstrdup(cp+1);
	*cp = '\0';	/* make location of ! end of string, thus str only contains nick */
	source->nick = xstrdup(str);

	return source;
}


Msg *
msg_new(void)
{
	Msg    *msg;
	
	msg = xmalloc(sizeof msg[0]);
	msg->src = nil;
	msg->cmd = nil;
	msg->params = nil;
	msg->nparams= 0;

	return msg;
}


void
msg_free(Msg *msg)
{
	int	i;

	if (msg == nil)
		return;

	for (i = 0; i < msg->nparams; ++i)
		free(msg->params[i]);
	free(msg->params);
	free(msg->cmd);
	if (msg->src != nil) {
		free(msg->src->nick);
		msg->src->server = nil;		/* server is always same as nick or nil */
		free(msg->src->user);
		free(msg->src->host);
		free(msg->src);
	}
	free(msg);
}


Queue *
qnew(void)
{
	Queue *new;

	new = xmalloc(sizeof new[0]);
	new->first = nil;
	pthread_mutex_init(&new->mutex, nil);
	pthread_cond_init(&new->nonempty, nil);

	return new;
}


void
qappend(Queue *q, int type, void *data)
{
	Elem   *new;
	Elem   *oldlast;

	assert(q != nil);
	assert(data != nil);

	new = xmalloc(sizeof new[0]);
	new->data = data;
	new->type = type;
	new->next = nil;

	pthread_mutex_lock(&q->mutex);
	if (q->first == nil) {
		q->first = new;
	} else {
		oldlast = q->first;
		while (oldlast->next != nil)
			oldlast = oldlast->next;
		oldlast->next = new;
	}
	pthread_cond_signal(&q->nonempty);
	pthread_mutex_unlock(&q->mutex);
}


void
qprepend(Queue *q, int type, void *data)
{
	Elem   *new;

	assert(q != nil);
	assert(data != nil);

	new = xmalloc(sizeof new[0]);
	new->data = data;
	new->type = type;
	new->next = nil;

	pthread_mutex_lock(&q->mutex);
	new->next = q->first;
	q->first = new;
	pthread_cond_signal(&q->nonempty);
	pthread_mutex_unlock(&q->mutex);
}


void *
qremove(Queue *q, int *typep)
{
	void   *r;
	Elem   *newfirst;

	assert(q != nil);

	pthread_mutex_lock(&q->mutex);
	if (q->first == nil)
		pthread_cond_wait(&q->nonempty, &q->mutex);

	assert(q->first != nil);

	r = q->first->data;
	if (typep != nil)
		(*typep) = q->first->type;
	newfirst = q->first->next;
	free(q->first);
	q->first = newfirst;
	pthread_mutex_unlock(&q->mutex);

	return r;
}


void
qappendf(Queue *q, int type, const char *fmt, ...)
{
	char   *tmp;
	va_list ap;

	va_start(ap, fmt);
	vasprintf(&tmp, fmt, ap);
	va_end(ap);

	qappend(q, type, tmp);
}


void
qprependf(Queue *q, int type, const char *fmt, ...)
{
	char   *tmp;
	va_list ap;

	va_start(ap, fmt);
	vasprintf(&tmp, fmt, ap);
	va_end(ap);

	qprepend(q, type, tmp);
}


void
qflushout(void)
{
	Elem   *newfirst;
	Elem   *newcur;
	Elem   *cur, *tmp;

	pthread_mutex_lock(&outq->mutex);

	newfirst = newcur = nil;
	cur = outq->first;
	while (cur != nil) {
		if (cur->type == OUTTYPEimmediate) {
			if (newfirst == nil) {
				newfirst = newcur = cur;
				newfirst->next = nil;
			} else {
				newcur->next = cur;
				newcur = newcur->next;
				newcur->next = nil;
			}
			cur = cur->next;
		} else {
			free(cur->data);
			tmp = cur;
			cur = cur->next;
			free(tmp);
		}
	}
	outq->first = newfirst;

	pthread_mutex_unlock(&outq->mutex);
}


Nicks *
nnew(const char *file)
{
	Nicks  *n;
	FILE   *fp;
	char    buf[NICK_MAXLEN + 1];

	n = xmalloc(sizeof n[0]);
	n->first = nil;

	fp = fopen(file, "r");
	if (fp == nil)
		err(1, "opening %s", file);

	/* read nicks from file and add them to the Nicks structure */
	for (;;) {
		if (fgets(buf, sizeof buf, fp) == nil) {
			if (ferror(fp))
				err(1, "reading from %s", file);
			break;
		}
		if (strchr(buf, '\n') != nil)
			buf[strlen(buf) - 1] = '\0';
		nadd(n, xstrdup(buf));
	}

	fclose(fp);

	return n;
}


void
nfree(Nicks *n)
{
	Nick   *cur, *tmp;

	for (cur = n->first;  cur != nil;  tmp = cur->next, free(cur), cur = tmp)
		free(cur->name);
	free(n);
}


void
nadd(Nicks *n, char *name)
{
	Nick   *new;
	Nick  **ptr;
	int	wslen;

	/* remove leading and trailing whitespace from nick */
	wslen = strspn(name, " \t\n");
	memmove(name, name+wslen, strlen(name+wslen) + 1);
	if (strpbrk(name, " \t\n") != nil)
		*(strpbrk(name, " \t\n")) = '\0';

	if (nhas(n, name) || strlen(name) == 0)	/* avoid duplicates, refuse empty nicks */
		return;

	new = xmalloc(sizeof new[0]);
	new->name = name;
	new->next = nil;

	ptr = &n->first;
	while (*ptr != nil)
		ptr = &(*ptr)->next;
	*ptr = new;
}


void
nremove(Nicks *n, char *name)
{
	Nick   *tmp;
	Nick  **newnext;

	newnext = &n->first;
	while (*newnext != nil) {
		if (strequiv((*newnext)->name, name)) {
			tmp = *newnext;
			*newnext = (*newnext)->next;
			free(tmp->name);
			free(tmp);

			return;
		} else {
			newnext = &(*newnext)->next;
		}
	}
}


int
nhas(Nicks *n, const char *name)
{
	Nick   *cur;

	for (cur = n->first; cur != nil; cur = cur->next)
		if (strequiv(cur->name, name))
			return 1;
	return 0;
}


void
nprint(Nicks *n)
{
	Nick   *cur;

	fprintf(stderr, "bot: nprint: ");
	for (cur = n->first; cur != nil; cur = cur->next)
		fprintf(stderr, "`%s' ", cur->name);
	fprintf(stderr, "\n");
}


void
nsave(Nicks *n, const char *file)
{
	Nick   *cur;
	FILE   *fp;

	fp = fopen(file, "w");
	if (fp == nil) {
		warn("opening %s", file);
		return;
	}
	
	for (cur = n->first; cur != nil; cur = cur->next)
		fprintf(fp, "%s\n", cur->name);
	if (ferror(fp))
		warnx("ferror after writing %s", file);

	fclose(fp);
}


Buf *
bnew(int fd)
{
	Buf    *new;

	assert(fd >= 0);

	new = xmalloc(sizeof new[0]);
	new->fd = fd;
	new->s[0] = '\0';
	new->slen = 0;
	new->eof = 0;

	return new;
}


/* trailing newline is kept if present, there could be no newline at all */
int
breadln(Buf *b, char *dest, int destsize)
{
	int	count;
	char   *newline;
	int	flushtonewline;

	assert(destsize >= sizeof b->s);

	if (b->eof)
		return 0;

	flushtonewline = 0;
	while (strchr(b->s, '\n') == nil) {
		if (b->slen == sizeof b->s - 1) {
			/* line too long, flush entire line */
			flushtonewline = 1;
			b->s[0] = '\0';
			b->slen = 0;
		}

		count = read(b->fd, b->s + b->slen, (BBUFFERSIZE - 1) - b->slen);
		if (count == 0) {
			b->eof = 1;

			memmove(dest, b->s, b->slen);
			dest[b->slen] = '\0';
			count = b->slen;
			b->s[0] = '\0';
			b->slen = 0;

			return count;
		}
		if (count == -1)
			return -1;
		b->slen += count;
		b->s[b->slen] = '\0';

		if (flushtonewline) {
			newline = strchr(b->s, '\n');
			if (newline == nil) {
				b->s[0] = '\0';
				b->slen = 0;
			} else {
				memmove(b->s, newline+1, b->slen - (newline - b->s + 1) + 1);
				b->slen -= newline - b->s + 1;
				flushtonewline = 0;
			}
		}
	}

	newline = strchr(b->s, '\n');
	assert(newline != nil);

	count = newline - b->s + 1;
	strncpy(dest, b->s, count);
	dest[count] = '\0';
	memmove(b->s, b->s+count, b->slen - count + 1);
	b->slen -= count;

	return count;
}