wm: ticl

ref: 202447793107a549c7e418dc3b2208caa89a0bb3
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>
char *strlcpy(char *dst, const char *src, size_t n)
{
	return strncpy(dst, src, n);
}
#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 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,
	DONE
};

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

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 vuser	**fdtovuser;	/* fd -> vuser	pointer	*/
static int		  fdcap;	/* fdtovuser capacity	*/
static struct htable	 *usertofds;	/* user -> array of fds of clones
					indexed according to networks array */

/* 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 *);
void	 net_del_raw(int);
void	 user_add(char *, int, int);
void	 user_del(char *, char *);
int	 vuser_add(int, char *);
void	 vuser_del(int, char *);
int	*vuser_get_fds(char *, int);
void	 nick_add_symb(char *, int);
void	 privmsg_update(char *, char *, int);
void	 print_table(void);
void	 print_htable(void);
void	 print_users(void);
void	 print_border(void);

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, *fds, nev;	/* no. of returned events to handle */
	time_t	timeout;
	struct htiter it;

	/* 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));
	fdtovuser = emalloc((size_t)fdcap * sizeof(struct vuser *));
	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 != DONE) {
		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);
				if (break_evloop) {
					break_evloop = FALSE;
					break;
				}
			} else {
				fatal("unknown event");
			}
		}
	}

	/* cleanup */
	snprintf(msg, sizeof(msg), "QUIT :relay shutting down\r\n");
	
	/* delete and free all the users with their fds */
	htiter_init(&it);
	while(htiterate(usertofds, &it)) {
		fds = (int *)it.node->val;
		for (i = 0; i < netlen; i++) {
			if (fds[i] > 0)
				vuser_del(fds[i], msg);
		}
	}
	htdestroy(usertofds);

	/* delete and free all the networks with their fd */
	for (i = 0; i < netlen; i++) {
		vuser_del(networks[i].fd, msg);
		net_del_raw(i);
	}
	free(networks);

	free(fdtovuser);
	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 :unlinking %s\r\n", networks[i].name);
					net_del(i, msg);
					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, "exit") == 0) {
		state = DONE;
	} 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 == RESET) {
		state = CLONING;
		htiter_init(&it);
	}
	log1(".");

	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 && fdtovuser[networks[i].fd]->ready)
				fds[i] = vuser_add(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 (fdtovuser[fd]->user == NULL) {	/* linker */
		snprintf(msg, sizeof(msg), "NICK linker\r\n");
		writeall(fd, msg);
		snprintf(msg, sizeof(msg), "USER linker 0 * :linker\r\n");
		writeall(fd, msg);
	} else {			/* user */
		snprintf(msg, sizeof(msg), "NICK %s\r\n", fdtovuser[fd]->user);
		writeall(fd, msg);
		snprintf(msg, sizeof(msg), "USER user 0 * :user\r\n");
		writeall(fd, msg);
	}
}

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

	netid = fdtovuser[fd]->netid;

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

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

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

	/* first column */
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "NOTICE") == 0) {	/* ignore */
		return;
	} else if (strcmp(cmd, "ERROR") == 0) {
		fatal("%d: %s", fd, backup);
	} 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, "003") == 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, "_");
		fdtovuser[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);
			goto del;
		} 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 (fdtovuser[fd]->user == NULL) {	/* if linker */
			nick_add_symb(nick, netid);
			if ((fds = htsearch(usertofds, nick)) == NULL)
				return;

			split(&buf, ':');	/* set buf to msg */
			privmsg_update(privmsg, buf, netid);
			for (i = 0; i < netlen; i++) {
				if (fds[i] > 0) {
					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 linker) */
			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);
			snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", user, privmsg);
			writeall(fds[i], msg);
		}
		return;
	}
	else if (strcmp(cmd, "353") == 0) {
		fdtovuser[fd]->ready = TRUE;
		/* FALLBACK */
	}
	/* these messages are handled by linker */
	if (fdtovuser[fd]->user != NULL) {	/* if clone */
		if ((strcmp(cmd, "353") == 0) ||
		    (strcmp(cmd, "JOIN") == 0) ||
		    (strcmp(cmd, "QUIT") == 0) ||
		    (strcmp(cmd, "PART") == 0) ||
		    (strcmp(cmd, "KICK") == 0) ||
		    (strcmp(cmd, "NICK") == 0)) {
			return;
		}
	} else 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, lnick) != 0)
				user_add(nick, netid, FALSE);
		}
		return;
	} else if (strcmp(cmd, "JOIN") == 0) {
		/* if real user */
		if ((strcmp(nick, lnick) != 0) &&
		    (vuser_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);
			break_evloop = TRUE;
			return;
		}
		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);
		htmodkey(usertofds, nick, newnick);
		snprintf(msg, sizeof(msg), "NICK %s\r\n", newnick);
		for (i = 0; i < netlen; i++) {
			if (fds[i] > 0) {
				fdtovuser[fds[i]]->user = newnick;
				writeall(fds[i], msg);
			}
		}
		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 linker is being kicked, delete the network */
		if (strcmp(user, lnick) == 0) {
			snprintf(msg, sizeof(msg), "QUIT :netdel: %s (%s is kicked by %s)\r\n",
					networks[netid].name, lnick, nick);
			net_del(netid, msg);
			break_evloop = TRUE;
			return;
		}

		/* if message is from a real user, delete the user */
		if ((fds = vuser_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);
			break_evloop = TRUE;
			return;
		}

		/* delete the kicked clone */
		snprintf(msg, sizeof(msg), "QUIT :kicked by %s\r\n", nick);
		vuser_del(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 linker */
		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);
		break_evloop = TRUE;
		return;
	}
	warnf("%d: %s", fd, backup);
	return;
del:
	if (fdtovuser[fd]->user == NULL)
		net_del(netid, msg);
	else
		vuser_del(fd, msg);
	break_evloop = TRUE;
	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	= vuser_add(netlen, NULL);
	netlen++;
}

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

	htiter_init(&it);
	htiter_init(&lit);
	while (htiterate(usertofds, &it)) {
		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)
				vuser_del(fds[netid], msg);
			fds[netid] = fds[netlen-1];
			if (fds[netid] > 0)
				fdtovuser[fds[netid]]->netid = netid;
			fds[netlen-1] = 0;
		}
		lit = it;
	}

	vuser_del(networks[netid].fd, msg);
	net_del_raw(netid);
	networks[netid] = networks[netlen-1];
	if (networks[netid].fd > 0)
		fdtovuser[networks[netid].fd]->netid = netid;
	netlen--;
}

void
net_del_raw(int netid)
{
	struct network *n = &networks[netid];
	free(n->name);
	free(n->symb);
	free(n->host);
	free(n->port);
	free(n->chan);
}

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 && fdtovuser[networks[i].fd]->ready)
				fds[i] = vuser_add(i, nick);
		}
	}

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

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

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

int
vuser_add(int netid, char *user)
{
	int fd;
	struct network *n;
	struct vuser *vuser;

	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)
		log1("%d: netadd: %s[%s]", fd, n->name, n->symb);
	else
		log1("%d: add[%s]: %s", fd, n->symb, user);

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

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

	fdtovuser[fd] = vuser;

	return fd;
}

void
vuser_del(int fd, char *msg)
{
	char *user = fdtovuser[fd]->user;
	int netid = fdtovuser[fd]->netid;
	int *fds;

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

	if (fdtovuser[fd]->ready)
		writeall(fd, msg);
	close(fd);
	free(fdtovuser[fd]);
	fdtovuser[fd] = NULL;
}

int *
vuser_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) && (fdtovuser[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)
{
	char d;		/* delimiter */
	char *n;

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

		/* check if the word is nick */
		if (vuser_get_fds(src, netid) != NULL)
			*strrchr(src, '[') = '\0';

		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 (fdtovuser[networks[i].fd]->suffix > 0)
			printf("(%d)\t", fdtovuser[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) && (fdtovuser[fds[i]]->suffix > 0))
				printf("(%d)", fdtovuser[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_border(void)
{
	int i;
	for (i = 0; i < 64; i++)
		printf("-");
	printf("\n");
}