wm: ticl

ref: 8c6dee8c450f9a7d9b97e51d47429d353a42ff9d
dir: /main.c/

View raw version
/*
 * This work is dedicated to the public domain.
 * See COPYING file for more information.
 */

#include <errno.h>
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>

#ifdef __gnu_linux__
#include <sys/epoll.h>
#else
#include <sys/event.h>
#endif

#define LOG_LEVEL	1

#include "htable.h"
#include "util.c"

#define CLONE_COOLDOWN	1
#define CLONE_ADDEND	10
#define RECONN_TIME	10

#define FD_ADDEND	100
#define NET_ADDEND	10
#define USER_ADDEND	100

#define BUFSIZE		1024
#define NICK_LEN	16
#define HANDLE_EVENTS	10	/* no. of events to handle at a time */

#define EV_READ		1
#define EV_WRITE	2

enum {
	IDLE = 0,
	RESET,
	CLONING,
	EXIT
};

struct fd_data {
	char		*user;	/* nick		*/
	int		netid;	/* net index	*/
	int	 	suffix;	/* suffix count	*/
	int	 	ready;	/* joined	*/
};

struct fd_ref {
	char		*user;	/* nick		*/
	int		netid;	/* net index	*/
	struct fd_ref	*next;	/* next node	*/
};

struct network {
	int		 fd;	/* fd		*/
	char		*name;	/* name		*/
	char		*symb;	/* symbol	*/
	char		*host;	/* host		*/
	char		*port;	/* port		*/
	char		*chan;	/* channel	*/
};

static time_t		 ntime = -1;	/* next timeout		*/
static int		 state = IDLE;	/* program state 	*/
static int		 fifofd;	/* fifo fd		*/
static int		 break_evloop;	/* break event loop	*/
static char		*fifopath;	/* fifo path		*/
static char		 msg[BUFSIZE];	/* message buffer	*/

#ifdef __gnu_linux__
static int		 epfd;		/* epoll instance	*/
#else
static int		 kqfd;		/* kqueue instance	*/
#endif

static struct network	 *networks;	/* networks array	*/
static int		  netlen;	/* array length		*/
static int		  netcap;	/* array capacity	*/
static struct fd_data	**fdtodata;	/* fd -> data pointer	*/
static int		  fdcap;	/* fdtodata capacity	*/
static struct htable	 *usertofds;	/* user -> array of fds of clones
					indexed according to networks array */

static struct fd_ref	 *reconn_list_head, /* re-connection queue */
			 *reconn_list_tail;

/* functions prototype */
void	 fd_register(int, int);
void	 fifo_read(void);
time_t	 event_timeout(void);
void	 fd_write(int);
void	 fd_read(int);
void	 net_add(char *, char *, char *, char *, char *);
void	 net_del(int, char *, int);
void	 user_add(char *, int, int);
void	 user_del(char *, char *);
void	 reconn_list_add(char *, int);
void	 reconn_list_del(char *, int);
int	 fd_new(int, char *);
void	 fd_del(int, char *, int);
void	 fd_reconn(int, char *);
int	*clone_get_fds(char *, int);
void	 nick_add_symb(char *, int);
void	 privmsg_update(char *, char *, int, int);
void	 print_table(void);
void	 print_htable(void);
void	 print_users(void);
void	 print_reconn_list(void);
void	 print_border(void);
ssize_t	 writeall(int, char *);

int
main(int argc, char *argv[])
{
#ifdef __gnu_linux__
	struct epoll_event	epevs[HANDLE_EVENTS];
#else
	struct kevent		kevs[HANDLE_EVENTS];
	struct timespec		tmsp;
#endif
	int	i, fd, nev;	/* no. of returned events to handle */
	time_t	timeout;

	/* set stdout to unbufferd */
	setvbuf(stdout, NULL, _IONBF, 0);

	/* handle arguments */
	if (argc != 2)
		fatal("usage: ticl fifo");
	else
		fifopath = argv[1];

	/* init global variables */
	netcap = NET_ADDEND;
	fdcap = FD_ADDEND;
	networks = emalloc((size_t)netcap * sizeof(struct network));
	fdtodata = emalloc((size_t)fdcap * sizeof(struct fd_data *));
	usertofds = htcreate(hash_str, (key_cmp_fn *)strcmp, free, free,
			USER_ADDEND);

#ifdef __gnu_linux__
	if ((epfd = epoll_create1(0)) == -1)
		fatal("epoll_create1:");
#else
	if ((kqfd = kqueue()) == -1)
		fatal("kqueue:");
#endif
	fifofd = fifo_open(fifopath);
	fd_register(fifofd, EV_READ);

	/* event loop */
	while (state != EXIT) {
		if (ntime == -1) {
			timeout = -1;
		} else {
			timeout = ntime - time(NULL);
			if (timeout < 0)
				timeout = 0;
		}
#ifdef __gnu_linux__
		nev = epoll_wait(epfd, epevs, HANDLE_EVENTS, (int)timeout * 1000);
		if (nev == -1)
			fatal("epoll_wait:");
#else
		tmsp.tv_sec = timeout;
		tmsp.tv_nsec = 0;
		nev = kevent(kqfd, NULL, 0, kevs, HANDLE_EVENTS, timeout < 0 ? NULL : &tmsp);
		if (nev == -1)
			fatal("kevent:");
#endif
		else if (nev == 0) {
			ntime = event_timeout();
			continue;
		}

		for (i = 0; i < nev; i++) {
#ifdef __gnu_linux__
			fd = epevs[i].data.fd;
#else
			fd = (int)kevs[i].ident;
#endif
			if (fd == fifofd) {
				fifo_read();
				break;
#ifdef __gnu_linux__
			} else if (epevs[i].events & EPOLLOUT) {
#else
			} else if (kevs[i].filter == EVFILT_WRITE) {
#endif
				fd_write(fd);
#ifdef __gnu_linux__
			} else if (epevs[i].events & EPOLLIN) {
#else
			} else if (kevs[i].filter == EVFILT_READ) {
#endif
				fd_read(fd);
			} else {
				fatal("unknown event");
			}

			if (reconn_list_head != NULL && ntime == -1)
				ntime = time(0) + RECONN_TIME;
			if (break_evloop) {
				break_evloop = FALSE;
				break;
			}
		}
	}

	/*
	 * delete and free all the networks with their users
	 *	- delete in reverse order to prevent swapping.
	 */
	snprintf(msg, sizeof(msg), "QUIT :relay shutting down\r\n");
	for (i = netlen-1; i >= 0; i--)
		net_del(i, msg, FALSE);

	free(networks);
	free(fdtodata);
	htdestroy(usertofds);
	return 0;
}

void
fd_register(int fd, int mode)
{
#ifdef __gnu_linux__
	struct epoll_event ev;

	if (mode == EV_READ)
		ev.events = EPOLLIN;
	else if (mode == EV_WRITE)
		ev.events = EPOLLOUT;
	else
		fatal("unknown event mode");

	ev.data.fd = fd;
	if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
		fatal("epoll_ctl:");
#else
	struct kevent ev;
	int filter;

	if (mode == EV_READ)
		filter = EVFILT_READ;
	else if (mode == EV_WRITE)
		filter = EVFILT_WRITE;
	else
		fatal("unknown event mode");

	EV_SET(&ev, fd, filter, EV_ADD, 0, 0, 0);
	if (kevent(kqfd, &ev, 1, NULL, 0, NULL) == -1)
		fatal("kevent:");
#endif
}

void
fifo_read(void)
{
	char	 buffer[BUFSIZE];
	char	*buf;
	char	*cmd;
	ssize_t	 n;

	n = readline(fifofd, buffer, sizeof(buffer));
	if (n == -1) {
		fatal("read:");
	} else if (n == 0) {
		/* reopen fifo again */
		close(fifofd);
		fifofd = fifo_open(fifopath);
		fd_register(fifofd, EV_READ);
		return;
	}
	if (*buffer == '\0')
		return;

	buf = buffer;
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "netadd") == 0) {
		char *name = split(&buf, ' ');
		char *symb = split(&buf, ' ');
		char *host = split(&buf, ' ');
		char *port = split(&buf, ' ');
		char *chan = buf;
		if (!*name || !*symb || !*host || !*port || !*chan)
			warnf("usage: netadd <name> <symbol> <host> <port> <channel>");
		else
			net_add(name, symb, host, port, chan);
	} else if (strcmp(cmd, "netdel") == 0) {
		char *name = buf;
		if (!*name) {
			warnf("usage: netdel <name>");
		} else {
			int i;
			for (i = 0; i < netlen; i++) {
				if (strcmp(name, networks[i].name) == 0) {
					snprintf(msg, sizeof(msg), "QUIT :netdel: %s\r\n", networks[i].name);
					net_del(i, msg, FALSE);
					return;
				}
			}
			warnf("%s: network doesn't exist", name);
		}
	} else if (strcmp(cmd, "print") == 0) {
		print_table();
	} else if (strcmp(cmd, "htable") == 0) {
		print_htable();
	} else if (strcmp(cmd, "users") == 0) {
		print_users();
	} else if (strcmp(cmd, "reconn") == 0) {
		print_reconn_list();
	} else if (strcmp(cmd, "exit") == 0) {
		state = EXIT;
	} else {
		warnf("%s is not a command", cmd);
	}
}

time_t
event_timeout(void)
{
	static struct htiter it;

	int i, j, *fds;
	char *user;

	if (state == IDLE) {
		struct fd_ref *tmp;
		debug(1, "Reconnecting");
		while(reconn_list_head != NULL) {
			user = reconn_list_head->user;
			i = reconn_list_head->netid;

			if (user == NULL) {
				networks[i].fd = fd_new(i, user);
			} else {
				if ((fds = htsearch(usertofds, user)) == NULL)
					fatal("%s: user doesn't exist", user);
				if (fds[i] == -3 &&
				    networks[i].fd > 0 &&
				    fdtodata[networks[i].fd]->ready)
					fds[i] = fd_new(i, user);
			}
			tmp = reconn_list_head;
			reconn_list_head = reconn_list_head->next;
			free(tmp);
		}
		reconn_list_tail = reconn_list_head;
		return -1;
	} else if (state == RESET) {
		state = CLONING;
		htiter_init(&it);
	}
	debug(1, ".");

	j = 0;
	while (htiterate(usertofds, &it)) {
		user = (char *)it.node->key;
		fds = (int *)it.node->val;
		for (i = 0; i < netlen; i++) {
			if (fds[i] == 0 && networks[i].fd > 0 &&
					fdtodata[networks[i].fd]->ready)
				fds[i] = fd_new(i, user);
		}
		j++;
		if (j >= CLONE_ADDEND)
			return time(NULL) + CLONE_COOLDOWN;
	}
	state = IDLE;
	return -1;
}

void
fd_write(int fd)
{
#ifdef __gnu_linux__
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.fd = fd;
	if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1)
		fatal("epoll_ctl:");
#else
	struct kevent ev;
	EV_SET(&ev, fd, EV_WRITE, EV_DISABLE, 0, 0, 0);
	if (kevent(kqfd, &ev, 1, NULL, 0, NULL) == -1)
		fatal("kevent:");
	fd_register(fd, EV_READ);
#endif
	if (fdtodata[fd]->user == NULL) {	/* watcher */
		snprintf(msg, sizeof(msg), "NICK watcher\r\nUSER watcher 0 * :watcher\r\n");
		writeall(fd, msg);
	} else {			/* user */
		snprintf(msg, sizeof(msg), "NICK %s\r\nUSER user 0 * :user\r\n", fdtodata[fd]->user);
		writeall(fd, msg);
	}
}

void
fd_read(int fd)
{
	char	 buffer	[BUFSIZE];
	char	 backup	[BUFSIZE];
	char	 wnick	[NICK_LEN];	/* watcher nick */
	char	*buf;
	char	*cmd;
	char	*nick;
	int	 i, netid;
	ssize_t	 n;

	netid = fdtodata[fd]->netid;

	n = readline(fd, buffer, sizeof(buffer));
	if (n == -1) {
		warnf("%d: read:", fd);
		snprintf(msg, sizeof(msg), "QUIT :read failed\r\n");
		fd_reconn(fd, msg);
		return;
	} else if (n == 0) {
		warnf("%d: read: connection closed", fd);
		snprintf(msg, sizeof(msg), "QUIT :connection closed\r\n");
		fd_reconn(fd, msg);
		return;
	}
	if (*buffer == '\0')
		return;

	/* clone the buffer */
	strlcpy(backup, buffer, sizeof(backup));
	buf = buffer;

	/* set watcher nick */
	strlcpy(wnick, "watcher", sizeof(wnick));
	for (i = 0; i < fdtodata[fd]->suffix; i++)
		strcat(wnick, "_");

	/* first column */
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "NOTICE") == 0) {	/* ignore */
		return;
	} else if (strcmp(cmd, "ERROR") == 0) {
		warnf("%d: %s", fd, backup);
		snprintf(msg, sizeof(msg), "QUIT :ERROR\r\n");
		fd_reconn(fd, msg);
		return;
	} else if (strcmp(cmd, "PING") == 0) {
		snprintf(msg, sizeof(msg), "PONG %s\r\n", buf);
		writeall(fd, msg);
		return;
	}
	/* strip nick from first column */
	nick = split(&cmd, '!');
	if (nick[0] == ':')
		nick++;

	/* second column */
	cmd = split(&buf, ' ');

	/* ignore all the info messages */
	if ((strcmp(cmd, "002") == 0) ||
	    (strcmp(cmd, "003") == 0) ||
	    (strcmp(cmd, "004") == 0) ||
	    (strcmp(cmd, "005") == 0) ||
	    (strcmp(cmd, "251") == 0) ||
	    (strcmp(cmd, "252") == 0) ||
	    (strcmp(cmd, "253") == 0) || /* unknown connection(s) */
	    (strcmp(cmd, "254") == 0) ||
	    (strcmp(cmd, "255") == 0) ||
	    (strcmp(cmd, "265") == 0) ||
	    (strcmp(cmd, "266") == 0) ||
	    (strcmp(cmd, "250") == 0) ||
	    (strcmp(cmd, "332") == 0) ||
	    (strcmp(cmd, "333") == 0) ||
	    (strcmp(cmd, "375") == 0) ||
	    (strcmp(cmd, "372") == 0) ||
	    (strcmp(cmd, "376") == 0) ||
	    (strcmp(cmd, "396") == 0) ||
	    (strcmp(cmd, "366") == 0) ||
	    (strcmp(cmd, "MODE") == 0) ||
	    (strcmp(cmd, "TOPIC") == 0) ||
	    (strcmp(cmd, "NOTICE") == 0)) {
		return;
	} else if (strcmp(cmd, "432") == 0) {	/* Erroneous Nickname */
		fatal("%d: %s", fd, backup);
	} else if (strcmp(cmd, "433") == 0) {	/* Nickname already in use */
		split(&buf, ' ');
		nick = split(&buf, ' ');
		strcat(nick, "_");
		fdtodata[fd]->suffix++;
		if (strlen(nick) > NICK_LEN) {
			warnf("%s: nickname too long", nick);
			snprintf(msg, sizeof(msg), "QUIT :%s: nickname too long\r\n", nick);
			if (fdtodata[fd]->user == NULL)
				net_del(netid, msg, FALSE);
			else
				fd_del(fd, msg, FALSE);
			return;
		} else {
			snprintf(msg, sizeof(msg), "NICK %s\r\n", nick);
			writeall(fd, msg);
		}
		return;
	} else if (strcmp(cmd, "001") == 0) {
		snprintf(msg, sizeof(msg), "JOIN %s\r\n", networks[netid].chan);
		writeall(fd, msg);
		return;
	} else if (strcmp(cmd, "PRIVMSG") == 0) {
		char privmsg[BUFSIZE] = "";
		int *fds;

		if (fdtodata[fd]->user == NULL) {	/* if watcher */
			nick_add_symb(nick, netid);
			if ((fds = htsearch(usertofds, nick)) == NULL)
				return;

			split(&buf, ':');	/* set buf to msg */
			for (i = 0; i < netlen; i++) {
				printf("%s\n", buf);
				if (fds[i] > 0) {
					privmsg_update(privmsg, buf, netid, fds[i]);
					snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", networks[i].chan, privmsg);
					writeall(fds[i], msg);
				}
			}
		} else {
			char *netsymb;
			char *user = split(&buf, ' ');

			/* ignore messages from channel (it is handled by watcher) */
			if (user[0] == '#' || user[0] == '&')
				return;

			nick_add_symb(nick, netid);
			if ((fds = htsearch(usertofds, nick)) == NULL)
				return;

			/* split user nick and network symbol */
			*strrchr(user, ']') = '\0';
			netsymb = strrchr(user, '[');
			*netsymb++ = '\0';

			/* get the network index */
			for (i = 0; i < netlen; i++) {
				if (strcmp(netsymb, networks[i].symb) == 0)
					break;
			}

			split(&buf, ':');	/* set buf to msg */
			privmsg_update(privmsg, buf, netid, fds[i]);
			snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", user, privmsg);
			writeall(fds[i], msg);
		}
		return;
	}
	else if (strcmp(cmd, "353") == 0) {
		fdtodata[fd]->ready = TRUE;
		/* FALLBACK */
	}

	/* from now, the messages are handled by watcher */
	if (fdtodata[fd]->user != NULL)	/* if clone */
		return;

	if (strcmp(cmd, "353") == 0) {
		char *nick;
		split(&buf, ':');
		state = RESET;
		ntime = 0;

		/* then add all new users */
		while (*(nick = split(&buf, ' ')) != '\0') {
			if (*nick == '@' ||
			    *nick == '&' ||
			    *nick == '~' ||
			    *nick == '%' ||
			    *nick == '+' ||
			    *nick == '\\')
				nick++;
			if (strcmp(nick, wnick) != 0)
				user_add(nick, netid, FALSE);
		}
		return;
	} else if (strcmp(cmd, "JOIN") == 0) {
		/* if real user */
		if ((strcmp(nick, wnick) != 0) &&
		    (clone_get_fds(nick, netid) == NULL)) {
			if (state != IDLE)
				warnf("%s: ignored (network cloning)", nick);
			else
				user_add(nick, netid, TRUE);
		}
		return;
	} else if ((strcmp(cmd, "QUIT") == 0) || (strcmp(cmd, "PART") == 0)) {
		split(&buf, ':'); /* ignore ':' and assign QUIT/PART msg to buf */
		snprintf(msg, sizeof(msg), "QUIT :%s\r\n", buf);
		nick_add_symb(nick, netid);
		if (htsearch(usertofds, nick) != NULL)
			user_del(nick, msg);
		return;
	} else if (strcmp(cmd, "NICK") == 0) {
		int *fds, i;
		char *newnick;

		nick_add_symb(nick, netid);
		if ((fds = htsearch(usertofds, nick)) == NULL)
			return;

		/* set buf to new nick */
		split(&buf, ':');
		/* allocate a new nick with net symbol and replace the old one */
		newnick = emalloc((strlen(buf) + strlen(networks[netid].symb) + 2 + 1) * sizeof(char));
		sprintf(newnick, "%s[%s]", buf, networks[netid].symb);
		snprintf(msg, sizeof(msg), "NICK %s\r\n", newnick);
		for (i = 0; i < netlen; i++) {
			if (fds[i] > 0) {
				fdtodata[fds[i]]->user = newnick;
				fdtodata[fds[i]]->suffix = 0;
				writeall(fds[i], msg);
			} else if (fds[i] == -3) {
				reconn_list_del(nick, i);
				reconn_list_add(newnick, i);
			}
		}
		htmodkey(usertofds, nick, newnick);
		return;
	} else if (strcmp(cmd, "KICK") == 0) {
		/* :<user_who_kicked>!~username@host KICK <channel> <user_who_is_being_kicked> :<kick_msg> */
		int *fds;
		char *user;

		split(&buf, ' ');		/* channel name				*/
		user = split(&buf, ' ');	/* user who is being kicked		*/
		split(&buf, ':');		/* ignore ':' and store reason to buf	*/

		/* if watcher is being kicked, delete the network */
		if (strcmp(user, wnick) == 0) {
			snprintf(msg, sizeof(msg), "QUIT :netdel: %s (%s is kicked by %s)\r\n",
					networks[netid].name, wnick, nick);
			fd_reconn(fd, msg);
			return;
		}

		/* if message is from a real user, delete the user */
		if ((fds = clone_get_fds(user, netid)) == NULL) {
			snprintf(msg, sizeof(msg), "QUIT :userdel: %s (kicked by %s)\r\n", user, nick);
			nick_add_symb(user, netid);
			user_del(user, msg);
			return;
		}

		/* delete the kicked clone */
		snprintf(msg, sizeof(msg), "QUIT :kicked by %s\r\n", nick);
		fd_reconn(fds[netid], msg);

		/* get the original user netid */
		for (i = 0; i < netlen; i++) {
			if (fds[i] == -1)
				break;
		}
		/* send notice in the channel through watcher */
		if (networks[i].fd > 0) {
			snprintf(msg, sizeof(msg), "PRIVMSG %s :%s is kicked by %s [%s]\r\n",
					networks[i].chan, user, nick, buf);
			writeall(networks[i].fd, msg);
		}
		return;
	}
	warnf("%d: %s", fd, backup);
	return;
}

void
net_add(char *name, char *symb, char *host, char *port, char *chan)
{
	struct network *n;
	int i;

	for (i = 0; i < netlen; i++) {
		if (strcmp(networks[i].name, name) == 0) {
			warnf("%s: network name already exists", name);
			return;
		}
		if (strcmp(networks[i].symb, symb) == 0) {
			warnf("%s: network symbol already exists", symb);
			return;
		}
		if ((strcmp(networks[i].host, host) == 0) &&
		    (strcmp(networks[i].port, port) == 0) &&
		    (strcmp(networks[i].chan, chan) == 0)) {
			warnf("%s:%s%s: network configuration already exists",
			    host, port, chan);
			return;
		}
	}

	/* if full, resize the network and user fds */
	if (netlen == netcap) {
		struct htiter it;
		htiter_init(&it);

		networks = erealloc(networks, (size_t)(netcap + NET_ADDEND) *
				sizeof(struct network));
		while (htiterate(usertofds, &it)) {
			it.node->val = erealloc(it.node->val,
					(size_t)(netcap + NET_ADDEND) *
					sizeof(int));
			/* zero out the extended array */
			for (i = netcap; i < netcap + NET_ADDEND; i++)
				((int *)it.node->val)[i] = 0;
		}
		netcap += NET_ADDEND;
	}

	/* add a network */
	n = &networks[netlen];
	n->name = strdup(name);
	n->symb = strdup(symb);
	n->host = strdup(host);
	n->port = strdup(port);
	n->chan = strdup(chan);
	n->fd	= fd_new(netlen, NULL);
	netlen++;
}

void
net_del(int netid, char *msg, int reconnect)
{
	int *fds;
	char *user;
	struct network *n;
	struct htiter lit, it;	/* last, current iterator */

	n = &networks[netid];
	htiter_init(&it);
	htiter_init(&lit);
	while (htiterate(usertofds, &it)) {
		user = (char *)it.node->key;
		fds = (int *)it.node->val;
		if (fds[netid] == -1) {
			user_del(it.node->key, msg);
			it = lit; /* this node is deleted */
		} else {
			if (fds[netid] > 0)
				fd_del(fds[netid], msg, FALSE);
			else if (fds[netid] == -3)
				reconn_list_del(user, netid);

			if (!reconnect) {
				if(netid != netlen-1) {
					fds[netid] = fds[netlen-1];
					if (fds[netid] > 0) {
						fdtodata[fds[netid]]->netid = netid;
					} else if (fds[netid] == -3) {
						reconn_list_del(user, netlen-1);
						reconn_list_add(user, netid);
					}
				}
				fds[netlen-1] = 0;
			} else {
				fds[netid] = 0;
			}
		}
		lit = it;
	}

	if (n->fd > 0)
		fd_del(n->fd, msg, reconnect);
	else if (n->fd == -3)
		reconn_list_del(NULL, netid);

	if (!reconnect) {
		free(n->name);
		free(n->symb);
		free(n->host);
		free(n->port);
		free(n->chan);
		/* swap */
		if(netid != netlen-1) {
			networks[netid] = networks[netlen-1];
			if (networks[netid].fd > 0) {
				fdtodata[n->fd]->netid = netid;
			} else if (networks[netid].fd == -3) {
				reconn_list_del(NULL, netlen-1);
				reconn_list_add(NULL, netid);
			}
		}
		netlen--;
	}
}

void
user_add(char *unick, int netid, int clone)
{
	size_t	 len;
	char	*nick;
	int	*fds;

	len = strlen(unick) + strlen(networks[netid].symb) + 2 + 1;
	if (len-1 > NICK_LEN) {
		warnf("%s[%s]: nickname too long", unick, networks[netid].symb);
		return;
	}

	/* resize hash table if storage is low */
	if ((usertofds->cap - usertofds->len) < USER_ADDEND)
		usertofds = htresize(usertofds, usertofds->cap + USER_ADDEND);

	/* allocate a new user */
	nick = emalloc(len * sizeof(char));
	fds = ecalloc((size_t)netcap, sizeof(int));
	sprintf(nick, "%s[%s]", unick, networks[netid].symb);
	fds[netid] = -1;

	if (clone) {
		int i;
		for (i = 0; i < netlen; i++) {
			if (fds[i] == 0 && networks[i].fd > 0 &&
					fdtodata[networks[i].fd]->ready)
				fds[i] = fd_new(i, nick);
		}
	}

	if (htinsert(usertofds, nick, fds) == -1)
		fatal("%s: user already exists", nick);
}

void
user_del(char *user, char *msg)
{
	int i, *fds;

	if ((fds = htsearch(usertofds, user)) == NULL)
		fatal("%s: user doesn't exist", user);
	for (i = 0; i < netlen; i++) {
		if (fds[i] > 0) {
			fd_del(fds[i], msg, FALSE);
		} else if (fds[i] == -3) {
			reconn_list_del(user, i);
		}
	}
	htremove(usertofds, user);
}

void
reconn_list_add(char *user, int netid)
{
	struct fd_ref *node;

	node = emalloc(sizeof(struct fd_ref));
	node->user = user;
	node->netid = netid;
	node->next = NULL;
	if (reconn_list_tail == NULL) {
		reconn_list_head = reconn_list_tail = node;
	} else {
		reconn_list_tail->next = node;
		reconn_list_tail = reconn_list_tail->next;
	}
}

void
reconn_list_del(char *user, int netid)
{
	struct fd_ref *n, *pn;	/* current and previous node */

	n = reconn_list_head;
	pn = NULL;
	while (n != NULL) {
		if (n->netid == netid &&
		    ((user == NULL && n->user == NULL) ||
		     (user != NULL && n->user != NULL &&
		      (strcmp(n->user, user) == 0)))) {
			if (n == reconn_list_head && n == reconn_list_tail)
				reconn_list_head = reconn_list_tail = NULL;
			else if (n == reconn_list_head)
				reconn_list_head = n->next;
			else if (n == reconn_list_tail)
				reconn_list_tail = pn;
			else
				pn->next = n->next;
			free(n);
			return;
		}
		pn = n;
		n = n->next;
	}
	fatal("%d:%s: failed to find in re-connection list", netid, user);
}

int
fd_new(int netid, char *user)
{
	int fd;
	struct network *n;
	struct fd_data *data;

	n = &networks[netid];
	fd = dial(n->host, n->port);
	if (fd == -1) {
		warnf("%s:%s: failed to connect", n->host, n->port);
		return -2;
	}
	fd_register(fd, EV_WRITE);

	if (user == NULL)
		debug(1, "%d: netadd: %s[%s]", fd, n->name, n->symb);
	else
		debug(1, "%d: add[%s]: %s", fd, n->symb, user);

	if (fd+1 > fdcap) {
		fdcap *= 2;
		fdtodata = erealloc(fdtodata, (size_t)fdcap * sizeof(struct fd_data *));
	}

	data = emalloc(1 * sizeof(struct fd_data));
	data->netid = netid;
	data->user = user;
	data->suffix = 0;
	data->ready = FALSE;

	fdtodata[fd] = data;

	return fd;
}

void
fd_del(int fd, char *msg, int reconnection)
{
	char	*user;
	int	 netid;
	int	*fds;

	user = fdtodata[fd]->user;
	netid = fdtodata[fd]->netid;

	if (user == NULL) {
		debug(1, "%d: netdel: %s[%s]", fd, networks[netid].name, networks[netid].symb);
		networks[netid].fd = reconnection ? -3 : -2;
	} else {
		debug(1, "%d: del[%s]: %s", fd, networks[netid].symb, user);
		if ((fds = htsearch(usertofds, user)) == NULL)
			fatal("%s: user doesn't exist", user);
		fds[netid] = reconnection ? -3 : -2;
	}

	if (fdtodata[fd]->ready)
		writeall(fd, msg);
	close(fd);

	if (reconnection)
		reconn_list_add(user, netid);
	free(fdtodata[fd]);
	fdtodata[fd] = NULL;
	break_evloop = TRUE;
}

void
fd_reconn(int fd, char *msg)
{
	if (fdtodata[fd]->user == NULL)
		net_del(fdtodata[fd]->netid, msg, TRUE);
	else
		fd_del(fd, msg, TRUE);
}

int *
clone_get_fds(char *nick, int netid)
{
	unsigned int s;
	int *fds = NULL;

	/* count suffix */
	for (s = 0; nick[strlen(nick)-s-1] == '_'; s++);
	/* remove suffix */
	if (s > 0)
		nick[strlen(nick)-s] = '\0';

	fds = htsearch(usertofds, nick);
	/* if match but suffix doesn't match */
	if ((fds != NULL) && (fdtodata[fds[netid]]->suffix != (int)s))
		fds = NULL;

	/* add suffix back */
	if (s > 0)
		nick[strlen(nick)] = '_';

	return fds;
}

void
nick_add_symb(char *nick, int netid)
{
	strcat(nick, "[");
	strcat(nick, networks[netid].symb);
	strcat(nick, "]");
}

/*
 * trim all the nicknames to original nick
 * src will be destructed
 */
void
privmsg_update(char *dst, char *src, int netid, int fd)
{
	char d;		/* delimiter */
	char *n;
	int i, *fds;

	while (src != NULL) {
		n = strpbrk(src, " :;,<>@&~%+\\");
		if (n == NULL) {
			d = '\0';
		} else {
			d = *n;
			*n = '\0';
			n++;
		}

		/* check if the word is nick */
		if ((fds = clone_get_fds(src, netid)) != NULL) {
			int netid2 = fdtodata[fd]->netid;
			*strrchr(src, '[') = '\0';
			src[strlen(src)] = '[';
			if (fds[netid2] > 0) {
				nick_add_symb(dst, netid2);
				for (i = 0; i < fdtodata[fds[netid2]]->suffix; i++)
					strcat(dst, "_");
			}
		} else {
			strcat(dst, src);
		}

		strncat(dst, &d, 1);
		src = n;
	}
}

void
print_table(void)
{
	int	i, *fds, diff, tabs;
	char	*nick;
	struct htiter it;

	if (netlen == 0)
		return;

	print_border();
	/* print networks */
	printf("Networks\t\t");
	for (i = 0; i < netlen; i++) {
		printf("%s->%d", networks[i].symb, networks[i].fd);
		/* print suffix */
		if (networks[i].fd > 0 && fdtodata[networks[i].fd]->suffix > 0)
			printf("(%d)\t", fdtodata[networks[i].fd]->suffix);
		else
			printf("\t\t");
	}
	printf("\n");

	htiter_init(&it);
	while (htiterate(usertofds, &it)) {
		fds  = (int *)it.node->val;
		nick = (char *)it.node->key;
		/* print tabbed user nick */
		printf("%s", nick);
		diff = 24 - (int)strlen(nick);
		tabs = ((diff / 8) + (diff % 8 > 0));
		printf("%.*s", tabs, "\t\t\t");
		/* print tabbed fds */
		for (i = 0; i < netlen; i++) {
			printf("%d", fds[i]);
			/* print suffix */
			if ((fds[i] > 0) && (fdtodata[fds[i]]->suffix > 0))
				printf("(%d)", fdtodata[fds[i]]->suffix);
			printf("\t\t");
		}
		printf("\n");
	}
	print_border();
}

void
print_htable(void)
{
	struct htiter it;
	int index = -1;

	print_border();
	htiter_init(&it);
	while (htiterate(usertofds, &it)) {
		if (index != (int)it.index) {
			/* ignore first new line */
			if (index != -1)
				printf("\n");
			printf("%d", it.index-1);
			index = (int)it.index;
		}
		printf(" -> %s", (char *)it.node->key);
	}
	printf("\n");
	print_border();
}

void
print_users(void)
{
	struct htiter it;
	int i = 0;

	print_border();
	htiter_init(&it);
	while (htiterate(usertofds, &it))
		printf("%d: %s\n", i++, (char *)it.node->key);
	print_border();
}

void
print_reconn_list(void)
{
	struct fd_ref *n;

	print_border();
	n = reconn_list_head;
	while(n != NULL) {
		printf("%d: %s\n", n->netid, n->user);
		n = n->next;
	}
	print_border();
}

void
print_border(void)
{
	int i;
	for (i = 0; i < 64; i++)
		printf("-");
	printf("\n");
}

ssize_t
writeall(int fd, char *buf)
{
	ssize_t left, sent, n;
	left = (ssize_t)strlen(buf);
	sent = 0;
	while (sent < left) {
		if ((n = write(fd, buf+sent, (size_t)left)) == -1) {
			warnf("%d: write:", fd);
			snprintf(msg, sizeof(msg), "QUIT :write failed\r\n");
			fd_reconn(fd, msg);
			return -1;
		}
		sent += n;
		left -= n;
	}
	return sent;
}