wm: ticl

ref: 7b4e86b59d7f8a4f6304a2312a4c687fb06677e4
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>
#include <bsd/err.h>
#include <bsd/stdlib.h>
#include <bsd/string.h>
#else
#include <sys/event.h>
#endif

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

#define CLONE_COOLDOWN	1
#define CLONE_ADDEND	10
/* #define PING_TIMEOUT	240 */

#define EVENT_ADDEND	100
#define NET_ADDEND	10
#define USER_ADDEND	100

#define BUFSIZE		1024
#define NICK_LEN	16
#define MAX_EVENTS	10

#define EV_READ 1
#define EV_WRITE 2

enum {
	IDLE = 0,
	RESET,
	CLONING
};

struct network {
	int	 id;	/* event index	*/
	char	*name;	/* name		*/
	char	*symb;	/* symbol	*/
	char	*host;	/* host		*/
	char	*port;	/* port		*/
	char	*chan;	/* channel	*/
	int	 ready;	/* joined	*/
};

struct event {
	int	 fd;		/* event fd	*/
	int	 netid;		/* net index	*/
	char	*user;		/* user nick	*/
	int	 suffix;	/* suffix count	*/
	time_t	 time;		/* last response */
	int	 ready;		/* joined	*/
};


static int	 debug;
static int	 done;			/* program state  */
static int	 fifofd;		/* fifo fd	  */
static char	*fifopath;		/* fifo path	  */
static char	 msg [BUFSIZE];		/* message buffer */
static int	 timeout_id = -1;

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

static int	*fdtoid;		/* fd to event id  */
static int	 fdslen;		/* maximum fd	   */

static struct event	*events;	/* events array	   */
static int		 evslen;	/* array length	   */
static int		 evscap;	/* array capacity  */

static struct network	*networks;	/* networks array  */
static int		 netlen;	/* array length	   */
static int		 netcap;	/* array capacity  */
static int		 state = IDLE;

/*
 * hash table of users
 * key   -> <user_nick> + '[' + <network_symbol> + ']'
 * value -> array of all user's clones event ids indexed
 *	    corresponding to its connected network
 */
static struct Htable	*users;

/* functions prototype */

void	 fd_register(int, int);
void	 fifo_read(void);
time_t	 event_timeout(void);
void	 event_write(int);
int	 event_read(int);
void	 net_add(char *, char *, char *, char *, char *);
void	 net_del(char *);
void	 net_del_raw(int);
void	 net_users_add(int, char *);
void	 user_add(char *, int, int);
void	 user_del(char *, char *);
int	*user_event_ids(char *, int);
int	 clone_add(char *, int);
int	 event_add(int, int, char *);
void	 event_del(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 [MAX_EVENTS];	/* maximum event to handle */
#else
	struct kevent	kevs [MAX_EVENTS];	/* maximum event to handle */
	struct timespec	tmsp;
#endif
	int	i, r, fd, id, nev;
	time_t	timeout;
	time_t	ntime;		/* next timeout time */

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

	/* check arguments */
	if (argc != 2) {
		printf("usage: %s fifo\n", getprogname());
		return 0;
	} else {
		fifopath = argv[1];
	}

	/* init global variables */
	fdslen = evscap = EVENT_ADDEND;
	netcap = NET_ADDEND;
	fdtoid = ecalloc((size_t)fdslen, sizeof(int));
	events = ecalloc((size_t)evscap, sizeof(struct event));
	networks = ecalloc((size_t)netcap, sizeof(struct network));
	users = htcreate((KeyLenFn *)strlen, (KeyCmpFn *)strcmp, free, free,
			 USER_ADDEND);

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

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

		if (debug) {
			printf("fds:");
			for (i = 0; i < nev; i++) {
				printf(" %d", epevs[i].data.fd);
			}
			printf("\n");
		}

		for (i = 0; i < nev; i++) {
#ifdef __gnu_linux__
			fd = epevs[i].data.fd;
#else
			fd = (int)kevs[i].ident;
#endif
			id = fdtoid[fd];
			if (fd == fifofd) {
				fifo_read();
#ifdef __gnu_linux__
			} else if (epevs[i].events & EPOLLOUT) {
#else
			} else if (kevs[i].filter == EVFILT_WRITE) {
#endif
				event_write(id);
#ifdef __gnu_linux__
			} else if (epevs[i].events & EPOLLIN) {
#else
			} else if (kevs[i].filter == EVFILT_READ) {
#endif
				r = event_read(id);
				if (r == 1) {
					timeout_id = -1;
					ntime = 0;
				} else if (r == -2) {
					break;
				}
			} else {
				errx(1, "unknown event");
			}
		}
	}

	/*
	 * cleanup
	 */
	snprintf(msg, sizeof(msg), "QUIT :linker shutting down\r\n");
	for (i = 0; i < evslen; i++) {
		writeall(events[i].fd, msg);
		close(events[i].fd);
	}

	free(fdtoid);
	free(events);

	/* delete all the networks */
	for (i = 0; i < netlen; i++)
		net_del_raw(i);
	free(networks);

	/* delete all the users */
	htdestroy(users);

	return 0;
}

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

	if (type == EV_READ)
		ev.events = EPOLLIN;
	else if (type == EV_WRITE)
		ev.events = EPOLLOUT;
	else
		errx(1, "unkown event type");

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

	if (type == EV_READ)
		filter = EVFILT_READ;
	else if (type == EV_WRITE)
		filter = EVFILT_WRITE;
	else
		errx(1, "unkown event type");

	EV_SET(&ev, fd, filter, EV_ADD, 0, 0, 0);
	if (kevent(kqfd, &ev, 1, NULL, 0, NULL) == -1)
		err(1, "kevent");
#endif
	/* printf("added fd: %d\n", fd); */
}

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

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

	if (!*buffer)
		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)
			printf("usage: netadd <name> <symbol> <host> <port> <channel>\n");
		else
			net_add(name, symb, host, port, chan);
	} else if (strcmp(cmd, "netdel") == 0) {
		char *name = buf;
		if (!*name)
			printf("usage: netdel <name>\n");
		else
			net_del(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) {
		done = TRUE;
	} else {
		warnx("%s is not a command", cmd);
	}
}

time_t
event_timeout(void)
{
	static Htiter it = {0};

	int i, j, *ids;
	char *nick;
	struct network *n;
	time_t ntime;

	/* if (timeout_id != -1) {
		printf("%d: PING\n", events[timeout_id].fd);
		snprintf(msg, sizeof(msg), "PING %s\r\n",
			 networks[events[timeout_id].netid].host);
		writeall(events[timeout_id].fd, msg);
		events[timeout_id].time = time(NULL);
	} else { */
		if (state == RESET) {
			state = CLONING;
			it.index = 0;
			it.node = NULL;
		}
		printf("Adding %d bot\n", CLONE_ADDEND);

		j = 0;
		while (htiterate(users, &it)) {
			nick = (char *)it.node->key;
			ids = (int *)it.node->val;
			for (i = 0; i < netlen; i++) {
				n = &networks[i];
				if (!n->ready || ids[i] != 0)
					continue;
				ids[i] = clone_add(nick, i);
			}
			if (++j >= CLONE_ADDEND)
				goto calculate;
		}
		state = IDLE;
		it.index = 0;
		it.node = NULL;
	/* } */

calculate:
	ntime = -1;
	timeout_id = -1;

	if (state == CLONING)
		ntime = time(NULL) + CLONE_COOLDOWN;

	/* for(i = 0; i < evslen; i++) {
		if ((events[i].time != 0)
		    && ((ntime == -1) || (events[i].time + PING_TIMEOUT < ntime))) {
			ntime = events[i].time + PING_TIMEOUT;
			timeout_id = i;
		}
	} */
	return ntime;
}

void
event_write(int id)
{
	int fd = events[id].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)
		err(1, "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)
		err(1, "kevent");
	fd_register(fd, EV_READ);
#endif
	if (events[id].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", events[id].user);
		writeall(fd, msg);
		snprintf(msg, sizeof(msg), "USER user 0 * :user\r\n");
		writeall(fd, msg);
	}
	events[id].time = time(NULL);
}

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

	fd = events[id].fd;
	netid = events[id].netid;
	events[id].time = time(NULL);

	n = readline(fd, buffer, sizeof(buffer));
	if (n == -1) {
		warn("%d: read", fd);
		event_del(id);
		return 0;
	} else if (n == 0) {
		warnx("%d: connection closed", fd);
		event_del(id);
		return 0;
	}

	if (!*buffer)
		return 0;

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

	/* set linker nick */
	strlcpy(lnick, "linker", sizeof(lnick));
	for (i = 0; i < events[id].suffix; i++)
		strcat(lnick, "_");

	/* first column */
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "NOTICE") == 0) {
		return 0;
	} else if (strcmp(cmd, "ERROR") == 0) {
		goto printbuffer;
	} else if (strcmp(cmd, "PING") == 0) {
		snprintf(msg, sizeof(msg), "PONG %s\r\n", buf);
		writeall(fd, msg);
		return 0;
	}
	/* 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 0;
	} else if (strcmp(cmd, "433") == 0) {	/* Nickname already in use */
		split(&buf, ' ');
		nick = split(&buf, ' ');
		strcat(nick, "_");
		events[id].suffix++;
		if (strlen(nick) > NICK_LEN) {
			warnx("nick '%s' is too big", nick);
			if (strcmp(nick, lnick) == 0) {
				net_del(networks[netid].name);
			} else {
				snprintf(msg, sizeof(msg), "QUIT :nick is too big\r\n");
				user_del(nick, msg);
			}
		} else {
			snprintf(msg, sizeof(msg), "NICK %s\r\n", nick);
			writeall(fd, msg);
		}
		return 0;
	} else if (strcmp(cmd, "001") == 0) {
		snprintf(msg, sizeof(msg), "JOIN %s\r\n", networks[netid].chan);
		writeall(fd, msg);
		return 0;
	} else if (strcmp(cmd, "PRIVMSG") == 0) {
		char privmsg [BUFSIZE] = "";
		int *ids;

		if (events[id].user == NULL) {	/* if linker */
			nick_add_symb(nick, netid);
			if ((ids = htsearch(users, nick)) == NULL)
				return 0;

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

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

			nick_add_symb(nick, netid);
			if ((ids = htsearch(users, nick)) == NULL)
				return 0;

			/* 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(events[ids[i]].fd, msg);
		}
		return 0;
	}
	/* these messages are handled by linker */
	if (events[id].user != NULL) {	/* if clone */
		if (strcmp(cmd, "353") == 0) {
			events[id].ready = TRUE;
			return 0;
		} else if ((strcmp(cmd, "JOIN") == 0)
		    || (strcmp(cmd, "QUIT") == 0)
		    || (strcmp(cmd, "PART") == 0)
		    || (strcmp(cmd, "KICK") == 0)
		    || (strcmp(cmd, "NICK") == 0)) {
			return 0;
		}
	} else if (strcmp(cmd, "353") == 0) {
		char *nick;
		split(&buf, ':');
		networks[netid].ready = TRUE;
		state = RESET;

		/* 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 1;
	} else if (strcmp(cmd, "JOIN") == 0) {
		if ((strcmp(nick, lnick) != 0)
		    && (user_event_ids(nick, netid) == NULL)) {	/* if not clone */
			if (state != IDLE)
				warn("ignoring user '%s' due to network cloning", nick);
			else
				user_add(nick, netid, TRUE);
		}
		return 0;
	} else if ((strcmp(cmd, "QUIT") == 0)
		   || (strcmp(cmd, "PART") == 0)) {
		snprintf(msg, sizeof(msg), "QUIT :%s\r\n", buf);
		nick_add_symb(nick, netid);
		if (htsearch(users, nick) != NULL)
			user_del(nick, msg);
		return -2;
	} else if (strcmp(cmd, "NICK") == 0) {
		int *ids, i;
		char *newnick;

		nick_add_symb(nick, netid);
		if ((ids = htsearch(users, nick)) == NULL)
			return 0;

		/* set buf to new nick */
		split(&buf, ':');
		/* allocate a newnick and append the netsym and then replace the old */
		newnick = ecalloc(strlen(buf) + strlen(networks[netid].symb) + 2 + 1, sizeof(char));
		sprintf(newnick, "%s[%s]", buf, networks[netid].symb);
		htsetkey(users, nick, newnick);

		snprintf(msg, sizeof(msg), "NICK %s\r\n", newnick);
		for (i = 0; i < netlen; i++) {
			events[ids[i]].user = newnick;
			if (ids[i] > 0)
				writeall(events[ids[i]].fd, msg);
		}
		return 0;
	} else if (strcmp(cmd, "KICK") == 0) {
		/* :<nick_which_is_kicking>!~user@host KICK <channel> <nick_which_has_been_kicked> :<kick_msg> */
		int *ids;
		char *chan = split(&buf, ' ');
		char *user = split(&buf, ' ');

		/* set the quit msg */
		snprintf(msg, sizeof(msg), "QUIT : kicked by %s\r\n", nick);

		/* delete whole network if it is the linker */
		if (strcmp(user, lnick) == 0) {
			net_del(networks[netid].name);
			return 0;
		}

		/* delete the user if the message from the same network */
		if ((ids = user_event_ids(user, netid)) == NULL) {
			nick_add_symb(user, netid);
			user_del(user, msg);
			return 0;
		}

		/* close the kicked fd */
		writeall(fd, msg);
		event_del(id);

		/*
		 * send notice in the channel through linker
		 */
		/* get the original user netid */
		for (i = 0; i < netlen; i++) {
			if (ids[i] == -1)
				break;
		}
		/* set buf to msg */
		split(&buf, ':');
		/* remove netsymb and suffix */
		*strrchr(user, '[') = '\0';
		/* send notice */
		snprintf(msg, sizeof(msg),
			"PRIVMSG %s :%s was kicked out by %s from network %s %s [%s]\r\n",
			networks[i].chan, user, nick, networks[netid].name, chan, buf);
		writeall(events[networks[i].id].fd, msg);
		return 0;
	}
printbuffer:
	printf("%d: %s\n", fd, backup);
	return 0;
}

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

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

	/* resize if full */
	if (netlen == netcap) {
		Htiter it = {0};
		networks = realloc0(networks, sizeof(struct network) * (size_t)netcap,
				     sizeof(struct network) * (size_t)(netcap + NET_ADDEND));
		while (htiterate(users, &it))
			it.node->val = realloc0(it.node->val, sizeof(int) * (size_t)netcap,
						sizeof(int) * (size_t)(netcap + NET_ADDEND));
		netcap += NET_ADDEND;
	}

	/* connect */
	if ((fd = dial(host, port)) == -1)
		return;
	/* add a network */
	n = &networks[netlen];
	n->id = event_add(fd, netlen, NULL);
	n->name = strdup(name);
	n->symb = strdup(symb);
	n->host = strdup(host);
	n->port = strdup(port);
	n->chan = strdup(chan);
	n->ready = FALSE;
	netlen++;

	printf("%d: network '%s' added\n", fd, name);
}

void
net_del(char *name)
{
	int	i, netid, *ids;

	Htiter	it	= {0};	/* current iterator */
	Htiter	lastit	= {0};	/* last iterator */

	/* get netid */
	netid = -1;
	for (i = 0; i < netlen; i++) {
		if (strcmp(name, networks[i].name) == 0) {
			netid = i;
			break;
		}
	}
	if (netid == -1) {
		warnx("network '%s' doesn't exist", name);
		return;
	}

	/* set the quit msg */
	snprintf(msg, sizeof(msg), "QUIT :unlinking network %s\r\n", name);

	/* reconstruct the user-clones table */
	while (htiterate(users, &it)) {
		ids = (int *)it.node->val;
		/* delete all the users of deleting network */
		if (ids[netid] == -1) {
			user_del(it.node->key, msg);
			/* this node is deleted */
			it = lastit;
		/* delete the clones */
		} else {
			if (ids[netid] > 0) {
				writeall(events[ids[netid]].fd, msg);
				event_del(ids[netid]);
			}
			/* swap last with current one */
			ids[netid] = ids[netlen-1];
		}
		lastit = it;
	}

	/* set last netid of events to current netid. */
	for (i = 0; i < evslen; i++) {
		if (events[i].netid == netlen-1)
			events[i].netid = netid;
	}

	writeall(events[networks[netid].id].fd, msg);
	event_del(networks[netid].id);
	net_del_raw(netid);
	printf("%d: network '%s' deleted\n", events[networks[netid].id].fd, name);
	/* swap the network with the last */
	networks[netid] = networks[netlen-1];
	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	*ids, i;

	len = strlen(unick) + strlen(networks[netid].symb) + 2 + 1;

	/* too long nick */
	if (len-1 > NICK_LEN) {
		warnx("nick '%s' is too big", unick);
		return;
	}

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

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

	if (debug)
		printf("useradd: %s\n", nick);

	if (clone) {
		/* clone the user on all other network */
		for (i = 0; i < netlen; i++) {
			if (networks[i].ready && i != netid)
				ids[i] = clone_add(nick, i);
		}
	}

	/* insert it to the users hash table */
	if (htinsert(users, nick, ids) == -1)
		/* this shouldn't happen as it was already checked */
		errx(1, "user '%s' already exists", nick);
}

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

	if ((ids = htsearch(users, nick)) == NULL)
		errx(1, "user '%s' 1 doesn't exists", nick);
	for (i = 0; i < netlen; i++) {
		if (ids[i] <= 0)
			continue;
		printf("%d: del[%s]: %s\n", events[ids[i]].fd,
		    networks[i].symb, nick);

		if (events[ids[i]].ready)
			writeall(events[ids[i]].fd, msg);
		event_del(ids[i]);
	}
	htremove(users, nick);
}

int *
user_event_ids(char *nick, int netid)
{
	unsigned int s;
	int *ids = NULL;

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

	ids = htsearch(users, nick);
	/* if match but suffix doesn't match */
	if ((ids != NULL) && (events[ids[netid]].suffix != (int)s))
		ids = NULL;

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

	return ids;
}

int
clone_add(char *nick, int netid)
{
	struct network	*n;
	int		 fd;

	n = &networks[netid];
	if ((fd = dial(n->host, n->port)) == -1) {
		warn("enable to connect to %s\n", n->host);
		return -1;
	}
	printf("%d: add[%s]: %s\n", fd, n->symb, nick);
	return event_add(fd, netid, nick);
}

int
event_add(int fd, int netid, char *user)
{
	int i = evslen;

	if (evslen == evscap) {
		events = realloc0(events, sizeof(struct event) * (size_t)evscap,
				   sizeof(struct event) * (size_t)(evscap + EVENT_ADDEND));
		evscap += EVENT_ADDEND;
	}

	events[i].fd	 = fd;
	events[i].netid	 = netid;
	events[i].user	 = user;
	events[i].suffix = 0;
	events[i].time	 = 0;
	events[i].ready	 = FALSE;

	fd_register(fd, EV_WRITE);

	if (fd == fdslen - 1) {
		fdtoid = realloc0(fdtoid, sizeof(int) * (size_t)fdslen,
				   sizeof(int) * (size_t)fdslen * 2);
		fdslen *= 2;
	}
	fdtoid[fd] = i;

	return evslen++;
}

void
event_del(int id)
{
	int *ids;
	int  l = evslen - 1;		/* last id */

	/* swap id */
	if (events[l].user == NULL) {	/* if linker */
		networks[events[l].netid].id = id;
	} else {			/* else user */
		if ((ids = htsearch(users, events[l].user)) == NULL)
			errx(1, "user '%s' 2 doesn't exists", events[l].user);
		ids[events[l].netid] = id;
	}

	/* disable id */
	if (events[id].user == NULL) {	/* if linker */
		networks[events[id].netid].id = -2;
	} else {			/* else user */
		if ((ids = htsearch(users, events[id].user)) == NULL)
			errx(1, "user '%s' 3 doesn't exists", events[id].user);
		ids[events[id].netid] = -2;
	}

	close(events[id].fd);

	events[id] = events[l];
	evslen--;
	fdtoid[events[id].fd] = id;
}

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 (user_event_ids(src, netid) != NULL)
			*strrchr(src, '[') = '\0';

		strcat(dst, src);
		strncat(dst, &d, 1);

		src = n;
	}
}

void
print_table(void)
{
	int	i, *ids, diff, tabs;
	Htiter	it = {0};
	char	*nick;

	if (netlen == 0)
		return;

	print_border();
	/* print networks */
	printf("struct networks\t\t");
	for (i = 0; i < netlen; i++)
		printf("%s(%d)\t", networks[i].symb, events[networks[i].id].fd);
	printf("\n");

	while (htiterate(users, &it)) {
		ids  = (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 clones ids */
		for (i = 0; i < netlen; i++) {
			printf("%d", ids[i]);
			/* print suffix */
			if ((ids[i] > 0) && (events[ids[i]].suffix > 0))
				printf("(%d)", events[ids[i]].suffix);
			printf("\t");
		}
		printf("\n");
	}
	print_border();
}

void
print_htable(void)
{
	Htiter	it = {0};
	int	index = -1;

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

void
print_users(void)
{
	Htiter	it = {0};
	int i = 0;

	print_border();
	while (htiterate(users, &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");
}