changeset 958:533639ec5e9c

misc: use sys/queue.h
author David Demelier <markand@malikania.fr>
date Thu, 21 Jan 2021 23:15:20 +0100
parents 3167c51f0c84
children 0d6e2a89fee5
files irccd/main.c irccdctl/main.c lib/CMakeLists.txt lib/irccd/channel.c lib/irccd/channel.h lib/irccd/irccd.c lib/irccd/irccd.h lib/irccd/jsapi-plugin.c lib/irccd/jsapi-server.c lib/irccd/limits.h lib/irccd/list.h lib/irccd/peer.c lib/irccd/peer.h lib/irccd/plugin.c lib/irccd/plugin.h lib/irccd/rule.c lib/irccd/rule.h lib/irccd/server.c lib/irccd/server.h lib/irccd/set.h lib/irccd/transport.c lib/irccd/transport.h tests/CMakeLists.txt tests/test-bot.c tests/test-channel.c tests/test-rule.c
diffstat 26 files changed, 676 insertions(+), 673 deletions(-) [+]
line wrap: on
line diff
--- a/irccd/main.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/irccd/main.c	Thu Jan 21 23:15:20 2021 +0100
@@ -34,31 +34,4 @@
 	(void)argc;
 	(void)argv;
 
-	struct irc_server s = {
-		.name = "malikania",
-		.username = "circ",
-		.nickname = "circ",
-		.hostname = "malikania.fr",
-		.commandchar = "!",
-		.port = 6697,
-		.flags = IRC_SERVER_FLAGS_SSL | IRC_SERVER_FLAGS_JOIN_INVITE
-	};
-	struct irc_plugin p = {
-		.name = "test"
-	};
-	struct irc_rule r = {
-		.action = IRC_RULE_DROP
-	};
-
-	irc_rule_add(r.events, "onMe");
-
-	irc_log_set_verbose(true);
-	irc_bot_init();
-	irc_bot_insert_rule(&r, 0);
-	irc_transport_bind("/tmp/irccd.sock");
-	irc_server_join(&s, "#test", NULL);
-	irc_bot_add_server(irc_util_memdup(&s, sizeof (s)));
-	irc_js_plugin_open(&p, "/Users/markand/test.js");
-	irc_bot_add_plugin(&p);
-	irc_bot_run();
 }
--- a/irccdctl/main.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/irccdctl/main.c	Thu Jan 21 23:15:20 2021 +0100
@@ -40,17 +40,17 @@
 	.sun_family = PF_LOCAL,
 	.sun_path = "/tmp/irccd.sock"
 };
-static char in[IRC_BUF_MAX];
-static char out[IRC_BUF_MAX];
+static char in[IRC_BUF_LEN];
+static char out[IRC_BUF_LEN];
 
 static char *
 poll(void)
 {
-	static char ret[IRC_BUF_MAX];
+	static char ret[IRC_BUF_LEN];
 	char *nl;
 
 	while (!(nl = strstr(in, "\n"))) {
-		char buf[IRC_BUF_MAX] = {0};
+		char buf[IRC_BUF_LEN] = {0};
 		ssize_t nr;
 
 		if ((nr = recv(sock, buf, sizeof (buf) - 1, 0)) <= 0)
@@ -97,7 +97,7 @@
 static void
 req(const char *fmt, ...)
 {
-	char buf[IRC_BUF_MAX];
+	char buf[IRC_BUF_LEN];
 	va_list ap;
 
 	va_start(ap, fmt);
@@ -404,7 +404,7 @@
 show_whois(char *line)
 {
 	const char *args[6] = {0};
-	char *p, *token;
+	//char *p, *token;
 
 	if (irc_util_split(line, args, 6) >= 4) {
 		printf("event:     onWhois\n");
@@ -413,9 +413,7 @@
 		printf("username:  %s\n", args[3]);
 		printf("hostname:  %s\n", args[4]);
 		printf("username:  %s\n", args[5]);
-		printf("channels:  ");
-
-		
+		//printf("channels:  %s\n", args[6]);
 	}
 }
 
--- a/lib/CMakeLists.txt	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/CMakeLists.txt	Thu Jan 21 23:15:20 2021 +0100
@@ -41,8 +41,6 @@
 	irccd/rule.h
 	irccd/server.c
 	irccd/server.h
-	irccd/set.h
-	irccd/list.h
 	irccd/subst.c
 	irccd/subst.h
 	irccd/transport.c
--- a/lib/irccd/channel.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/channel.c	Thu Jan 21 23:15:20 2021 +0100
@@ -22,64 +22,76 @@
 
 #include "channel.h"
 #include "util.h"
-#include "set.h"
+
+static inline struct irc_channel_user *
+find(const struct irc_channel *ch, const char *nickname)
+{
+	struct irc_channel_user *u;
 
-static inline int
-cmp(const struct irc_channel_user *u1, const struct irc_channel_user *u2)
-{
-	return strcmp(u1->nickname, u2->nickname);
+	LIST_FOREACH(u, &ch->users, link)
+		if (strcmp(u->nickname, nickname) == 0)
+			return u;
+
+	return NULL;
 }
 
-static inline struct irc_channel_user *
-find(const struct irc_channel *ch, const char *nick)
+struct irc_channel *
+irc_channel_new(const char *name, const char *password, bool joined)
 {
-	struct irc_channel_user key = {0};
+	assert(name);
+
+	struct irc_channel *ch;
+
+	ch = irc_util_calloc(1, sizeof (*ch));
+	ch->joined = joined;
 
-	strlcpy(key.nickname, nick, sizeof (key.nickname));
+	strlcpy(ch->name, name, sizeof (ch->name));
+	strlcpy(ch->password, password ? password : "", sizeof (ch->password));
 
-	return IRC_SET_FIND(ch->users, ch->usersz, &key, cmp);
+	LIST_INIT(&ch->users);
+
+	return ch;
 }
 
 void
-irc_channel_add(struct irc_channel *ch, const char *nick, char mode)
+irc_channel_add(struct irc_channel *ch, const char *nickname, char mode, char symbol)
 {
 	assert(ch);
-	assert(nick);
+	assert(nickname);
 
-	if (find(ch, nick))
+	struct irc_channel_user *user;
+
+	if (find(ch, nickname))
 		return;
 
-	struct irc_channel_user u = {0};
+	user = irc_util_malloc(sizeof (*user));
+	user->mode = mode;
+	user->symbol = symbol;
+	strlcpy(user->nickname, nickname, sizeof (user->nickname));
 
-	strlcpy(u.nickname, nick, sizeof (u.nickname));
-	u.mode = mode;
-
-	IRC_SET_ALLOC_PUSH(&ch->users, &ch->usersz, &u, cmp);
+	LIST_INSERT_HEAD(&ch->users, user, link);
 }
 
 void
-irc_channel_set_user_mode(struct irc_channel *ch, const char *nick, char mode)
+irc_channel_update(struct irc_channel *ch,
+                   const char *nickname,
+                   const char *newnickname,
+                   char mode,
+                   char symbol)
 {
 	assert(ch);
-	assert(nick);
+	assert(nickname);
 
 	struct irc_channel_user *user;
 
-	if ((user = find(ch, nick)))
-		user->mode = mode;
-}
-
-void
-irc_channel_set_user_nick(struct irc_channel *ch, const char *nick, const char *newnick)
-{
-	assert(ch);
-	assert(nick);
-	assert(newnick);
-
-	struct irc_channel_user *user;
-
-	if ((user = find(ch, nick)))
-		strlcpy(user->nickname, newnick, sizeof (user->nickname));
+	if ((user = find(ch, nickname))) {
+		if (newnickname)
+			strlcpy(user->nickname, newnickname, sizeof (user->nickname));
+		if (mode != -1 && symbol != -1) {
+			user->mode = mode;
+			user->symbol = symbol;
+		}
+	}
 }
 
 void
@@ -87,8 +99,11 @@
 {
 	assert(ch);
 
-	free(ch->users);
-	ch->users = 0;
+	struct irc_channel_user *user, *tmp;
+
+	LIST_FOREACH_SAFE(user, &ch->users, link, tmp)
+		free(user);
+	LIST_INIT(&ch->users);
 }
 
 void
@@ -100,7 +115,7 @@
 	struct irc_channel_user *user;
 
 	if ((user = find(ch, nick)))
-		IRC_SET_ALLOC_REMOVE(&ch->users, &ch->usersz, user);
+		LIST_REMOVE(user, link);
 }
 
 void
@@ -109,5 +124,5 @@
 	assert(ch);
 
 	irc_channel_clear(ch);
-	memset(ch, 0, sizeof (*ch));
+	free(ch);
 }
--- a/lib/irccd/channel.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/channel.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,32 +19,35 @@
 #ifndef IRCCD_CHANNEL_H
 #define IRCCD_CHANNEL_H
 
+#include <sys/queue.h>
 #include <stdbool.h>
 #include <stddef.h>
 
 #include "limits.h"
 
 struct irc_channel_user {
-	char nickname[IRC_NICKNAME_MAX];
+	char nickname[IRC_NICKNAME_LEN];
 	char mode;
+	char symbol;
+	LIST_ENTRY(irc_channel_user) link;
 };
 
 struct irc_channel {
-	char name[IRC_CHANNEL_MAX];
-	char password[IRC_PASSWORD_MAX];
-	struct irc_channel_user *users;
-	size_t usersz;
+	char name[IRC_CHANNEL_LEN];
+	char password[IRC_PASSWORD_LEN];
 	bool joined;
+	LIST_HEAD(, irc_channel_user) users;
+	LIST_ENTRY(irc_channel) link;
 };
 
+struct irc_channel *
+irc_channel_new(const char *, const char *, bool);
+
 void
-irc_channel_add(struct irc_channel *, const char *, char);
+irc_channel_add(struct irc_channel *, const char *, char, char);
 
 void
-irc_channel_set_user_mode(struct irc_channel *, const char *, char);
-
-void
-irc_channel_set_user_nick(struct irc_channel *, const char *, const char *);
+irc_channel_update(struct irc_channel *, const char *, const char *, char, char);
 
 void
 irc_channel_clear(struct irc_channel *);
--- a/lib/irccd/irccd.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/irccd.c	Thu Jan 21 23:15:20 2021 +0100
@@ -27,13 +27,11 @@
 
 #include "event.h"
 #include "irccd.h"
-#include "list.h"
 #include "log.h"
 #include "peer.h"
 #include "plugin.h"
 #include "rule.h"
 #include "server.h"
-#include "set.h"
 #include "transport.h"
 #include "util.h"
 
@@ -47,22 +45,15 @@
 	void *data;
 };
 
-struct irc irc;
+struct irc irc = {
+	.servers = LIST_HEAD_INITIALIZER(),
+	.peers = LIST_HEAD_INITIALIZER(),
+	.plugins = LIST_HEAD_INITIALIZER(),
+	.rules = TAILQ_HEAD_INITIALIZER(irc.rules)
+};
 
 static int pipes[2];
 
-static int
-cmp_plugin(const struct irc_plugin *p1, const struct irc_plugin *p2)
-{
-	return strcmp(p1->name, p2->name);
-}
-
-static int
-cmp_peer(const struct irc_peer *p1, const struct irc_peer *p2)
-{
-	return p1->fd - p2->fd;
-}
-
 static bool
 is_command(const struct irc_plugin *p, const struct irc_event *ev)
 {
@@ -105,67 +96,80 @@
 {
 	switch (ev->type) {
 	case IRC_EVENT_COMMAND:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->message.channel, ev->message.origin, p->name, "onCommand");
 	case IRC_EVENT_CONNECT:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    NULL, NULL, p->name, "onConnect");
 	case IRC_EVENT_DISCONNECT:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    NULL, NULL, p->name, "onDisconnect");
 	case IRC_EVENT_INVITE:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->invite.channel, ev->invite.origin, p->name, "onInvite");
 	case IRC_EVENT_JOIN:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->join.channel, ev->join.origin, p->name, "onJoin");
 	case IRC_EVENT_KICK:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->kick.channel, ev->kick.origin, p->name, "onKick");
 		break;
 	case IRC_EVENT_ME:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->message.channel, ev->message.origin, p->name, "onMe");
 	case IRC_EVENT_MESSAGE:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->message.channel, ev->message.origin, p->name, "onMessage");
 	case IRC_EVENT_MODE:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->mode.channel, ev->mode.origin, p->name, "onMode");
 	case IRC_EVENT_NAMES:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->names.channel, NULL, p->name, "onNames");
 	case IRC_EVENT_NICK:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    NULL, ev->nick.origin, p->name, "onNick");
 	case IRC_EVENT_NOTICE:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->notice.channel, ev->notice.origin, p->name, "onNotice");
 	case IRC_EVENT_PART:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->part.channel, ev->part.origin, p->name, "onPart");
 	case IRC_EVENT_TOPIC:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    ev->topic.channel, ev->topic.origin, p->name, "onTopic");
 	case IRC_EVENT_WHOIS:
-		return irc_rule_matchlist(irc.rules, irc.rulesz, ev->server->name,
+		return irc_rule_matchlist(&irc.rules, ev->server->name,
 		    NULL, NULL, p->name, "onWhois");
 	default:
 		return true;
 	}
 }
 
+static inline size_t
+pollable(void)
+{
+	const struct irc_server *s;
+	const struct irc_peer *p;
+	size_t i = 2;                   /* pipe + transport. */
+
+	LIST_FOREACH(s, &irc.servers, link)
+		++i;
+	LIST_FOREACH(p, &irc.peers, link)
+		++i;
+
+	return i;
+}
+
 static struct pkg
 prepare(void)
 {
+	struct irc_peer *p;
+	struct irc_server *s;
 	struct pkg pkg = {0};
 	size_t i = 0;
 
-	pkg.fdsz += 1;                  /* pipe  */
-	pkg.fdsz += 1;                  /* transport fd */
-	pkg.fdsz += irc.serversz;       /* servers */
-	pkg.fdsz += irc.peersz;         /* transport peers */
-
+	pkg.fdsz = pollable();
 	pkg.fds = irc_util_calloc(pkg.fdsz, sizeof (*pkg.fds));
 
 	/* pipe */
@@ -175,9 +179,9 @@
 	/* transport */
 	irc_transport_prepare(&pkg.fds[i++]);
 
-	for (size_t p = 0; p < irc.peersz; ++p)
-		irc_peer_prepare(&irc.peers[p], &pkg.fds[i++]);
-	for (struct irc_server *s = irc.servers; s; s = s->next)
+	LIST_FOREACH(p, &irc.peers, link)
+		irc_peer_prepare(p, &pkg.fds[i++]);
+	LIST_FOREACH(s, &irc.servers, link)
 		irc_server_prepare(s, &pkg.fds[i++]);
 
 	return pkg;
@@ -186,20 +190,21 @@
 static inline void
 broadcast(const struct irc_event *ev)
 {
-	char buf[IRC_MESSAGE_MAX];
+	char buf[IRC_BUF_LEN];
+	struct irc_peer *p;
 
 	if (!irc_event_str(ev, buf, sizeof (buf)))
 		return;
 
-	for (size_t i = 0; i < irc.peersz; ++i)
-		if (irc.peers[i].is_watching)
-			irc_peer_send(&irc.peers[i], buf);
+	LIST_FOREACH(p, &irc.peers, link)
+		if (p->is_watching)
+			irc_peer_send(p, buf);
 }
 
 static void
 invoke(struct irc_event *ev)
 {
-	struct irc_plugin *plgcmd = NULL;
+	struct irc_plugin *p, *tmp, *plgcmd = NULL;
 
 	/*
 	 * Invoke for every plugin the event verbatim. Then, the event may match
@@ -215,11 +220,11 @@
 	 * onMessage for hangman and logger but onCommand for ask. As such call
 	 * hangman and logger first and modify event before ask.
 	 */
-	for (size_t i = 0; i < irc.pluginsz; ++i) {
-		if (is_command(&irc.plugins[i], ev))
-			plgcmd = &irc.plugins[i];
-		else if (invokable(&irc.plugins[i], ev))
-			irc_plugin_handle(&irc.plugins[i], ev);
+	LIST_FOREACH_SAFE(p, &irc.plugins, link, tmp) {
+		if (is_command(p, ev))
+			plgcmd = p;
+		else if (invokable(p, ev))
+			irc_plugin_handle(p, ev);
 	}
 
 	if (plgcmd && invokable(plgcmd, ev))
@@ -244,7 +249,7 @@
 process(struct pkg *pkg)
 {
 	struct irc_server *s;
-	struct irc_peer peer;
+	struct irc_peer *p, *ptmp;
 	struct irc_event ev;
 
 	if (poll(pkg->fds, pkg->fdsz, 1000) < 0 && errno != EINTR)
@@ -258,20 +263,19 @@
 	for (size_t i = 0; i < pkg->fdsz; ++i) {
 		pipe_flush(&pkg->fds[i]);
 
-		IRC_LIST_FOREACH(irc.servers, s)
+		LIST_FOREACH(s, &irc.servers, link)
 			irc_server_flush(s, &pkg->fds[i]);
 
 		/* Accept new transport client. */
-		if (irc_transport_flush(&pkg->fds[i], &peer))
-			IRC_SET_ALLOC_PUSH(&irc.peers, &irc.peersz, &peer, cmp_peer);
+		if ((p = irc_transport_flush(&pkg->fds[i])))
+			LIST_INSERT_HEAD(&irc.peers, p, link);
 
 		/* Flush clients. */
-		for (size_t p = 0; p < irc.peersz; ) {
-			if (!irc_peer_flush(&irc.peers[p], &pkg->fds[i])) {
-				irc_peer_finish(&irc.peers[p]);
-				IRC_SET_ALLOC_REMOVE(&irc.peers, &irc.peersz, &irc.peers[p]);
-			} else
-				++p;
+		LIST_FOREACH_SAFE(p, &irc.peers, link, ptmp) {
+			if (!irc_peer_flush(p, &pkg->fds[i])) {
+				irc_peer_finish(p);
+				LIST_REMOVE(p, link);
+			}
 		}
 	}
 
@@ -279,7 +283,7 @@
 	 * For every server, poll any kind of new event and pass them to the
 	 * plugin unless the rules explicitly disallow us to do so.
 	 */
-	IRC_LIST_FOREACH(irc.servers, s) {
+	LIST_FOREACH(s, &irc.servers, link) {
 		while (irc_server_poll(s, &ev)) {
 			broadcast(&ev);
 			invoke(&ev);
@@ -294,6 +298,18 @@
 	free(pkg->fds);
 }
 
+static inline size_t
+rulescount(void)
+{
+	const struct irc_rule *r;
+	size_t total = 0;
+
+	TAILQ_FOREACH(r, &irc.rules, link)
+		total++;
+
+	return total;
+}
+
 void
 irc_bot_init(void)
 {
@@ -304,24 +320,22 @@
 }
 
 void
-irc_bot_add_server(struct irc_server *s)
+irc_bot_server_add(struct irc_server *s)
 {
 	assert(s);
 
 	irc_server_incref(s);
 	irc_server_connect(s);
 
-	IRC_LIST_ADD(irc.servers, s);
-
-	irc.serversz++;
+	LIST_INSERT_HEAD(&irc.servers, s, link);
 }
 
 struct irc_server *
-irc_bot_find_server(const char *name)
+irc_bot_server_find(const char *name)
 {
 	struct irc_server *s;
 
-	for (s = irc.servers; s; s = s->next)
+	LIST_FOREACH(s, &irc.servers, link)
 		if (strcmp(s->name, name) == 0)
 			return s;
 
@@ -329,11 +343,11 @@
 }
 
 void
-irc_bot_remove_server(const char *name)
+irc_bot_server_remove(const char *name)
 {
 	struct irc_server *s;
 
-	if (!(s = irc_bot_find_server(name)))
+	if (!(s = irc_bot_server_find(name)))
 		return;
 
 	irc_server_disconnect(s);
@@ -344,43 +358,43 @@
 		.server = s
 	});
 
-	IRC_LIST_REMOVE(irc.servers, s);
-
+	LIST_REMOVE(s, link);
 	irc_server_decref(s);
-	irc.serversz--;
 }
 
 void
-irc_bot_clear_servers(void)
+irc_bot_server_clear(void)
 {
-	struct irc_server *s, *next;
+	struct irc_server *s, *tmp;
 
-	IRC_LIST_FOREACH_SAFE(irc.servers, s, next)
-		irc_bot_remove_server(s->name);
+	LIST_FOREACH_SAFE(s, &irc.servers, link, tmp)
+		irc_bot_server_remove(s->name);
 }
 
 void
-irc_bot_add_plugin(const struct irc_plugin *p)
+irc_bot_add_plugin(struct irc_plugin *p)
 {
 	assert(p);
 
-	IRC_SET_ALLOC_PUSH(&irc.plugins, &irc.pluginsz, p, cmp_plugin);
+	LIST_INSERT_HEAD(&irc.plugins, p, link);
 
 	irc_log_info("plugin %s: %s", p->name, p->description);
 	irc_log_info("plugin %s: version %s, from %s (%s license)", p->name,
 	    p->version, p->author, p->license);
 
-	irc_plugin_load(&irc.plugins[irc.pluginsz - 1]);
+	irc_plugin_load(p);
 }
 
 struct irc_plugin *
 irc_bot_find_plugin(const char *name)
 {
-	struct irc_plugin key = {0};
+	struct irc_plugin *p;
 
-	strlcpy(key.name, name, sizeof (key.name));
+	LIST_FOREACH(p, &irc.plugins, link)
+		if (strcmp(p->name, name) == 0)
+			return p;
 
-	return IRC_SET_FIND(irc.plugins, irc.pluginsz, &key, cmp_plugin);
+	return NULL;
 }
 
 void
@@ -394,37 +408,49 @@
 	irc_plugin_unload(p);
 	irc_plugin_finish(p);
 
-	IRC_SET_ALLOC_REMOVE(&irc.plugins, &irc.pluginsz, p);
+	LIST_REMOVE(p, link);
 }
 
-bool
-irc_bot_insert_rule(const struct irc_rule *rule, size_t i)
+void
+irc_bot_rule_insert(struct irc_rule *rule, size_t index)
 {
 	assert(rule);
 
-	if (irc.rulesz >= IRC_RULE_MAX) {
-		errno = ENOMEM;
-		return false;
-	}
+	if (index == 0)
+		TAILQ_INSERT_HEAD(&irc.rules, rule, link);
+	else if (index >= rulescount())
+		TAILQ_INSERT_TAIL(&irc.rules, rule, link);
+	else {
+		struct irc_rule *pos = TAILQ_FIRST(&irc.rules);
 
-	if (i >= irc.rulesz)
-		i = irc.rulesz;
+		for (size_t i = 0; i < index; ++i)
+			pos = TAILQ_NEXT(pos, link);
 
-	memmove(&irc.rules[i + 1], &irc.rules[i], sizeof (*irc.rules) * (irc.rulesz++ - i));
-	memcpy(&irc.rules[i], rule, sizeof (*rule));
-
-	return true;
+		TAILQ_INSERT_AFTER(&irc.rules, pos, rule, link);
+	}
 }
 
 void
-irc_bot_remove_rule(size_t i)
+irc_bot_rule_remove(size_t index)
 {
-	assert(i < irc.rulesz);
+	assert(index < rulescount());
+
+	struct irc_rule *pos = TAILQ_FIRST(&irc.rules);
+
+	for (size_t i = 0; i < index; ++i)
+		pos = TAILQ_NEXT(pos, link);
 
-	if (i + 1 >= irc.rulesz)
-		irc.rulesz--;
-	else
-		memmove(&irc.rules[i], &irc.rules[i + 1], sizeof (*irc.rules) * (irc.rulesz-- - i));
+	TAILQ_REMOVE(&irc.rules, pos, link);
+}
+
+void
+irc_bot_rule_clear(void)
+{
+	struct irc_rule *r, *tmp;
+
+	TAILQ_FOREACH_SAFE(r, &irc.rules, link, tmp)
+		irc_rule_finish(r);
+	TAILQ_INIT(&irc.rules);
 }
 
 void
--- a/lib/irccd/irccd.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/irccd.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,57 +19,52 @@
 #ifndef IRCCD_H
 #define IRCCD_H
 
-#include <stdbool.h>
-#include <stddef.h>
-
-#include "rule.h"
+#include <sys/queue.h>
 
-#define IRC_BOT_RULE_MAX 256
-
-struct irc_server;
-struct irc_plugin;
-struct irc_peer;
+#include "peer.h"
+#include "plugin.h"
+#include "rule.h"
+#include "server.h"
 
 extern struct irc {
-	struct irc_peer *peers;
-	size_t peersz;
-	struct irc_plugin *plugins;
-	size_t pluginsz;
-	struct irc_server *servers;
-	size_t serversz;
-	struct irc_rule rules[IRC_BOT_RULE_MAX];
-	size_t rulesz;
+	struct irc_server_list servers;
+	struct irc_peer_list peers;
+	struct irc_plugin_list plugins;
+	struct irc_rule_list rules;
 } irc;
 
 void
 irc_bot_init(void);
 
 void
-irc_bot_add_server(struct irc_server *);
+irc_bot_server_add(struct irc_server *);
 
 struct irc_server *
-irc_bot_find_server(const char *);
+irc_bot_server_find(const char *);
 
 void
-irc_bot_remove_server(const char *);
+irc_bot_server_remove(const char *);
 
 void
-irc_bot_clear_servers(void);
+irc_bot_server_clear(void);
 
 void
-irc_bot_add_plugin(const struct irc_plugin *);
+irc_bot_plugin_add(struct irc_plugin *);
 
 struct irc_plugin *
-irc_bot_find_plugin(const char *);
+irc_bot_plugin_find(const char *);
 
 void
-irc_bot_remove_plugin(const char *);
-
-bool
-irc_bot_insert_rule(const struct irc_rule *, size_t);
+irc_bot_plugin_remove(const char *);
 
 void
-irc_bot_remove_rule(size_t);
+irc_bot_rule_insert(struct irc_rule *, size_t);
+
+void
+irc_bot_rule_remove(size_t);
+
+void
+irc_bot_rule_clear(void);
 
 void
 irc_bot_post(void (*)(void *), void *);
--- a/lib/irccd/jsapi-plugin.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/jsapi-plugin.c	Thu Jan 21 23:15:20 2021 +0100
@@ -134,7 +134,7 @@
 find(duk_context *ctx)
 {
 	const char *name = duk_require_string(ctx, 0);
-	struct irc_plugin *plg = irc_bot_find_plugin(name);
+	struct irc_plugin *plg = irc_bot_plugin_find(name);
 
 	if (!plg)
 		duk_error(ctx, DUK_ERR_REFERENCE_ERROR, "plugin %s not found", name);
@@ -171,10 +171,13 @@
 static duk_ret_t
 Plugin_list(duk_context *ctx)
 {
+	size_t i = 0;
+	struct irc_plugin *p;
+
 	duk_push_array(ctx);
 
-	for (size_t i = 0; i < irc.pluginsz; ++i) {
-		duk_push_string(ctx, irc.plugins[i].name);
+	LIST_FOREACH(p, &irc.plugins, link) {
+		duk_push_string(ctx, p->name);
 		duk_put_prop_index(ctx, -2, i++);
 	}
 
@@ -201,7 +204,7 @@
 Plugin_unload(duk_context *ctx)
 {
 	/* Use find so it can raise ReferenceError if not found. */
-	irc_bot_remove_plugin(find(ctx)->name);
+	irc_bot_plugin_remove(find(ctx)->name);
 
 	return 0;
 }
--- a/lib/irccd/jsapi-server.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/jsapi-server.c	Thu Jan 21 23:15:20 2021 +0100
@@ -23,7 +23,6 @@
 #include "channel.h"
 #include "irccd.h"
 #include "jsapi-server.h"
-#include "list.h"
 #include "server.h"
 #include "util.h"
 
@@ -138,6 +137,9 @@
 Server_prototype_info(duk_context *ctx)
 {
 	const struct irc_server *s = self(ctx);
+	const struct irc_channel *c;
+	const struct irc_channel_user *u;
+	size_t ci = 0, ui = 0;
 
 	duk_push_object(ctx);
 	duk_push_string(ctx, s->name);
@@ -159,28 +161,28 @@
 
 	duk_push_array(ctx);
 
-	for (size_t c = 0; c < s->channelsz; ++c) {
+	LIST_FOREACH(c, &s->channels, link) {
 		duk_push_object(ctx);
-		duk_push_string(ctx, s->channels[c].name);
+		duk_push_string(ctx, c->name);
 		duk_put_prop_string(ctx, -2, "name");
-		duk_push_boolean(ctx, s->channels[c].joined);
+		duk_push_boolean(ctx, c->joined);
 		duk_put_prop_string(ctx, -2, "joined");
 		duk_push_array(ctx);
 
-		for (size_t n = 0; n < s->channels[c].usersz; ++n) {
+		LIST_FOREACH(u, &c->users, link) {
 			duk_push_object(ctx);
-			duk_push_string(ctx, s->channels[c].users[n].nickname);
+			duk_push_string(ctx, u->nickname);
 			duk_put_prop_string(ctx, -2, "nickname");
-			if (s->channels[c].users[n].mode)
-				duk_push_sprintf(ctx, "%c", s->channels[c].users[n].mode);
+			if (u->mode)
+				duk_push_sprintf(ctx, "%c", u->mode);
 			else
 				duk_push_null(ctx);
 			duk_put_prop_string(ctx, -2, "mode");
-			duk_put_prop_index(ctx, -2, n);
+			duk_put_prop_index(ctx, -2, ui++);
 		}
 
 		duk_put_prop_string(ctx, -2, "users");
-		duk_put_prop_index(ctx, -2, c);
+		duk_put_prop_index(ctx, -2, ci++);
 	}
 
 	duk_put_prop_string(ctx, -2, "channels");
@@ -484,9 +486,7 @@
 static duk_ret_t
 Server_add(duk_context *ctx)
 {
-	struct irc_server *sv = require(ctx, 0);
-
-	irc_bot_add_server(sv);
+	irc_bot_server_add(require(ctx, 0));
 
 	return 0;
 }
@@ -495,7 +495,7 @@
 Server_find(duk_context *ctx)
 {
 	const char *name = duk_require_string(ctx, 0);
-	struct irc_server *s = irc_bot_find_server(name);
+	struct irc_server *s = irc_bot_server_find(name);
 
 	if (!s)
 		return 0;
@@ -512,7 +512,7 @@
 
 	duk_push_object(ctx);
 
-	IRC_LIST_FOREACH(irc.servers, s) {
+	LIST_FOREACH(s, &irc.servers, link) {
 		irc_jsapi_server_push(ctx, s);
 		duk_put_prop_string(ctx, -2, s->name);
 	}
@@ -523,7 +523,7 @@
 static duk_ret_t
 Server_remove(duk_context *ctx)
 {
-	irc_bot_remove_server(duk_require_string(ctx, 0));
+	irc_bot_server_remove(duk_require_string(ctx, 0));
 
 	return 0;
 }
--- a/lib/irccd/limits.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/limits.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,27 +19,24 @@
 #ifndef IRCCD_LIMITS_H
 #define IRCCD_LIMITS_H
 
-/*
- * Those were IRC limits but not strictly following RFC 1459 because lots of
- * servers allow higher limits.
- */
-#define IRC_NICKNAME_MAX        32
-#define IRC_USERNAME_MAX        32
-#define IRC_REALNAME_MAX        64
-#define IRC_CHANNEL_MAX         64
-#define IRC_PASSWORD_MAX        64
-#define IRC_CTCPVERSION_MAX     128
-#define IRC_USERMODES_MAX       16
-
-#define IRC_MESSAGE_MAX         512
-#define IRC_ARGS_MAX            32
+/* Server limits. */
+#define IRC_NICKNAME_LEN        32      /* Nickname. */
+#define IRC_USERNAME_LEN        32      /* User name. */
+#define IRC_REALNAME_LEN        64      /* Real name. */
+#define IRC_CHANNEL_LEN         64      /* Channel name. */
+#define IRC_PASSWORD_LEN        64      /* Password length. */
+#define IRC_CTCPVERSION_LEN     64      /* Custom CTCP version answer. */
+#define IRC_USERMODES_LEN       8       /* Number of modes (e.g. ohv). */
+#define IRC_CMDCHAR_LEN         4       /* Prefix for plugin commands (e.g. !). */
 
 /* Network limits. */
-#define IRC_HOST_MAX            32
-#define IRC_BUF_MAX             128000
+#define IRC_HOST_LEN            64      /* Hostname length.. */
+#define IRC_BUF_LEN             128000  /* Network buffer input/output. */
 
-/* Types limits. */
-#define IRC_NAME_MAX            16
-#define IRC_COMMANDCHAR_MAX     8
+/* Generic limits. */
+#define IRC_ID_LEN              16      /* Plugin/server identifiers. */
+
+/* Rule limits. */
+#define IRC_RULE_LEN            1024    /* Space-separated list of values. */
 
 #endif /* !IRCCD_LIMITS_H */
--- a/lib/irccd/list.h	Thu Jan 21 15:34:25 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-/*
- * list.h -- generic macros to manipulate linked lists
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_LIST_H
-#define IRCCD_LIST_H
-
-#define IRC_LIST_ADD(h, o)                                              \
-do {                                                                    \
-        if ((h))                                                        \
-                (h)->prev = (o);                                        \
-        (o)->next = (h);                                                \
-        (h) = (o);                                                      \
-} while (0)
-
-#define IRC_LIST_REMOVE(h, o)                                           \
-do {                                                                    \
-        if ((o)->prev)                                                  \
-                (o)->prev->next = (o)->next;                            \
-        if ((o)->next)                                                  \
-                (o)->next->prev = (o)->prev;                            \
-        if ((o) == (h))                                                 \
-                (h) = (o)->next;                                        \
-        (o)->next = (o)->prev = NULL;                                   \
-} while (0)
-
-#define IRC_LIST_FOREACH(h, s)                                          \
-        for ((s) = (h); (s); (s) = (s)->next)
-
-#define IRC_LIST_FOREACH_SAFE(h, s, tmp)                                \
-        for ((s) = (h); s && ((tmp) = (s)->next, 1); (s) = (tmp))
-
-#endif /* IRCCD_LIST_H */
--- a/lib/irccd/peer.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/peer.c	Thu Jan 21 23:15:20 2021 +0100
@@ -67,7 +67,7 @@
 {
 	struct irc_server *s;
 
-	if (!(s = irc_bot_find_server(id))) {
+	if (!(s = irc_bot_server_find(id))) {
 		irc_peer_send(p, "server %s not found", id);
 		return NULL;
 	}
@@ -98,8 +98,7 @@
 
 		irc_server_disconnect(s);
 	} else
-		for (struct irc_server *s = irc.servers; s; s = s->next)
-			irc_server_disconnect(s);
+		irc_bot_server_clear();
 
 	return ok(p);
 }
@@ -246,16 +245,25 @@
 {
 	(void)line;
 
-	char out[IRC_BUF_MAX] = "OK ";
+	struct irc_server *s;
+	FILE *fp;
+	char *out;
+	size_t outsz;
+
+	fp = open_memstream(&out, &outsz);
 
-	for (struct irc_server *s = irc.servers; s; s = s->next) {
-		if (strlcat(out, s->name, sizeof (out)) >= sizeof (out))
-			return EMSGSIZE;
-		if (s->next && strlcat(out, " ", sizeof (out)) >= sizeof (out))
-			return EMSGSIZE;
+	fprintf(fp, "OK ");
+
+	LIST_FOREACH(s, &irc.servers, link) {
+		fprintf(fp, "%s", s->name);
+
+		if (LIST_NEXT(s, link))
+			fputc(' ', fp);
 	}
 
+	fclose(fp);
 	irc_peer_send(p, out);
+	free(out);
 
 	return 0;
 }
@@ -411,13 +419,24 @@
 	return true;
 }
 
+struct irc_peer *
+irc_peer_new(int fd)
+{
+	struct irc_peer *p;
+
+	p = irc_util_calloc(1, sizeof (*p));
+	p->fd = fd;
+
+	return p;
+}
+
 bool
 irc_peer_send(struct irc_peer *p, const char *fmt, ...)
 {
 	assert(p);
 	assert(fmt);
 
-	char buf[IRC_BUF_MAX];
+	char buf[IRC_BUF_LEN];
 	va_list ap;
 	size_t len, avail, required;
 
@@ -474,5 +493,5 @@
 	assert(p);
 
 	close(p->fd);
-	memset(p, 0, sizeof (*p));
+	free(p);
 }
--- a/lib/irccd/peer.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/peer.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,6 +19,7 @@
 #ifndef IRCCD_PEER_H
 #define IRCCD_PEER_H
 
+#include <sys/queue.h>
 #include <stdbool.h>
 
 #include "limits.h"
@@ -28,10 +29,16 @@
 struct irc_peer {
 	int fd;
 	bool is_watching;
-	char in[IRC_BUF_MAX];
-	char out[IRC_BUF_MAX];
+	char in[IRC_BUF_LEN];
+	char out[IRC_BUF_LEN];
+	LIST_ENTRY(irc_peer) link;
 };
 
+LIST_HEAD(irc_peer_list, irc_peer);
+
+struct irc_peer *
+irc_peer_new(int);
+
 bool
 irc_peer_send(struct irc_peer *, const char *, ...);
 
--- a/lib/irccd/plugin.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/plugin.c	Thu Jan 21 23:15:20 2021 +0100
@@ -17,8 +17,7 @@
  */
 
 #include <assert.h>
-#include <stddef.h>
-#include <string.h>
+#include <stdlib.h>
 
 #include "plugin.h"
 
@@ -169,5 +168,5 @@
 	if (plg->finish)
 		plg->finish(plg);
 
-	memset(plg, 0, sizeof (*plg));
+	free(plg);
 }
--- a/lib/irccd/plugin.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/plugin.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,6 +19,7 @@
 #ifndef IRCCD_PLUGIN_H
 #define IRCCD_PLUGIN_H
 
+#include <sys/queue.h>
 #include <stdbool.h>
 
 #include "limits.h"
@@ -26,7 +27,7 @@
 struct irc_event;
 
 struct irc_plugin {
-	char name[IRC_NAME_MAX];
+	char name[IRC_ID_LEN];
 	const char *license;
 	const char *version;
 	const char *author;
@@ -51,8 +52,12 @@
 	void (*handle)(struct irc_plugin *, const struct irc_event *);
 
 	void (*finish)(struct irc_plugin *);
+
+	LIST_ENTRY(irc_plugin) link;
 };
 
+LIST_HEAD(irc_plugin_list, irc_plugin);
+
 void
 irc_plugin_set_template(struct irc_plugin *, const char *, const char *);
 
--- a/lib/irccd/rule.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/rule.c	Thu Jan 21 23:15:20 2021 +0100
@@ -20,24 +20,25 @@
 #include <ctype.h>
 #include <errno.h>
 #include <stdio.h>
+#include <stdlib.h>
 #include <string.h>
 #include <strings.h>
 
 #include "rule.h"
 #include "util.h"
 
-static void
+static inline void
 lower(char *dst, const char *src)
 {
 	while (*src)
 		*dst++ = tolower(*src++);
 }
 
-static inline char *
+static char *
 find(const char *str, const char *value)
 {
-	char strlower[IRC_RULE_MAX] = {0};
-	char valuelower[IRC_RULE_MAX] = {0};
+	char strlower[IRC_RULE_LEN] = {0};
+	char valuelower[IRC_RULE_LEN] = {0};
 	char *p;
 
 	lower(strlower, str);
@@ -49,7 +50,7 @@
 	return NULL;
 }
 
-static inline bool
+static bool
 match(const char *str, const char *value)
 {
 	size_t len;
@@ -75,6 +76,17 @@
 	return strncasecmp(p, value, len) == 0;
 }
 
+struct irc_rule *
+irc_rule_new(enum irc_rule_action action)
+{
+	struct irc_rule *r;
+
+	r = irc_util_calloc(1, sizeof (*r));
+	r->action = action;
+
+	return r;
+}
+
 bool
 irc_rule_add(char *str, const char *value)
 {
@@ -86,7 +98,7 @@
 	slen = strlen(str);
 	vlen = strlen(value);
 
-	if (vlen + 1 >= IRC_RULE_MAX - slen) {
+	if (vlen + 1 >= IRC_RULE_LEN - slen) {
 		errno = ENOMEM;
 		return false;
 	}
@@ -109,7 +121,7 @@
 	vlen = strlen(value) + 1;       /* includes ':' */
 
 	assert(pos[vlen - 1] == ':');
-	memmove(&pos[0], &pos[vlen], IRC_RULE_MAX - (&pos[vlen] - str));
+	memmove(&pos[0], &pos[vlen], IRC_RULE_LEN - (&pos[vlen] - str));
 }
 
 bool
@@ -128,8 +140,7 @@
 }
 
 bool
-irc_rule_matchlist(const struct irc_rule *rules,
-                   size_t rulesz,
+irc_rule_matchlist(const struct irc_rule_list *rules,
                    const char *server,
                    const char *channel,
                    const char *origin,
@@ -137,10 +148,11 @@
                    const char *event)
 {
 	bool result = true;
+	struct irc_rule *r;
 
-	for (size_t i = 0; i < rulesz; ++i)
-		if (irc_rule_match(&rules[i], server, channel, origin, plugin, event))
-			result = rules[i].action == IRC_RULE_ACCEPT;
+	TAILQ_FOREACH(r, rules, link)
+		if (irc_rule_match(r, server, channel, origin, plugin, event))
+			result = r->action == IRC_RULE_ACCEPT;
 
 	return result;
 }
@@ -150,5 +162,5 @@
 {
 	assert(rule);
 
-	memset(rule, 0, sizeof (*rule));
+	free(rule);
 }
--- a/lib/irccd/rule.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/rule.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,10 +19,11 @@
 #ifndef IRCCD_RULE_H
 #define IRCCD_RULE_H
 
+#include <sys/queue.h>
 #include <stdbool.h>
 #include <stddef.h>
 
-#define IRC_RULE_MAX 1024
+#include "limits.h"
 
 enum irc_rule_action {
 	IRC_RULE_ACCEPT,
@@ -31,13 +32,19 @@
 
 struct irc_rule {
 	enum irc_rule_action action;
-	char servers[IRC_RULE_MAX];
-	char channels[IRC_RULE_MAX];
-	char origins[IRC_RULE_MAX];
-	char plugins[IRC_RULE_MAX];
-	char events[IRC_RULE_MAX];
+	char servers[IRC_RULE_LEN];
+	char channels[IRC_RULE_LEN];
+	char origins[IRC_RULE_LEN];
+	char plugins[IRC_RULE_LEN];
+	char events[IRC_RULE_LEN];
+	TAILQ_ENTRY(irc_rule) link;
 };
 
+TAILQ_HEAD(irc_rule_list, irc_rule);
+
+struct irc_rule *
+irc_rule_new(enum irc_rule_action);
+
 bool
 irc_rule_add(char *, const char *);
 
@@ -53,8 +60,7 @@
                const char *);
 
 bool
-irc_rule_matchlist(const struct irc_rule *,
-                   size_t,
+irc_rule_matchlist(const struct irc_rule_list *,
                    const char *,
                    const char *,
                    const char *,
--- a/lib/irccd/server.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/server.c	Thu Jan 21 23:15:20 2021 +0100
@@ -40,20 +40,19 @@
 #include "event.h"
 #include "log.h"
 #include "server.h"
-#include "set.h"
 #include "util.h"
 
 struct origin {
-	char nickname[IRC_NICKNAME_MAX];
-	char username[IRC_USERNAME_MAX];
-	char host[IRC_HOST_MAX];
+	char nickname[IRC_NICKNAME_LEN];
+	char username[IRC_USERNAME_LEN];
+	char host[IRC_HOST_LEN];
 };
 
 struct message {
 	char *prefix;
 	char *cmd;
-	char *args[IRC_ARGS_MAX];
-	char buf[IRC_MESSAGE_MAX];
+	char *args[32];
+	char buf[512];
 };
 
 static inline void
@@ -88,7 +87,7 @@
 	scan(&ptr, &msg->cmd);                         /* command */
 
 	/* And finally arguments. */
-	for (a = 0; *ptr && a < IRC_ARGS_MAX; ++a) {
+	for (a = 0; *ptr && a < IRC_UTIL_SIZE(msg->args); ++a) {
 		if (*ptr == ':') {
 			msg->args[a] = ptr + 1;
 			ptr = strchr(ptr, '\0');
@@ -96,7 +95,7 @@
 			scan(&ptr, &msg->args[a]);
 	}
 
-	if (a >= IRC_ARGS_MAX)
+	if (a >= IRC_UTIL_SIZE(msg->args))
 		return errno = EMSGSIZE, false;
 	if (msg->cmd == NULL)
 		return errno = EBADMSG, false;
@@ -120,12 +119,6 @@
 	return strncmp(s->nickname, nick, strlen(s->nickname)) == 0;
 }
 
-static int
-cmp_channel(const struct irc_channel *c1, const struct irc_channel *c2)
-{
-	return strcmp(c1->name, c2->name);
-}
-
 #if 0
 
 static const struct origin *
@@ -149,46 +142,48 @@
 static void
 add_nick(const struct irc_server *s, struct irc_channel *ch, const char *nick)
 {
-	char mode = 0;
+	char mode = 0, symbol = 0;
 
 	for (size_t i = 0; i < IRC_UTIL_SIZE(s->prefixes); ++i) {
 		if (nick[0] == s->prefixes[i].token) {
 			mode = s->prefixes[i].mode;
+			symbol = s->prefixes[i].token;
 			++nick;
 			break;
 		}
 	}
 
-	irc_channel_add(ch, nick, mode);
+	irc_channel_add(ch, nick, mode, symbol);
 }
 
 static struct irc_channel *
 add_channel(struct irc_server *s, const char *name, const char *password, bool joined)
 {
-	struct irc_channel chnew = {0}, *ch;
+	struct irc_channel *ch;
 
 	if ((ch = irc_server_find(s, name))) {
 		ch->joined = joined;
 		return ch;
 	}
 
-	strlcpy(chnew.name, name, sizeof (chnew.name));
+	ch = irc_util_calloc(1, sizeof (*ch));
+	ch->joined = joined;
+	strlcpy(ch->name, name, sizeof (ch->name));
 
 	if (password)
-		strlcpy(chnew.password, password, sizeof (chnew.password));
-
-	chnew.joined = joined;
+		strlcpy(ch->password, password, sizeof (ch->password));
 
-	IRC_SET_ALLOC_PUSH(&s->channels, &s->channelsz, &chnew, cmp_channel);
+	LIST_INIT(&ch->users);
+	LIST_INSERT_HEAD(&s->channels, ch, link);
 
-	return irc_server_find(s, name);
+	return ch;
 }
 
 static void
-remove_channel(struct irc_server *s, struct irc_channel *ch)
+remove_channel(struct irc_channel *ch)
 {
-	irc_channel_clear(ch);
-	IRC_SET_ALLOC_REMOVE(&s->channels, &s->channelsz, ch);
+	irc_channel_finish(ch);
+	LIST_REMOVE(ch, link);
 }
 
 bool
@@ -248,11 +243,13 @@
 {
 	(void)msg;
 
+	struct irc_channel *ch;
+
 	s->state = IRC_SERVER_STATE_CONNECTED;
 
 	/* Now join all channels that were requested. */
-	for (size_t i = 0; i < s->channelsz; ++i)
-		irc_server_join(s, s->channels[i].name, s->channels[i].password);
+	LIST_FOREACH(ch, &s->channels, link)
+		irc_server_join(s, ch->name, ch->password);
 
 	ev->type = IRC_EVENT_CONNECT;
 }
@@ -354,7 +351,7 @@
 	ch = add_channel(s, ev->part.channel, NULL, true);
 
 	if (is_self(s, ev->part.origin) == 0)
-		remove_channel(s, ch);
+		remove_channel(ch);
 	else
 		irc_channel_remove(ch, ev->part.origin);
 }
@@ -452,8 +449,9 @@
 	(void)msg;
 
 	FILE *fp;
+	size_t length;
 	const struct irc_channel *ch;
-	size_t length;
+	const struct irc_channel_user *u;
 
 	ev->type = IRC_EVENT_NAMES;
 	ev->names.channel = strdup(msg->args[1]);
@@ -462,15 +460,13 @@
 	ch = irc_server_find(s, ev->names.channel);
 	fp = open_memstream(&ev->names.names, &length);
 
-	for (size_t i = 0; i < ch->usersz; ++i) {
-		const struct irc_channel_user *u = &ch->users[i];
-
+	LIST_FOREACH(u, &ch->users, link) {
 		if (u->mode)
 			fprintf(fp, "%c", sym(s, u->mode));
 
 		fprintf(fp, "%s", u->nickname);
 
-		if (i + 1 < ch->usersz)
+		if (LIST_NEXT(u, link))
 			fputc(' ', fp);
 	}
 
@@ -481,6 +477,7 @@
 handle_whoisuser(struct irc_server *s, struct irc_event *ev, struct message *msg)
 {
 	(void)s;
+	(void)ev;
 	(void)msg;
 
 	s->bufwhois.nickname = strdup(msg->args[1]);
@@ -581,6 +578,8 @@
 static void
 clear(struct irc_server *s)
 {
+	struct irc_channel *ch, *tmp;
+
 	s->state = IRC_SERVER_STATE_DISCONNECTED;
 
 	if (s->fd != 0) {
@@ -604,6 +603,12 @@
 		s->ctx = NULL;
 	}
 #endif
+
+	LIST_FOREACH_SAFE(ch, &s->channels, link, tmp) {
+		irc_channel_finish(ch);
+		free(ch);
+	}
+	LIST_INIT(&s->channels);
 }
 
 static bool
@@ -945,6 +950,36 @@
 	},
 };
 
+struct irc_server *
+irc_server_new(const char *name,
+               const char *nickname,
+               const char *username,
+               const char *realname,
+               const char *hostname,
+               unsigned int port)
+{
+	assert(name);
+	assert(nickname);
+	assert(username);
+	assert(realname);
+	assert(hostname);
+
+	struct irc_server *s;
+
+	s = irc_util_calloc(1, sizeof (*s));
+	s->port = port;
+
+	strlcpy(s->name, name, sizeof (s->name));
+	strlcpy(s->nickname, nickname, sizeof (s->nickname));
+	strlcpy(s->username, username, sizeof (s->username));
+	strlcpy(s->realname, realname, sizeof (s->realname));
+	strlcpy(s->hostname, hostname, sizeof (s->hostname));
+
+	LIST_INIT(&s->channels);
+
+	return s;
+}
+
 void
 irc_server_connect(struct irc_server *s)
 {
@@ -1017,11 +1052,13 @@
 	assert(s);
 	assert(name);
 
-	struct irc_channel key = {0};
+	struct irc_channel *ch;
 
-	strlcpy(key.name, name, sizeof (key.name));
+	LIST_FOREACH(ch, &s->channels, link)
+		if (strcmp(ch->name, name) == 0)
+			return ch;
 
-	return IRC_SET_FIND(s->channels, s->channelsz, &key, cmp_channel);
+	return NULL;
 }
 
 bool
@@ -1030,7 +1067,7 @@
 	assert(s);
 	assert(fmt);
 
-	char buf[IRC_BUF_MAX];
+	char buf[IRC_BUF_LEN];
 	va_list ap;
 	size_t len, avail, required;
 
@@ -1251,8 +1288,6 @@
 
 	if (--s->refc == 0) {
 		clear(s);
-		//free(s->whois.channels);
-		free(s->channels);
 		free(s);
 	}
 }
--- a/lib/irccd/server.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/server.h	Thu Jan 21 23:15:20 2021 +0100
@@ -19,6 +19,7 @@
 #ifndef IRCCD_SERVER_H
 #define IRCCD_SERVER_H
 
+#include <sys/queue.h>
 #include <stdbool.h>
 #include <stddef.h>
 
@@ -75,31 +76,29 @@
 
 struct irc_server {
 	/* Connection settings. */
-	char name[IRC_NAME_MAX];
-	char hostname[IRC_HOST_MAX];
-	char password[IRC_PASSWORD_MAX];
+	char name[IRC_ID_LEN];
+	char hostname[IRC_HOST_LEN];
+	char password[IRC_PASSWORD_LEN];
 	unsigned short port;
 	enum irc_server_flags flags;
 
 	/* Plugin prefix. */
-	char commandchar[IRC_COMMANDCHAR_MAX];
+	char commandchar[IRC_CMDCHAR_LEN];
 
 	/* IRC identity. */
-	char nickname[IRC_NICKNAME_MAX];
-	char username[IRC_USERNAME_MAX];
-	char realname[IRC_REALNAME_MAX];
-	char ctcpversion[IRC_CTCPVERSION_MAX];
+	char nickname[IRC_NICKNAME_LEN];
+	char username[IRC_USERNAME_LEN];
+	char realname[IRC_REALNAME_LEN];
+	char ctcpversion[IRC_CTCPVERSION_LEN];
 
-	/* Joined channels. */
-	struct irc_channel *channels;
-	size_t channelsz;
+	LIST_HEAD(, irc_channel) channels;
 
 	/* Network connectivity. */
 	int fd;
 	struct addrinfo *ai;
 	struct addrinfo *aip;
-	char in[IRC_BUF_MAX];
-	char out[IRC_BUF_MAX];
+	char in[IRC_BUF_LEN];
+	char out[IRC_BUF_LEN];
 	enum irc_server_state state;
 
 	/* OpenSSL support. */
@@ -113,14 +112,24 @@
 
 	/* Reference count. */
 	size_t refc;
-	struct irc_server *next;
-	struct irc_server *prev;
 
 	/* IRC server settings. */
 	char chantypes[8];
 	struct irc_server_prefix prefixes[16];
+
+	LIST_ENTRY(irc_server) link;
 };
 
+LIST_HEAD(irc_server_list, irc_server);
+
+struct irc_server *
+irc_server_new(const char *,
+               const char *,
+               const char *,
+               const char *,
+               const char *,
+               unsigned int);
+
 void
 irc_server_connect(struct irc_server *);
 
--- a/lib/irccd/set.h	Thu Jan 21 15:34:25 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/*
- * set.h -- generic macros to insert/remove in sorted arrays
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_SET_H
-#define IRCCD_SET_H
-
-#include <string.h>
-#include <stdlib.h>
-
-#include "util.h"
-
-typedef int (*irc_set_cmp)(const void *, const void *);
-
-#define IRC_SET_FIND(a, asz, o, f)                                              \
-        bsearch(o, a, asz, sizeof (*(o)), (irc_set_cmp)f)
-
-#define IRC_SET_ALLOC_PUSH(a, asz, o, f)                                        \
-do {                                                                            \
-        *(a) = irc_util_reallocarray(*(a), ++(*(asz)), sizeof (*(o)));          \
-        memcpy(*(a) + ((*asz) - 1), o, sizeof (*o));                            \
-        qsort(*(a), *(asz), sizeof (*o), (irc_set_cmp)f);                       \
-} while (0)
-
-#define IRC_SET_ALLOC_REMOVE(a, asz, o)                                         \
-do {                                                                            \
-        if (--(*(asz)) == 0) {                                                  \
-                free(*(a));                                                     \
-                *(a) = NULL;                                                    \
-        } else {                                                                \
-                memmove(o, o + 1, sizeof (*(o)) * (*(asz) - ((o) - *(a))));     \
-                *(a) = irc_util_reallocarray(*(a), *(asz), sizeof (*(o)));      \
-        }                                                                       \
-} while (0)
-
-#define IRC_SET_PUSH(a, asz, o, f)                                              \
-do {                                                                            \
-        memcpy(a + (*(asz))++, o, sizeof (*o));                                 \
-        qsort(a, *(asz), sizeof (*o), (irc_set_cmp)f);                          \
-} while (0)
-
-#define IRC_SET_REMOVE(a, asz, o)                                               \
-do {                                                                            \
-        if (--(*asz) != 0)                                                      \
-                memmove(o, o + 1, sizeof (*(o)) * (*(asz) - ((o) - (a))));      \
-} while (0)
-
-#endif /* !IRCCD_SET_H */
--- a/lib/irccd/transport.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/transport.c	Thu Jan 21 23:15:20 2021 +0100
@@ -31,6 +31,7 @@
 #include "log.h"
 #include "transport.h"
 #include "peer.h"
+#include "util.h"
 
 static struct sockaddr_un addr;
 static int fd = -1;
@@ -85,27 +86,29 @@
 	pfd->events = POLLIN;
 }
 
-bool
-irc_transport_flush(const struct pollfd *pfd, struct irc_peer *peer)
+struct irc_peer *
+irc_transport_flush(const struct pollfd *pfd)
 {
 	assert(pfd);
-	assert(peer);
+
+	struct irc_peer *peer;
+	int newfd;
 
 	if (fd < 0 || pfd->fd != fd || !(pfd->revents & POLLIN))
-		return false;
-
-	memset(peer, 0, sizeof (*peer));
+		return NULL;
 
-	if ((peer->fd = accept(fd, NULL, NULL)) < 0) {
+	if ((newfd = accept(fd, NULL, NULL)) < 0) {
 		irc_log_warn("transport: %s", strerror(errno));
-		return false;
+		return NULL;
 	}
 
+	peer = irc_peer_new(newfd);
+
 	irc_log_info("transport: new client connected");
 	irc_peer_send(peer, "IRCCD %d.%d.%d", IRCCD_VERSION_MAJOR,
 	    IRCCD_VERSION_MINOR, IRCCD_VERSION_PATCH);
 
-	return true;
+	return peer;
 }
 
 void
--- a/lib/irccd/transport.h	Thu Jan 21 15:34:25 2021 +0100
+++ b/lib/irccd/transport.h	Thu Jan 21 23:15:20 2021 +0100
@@ -33,8 +33,8 @@
 void
 irc_transport_prepare(struct pollfd *);
 
-bool
-irc_transport_flush(const struct pollfd *, struct irc_peer *);
+struct irc_peer *
+irc_transport_flush(const struct pollfd *);
 
 void
 irc_transport_finish(void);
--- a/tests/CMakeLists.txt	Thu Jan 21 15:34:25 2021 +0100
+++ b/tests/CMakeLists.txt	Thu Jan 21 23:15:20 2021 +0100
@@ -22,7 +22,7 @@
 	TESTS
 	test-bot
 	test-channel
-	test-event
+	#test-event
 	#test-dl-plugin
 	test-log
 	test-rule
--- a/tests/test-bot.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/tests/test-bot.c	Thu Jan 21 23:15:20 2021 +0100
@@ -25,66 +25,52 @@
 #include <irccd/server.h>
 #include <irccd/util.h>
 
-static struct irc_server *
-server_new(const char *name)
-{
-	struct irc_server *s;
-
-	s = irc_util_calloc(1, sizeof (*s));
-	strlcpy(s->name, name, sizeof (s->name));
-
-	return s;
-}
-
 static void
 clean(void *udata)
 {
 	(void)udata;
 
-	irc_bot_clear_servers();
+	irc_bot_server_clear();
 }
 
 GREATEST_TEST
 servers_add(void)
 {
-	struct irc_server *s1, *s2, *s3;
+	struct irc_server *s, *s1, *s2, *s3;
 
-	s1 = server_new("malikania");
-	s2 = server_new("freenode");
-	s3 = server_new("oftc");
+	s1 = irc_server_new("malikania", "x", "x", "x", "localhost", 6667);
+	s2 = irc_server_new("freenode", "x", "x", "x", "localhost", 6667);
+	s3 = irc_server_new("oftc", "x", "x", "x", "localhost", 6667);
 
 	/* irc.servers -> s1 */
-	irc_bot_add_server(s1);
-	GREATEST_ASSERT_EQ(1, irc.serversz);
-	GREATEST_ASSERT_EQ(1, s1->refc);
-	GREATEST_ASSERT_EQ(s1, irc.servers);
-	GREATEST_ASSERT_EQ(NULL, s1->prev);
-	GREATEST_ASSERT_EQ(NULL, s1->next);
+	irc_bot_server_add(s1);
+	s = LIST_FIRST(&irc.servers);
+	GREATEST_ASSERT_EQ(1, s->refc);
+	GREATEST_ASSERT_EQ(s, s1);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT(!s);
 
 	/* irc.servers -> s2 -> s1 */
-	irc_bot_add_server(s2);
-	GREATEST_ASSERT_EQ(2, irc.serversz);
-	GREATEST_ASSERT_EQ(1, s1->refc);
-	GREATEST_ASSERT_EQ(1, s2->refc);
-	GREATEST_ASSERT_EQ(s2, irc.servers);
-	GREATEST_ASSERT_EQ(s1, s2->next);
-	GREATEST_ASSERT_EQ(NULL, s2->prev);
-	GREATEST_ASSERT_EQ(NULL, s1->next);
-	GREATEST_ASSERT_EQ(s2, s1->prev);
+	irc_bot_server_add(s2);
+	s = LIST_FIRST(&irc.servers);
+	GREATEST_ASSERT_EQ(1, s->refc);
+	GREATEST_ASSERT_EQ(s, s2);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT_EQ(s, s1);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT(!s);
 
 	/* irc.servers -> s3 -> s2 -> s1 */
-	irc_bot_add_server(s3);
-	GREATEST_ASSERT_EQ(3, irc.serversz);
-	GREATEST_ASSERT_EQ(1, s1->refc);
-	GREATEST_ASSERT_EQ(1, s2->refc);
-	GREATEST_ASSERT_EQ(1, s3->refc);
-	GREATEST_ASSERT_EQ(s3, irc.servers);
-	GREATEST_ASSERT_EQ(s2, s3->next);
-	GREATEST_ASSERT_EQ(NULL, s3->prev);
-	GREATEST_ASSERT_EQ(s1, s2->next);
-	GREATEST_ASSERT_EQ(s3, s2->prev);
-	GREATEST_ASSERT_EQ(NULL, s1->next);
-	GREATEST_ASSERT_EQ(s2, s1->prev);
+	irc_bot_server_add(s3);
+	s = LIST_FIRST(&irc.servers);
+	GREATEST_ASSERT_EQ(1, s->refc);
+	GREATEST_ASSERT_EQ(s, s3);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT_EQ(s, s2);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT_EQ(s, s1);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT(!s);
 
 	GREATEST_PASS();
 }
@@ -92,11 +78,11 @@
 GREATEST_TEST
 servers_remove(void)
 {
-	struct irc_server *s1, *s2, *s3;
+	struct irc_server *s, *s1, *s2, *s3;
 
-	s1 = server_new("1");
-	s2 = server_new("2");
-	s3 = server_new("3");
+	s1 = irc_server_new("1", "x", "x", "x", "localhost", 6667);
+	s2 = irc_server_new("2", "x", "x", "x", "localhost", 6667);
+	s3 = irc_server_new("3", "x", "x", "x", "localhost", 6667);
 
 	/* Protect deletion from irc_bot_remove_server. */
 	irc_server_incref(s1);
@@ -104,46 +90,42 @@
 	irc_server_incref(s3);
 
 	/* irc.servers -> s3 -> s2 -> s1 */
-	irc_bot_add_server(s1);
-	irc_bot_add_server(s2);
-	irc_bot_add_server(s3);
+	irc_bot_server_add(s1);
+	irc_bot_server_add(s2);
+	irc_bot_server_add(s3);
 
 	/* irc.servers -> s3 -> [s2] -> s1 */
 	/* irc.servers -> s3 -> s1 */
-	irc_bot_remove_server(s2->name);
-	GREATEST_ASSERT_EQ(2, irc.serversz);
+	irc_bot_server_remove(s2->name);
 	GREATEST_ASSERT_EQ(2, s1->refc);
 	GREATEST_ASSERT_EQ(1, s2->refc);
 	GREATEST_ASSERT_EQ(2, s3->refc);
-	GREATEST_ASSERT_EQ(NULL, s2->next);
-	GREATEST_ASSERT_EQ(NULL, s2->prev);
-	GREATEST_ASSERT_EQ(s1, s3->next);
-	GREATEST_ASSERT_EQ(NULL, s3->prev);
-	GREATEST_ASSERT_EQ(NULL, s1->next);
-	GREATEST_ASSERT_EQ(s3, s1->prev);
+	s = LIST_FIRST(&irc.servers);
+	GREATEST_ASSERT_EQ(s, s3);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT_EQ(s, s1);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT(!s);
 
 	/* irc.servers -> s3 -> [s1] */
 	/* irc.servers -> s3 */
-	irc_bot_remove_server(s1->name);
-	GREATEST_ASSERT_EQ(1, irc.serversz);
+	irc_bot_server_remove(s1->name);
 	GREATEST_ASSERT_EQ(1, s1->refc);
 	GREATEST_ASSERT_EQ(1, s2->refc);
 	GREATEST_ASSERT_EQ(2, s3->refc);
-	GREATEST_ASSERT_EQ(NULL, s1->next);
-	GREATEST_ASSERT_EQ(NULL, s1->prev);
-	GREATEST_ASSERT_EQ(NULL, s3->next);
-	GREATEST_ASSERT_EQ(NULL, s3->prev);
+	s = LIST_FIRST(&irc.servers);
+	GREATEST_ASSERT_EQ(s, s3);
+	s = LIST_NEXT(s, link);
+	GREATEST_ASSERT(!s);
 
 	/* irc.servers -> [s3] */
 	/* irc.servers -> NULL */
-	irc_bot_remove_server(s3->name);
-	GREATEST_ASSERT_EQ(0, irc.serversz);
-	GREATEST_ASSERT_EQ(NULL, irc.servers);
+	irc_bot_server_remove(s3->name);
 	GREATEST_ASSERT_EQ(1, s1->refc);
 	GREATEST_ASSERT_EQ(1, s2->refc);
 	GREATEST_ASSERT_EQ(1, s3->refc);
-	GREATEST_ASSERT_EQ(NULL, s3->next);
-	GREATEST_ASSERT_EQ(NULL, s3->prev);
+	s = LIST_FIRST(&irc.servers);
+	GREATEST_ASSERT(!s);
 
 	irc_server_decref(s1);
 	irc_server_decref(s2);
@@ -157,31 +139,24 @@
 {
 	struct irc_server *s1, *s2, *s3;
 
-	s1 = server_new("1");
-	s2 = server_new("2");
-	s3 = server_new("3");
+	s1 = irc_server_new("1", "x", "x", "x", "localhost", 6667);
+	s2 = irc_server_new("2", "x", "x", "x", "localhost", 6667);
+	s3 = irc_server_new("3", "x", "x", "x", "localhost", 6667);
 
 	/* Protect deletion from irc_bot_remove_server. */
 	irc_server_incref(s1);
 	irc_server_incref(s2);
 	irc_server_incref(s3);
 
-	irc_bot_add_server(s1);
-	irc_bot_add_server(s2);
-	irc_bot_add_server(s3);
-	irc_bot_clear_servers();
+	irc_bot_server_add(s1);
+	irc_bot_server_add(s2);
+	irc_bot_server_add(s3);
+	irc_bot_server_clear();
 
-	GREATEST_ASSERT_EQ(0, irc.serversz);
-	GREATEST_ASSERT_EQ(NULL, irc.servers);
 	GREATEST_ASSERT_EQ(1, s1->refc);
-	GREATEST_ASSERT_EQ(NULL, s1->next);
-	GREATEST_ASSERT_EQ(NULL, s1->prev);
 	GREATEST_ASSERT_EQ(1, s2->refc);
-	GREATEST_ASSERT_EQ(NULL, s2->next);
-	GREATEST_ASSERT_EQ(NULL, s2->prev);
 	GREATEST_ASSERT_EQ(1, s3->refc);
-	GREATEST_ASSERT_EQ(NULL, s3->next);
-	GREATEST_ASSERT_EQ(NULL, s3->prev);
+	GREATEST_ASSERT(!LIST_FIRST(&irc.servers));
 	GREATEST_PASS();
 }
 
--- a/tests/test-channel.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/tests/test-channel.c	Thu Jan 21 23:15:20 2021 +0100
@@ -24,33 +24,51 @@
 GREATEST_TEST
 basics_add(void)
 {
-	struct irc_channel ch = {0};
+	struct irc_channel *ch;
+	struct irc_channel_user *user;
+
+	ch = irc_channel_new("#test", NULL, true);
+	GREATEST_ASSERT_STR_EQ("#test", ch->name);
+	GREATEST_ASSERT_STR_EQ("", ch->password);
+	GREATEST_ASSERT(ch->joined);
 
-	irc_channel_add(&ch, "markand", '@');
-	GREATEST_ASSERT_EQ(ch.usersz, 1U);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
+	irc_channel_add(ch, "markand", 'o', '@');
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_add(&ch, "markand", '@');
-	GREATEST_ASSERT_EQ(ch.usersz, 1U);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
+	irc_channel_add(ch, "markand", '+', '@');
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_add(&ch, "jean", 0);
-	GREATEST_ASSERT_EQ(ch.usersz, 2U);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, 0);
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "jean");
-	GREATEST_ASSERT_EQ(ch.users[1].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[1].nickname, "markand");
+	irc_channel_add(ch, "jean", 'h', '+');
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ('h', user->mode);
+	GREATEST_ASSERT_EQ('+', user->symbol);
+	GREATEST_ASSERT_STR_EQ("jean", user->nickname);
+	user = LIST_NEXT(user, link);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_add(&ch, "zoe", 0);
-	GREATEST_ASSERT_EQ(ch.usersz, 3U);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, 0);
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "jean");
-	GREATEST_ASSERT_EQ(ch.users[1].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[1].nickname, "markand");
-	GREATEST_ASSERT_EQ(ch.users[2].mode, 0);
-	GREATEST_ASSERT_STR_EQ(ch.users[2].nickname, "zoe");
+	irc_channel_add(ch, "zoe", 0, 0);
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ(0, user->mode);
+	GREATEST_ASSERT_EQ(0, user->symbol);
+	GREATEST_ASSERT_STR_EQ("zoe", user->nickname);
+	user = LIST_NEXT(user, link);
+	GREATEST_ASSERT_EQ('h', user->mode);
+	GREATEST_ASSERT_EQ('+', user->symbol);
+	GREATEST_ASSERT_STR_EQ("jean", user->nickname);
+	user = LIST_NEXT(user, link);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
+
+	irc_channel_finish(ch);
 
 	GREATEST_PASS();
 }
@@ -58,58 +76,65 @@
 GREATEST_TEST
 basics_remove(void)
 {
-	struct irc_channel ch = {0};
+	struct irc_channel *ch;
+	struct irc_channel_user *user;
 
-	irc_channel_add(&ch, "markand", '@');
-	irc_channel_add(&ch, "jean", 0);
-	irc_channel_add(&ch, "zoe", 0);
+	ch = irc_channel_new("#test", NULL, true);
+
+	irc_channel_add(ch, "markand", 'o', '@');
+	irc_channel_add(ch, "jean", 0, 0);
+	irc_channel_add(ch, "zoe", 0, 0);
 
-	irc_channel_remove(&ch, "jean");
-	GREATEST_ASSERT_EQ(ch.usersz, 2U);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
-	GREATEST_ASSERT_EQ(ch.users[1].mode, 0);
-	GREATEST_ASSERT_STR_EQ(ch.users[1].nickname, "zoe");
+	irc_channel_remove(ch, "jean");
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ(0, user->mode);
+	GREATEST_ASSERT_EQ(0, user->symbol);
+	GREATEST_ASSERT_STR_EQ("zoe", user->nickname);
+	user = LIST_NEXT(user, link);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_remove(&ch, "zoe");
-	GREATEST_ASSERT_EQ(ch.usersz, 1U);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
+	irc_channel_remove(ch, "zoe");
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_remove(&ch, "markand");
-	GREATEST_ASSERT_EQ(ch.usersz, 0U);
-	GREATEST_ASSERT(!ch.users);
+	irc_channel_remove(ch, "markand");
+	GREATEST_ASSERT(!LIST_FIRST(&ch->users));
+
+	irc_channel_finish(ch);
 
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
-basics_set_mode(void)
+basics_update(void)
 {
-	struct irc_channel ch = {0};
+	struct irc_channel *ch;
+	struct irc_channel_user *user;
 
-	irc_channel_add(&ch, "jean", '@');
-	irc_channel_set_user_mode(&ch, "jean", '+');
-	irc_channel_set_user_mode(&ch, "nobody", '+');
+	ch = irc_channel_new("#test", NULL, true);
 
-	GREATEST_ASSERT_EQ(ch.usersz, 1);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, '+');
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "jean");
-	GREATEST_PASS();
-}
+	irc_channel_add(ch, "markand", 'o', '@');
+	irc_channel_add(ch, "jean", 0, 0);
+	irc_channel_add(ch, "zoe", 0, 0);
+	
+	irc_channel_update(ch, "zoe", NULL, 'o', '@');
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("zoe", user->nickname);
 
-GREATEST_TEST
-basics_set_nick(void)
-{
-	struct irc_channel ch = {0};
+	irc_channel_update(ch, "zoe", "eoz", -1, -1);
+	user = LIST_FIRST(&ch->users);
+	GREATEST_ASSERT_EQ('o', user->mode);
+	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_STR_EQ("eoz", user->nickname);
 
-	irc_channel_add(&ch, "jean", '@');
-	irc_channel_set_user_nick(&ch, "jean", "francis");
-	irc_channel_set_user_nick(&ch, "nobody", "francis");
+	irc_channel_finish(ch);
 
-	GREATEST_ASSERT_EQ(ch.usersz, 1);
-	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
-	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "francis");
 	GREATEST_PASS();
 }
 
@@ -117,8 +142,7 @@
 {
 	GREATEST_RUN_TEST(basics_add);
 	GREATEST_RUN_TEST(basics_remove);
-	GREATEST_RUN_TEST(basics_set_mode);
-	GREATEST_RUN_TEST(basics_set_nick);
+	GREATEST_RUN_TEST(basics_update);
 }
 
 GREATEST_MAIN_DEFS();
--- a/tests/test-rule.c	Thu Jan 21 15:34:25 2021 +0100
+++ b/tests/test-rule.c	Thu Jan 21 23:15:20 2021 +0100
@@ -27,50 +27,61 @@
 {
 	(void)udata;
 
-	memset(&irc, 0, sizeof (irc));
+	irc_bot_rule_clear();
 }
 
 GREATEST_TEST
 basics_insert(void)
 {
-	struct irc_rule r1 = {0}, r2 = {0};
+	struct irc_rule *r, *r1, *r2;
 
-	irc_rule_add(r1.servers, "s1");
-	irc_rule_add(r2.servers, "s2");
+	r1 = irc_rule_new(IRC_RULE_DROP);
+	r2 = irc_rule_new(IRC_RULE_DROP);
+
+	irc_rule_add(r1->servers, "s1");
+	irc_rule_add(r2->servers, "s2");
 
-	irc_bot_insert_rule(&r1, 0);
-	irc_bot_insert_rule(&r2, 0);
+	irc_bot_rule_insert(r1, 0);
+	irc_bot_rule_insert(r2, 0);
 
-	GREATEST_ASSERT_EQ(2U, irc.rulesz);
-	GREATEST_ASSERT(memcmp(&irc.rules[0], &r2, sizeof (r2)) == 0);
-	GREATEST_ASSERT(memcmp(&irc.rules[1], &r1, sizeof (r1)) == 0);
+	r = TAILQ_FIRST(&irc.rules);
+	GREATEST_ASSERT_EQ(r2, r);
+	r = TAILQ_NEXT(r, link);
+	GREATEST_ASSERT_EQ(r1, r);
+
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 basics_remove(void)
 {
-	struct irc_rule r1 = {0}, r2 = {0}, r3 = {0};
+	struct irc_rule *r, *r1, *r2, *r3;
 
-	irc_rule_add(r1.servers, "s1");
-	irc_rule_add(r2.servers, "s2");
-	irc_rule_add(r3.servers, "s3");
+	r1 = irc_rule_new(IRC_RULE_DROP);
+	r2 = irc_rule_new(IRC_RULE_DROP);
+	r3 = irc_rule_new(IRC_RULE_DROP);
 
-	irc_bot_insert_rule(&r1, -1);
-	irc_bot_insert_rule(&r2, -1);
-	irc_bot_insert_rule(&r3, -1);
-	irc_bot_remove_rule(1);
+	irc_rule_add(r1->servers, "s1");
+	irc_rule_add(r2->servers, "s2");
+	irc_rule_add(r3->servers, "s3");
 
-	GREATEST_ASSERT_EQ(2U, irc.rulesz);
-	GREATEST_ASSERT(memcmp(&irc.rules[0], &r1, sizeof (r1)) == 0);
-	GREATEST_ASSERT(memcmp(&irc.rules[1], &r3, sizeof (r3)) == 0);
+	irc_bot_rule_insert(r1, -1);
+	irc_bot_rule_insert(r2, -1);
+	irc_bot_rule_insert(r3, -1);
+	irc_bot_rule_remove(1);
 
-	irc_bot_remove_rule(1);
-	GREATEST_ASSERT_EQ(1U, irc.rulesz);
-	GREATEST_ASSERT(memcmp(&irc.rules[0], &r1, sizeof (r1)) == 0);
+	r = TAILQ_FIRST(&irc.rules);
+	GREATEST_ASSERT_EQ(r1, r);
+	r = TAILQ_NEXT(r, link);
+	GREATEST_ASSERT_EQ(r3, r);
 
-	irc_bot_remove_rule(0);
-	GREATEST_ASSERT_EQ(0U, irc.rulesz);
+	irc_bot_rule_remove(1);
+	r = TAILQ_FIRST(&irc.rules);
+	GREATEST_ASSERT_EQ(r1, r);
+
+	irc_bot_rule_remove(0);
+	r = TAILQ_FIRST(&irc.rules);
+	GREATEST_ASSERT(!r);
 
 	GREATEST_PASS();
 }
@@ -121,38 +132,37 @@
 {
 	(void)udata;
 
-	struct irc_rule r1 = {0}, r2 = {0}, r31 = {0}, r32 = {0};
+	struct irc_rule *r1, *r2, *r31, *r32;
 
-	/* Deinit irccd first. */
-	memset(&irc, 0, sizeof (irc));
+	irc_bot_rule_clear();
 
 	/* #1 */
-	r1.action = IRC_RULE_DROP;
-	irc_rule_add(r1.channels, "#staff");
-	irc_rule_add(r1.events, "onCommand");
-	irc_bot_insert_rule(&r1, -1);
+	r1 = irc_rule_new(IRC_RULE_DROP);
+	irc_rule_add(r1->channels, "#staff");
+	irc_rule_add(r1->events, "onCommand");
+	irc_bot_rule_insert(r1, -1);
 
 	/* #2 */
-	r2.action = IRC_RULE_ACCEPT;
-	irc_rule_add(r2.servers, "unsafe");
-	irc_rule_add(r2.channels, "#staff");
-	irc_rule_add(r2.events, "onCommand");
-	irc_bot_insert_rule(&r2, -1);
+	r2 = irc_rule_new(IRC_RULE_ACCEPT);
+	irc_rule_add(r2->servers, "unsafe");
+	irc_rule_add(r2->channels, "#staff");
+	irc_rule_add(r2->events, "onCommand");
+	irc_bot_rule_insert(r2, -1);
 
 	/* #3-1 */
-	r31.action = IRC_RULE_DROP;
-	irc_rule_add(r31.plugins, "game");
-	irc_bot_insert_rule(&r31, -1);
+	r31 = irc_rule_new(IRC_RULE_DROP);
+	irc_rule_add(r31->plugins, "game");
+	irc_bot_rule_insert(r31, -1);
 
 	/* #3-2 */
-	r32.action = IRC_RULE_ACCEPT;
-	irc_rule_add(r32.servers, "malikania");
-	irc_rule_add(r32.servers, "localhost");
-	irc_rule_add(r32.channels, "#games");
-	irc_rule_add(r32.plugins, "game");
-	irc_rule_add(r32.events, "onCommand");
-	irc_rule_add(r32.events, "onMessage");
-	irc_bot_insert_rule(&r32, -1);
+	r32 = irc_rule_new(IRC_RULE_ACCEPT);
+	irc_rule_add(r32->servers, "malikania");
+	irc_rule_add(r32->servers, "localhost");
+	irc_rule_add(r32->channels, "#games");
+	irc_rule_add(r32->plugins, "game");
+	irc_rule_add(r32->events, "onCommand");
+	irc_rule_add(r32->events, "onMessage");
+	irc_bot_rule_insert(r32, -1);
 };
 
 GREATEST_TEST
@@ -239,19 +249,19 @@
 solve_match7(void)
 {
 	/* Allowed */
-	GREATEST_ASSERT(irc_rule_matchlist(irc.rules, irc.rulesz, "malikania", "#staff", "", "a", "onMessage"));
+	GREATEST_ASSERT(irc_rule_matchlist(&irc.rules, "malikania", "#staff", "", "a", "onMessage"));
 
 	/* Allowed */
-	GREATEST_ASSERT(irc_rule_matchlist(irc.rules, irc.rulesz, "freenode", "#staff", "", "b", "onTopic"));
+	GREATEST_ASSERT(irc_rule_matchlist(&irc.rules, "freenode", "#staff", "", "b", "onTopic"));
 
 	/* Not allowed */
-	GREATEST_ASSERT(!irc_rule_matchlist(irc.rules, irc.rulesz, "malikania", "#staff", "", "", "onCommand"));
+	GREATEST_ASSERT(!irc_rule_matchlist(&irc.rules, "malikania", "#staff", "", "", "onCommand"));
 
 	/* Not allowed */
-	GREATEST_ASSERT(!irc_rule_matchlist(irc.rules, irc.rulesz, "freenode", "#staff", "", "c", "onCommand"));
+	GREATEST_ASSERT(!irc_rule_matchlist(&irc.rules, "freenode", "#staff", "", "c", "onCommand"));
 
 	/* Allowed */
-	GREATEST_ASSERT(irc_rule_matchlist(irc.rules, irc.rulesz, "unsafe", "#staff", "", "c", "onCommand"));
+	GREATEST_ASSERT(irc_rule_matchlist(&irc.rules, "unsafe", "#staff", "", "c", "onCommand"));
 
 	GREATEST_PASS();
 }
@@ -260,29 +270,29 @@
 solve_match8(void)
 {
 	/* Allowed */
-	GREATEST_ASSERT(irc_rule_matchlist(irc.rules, irc.rulesz, "malikania", "#games", "", "game", "onMessage"));
+	GREATEST_ASSERT(irc_rule_matchlist(&irc.rules, "malikania", "#games", "", "game", "onMessage"));
 
 	/* Allowed */
-	GREATEST_ASSERT(irc_rule_matchlist(irc.rules, irc.rulesz, "localhost", "#games", "", "game", "onMessage"));
+	GREATEST_ASSERT(irc_rule_matchlist(&irc.rules, "localhost", "#games", "", "game", "onMessage"));
 
 	/* Allowed */
-	GREATEST_ASSERT(irc_rule_matchlist(irc.rules, irc.rulesz, "malikania", "#games", "", "game", "onCommand"));
+	GREATEST_ASSERT(irc_rule_matchlist(&irc.rules, "malikania", "#games", "", "game", "onCommand"));
 
 	/* Not allowed */
-	GREATEST_ASSERT(!irc_rule_matchlist(irc.rules, irc.rulesz, "malikania", "#games", "", "game", "onQuery"));
+	GREATEST_ASSERT(!irc_rule_matchlist(&irc.rules, "malikania", "#games", "", "game", "onQuery"));
 
 	/* Not allowed */
-	GREATEST_ASSERT(!irc_rule_matchlist(irc.rules, irc.rulesz, "freenode", "#no", "", "game", "onMessage"));
+	GREATEST_ASSERT(!irc_rule_matchlist(&irc.rules, "freenode", "#no", "", "game", "onMessage"));
 
 	/* Not allowed */
-	GREATEST_ASSERT(!irc_rule_matchlist(irc.rules, irc.rulesz, "malikania", "#test", "", "game", "onMessage"));
+	GREATEST_ASSERT(!irc_rule_matchlist(&irc.rules, "malikania", "#test", "", "game", "onMessage"));
 	GREATEST_PASS();
 }
 
 GREATEST_TEST
 solve_match9(void)
 {
-	GREATEST_ASSERT(!irc_rule_matchlist(irc.rules, irc.rulesz, "MALIKANIA", "#STAFF", "", "SYSTEM", "onCommand"));
+	GREATEST_ASSERT(!irc_rule_matchlist(&irc.rules, "MALIKANIA", "#STAFF", "", "SYSTEM", "onCommand"));
 	GREATEST_PASS();
 }