changeset 1004:3ea3361f0fc7

irccd: now track modes
author David Demelier <markand@malikania.fr>
date Tue, 16 Feb 2021 18:37:22 +0100
parents bbb3d3075ec2
children ea9cf916330d
files .clang .clang-format .clang-tidy .editorconfig .hgignore .hgsigs .hgtags CHANGES.md CMakeLists.txt CONTRIBUTE.md CREDITS.md INSTALL.md LICENSE.md MIGRATING.md README.md STYLE.md cmake/IrccdDefinePlugin.cmake extern/LICENSE.libduktape.txt extern/LICENSE.libgreatest.txt extern/VERSION.libduktape.txt extern/VERSION.libgreatest.txt extern/libcompat/CHANGES.md extern/libcompat/CMakeLists.txt extern/libcompat/CREDITS.md extern/libcompat/LICENSE.md extern/libcompat/README.md extern/libcompat/extern/queue/sys/queue.h extern/libcompat/src/basename.c extern/libcompat/src/compat.c extern/libcompat/src/compat.h.in extern/libcompat/src/dirname.c extern/libcompat/src/err.c extern/libcompat/src/errc.c extern/libcompat/src/errx.c extern/libcompat/src/getopt.c extern/libcompat/src/getprogname.c extern/libcompat/src/pledge.c extern/libcompat/src/reallocarray.c extern/libcompat/src/recallocarray.c extern/libcompat/src/setprogname.c extern/libcompat/src/strdup.c extern/libcompat/src/strlcat.c extern/libcompat/src/strlcpy.c extern/libcompat/src/strndup.c extern/libcompat/src/strnlen.c extern/libcompat/src/strsep.c extern/libcompat/src/strtok_r.c extern/libcompat/src/strtonum.c extern/libcompat/src/verr.c extern/libcompat/src/verrc.c extern/libcompat/src/verrx.c extern/libcompat/src/vsyslog.c extern/libcompat/src/vwarn.c extern/libcompat/src/vwarnc.c extern/libcompat/src/vwarnx.c extern/libcompat/src/warn.c extern/libcompat/src/warnc.c extern/libcompat/src/warnx.c extern/libcompat/tests/CMakeLists.txt extern/libcompat/tests/test-bsd.c extern/libcompat/tests/test-dirent.c extern/libcompat/tests/test-dlfcn.c extern/libcompat/tests/test-posix.c extern/libcompat/win/dirent/dirent.h extern/libcompat/win/dlfcn/CMakeLists.txt extern/libcompat/win/dlfcn/dlfcn.c extern/libcompat/win/dlfcn/dlfcn.h extern/libduktape/CMakeLists.txt extern/libduktape/duk_config.h extern/libduktape/duktape.c extern/libduktape/duktape.h extern/libgreatest/CMakeLists.txt extern/libgreatest/greatest.h extern/libketopt/CMakeLists.txt extern/libketopt/ketopt.h irccd/CMakeLists.txt irccd/conf.y irccd/dl-plugin.c irccd/dl-plugin.h irccd/js-plugin.c irccd/js-plugin.h irccd/jsapi-chrono.c irccd/jsapi-chrono.h irccd/jsapi-directory.c irccd/jsapi-directory.h irccd/jsapi-file.c irccd/jsapi-file.h irccd/jsapi-hook.c irccd/jsapi-hook.h irccd/jsapi-irccd.c irccd/jsapi-irccd.h irccd/jsapi-logger.c irccd/jsapi-logger.h irccd/jsapi-plugin.c irccd/jsapi-plugin.h irccd/jsapi-rule.c irccd/jsapi-rule.h irccd/jsapi-server.c irccd/jsapi-server.h irccd/jsapi-system.c irccd/jsapi-system.h irccd/jsapi-timer.c irccd/jsapi-timer.h irccd/jsapi-unicode.c irccd/jsapi-unicode.h irccd/jsapi-util.c irccd/jsapi-util.h irccd/lex.l irccd/main.c irccd/peer.c irccd/peer.h irccd/transport.c irccd/transport.h irccd/unicode.c irccd/unicode.h irccdctl/CMakeLists.txt irccdctl/main.c lib/CMakeLists.txt lib/IrccdConfig.cmake lib/irccd.pc lib/irccd/channel.c lib/irccd/channel.h lib/irccd/config.h.in lib/irccd/conn.c lib/irccd/conn.h lib/irccd/event.c lib/irccd/event.h lib/irccd/hook.c lib/irccd/hook.h lib/irccd/irccd.c lib/irccd/irccd.h lib/irccd/limits.h lib/irccd/log.c lib/irccd/log.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/subst.c lib/irccd/subst.h lib/irccd/util.c lib/irccd/util.h man/CMakeLists.txt man/irccd-api-chrono.3 man/irccd-api-directory.3 man/irccd-api-file.3 man/irccd-api-hook.3 man/irccd-api-logger.3 man/irccd-api-plugin.3 man/irccd-api-rule.3 man/irccd-api-server.3 man/irccd-api-system.3 man/irccd-api-timer.3 man/irccd-api-unicode.3 man/irccd-api-util.3 man/irccd-api.3 man/irccd-ipc.7 man/irccd-templates.7 man/irccd-test.1 man/irccd.1 man/irccd.conf.5 man/irccdctl.1 plugins/CMakeLists.txt plugins/ask/CMakeLists.txt plugins/ask/ask.7 plugins/ask/ask.js plugins/auth/CMakeLists.txt plugins/auth/auth.7 plugins/auth/auth.js plugins/hangman/CMakeLists.txt plugins/hangman/hangman.7 plugins/hangman/hangman.js plugins/history/CMakeLists.txt plugins/history/history.7 plugins/history/history.js plugins/joke/CMakeLists.txt plugins/joke/joke.7 plugins/joke/joke.js plugins/links/CMakeLists.txt plugins/links/links.7 plugins/links/links.c plugins/logger/CMakeLists.txt plugins/logger/logger.7 plugins/logger/logger.js plugins/plugin/CMakeLists.txt plugins/plugin/plugin.7 plugins/plugin/plugin.js plugins/roulette/CMakeLists.txt plugins/roulette/roulette.7 plugins/roulette/roulette.js plugins/tictactoe/CMakeLists.txt plugins/tictactoe/tictactoe.7 plugins/tictactoe/tictactoe.js systemd/irccd.service tests/CMakeLists.txt tests/data/answers.conf tests/data/error.json tests/data/example-dl-plugin.c tests/data/example-plugin.js tests/data/joke/error-empty.json tests/data/joke/error-invalid.json tests/data/joke/error-not-array.json tests/data/joke/error-toobig.json tests/data/joke/jokes.json tests/data/root/file-1.txt tests/data/root/level-1/level-2/file-2.txt tests/data/root/lines.txt tests/data/timer.js tests/data/words-seq.conf tests/data/words.conf tests/test-bot.c tests/test-channel.c tests/test-dl-plugin.c tests/test-event.c tests/test-jsapi-chrono.c tests/test-jsapi-directory.c tests/test-jsapi-file.c tests/test-jsapi-irccd.c tests/test-jsapi-system.c tests/test-jsapi-timer.c tests/test-jsapi-unicode.c tests/test-jsapi-util.c tests/test-log.c tests/test-plugin-ask.c tests/test-plugin-auth.c tests/test-plugin-hangman.c tests/test-plugin-history.c tests/test-plugin-joke.c tests/test-plugin-logger.c tests/test-plugin-plugin.c tests/test-plugin-tictactoe.c tests/test-rule.c tests/test-subst.c tests/test-util.c
diffstat 21 files changed, 394 insertions(+), 288 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.md	Sun Feb 14 10:11:03 2021 +0100
+++ b/CHANGES.md	Tue Feb 16 18:37:22 2021 +0100
@@ -2,68 +2,72 @@
 =========================
 
 irccd 4.0.0 ????-??-??
-----------------------
+======================
 
 This is a major release. See MIGRATING.md file for more information.
 
 The biggest change is the rewrite from C++ to C. The only runtime dependency
 required is OpenSSL (if built with SSL support).
 
-irccd:
+irccd
+-----
 
 - Irccd keeps track of nicknames in channels by capturing join/part/kick and
   mode changes. It is now more convenient from the plugins to quickly inspect if
   someone is present on a channel.
 
-irccdctl:
+irccdctl
+--------
 
 - Commands `plugin-reload` and `plugin-unload` can be invoked without arguments.
 - New `plugin-template` and `plugin-path` command which are synonyms of
   `plugin-config` but for templates and paths respectively.
 
-plugins:
+plugins
+-------
 
 - tictactoe: now has a timeout in case of inactivity.
 
-misc:
+misc
+----
 
 - Split irccd-api manual page into individual irccd-api-<module> for a better
   readability.
 - New `irccd.conf` and `irccdctl.conf` syntax.
 
-network API:
+network API
+-----------
 
 - Network protocol uses plain text again.
 - Transport uses clear UNIX sockets only without passwords.
 
-javascript API:
+javascript API
+--------------
 
 - Brand new Irccd.Rule API to inspect and manage rules.
 - Brand new Irccd.Hook API to inspect and manage hooks.
 
 irccd 3.1.1 2021-01-04
-----------------------
+======================
 
 - Synchronize `ping-timeout` option in `[server]` to 1800 seconds by default,
 - Enable `auto-reconnect` option in `[server]` by default as specified in the
   manual page.
 
 irccd 3.1.0 2020-07-03
-----------------------
-
-irccd:
+======================
 
 - Added a new hook system. Hooks consist of an alternative approach to plugins
   to extend irccd in any language (#2342).
 
 irccd 3.0.3 2019-10-06
-----------------------
+======================
 
 - Fix errors in irccdctl.conf example file (#2398),
 - Add example of password in irccdctl.conf and irccd.conf (#2407).
 
 irccd 3.0.2 2019-09-22
-----------------------
+======================
 
 - Added *IRCCD_WITH_JS* CMake variable in irccd package (#2340),
 - Fixed trailing CTCP escape code (#2339),
@@ -72,18 +76,19 @@
 - Fixed invalid system configuration directory (#2263).
 
 irccd 3.0.1 2019-09-01
-----------------------
+======================
 
 - Fixed an invalid template escape sequence (#2250),
 - Updated the default configuration files (#2249),
 - Fix RPATH handling for private libraries like Duktape (#2257).
 
 irccd 3.0.0 2019-08-15
-----------------------
+======================
 
 This is a major release. See MIGRATING.md file for more information.
 
-irccd:
+irccd
+-----
 
 - New sections `[paths]` and `[paths.plugin]` have been added to control
   standard paths for both irccd and plugins (#611),
@@ -96,51 +101,59 @@
 - Section `[format]` is renamed to `[templates]` (#1671),
 - New commands are available as irccd arguments `info` and `version` (#1672).
 
-irccdctl:
+irccdctl
+--------
 
 - New option `ipv4` in `[connect]` (#945),
 - New option `-o` in `rule-add` (#947),
 - New option `-o` and `-O` in `rule-edit` (#947).
 
-irccd-test:
+irccd-test
+----------
 
 - A brand new `irccd-test` program has been added to tests plugins on the
   command line (#569).
 
-cmake:
+cmake
+-----
 
 - CMake no longer create a fake installation directory while building (#674),
 - All targets are placed into the `bin` directory while building (#715).
 
-network API:
+network API
+-----------
 
 - Network commands return an error code instead of a string (#739).
 
-javascript API:
+javascript API
+--------------
 
 - The Irccd.Timer API now runs on top of Boost.Asio and no longer have custom
   buggy code (#595),
 - New Irccd.Server.isSelf function (#735).
 
-internal:
+internal
+--------
 
 - The code is now based on Boost for many internal parts of the core, (#593),
   (#594), (#595), (#681), (#697),
 - The libircclient has been replaced by a simple homemade library (#581).
 
-misc:
+misc
+----
 
 - The documentation is in pure manual pages now (#1674),
 - All command line options are now in short form only (#1673).
 
-plugins:
+plugins
+-------
 
 - Introduce brand new joke plugin (#609),
 - Introduce brand new tictactoe plugin (#393),
 - Introduce brand new links plugin (#872).
 
 irccd 2.2.0 2017-09-26
-----------------------
+======================
 
 - Add new Irccd.Util.cut function (#635),
 - Add new irccdctl commands to edit rules (#641),
@@ -149,14 +162,14 @@
 - Fix identity.ctcp-version option (#690).
 
 irccd 2.1.3 2017-07-28
-----------------------
+======================
 
 - Rules are now case insensitive (#645),
 - Plugin hangman, history and logger are now case insensitive (#642),
 - Plugin hangman: fix successive word selection (#644).
 
 irccd 2.1.2 2017-06-02
-----------------------
+======================
 
 - Fix SSL initialization error in libircclient (#653),
 - Fix various SSL warnings (#652),
@@ -164,50 +177,57 @@
 - Fix case sensitivity in hangman and roulette (#642).
 
 irccd 2.1.1 2017-03-07
-----------------------
+======================
 
 - Fix invalid documented option transport.family,
 - Fix error when logs.type is set to console,
 - Fix invalid IPV6\_V6ONLY option in transports.
 
 irccd 2.1.0 2017-02-01
-----------------------
+======================
 
-irccd:
+irccd
+-----
 
 - Add SSL support in transports,
 - Add authentication support in transports,
 - Fix a warning about daemon on macOS.
 
-javascript API:
+javascript API
+--------------
 
 - New Irccd.File.lines function,
 - Various improvements in Irccd.File API.
 
-plugins:
+plugins
+-------
 
 - Add new format section for plugins,
 - Add unit tests for plugins.
 
-irccdctl:
+irccdctl
+--------
 
 - Added brand new plugin-config command,
 - Added aliases,
 - Added unit tests for irccdctl commands.
 
-libraries:
+libraries
+---------
 
 - Replaced jansson with Niels Lohmann's JSON library,
 - Updated Duktape to 1.5.1.
 
-misc:
+misc
+----
 
 - Patterns can now use shell escape sequences,
 - Added .editorconfig file,
 - Split documentation into topics,
 - The code is now split into several individual libraries.
 
-windows:
+windows
+-------
 
 - Get rid of QtIFW and uses NSIS, WIX on Windows,
 - Installer have components,
@@ -215,22 +235,22 @@
 - Added better support for cross-compiling using MinGW.
 
 irccd 2.0.3 2016-11-01
-----------------------
+======================
 
 - Fix various errors in logger plugin,
 - Fix quakenet support in auth plugin.
 
 irccd 2.0.2 2016-04-19
-----------------------
+======================
 
 - Fix CMake error preventing installation of irccd and irccdctl.
 
 irccd 2.0.1 2016-03-13
-----------------------
+======================
 
 - Plugin plugin: fix invalid usage.
 
 irccd 2.0.0 2016-03-01
-----------------------
+======================
 
 - Initial 2.0.0 release.
--- a/CREDITS.md	Sun Feb 14 10:11:03 2021 +0100
+++ b/CREDITS.md	Tue Feb 16 18:37:22 2021 +0100
@@ -16,3 +16,17 @@
 
 - uriparser, https://uriparser.github.io
   Simple URI parser.
+
+Individual
+----------
+
+People for using, commenting and reporting errors:
+
+- Pierre Choffet,
+- Yoan Giraud,
+- Léo Villeveygoux.
+
+Some people on the *##irc* channel on freenode for their detailed information:
+
+- Andrio,
+- jackal.
--- a/MIGRATING.md	Sun Feb 14 10:11:03 2021 +0100
+++ b/MIGRATING.md	Tue Feb 16 18:37:22 2021 +0100
@@ -78,6 +78,16 @@
   which must be string (password is optional).
 - The property `commandChar` which is provided in both the `Server` constructor
   and the `Server.info` returned object has been renamed to `prefix`.
+- The event `onMode` now takes four arguments: server, channel, mode and list
+  of arguments to the mode. The previous signature was mostly unusable.
+
+Plugins
+-------
+
+**logger**
+
+- Due to the `onMode` change the template `mode` no longer takes `limit`,
+  `user` and `mask` but a string `args` instead.
 
 Migrating from 2.x to 3.x
 =========================
--- a/README.md	Sun Feb 14 10:11:03 2021 +0100
+++ b/README.md	Tue Feb 16 18:37:22 2021 +0100
@@ -33,10 +33,3 @@
 ------
 
 The irccd application was written by David Demelier <markand@malikania.fr>
-
-Contributors
-------------
-
-- Pierre Choffet,
-- Yoan Giraud,
-- Léo Villeveygoux.
--- a/irccd/js-plugin.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/irccd/js-plugin.c	Tue Feb 16 18:37:22 2021 +0100
@@ -96,6 +96,19 @@
 }
 
 static void
+push_modes(duk_context *ctx, char **modes)
+{
+	size_t i = 0;
+
+	duk_push_array(ctx);
+
+	for (char **mode = modes; mode && *mode; ++mode) {
+		duk_push_string(ctx, *mode);
+		duk_put_prop_index(ctx, -2, i++);
+	}
+}
+
+static void
 push_names(duk_context *ctx, const struct irc_event *ev)
 {
 	const char *token;
@@ -104,7 +117,7 @@
 	duk_push_array(ctx);
 
 	for (size_t i = 0; (token = strtok_r(p, " ", &p)); ++i) {
-		irc_server_strip(ev->server, &token, NULL, NULL);
+		irc_server_strip(ev->server, &token);
 		duk_push_string(ctx, token);
 		duk_put_prop_index(ctx, -2, i);
 	}
@@ -113,9 +126,6 @@
 static void
 push_whois(duk_context *ctx, const struct irc_event *ev)
 {
-	const char *token;
-	char *p = ev->whois.channels;
-
 	duk_push_object(ctx);
 	duk_push_string(ctx, ev->whois.nickname);
 	duk_put_prop_string(ctx, -2, "nickname");
@@ -126,19 +136,16 @@
 	duk_push_string(ctx, ev->whois.hostname);
 	duk_put_prop_string(ctx, -2, "hostname");
 	duk_push_array(ctx);
-	for (size_t i = 0; (token = strtok_r(p, " ", &p)); ++i) {
-		char mode = 0, prefix = 0;
 
-		irc_server_strip(ev->server, &token, &mode, &prefix);
+	for (size_t i = 0; i < ev->whois.channelsz; ++i) {
 		duk_push_object(ctx);
-		duk_push_string(ctx, token);
+		duk_push_string(ctx, ev->whois.channels[i].name);
 		duk_put_prop_string(ctx, -2, "channel");
-		duk_push_sprintf(ctx, "%c", mode);
-		duk_put_prop_string(ctx, -2, "mode");
-		duk_push_sprintf(ctx, "%c", prefix);
-		duk_put_prop_string(ctx, -2, "prefix");
+		duk_push_int(ctx, ev->whois.channels[i].modes);
+		duk_put_prop_string(ctx, -2, "modes");
 		duk_put_prop_index(ctx, -2, i);
 	}
+
 	duk_put_prop_string(ctx, -2, "channels");
 }
 
@@ -373,9 +380,8 @@
 		    ev->message.channel, ev->message.message);
 		break;
 	case IRC_EVENT_MODE:
-		call(plg, "onMode", "Ss sss ss", ev->server, ev->mode.origin,
-		    ev->mode.channel, ev->mode.mode, ev->mode.limit,
-		    ev->mode.user, ev->mode.mask);
+		call(plg, "onMode", "Ss ssx", ev->server, ev->mode.origin,
+		    ev->mode.channel, ev->mode.mode, push_modes, ev->mode.args);
 		break;
 	case IRC_EVENT_NAMES:
 		call(plg, "onNames", "Ss x", ev->server, ev->names.channel,
--- a/irccd/jsapi-server.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/irccd/jsapi-server.c	Tue Feb 16 18:37:22 2021 +0100
@@ -167,6 +167,24 @@
 	duk_push_string(ctx, s->ident.username);
 	duk_put_prop_string(ctx, -2, "username");
 
+	/* Prefixes. */
+	duk_push_array(ctx);
+
+	for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes); ++i) {
+		if (s->params.prefixes[i].mode == 0)
+			continue;
+
+		duk_push_object(ctx);
+		duk_push_string(ctx, (char []) { s->params.prefixes[i].mode, '\0' });
+		duk_put_prop_string(ctx, -2, "mode");
+		duk_push_string(ctx, (char []) { s->params.prefixes[i].symbol, '\0' });
+		duk_put_prop_string(ctx, -2, "symbol");
+		duk_put_prop_index(ctx, -2, i);
+	}
+
+	duk_put_prop_string(ctx, -2, "prefixes");
+
+	/* Channels. */
 	duk_push_array(ctx);
 
 	LIST_FOREACH(c, &s->channels, link) {
@@ -181,11 +199,8 @@
 			duk_push_object(ctx);
 			duk_push_string(ctx, u->nickname);
 			duk_put_prop_string(ctx, -2, "nickname");
-			if (u->mode)
-				duk_push_sprintf(ctx, "%c", u->mode);
-			else
-				duk_push_null(ctx);
-			duk_put_prop_string(ctx, -2, "mode");
+			duk_push_int(ctx, u->modes);
+			duk_put_prop_string(ctx, -2, "modes");
 			duk_put_prop_index(ctx, -2, ui++);
 		}
 
--- a/lib/irccd/channel.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/channel.c	Tue Feb 16 18:37:22 2021 +0100
@@ -24,18 +24,6 @@
 #include "compat.h"
 #include "util.h"
 
-static inline struct irc_channel_user *
-find(const struct irc_channel *ch, const char *nickname)
-{
-	struct irc_channel_user *u;
-
-	LIST_FOREACH(u, &ch->users, link)
-		if (strcmp(u->nickname, nickname) == 0)
-			return u;
-
-	return NULL;
-}
-
 struct irc_channel *
 irc_channel_new(const char *name, const char *password, int joined)
 {
@@ -55,44 +43,33 @@
 }
 
 void
-irc_channel_add(struct irc_channel *ch, const char *nickname, char mode, char symbol)
+irc_channel_add(struct irc_channel *ch, const char *nickname, int modes)
 {
 	assert(ch);
 	assert(nickname);
 
 	struct irc_channel_user *user;
 
-	if (find(ch, nickname))
+	if (irc_channel_find(ch, nickname))
 		return;
 
 	user = irc_util_malloc(sizeof (*user));
-	user->mode = mode;
-	user->symbol = symbol;
+	user->modes = modes;
 	strlcpy(user->nickname, nickname, sizeof (user->nickname));
 
 	LIST_INSERT_HEAD(&ch->users, user, link);
 }
 
-void
-irc_channel_update(struct irc_channel *ch,
-                   const char *nickname,
-                   const char *newnickname,
-                   char mode,
-                   char symbol)
+struct irc_channel_user *
+irc_channel_find(struct irc_channel *ch, const char *nickname)
 {
-	assert(ch);
-	assert(nickname);
+	struct irc_channel_user *u;
 
-	struct irc_channel_user *user;
+	LIST_FOREACH(u, &ch->users, link)
+		if (strcmp(u->nickname, nickname) == 0)
+			return u;
 
-	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;
-		}
-	}
+	return NULL;
 }
 
 void
@@ -115,8 +92,10 @@
 
 	struct irc_channel_user *user;
 
-	if ((user = find(ch, nick)))
+	if ((user = irc_channel_find(ch, nick))) {
 		LIST_REMOVE(user, link);
+		free(user);
+	}
 }
 
 void
--- a/lib/irccd/channel.h	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/channel.h	Tue Feb 16 18:37:22 2021 +0100
@@ -26,8 +26,7 @@
 
 struct irc_channel_user {
 	char nickname[IRC_NICKNAME_LEN];
-	char mode;
-	char symbol;
+	int modes;
 	LIST_ENTRY(irc_channel_user) link;
 };
 
@@ -45,10 +44,10 @@
 irc_channel_new(const char *, const char *, int);
 
 void
-irc_channel_add(struct irc_channel *, const char *, char, char);
+irc_channel_add(struct irc_channel *, const char *, int);
 
-void
-irc_channel_update(struct irc_channel *, const char *, const char *, char, char);
+struct irc_channel_user *
+irc_channel_find(struct irc_channel *, const char *);
 
 void
 irc_channel_clear(struct irc_channel *);
--- a/lib/irccd/conn.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/conn.c	Tue Feb 16 18:37:22 2021 +0100
@@ -27,6 +27,8 @@
 
 #include "compat.h"
 #include "conn.h"
+#include "log.h"
+#include "server.h"
 #include "util.h"
 
 static void
@@ -122,9 +124,13 @@
 {
 	switch (SSL_get_error(conn->ssl, ret)) {
 	case SSL_ERROR_WANT_READ:
+		irc_log_debug("server %s: step %d now needs read condition",
+		    conn->sv->name, conn->ssl_step);
 		conn->ssl_cond = IRC_CONN_SSL_ACT_READ;
 		break;
 	case SSL_ERROR_WANT_WRITE:
+		irc_log_debug("server %s: step %d now needs write condition",
+		    conn->sv->name, conn->ssl_step);
 		conn->ssl_cond = IRC_CONN_SSL_ACT_WRITE;
 		break;
 	case SSL_ERROR_SSL:
@@ -142,10 +148,14 @@
 	int nr;
 
 	if ((nr = SSL_read(conn->ssl, dst, dstsz)) <= 0) {
+		irc_log_debug("server %s: SSL read incomplete", conn->sv->name);
 		conn->ssl_step = IRC_CONN_SSL_ACT_READ;
 		return update_ssl_state(conn, nr);
 	}
 
+	if (conn->ssl_cond)
+		irc_log_debug("server %s: condition back to normal", conn->sv->name);
+
 	conn->ssl_cond = IRC_CONN_SSL_ACT_NONE;
 	conn->ssl_step = IRC_CONN_SSL_ACT_NONE;
 
@@ -189,10 +199,14 @@
 	int ns;
 
 	if ((ns = SSL_write(conn->ssl, conn->out, strlen(conn->out))) <= 0) {
+		irc_log_debug("server %s: SSL write incomplete", conn->sv->name);
 		conn->ssl_step = IRC_CONN_SSL_ACT_WRITE;
 		return update_ssl_state(conn, ns);
 	}
 
+	if (conn->ssl_cond)
+		irc_log_debug("server %s: condition back to normal", conn->sv->name);
+
 	conn->ssl_cond = IRC_CONN_SSL_ACT_NONE;
 	conn->ssl_step = IRC_CONN_SSL_ACT_NONE;
 
@@ -271,8 +285,10 @@
 dial(struct irc_conn *conn)
 {
 	/* No more address available. */
-	if (conn->aip == NULL)
+	if (conn->aip == NULL) {
+		irc_log_warn("server %s: could not connect", conn->sv->name);
 		return irc_conn_disconnect(conn), -1;
+	}
 
 	for (; conn->aip; conn->aip = conn->aip->ai_next) {
 		if (create(conn) < 0)
@@ -308,7 +324,7 @@
 	snprintf(service, sizeof (service), "%hu", conn->port);
 
 	if ((ret = getaddrinfo(conn->hostname, service, &hints, &conn->ai)) != 0) {
-		// irc_log_warn gai_strerror(ret)
+		irc_log_warn("server %s: %s", conn->sv->name, gai_strerror(ret));
 		return -1;
 	}
 
@@ -336,9 +352,11 @@
 #if defined(IRCCD_WITH_SSL)
 	switch (conn->ssl_cond) {
 	case IRC_CONN_SSL_ACT_READ:
+		irc_log_debug("server %s: need read condition", conn->sv->name);
 		pfd->events |= POLLIN;
 		break;
 	case IRC_CONN_SSL_ACT_WRITE:
+		irc_log_debug("server %s: need write condition", conn->sv->name);
 		pfd->events |= POLLOUT;
 		break;
 	default:
@@ -352,6 +370,8 @@
 static inline int
 renegotiate(struct irc_conn *conn)
 {
+	irc_log_debug("server %s: renegociate step=%d", conn->sv->name, conn->ssl_step);
+
 	return conn->ssl_step == IRC_CONN_SSL_ACT_READ
 		? input(conn)
 		: output(conn);
@@ -417,12 +437,16 @@
 	case IRC_CONN_STATE_READY:
 		if (pfd->revents & (POLLERR | POLLHUP))
 			return irc_conn_disconnect(conn), -1;
-		if (conn->ssl_cond && renegotiate(conn) < 0)
-			return irc_conn_disconnect(conn), -1;
-		if (pfd->revents & POLLIN && input(conn) < 0)
-			return irc_conn_disconnect(conn), -1;
-		if (pfd->revents & POLLOUT && output(conn) < 0)
-			return irc_conn_disconnect(conn), -1;
+
+		if (conn->ssl_cond) {
+			if (renegotiate(conn) < 0)
+				return irc_conn_disconnect(conn), -1;
+		} else {
+			if (pfd->revents & POLLIN && input(conn) < 0)
+				return irc_conn_disconnect(conn), -1;
+			if (pfd->revents & POLLOUT && output(conn) < 0)
+				return irc_conn_disconnect(conn), -1;
+		}
 		break;
 	default:
 		break;
--- a/lib/irccd/conn.h	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/conn.h	Tue Feb 16 18:37:22 2021 +0100
@@ -30,6 +30,8 @@
 struct addrinfo;
 struct pollfd;
 
+struct irc_server;
+
 enum irc_conn_state {
 	IRC_CONN_STATE_NONE,            /* Nothing, default. */
 	IRC_CONN_STATE_CONNECTING,      /* Pending connect(2) call. */
@@ -61,6 +63,7 @@
 	char out[IRC_BUF_LEN];
 	enum irc_conn_state state;
 	enum irc_conn_flags flags;
+	struct irc_server *sv;
 
 #if defined(IRCCD_WITH_SSL)
 	SSL_CTX *ctx;
--- a/lib/irccd/event.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/event.c	Tue Feb 16 18:37:22 2021 +0100
@@ -66,12 +66,13 @@
 		    ev->message.message);
 		break;
 	case IRC_EVENT_MODE:
-		written = snprintf(str, strsz, "EVENT-MODE %s %s %s %s %s %s %s",
+		snprintf(str, strsz, "EVENT-MODE %s %s %s %s ",
 		    ev->server->name, ev->mode.origin, ev->mode.channel,
-		    ev->mode.mode,
-		    ev->mode.limit ? ev->mode.limit : "",
-		    ev->mode.user  ? ev->mode.user  : "",
-		    ev->mode.mask  ? ev->mode.mask  : "");
+		    ev->mode.mode);
+
+		for (char **mode = ev->mode.args; *mode; ++mode)
+			written = strlcat(str, *mode, strsz);
+
 		break;
 	case IRC_EVENT_NICK:
 		written = snprintf(str, strsz, "EVENT-NICK %s %s %s",
@@ -93,9 +94,11 @@
 		    ev->topic.topic);
 		break;
 	case IRC_EVENT_WHOIS:
+#if 0
 		snprintf(str, strsz, "EVENT-WHOIS %s %s %s %s %s %s",
 		    ev->server->name, ev->whois.nickname, ev->whois.username,
 		    ev->whois.realname, ev->whois.hostname, ev->whois.channels);
+#endif
 		break;
 	default:
 		break;
@@ -135,9 +138,9 @@
 		free(ev->mode.origin);
 		free(ev->mode.channel);
 		free(ev->mode.mode);
-		free(ev->mode.limit);
-		free(ev->mode.user);
-		free(ev->mode.mask);
+		for (char **p = ev->mode.args; p && *p; ++p)
+			free(*p);
+		free(ev->mode.args);
 		break;
 	case IRC_EVENT_NAMES:
 		free(ev->names.channel);
@@ -167,6 +170,8 @@
 		free(ev->whois.username);
 		free(ev->whois.realname);
 		free(ev->whois.hostname);
+		for (size_t i = 0; i < ev->whois.channelsz; ++i)
+			free(ev->whois.channels[i].name);
 		free(ev->whois.channels);
 		break;
 	default:
--- a/lib/irccd/event.h	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/event.h	Tue Feb 16 18:37:22 2021 +0100
@@ -81,9 +81,7 @@
 	char *origin;
 	char *channel;
 	char *mode;
-	char *limit;
-	char *user;
-	char *mask;
+	char **args;
 };
 
 struct irc_event_names {
@@ -119,7 +117,11 @@
 	char *username;
 	char *realname;
 	char *hostname;
-	char *channels;
+	struct {
+		char *name;
+		int modes;
+	} *channels;
+	size_t channelsz;
 };
 
 struct irc_event {
--- a/lib/irccd/hook.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/hook.c	Tue Feb 16 18:37:22 2021 +0100
@@ -52,6 +52,27 @@
 }
 
 static char **
+alloc_mode(const struct irc_hook *h, const struct irc_event *ev)
+{
+	size_t n = 6;
+	char **ret;
+
+	/* Ret contains now 6 values. */
+	ret = alloc(h, 5, "onMode", ev->server->name, ev->mode.origin,
+	    ev->mode.channel, ev->mode.mode);
+
+	for (char **mode = ev->mode.args; *mode; ++mode) {
+		ret = irc_util_reallocarray(ret, n + 1, sizeof (char *));
+		ret[n++] = *mode;
+	};
+
+	ret = irc_util_reallocarray(ret, n + 1, sizeof (char *));
+	ret[n] = NULL;
+
+	return ret;
+}
+
+static char **
 make_args(const struct irc_hook *h, const struct irc_event *ev)
 {
 	char **ret;
@@ -64,44 +85,42 @@
 		ret = alloc(h, 2, "onDisconnect", ev->server->name);
 		break;
 	case IRC_EVENT_INVITE:
-		ret = alloc(h, 3, "onInvite", ev->server->name, ev->invite.origin,
+		ret = alloc(h, 4, "onInvite", ev->server->name, ev->invite.origin,
 		    ev->invite.channel);
 		break;
 	case IRC_EVENT_JOIN:
-		ret = alloc(h, 3, "onJoin", ev->server->name, ev->join.origin,
+		ret = alloc(h, 4, "onJoin", ev->server->name, ev->join.origin,
 		    ev->join.channel);
 		break;
 	case IRC_EVENT_KICK:
-		ret = alloc(h, 5, "onKick", ev->server->name, ev->kick.origin,
+		ret = alloc(h, 6, "onKick", ev->server->name, ev->kick.origin,
 		    ev->kick.channel, ev->kick.target, ev->kick.reason);
 		break;
 	case IRC_EVENT_ME:
-		ret = alloc(h, 4, "onMe", ev->server->name, ev->message.origin,
+		ret = alloc(h, 5, "onMe", ev->server->name, ev->message.origin,
 		    ev->message.channel, ev->message.message);
 		break;
 	case IRC_EVENT_MESSAGE:
-		ret = alloc(h, 4, "onMessage", ev->server->name, ev->message.origin,
+		ret = alloc(h, 5, "onMessage", ev->server->name, ev->message.origin,
 		    ev->message.channel, ev->message.message);
 		break;
 	case IRC_EVENT_MODE:
-		ret = alloc(h, 7, "onMode", ev->server->name, ev->mode.origin,
-		    ev->mode.channel, ev->mode.mode, ev->mode.limit,
-		    ev->mode.user, ev->mode.mask);
+		ret = alloc_mode(h, ev);
 		break;
 	case IRC_EVENT_NICK:
-		ret = alloc(h, 3, "onNick", ev->server->name, ev->nick.origin,
+		ret = alloc(h, 4, "onNick", ev->server->name, ev->nick.origin,
 		    ev->nick.nickname);
 		break;
 	case IRC_EVENT_NOTICE:
-		ret = alloc(h, 4, "onNotice", ev->server->name, ev->notice.origin,
+		ret = alloc(h, 5, "onNotice", ev->server->name, ev->notice.origin,
 		    ev->notice.channel, ev->notice.notice);
 		break;
 	case IRC_EVENT_PART:
-		ret = alloc(h, 4, "onPart", ev->server->name, ev->part.origin,
+		ret = alloc(h, 5, "onPart", ev->server->name, ev->part.origin,
 		    ev->part.channel, ev->part.reason);
 		break;
 	case IRC_EVENT_TOPIC:
-		ret = alloc(h, 4, "onTopic", ev->server->name, ev->topic.origin,
+		ret = alloc(h, 5, "onTopic", ev->server->name, ev->topic.origin,
 		    ev->topic.channel, ev->topic.topic);
 		break;
 	default:
--- a/lib/irccd/server.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/server.c	Tue Feb 16 18:37:22 2021 +0100
@@ -79,15 +79,6 @@
 	return strncmp(s->ident.nickname, nick, strlen(s->ident.nickname)) == 0;
 }
 
-static void
-add_nick(const struct irc_server *s, struct irc_channel *ch, const char *nick)
-{
-	char mode = 0, prefix = 0;
-
-	irc_server_strip(s, &nick, &mode, &prefix);
-	irc_channel_add(ch, nick, mode, prefix);
-}
-
 static struct irc_channel *
 add_channel(struct irc_server *s, const char *name, const char *password, int joined)
 {
@@ -147,6 +138,16 @@
 	return line;
 }
 
+static inline int
+find_mode(struct irc_server *s, int mode)
+{
+	for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes); ++i)
+		if (s->params.prefixes[i].mode == mode)
+			return i;
+
+	return 0;
+}
+
 static void
 read_support_prefix(struct irc_server *s, const char *value)
 {
@@ -159,11 +160,11 @@
 
 	if (sscanf(value, fmt, modes, tokens) == 2) {
 		char *pm = modes;
-		char *tk = tokens;
+		char *sm = tokens;
 
-		for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes) && *pm && *tk; ++i) {
+		for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes) && *pm && *sm; ++i) {
 			s->params.prefixes[i].mode = *pm++;
-			s->params.prefixes[i].token = *tk++;
+			s->params.prefixes[i].symbol = *sm++;
 		}
 	}
 }
@@ -331,15 +332,64 @@
 	(void)ev;
 	(void)msg;
 
+	int action = 0, mode;
+	size_t nelem = 0, argindex = 2;
+	struct irc_channel *ch;
+	struct irc_channel_user *u;
+
 	ev->type = IRC_EVENT_MODE;
-	ev->mode.origin = strdup(msg->prefix);
-	ev->mode.channel = strdup(msg->args[0]);
-	ev->mode.mode = strdup(msg->args[1]);
-	ev->mode.limit = msg->args[2] ? strdup(msg->args[2]) : NULL;
-	ev->mode.user = msg->args[3] ? strdup(msg->args[3]) : NULL;
-	ev->mode.mask = msg->args[4] ? strdup(msg->args[4]) : NULL;
+	ev->mode.origin = irc_util_strdup(msg->prefix);
+	ev->mode.channel = irc_util_strdup(msg->args[0]);
+	ev->mode.mode = irc_util_strdup(msg->args[1]);
+
+	/* Create a NULL-sentineled list of arguments. */
+	for (size_t i = 2; i < IRC_ARGS_MAX && msg->args[i]; ++i) {
+		ev->mode.args = irc_util_reallocarray(ev->mode.args, nelem + 1, sizeof (char *));
+		ev->mode.args[nelem++] = irc_util_strdup(msg->args[i]);
+	}
+
+	/* Add the NULL sentinel. */
+	ev->mode.args = irc_util_reallocarray(ev->mode.args, nelem + 1, sizeof (char *));
+	ev->mode.args[nelem] = NULL;
+
+	if (!(ch = irc_server_find(s, ev->mode.channel)))
+		return;
+
+	for (const char *p = ev->mode.mode; *p; ++p) {
+		/* Determine if we're adding or removing a mode. */
+		if (*p == '+' || *p == '-') {
+			action = *p;
+			continue;
+		}
 
-	/* TODO: update nickname modes. */
+		/* All these mode require an argument but we don't use. */
+		switch (*p) {
+		case 'b':
+		case 'k':
+		case 'l':
+		case 'e':
+		case 'I':
+			++argindex;
+			continue;
+		}
+
+		/* Find which mode this symbol is (e.g. o=@). */
+		if ((mode = find_mode(s, *p)) == 0) {
+			++argindex;
+			continue;
+		}
+		if (!msg->args[argindex] || !(u = irc_channel_find(ch, msg->args[argindex]))) {
+			++argindex;
+			continue;
+		}
+
+		++argindex;
+
+		if (action == '+')
+			u->modes |= (1 << mode);
+		else
+			u->modes &= ~(1 << mode);
+	}
 }
 
 static void
@@ -433,35 +483,34 @@
 static void
 handle_names(struct irc_server *s, struct irc_event *ev, struct irc_conn_msg *msg)
 {
-	(void)s;
 	(void)ev;
-	(void)msg;
 
 	struct irc_channel *ch;
 	char *p, *token;
+	int modes = 0;
 
 	ch = add_channel(s, msg->args[2], NULL, 1);
 
 	/* Track existing nicknames into the given channel. */
-	for (p = msg->args[3]; (token = strtok_r(p, " ", &p)); )
-		if (strlen(token) > 0)
-			add_nick(s, ch, token);
+	for (p = msg->args[3]; (token = strtok_r(p, " ", &p)); ) {
+		if (strlen(token) == 0)
+			continue;
+
+		modes = irc_server_strip(s, (const char **)&token);
+		irc_channel_add(ch, token, modes);
+	}
 }
 
 static void
 handle_endofnames(struct irc_server *s, struct irc_event *ev, struct irc_conn_msg *msg)
 {
-	(void)s;
-	(void)ev;
-	(void)msg;
-
 	FILE *fp;
 	size_t length;
 	const struct irc_channel *ch;
 	const struct irc_channel_user *u;
 
 	ev->type = IRC_EVENT_NAMES;
-	ev->names.channel = strdup(msg->args[1]);
+	ev->names.channel = irc_util_strdup(msg->args[1]);
 
 	/* Construct a string list for every user in the channel. */
 	ch = irc_server_find(s, ev->names.channel);
@@ -470,8 +519,9 @@
 		err(1, "open_memstream");
 
 	LIST_FOREACH(u, &ch->users, link) {
-		if (u->symbol)
-			fprintf(fp, "%c", u->symbol);
+		for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes); ++i)
+			if (u->modes & (1 << i))
+				fprintf(fp, "%c", s->params.prefixes[i].symbol);
 
 		fprintf(fp, "%s", u->nickname);
 
@@ -521,26 +571,23 @@
 {
 	(void)ev;
 
-	size_t curlen, reqlen;
+	char *token, *p;
+	int modes;
 
-	curlen = s->bufwhois.channels ? strlen(s->bufwhois.channels) : 0;
-	reqlen = strlen(msg->args[2]);
+	if (!msg->args[2])
+		return;
 
-	/*
-	 * If there is already something, add a space at the end of the current
-	 * buffer.
-	 */
-	if (curlen > 0)
-		reqlen++;
+	for (p = msg->args[2]; (token = strtok_r(p, " ", &p)); ) {
+		modes = irc_server_strip(s, (const char **)&token);
+		s->bufwhois.channels = irc_util_reallocarray(
+		    s->bufwhois.channels,
+		    s->bufwhois.channelsz + 1,
+		    sizeof (*s->bufwhois.channels)
+		);
 
-	/* Now, don't forget */
-	s->bufwhois.channels = irc_util_realloc(s->bufwhois.channels, reqlen + 1);
-
-	if (curlen > 0) {
-		strcat(s->bufwhois.channels, " ");
-		strcat(s->bufwhois.channels, msg->args[2]);
-	} else
-		strcpy(s->bufwhois.channels, msg->args[2]);
+		s->bufwhois.channels[s->bufwhois.channelsz].name = irc_util_strdup(token);
+		s->bufwhois.channels[s->bufwhois.channelsz++].modes = modes;
+	}
 }
 
 static void
@@ -615,12 +662,21 @@
 {
 	s->state = IRC_SERVER_STATE_CONNECTED;
 
+	/*
+	 * Use multi-prefix extension to keep track of all combined "modes" in
+	 * a channel.
+	 *
+	 * https://ircv3.net/specs/extensions/multi-prefix-3.1.html
+	 */
+	irc_server_send(s, "CAP REQ :multi-prefix");
+
 	if (s->ident.password[0])
 		irc_server_send(s, "PASS %s", s->ident.password);
 
 	irc_server_send(s, "NICK %s", s->ident.nickname);
 	irc_server_send(s, "USER %s %s %s :%s", s->ident.username,
 	    s->ident.username, s->ident.username, s->ident.realname);
+	irc_server_send(s, "CAP END");
 }
 
 struct irc_server *
@@ -667,6 +723,8 @@
 	if (s->flags & IRC_SERVER_FLAGS_SSL)
 		s->conn.flags |= IRC_CONN_SSL;
 
+	s->conn.sv = s;
+
 	if (irc_conn_connect(&s->conn) < 0)
 		fail(s);
 	else
@@ -952,27 +1010,22 @@
 	return irc_server_send(s, "WHOIS %s", target);
 }
 
-void
-irc_server_strip(const struct irc_server *s, const char **nick, char *mode, char *prefix)
+int
+irc_server_strip(const struct irc_server *s, const char **what)
 {
 	assert(s);
-	assert(*nick);
+	assert(*what);
 
-	if (mode)
-		*mode = 0;
-	if (prefix)
-		*prefix = 0;
+	int modes = 0;
 
 	for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes); ++i) {
-		if (**nick == s->params.prefixes[i].token) {
-			if (mode)
-				*mode = s->params.prefixes[i].mode;
-			if (prefix)
-				*prefix = s->params.prefixes[i].token;
-			*nick += 1;
-			break;
+		if (**what == s->params.prefixes[i].symbol) {
+			modes |= 1 << i;
+			*what += 1;
 		}
 	}
+
+	return modes;
 }
 
 void
--- a/lib/irccd/server.h	Sun Feb 14 10:11:03 2021 +0100
+++ b/lib/irccd/server.h	Tue Feb 16 18:37:22 2021 +0100
@@ -75,7 +75,7 @@
 	unsigned int kicklen;
 	struct {
 		char mode;              /* Mode (e.g. ov). */
-		char token;             /* Symbol used (e.g. @+). */
+		char symbol;            /* Symbol used (e.g. @+). */
 	} prefixes[IRC_USERMODES_LEN];
 };
 
@@ -166,8 +166,8 @@
 int
 irc_server_whois(struct irc_server *, const char *);
 
-void
-irc_server_strip(const struct irc_server *, const char **, char *, char *);
+int
+irc_server_strip(const struct irc_server *, const char **);
 
 void
 irc_server_split(const char *, struct irc_server_user *);
--- a/man/irccd-api-server.3	Sun Feb 14 10:11:03 2021 +0100
+++ b/man/irccd-api-server.3	Tue Feb 16 18:37:22 2021 +0100
@@ -82,7 +82,7 @@
 .Fa parameters
 object which may have the following properties:
 .Pp
-.Bl -tag -compact -width "hostname (string)"
+.Bl -tag -width "hostname (string)"
 .It Fa name No (string)
 The unique identifier name.
 .It Fa hostname No (string)
@@ -115,7 +115,7 @@
 method returns the server information. The object have the following
 properties:
 .Pp
-.Bl -tag -compact -width "hostname (string)"
+.Bl -tag -width "hostname (string)"
 .It Va name No (string)
 The server unique name.
 .It Va hostname No (string)
@@ -125,19 +125,17 @@
 .It Va ssl No (bool)
 True if using ssl.
 .It Va channels No (array)
-An array of all channels. Each channel in the returned array contain the
-following properties:
-.Bl -tag -width xxx
+An array of all channels as objects. Each channel in the returned array contain
+the following properties:
+.Bl -tag -width "name (string)"
 .It Va name No (string)
 The channel name.
 .It Va joined No (bool)
 True if the daemon is actually present on this channel.
 .It Va users No (array)
-An array of users that consists of objects with three properties:
-.Va nickname , mode
-and
-.Va symbol
-as their nickname, the channel mode and the symbol representing this mode.
+An array of users that consists of objects with two properties:
+.Va nickname No (string) and modes Fa (int)
+as their nickname and modes as bitwise mask for this channel.
 .El
 .Pp
 .It Va realname No (string)
--- a/man/irccd-api.3	Sun Feb 14 10:11:03 2021 +0100
+++ b/man/irccd-api.3	Tue Feb 16 18:37:22 2021 +0100
@@ -46,7 +46,7 @@
 .Fn onLoad "
 .Fn onMe "server, origin, channel, message"
 .Fn onMessage "server, origin, channel, message"
-.Fn onMode "server, origin, channel, mode, limit, user, mask"
+.Fn onMode "server, origin, channel, mode, args"
 .Fn onNames "server, channel, list"
 .Fn onNick "server, origin, nickname"
 .Fn onNotice "server, origin, notice"
@@ -199,6 +199,8 @@
 The person who changed the mode.
 .It Fa mode No (string)
 The new mode.
+.It Fa args No (array)
+List of mode arguments as strings.
 .El
 .\" onNames
 .Ss onNames
@@ -293,7 +295,7 @@
 The
 .Fa info
 is an object with the following properties:
-.Bl -tag -width 20n -compact -offset Ds
+.Bl -tag -width "nickname (string)"
 .It Fa nickname No (string)
 The user nickname.
 .It Fa user No (string)
@@ -303,7 +305,14 @@
 .It Fa realname No (string)
 The real name used.
 .It Fa channels No (array)
-An optional list of channels joined.
+An optional list of channels joined by the user. Objects in the array are
+defined using the following properties:
+.Bl -tag -width "name (string)"
+.It Fa name No (string)
+The name of the channel.
+.It Fa modes No (int)
+A bitwise mask of modes applied the user has on this channel.
+.El
 .El
 .\" MODULES
 .Sh MODULES
--- a/plugins/logger/logger.js	Sun Feb 14 10:11:03 2021 +0100
+++ b/plugins/logger/logger.js	Tue Feb 16 18:37:22 2021 +0100
@@ -41,7 +41,7 @@
 	"me":           "%H:%M:%S * #{nickname} #{message}",
 	"message":      "%H:%M:%S #{nickname}: #{message}",
 	"invite":       "%H:%M:%S #{nickname} invited you on #{channel}",
-	"mode":         "%H:%M:%S :: #{nickname} set mode #{channel} #{mode} #{limit} #{user} #{mask}",
+	"mode":         "%H:%M:%S :: #{nickname} set mode #{channel} #{mode} #{args}",
 	"notice":       "%H:%M:%S [notice] #{channel} (#{nickname}) #{message}",
 	"part":         "%H:%M:%S << #{nickname} left #{channel} [#{reason}]",
 	"query":        "%H:%M:%S #{nickname}: #{message}",
@@ -135,15 +135,14 @@
 	}));
 }
 
-function onMode(server, origin, channel, mode, limit, user, mask)
+function onMode(server, origin, channel, mode, args)
 {
 	origin = origin.toLowerCase();
+	channel = channel.toLowerCase();
 
 	write("mode", keywords(server, channel, origin, {
 		"mode":         mode,
-		"limit":        limit,
-		"user":         user,
-		"mask":         mask
+		"args":         args.join(" "),
 	}));
 }
 
@@ -155,6 +154,7 @@
 function onNotice(server, origin, channel, notice)
 {
 	origin = origin.toLowerCase();
+	channel = channel.toLowerCase();
 
 	write("notice", keywords(server, channel, origin, {
 		"message": notice,
--- a/tests/test-channel.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/tests/test-channel.c	Tue Feb 16 18:37:22 2021 +0100
@@ -32,40 +32,33 @@
 	GREATEST_ASSERT_STR_EQ("", ch->password);
 	GREATEST_ASSERT(ch->joined);
 
-	irc_channel_add(ch, "markand", 'o', '@');
+	irc_channel_add(ch, "markand", 1);
 	user = LIST_FIRST(&ch->users);
-	GREATEST_ASSERT_EQ('o', user->mode);
-	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_EQ(1, user->modes);
 	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_add(ch, "markand", '+', '@');
+	irc_channel_add(ch, "markand", 2);
 	user = LIST_FIRST(&ch->users);
-	GREATEST_ASSERT_EQ('o', user->mode);
-	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_EQ(1, user->modes);
 	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_add(ch, "jean", 'h', '+');
+	irc_channel_add(ch, "jean", 4);
 	user = LIST_FIRST(&ch->users);
-	GREATEST_ASSERT_EQ('h', user->mode);
-	GREATEST_ASSERT_EQ('+', user->symbol);
+	GREATEST_ASSERT_EQ(4, user->modes);
 	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_EQ(1, user->modes);
 	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
-	irc_channel_add(ch, "zoe", 0, 0);
+	irc_channel_add(ch, "zoe", 0);
 	user = LIST_FIRST(&ch->users);
-	GREATEST_ASSERT_EQ(0, user->mode);
-	GREATEST_ASSERT_EQ(0, user->symbol);
+	GREATEST_ASSERT_EQ(0, user->modes);
 	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_EQ(4, user->modes);
 	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_EQ(1, user->modes);
 	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
 	irc_channel_finish(ch);
@@ -81,24 +74,21 @@
 
 	ch = irc_channel_new("#test", NULL, 1);
 
-	irc_channel_add(ch, "markand", 'o', '@');
-	irc_channel_add(ch, "jean", 0, 0);
-	irc_channel_add(ch, "zoe", 0, 0);
+	irc_channel_add(ch, "markand", 1);
+	irc_channel_add(ch, "jean", 0);
+	irc_channel_add(ch, "zoe", 0);
 
 	irc_channel_remove(ch, "jean");
 	user = LIST_FIRST(&ch->users);
-	GREATEST_ASSERT_EQ(0, user->mode);
-	GREATEST_ASSERT_EQ(0, user->symbol);
+	GREATEST_ASSERT_EQ(0, user->modes);
 	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_EQ(1, user->modes);
 	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
 	irc_channel_remove(ch, "zoe");
 	user = LIST_FIRST(&ch->users);
-	GREATEST_ASSERT_EQ('o', user->mode);
-	GREATEST_ASSERT_EQ('@', user->symbol);
+	GREATEST_ASSERT_EQ(1, user->modes);
 	GREATEST_ASSERT_STR_EQ("markand", user->nickname);
 
 	irc_channel_remove(ch, "markand");
@@ -109,40 +99,10 @@
 	GREATEST_PASS();
 }
 
-GREATEST_TEST
-basics_update(void)
-{
-	struct irc_channel *ch;
-	struct irc_channel_user *user;
-
-	ch = irc_channel_new("#test", NULL, 1);
-
-	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);
-
-	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_finish(ch);
-
-	GREATEST_PASS();
-}
-
 GREATEST_SUITE(suite_basics)
 {
 	GREATEST_RUN_TEST(basics_add);
 	GREATEST_RUN_TEST(basics_remove);
-	GREATEST_RUN_TEST(basics_update);
 }
 
 GREATEST_MAIN_DEFS();
--- a/tests/test-plugin-logger.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/tests/test-plugin-logger.c	Tue Feb 16 18:37:22 2021 +0100
@@ -43,13 +43,12 @@
 	if (!plugin)
 		errx(1, "could not load plugin");
 
-	irc_log_to_console();
 	irc_server_incref(server);
 	irc_plugin_set_template(plugin, "join", "join=#{server}:#{channel}:#{origin}:#{nickname}");
 	irc_plugin_set_template(plugin, "kick", "kick=#{server}:#{channel}:#{origin}:#{nickname}:#{target}:#{reason}");
 	irc_plugin_set_template(plugin, "me", "me=#{server}:#{channel}:#{origin}:#{nickname}:#{message}");
 	irc_plugin_set_template(plugin, "message", "message=#{server}:#{channel}:#{origin}:#{nickname}:#{message}");
-	irc_plugin_set_template(plugin, "mode", "mode=#{server}:#{origin}:#{channel}:#{mode}:#{limit}:#{user}:#{mask}");
+	irc_plugin_set_template(plugin, "mode", "mode=#{server}:#{origin}:#{channel}:#{mode}:#{args}");
 	irc_plugin_set_template(plugin, "notice", "notice=#{server}:#{origin}:#{channel}:#{message}");
 	irc_plugin_set_template(plugin, "part", "part=#{server}:#{channel}:#{origin}:#{nickname}:#{reason}");
 	irc_plugin_set_template(plugin, "query", "query=#{server}:#{origin}:#{nickname}:#{message}");
@@ -166,15 +165,13 @@
 		.server = server,
 		.mode = {
 			.origin = "jean!jean@localhost",
-			.channel = "chris",
-			.mode = "+i",
-			.limit = "l",
-			.user = "u",
-			.mask = "m"
+			.channel = "#staff",
+			.mode = "+ov",
+			.args = (char *[]) { "francis", "benoit", NULL }
 		}
 	});
 
-	GREATEST_ASSERT_STR_EQ("mode=test:jean!jean@localhost:chris:+i:l:u:m", last());
+	GREATEST_ASSERT_STR_EQ("mode=test:jean!jean@localhost:#staff:+ov:francis benoit", last());
 	GREATEST_PASS();
 }
 
--- a/tests/test-plugin-tictactoe.c	Sun Feb 14 10:11:03 2021 +0100
+++ b/tests/test-plugin-tictactoe.c	Tue Feb 16 18:37:22 2021 +0100
@@ -81,8 +81,8 @@
 
 	/* We need tw players on a channel to play the game. */
 	irc_server_join(server, "#tictactoe", NULL);
-	irc_channel_add(LIST_FIRST(&server->channels), "a", 0, 0);
-	irc_channel_add(LIST_FIRST(&server->channels), "b", 0, 0);
+	irc_channel_add(LIST_FIRST(&server->channels), "a", 0);
+	irc_channel_add(LIST_FIRST(&server->channels), "b", 0);
 
 	/* Fake server connected to send data. */
 	server->state = IRC_SERVER_STATE_CONNECTED;