changeset 974:342fb90f2512

irccdctl: re-implement many of the plugin-* commands
author David Demelier <markand@malikania.fr>
date Sun, 07 Feb 2021 14:36:28 +0100
parents e2a86096bc05
children 5ffc8350e84b
files CHANGES.md MIGRATING.md irccd/js-plugin.c irccd/main.c irccd/peer.c irccdctl/main.c lib/irccd/limits.h lib/irccd/server.c lib/irccd/server.h
diffstat 9 files changed, 759 insertions(+), 257 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.md	Wed Feb 03 20:45:00 2021 +0100
+++ b/CHANGES.md	Sun Feb 07 14:36:28 2021 +0100
@@ -15,6 +15,12 @@
   mode changes. It is now more convenient from the plugins to quickly inspect if
   someone is present on a channel.
 
+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.
+
 misc:
 
 - Split irccd-api manual page into individual irccd-api-<module> for a better
@@ -26,6 +32,10 @@
 - Network protocol uses plain text again.
 - Transport uses clear UNIX sockets only without passwords.
 
+javascript API:
+
+- Brand new Irccd.Rule API to inspect and manage rules.
+
 irccd 3.1.1 2021-01-04
 ----------------------
 
--- a/MIGRATING.md	Wed Feb 03 20:45:00 2021 +0100
+++ b/MIGRATING.md	Sun Feb 07 14:36:28 2021 +0100
@@ -17,7 +17,12 @@
 Irccdctl
 --------
 
-The `irccdctl.conf` is now using a custom syntax.
+- There is no longer configuration file because it now use a plain UNIX socket
+  to */tmp/irccd.sock* by default (may be changed through the `-s` option).
+- Aliases have been removed, please use shell scripts or aliases instead.
+- The `watch` command no longer produce JSON output but only the original
+  "human" format but may be used for scripts as it is honored through the
+  semantic versioning.
 
 Platform support
 ----------------
--- a/irccd/js-plugin.c	Wed Feb 03 20:45:00 2021 +0100
+++ b/irccd/js-plugin.c	Sun Feb 07 14:36:28 2021 +0100
@@ -141,27 +141,24 @@
 static const char **
 get_table(duk_context *ctx, const char *name, char ***ptable)
 {
-	char **list;
-	size_t listsz;
+	char **list = NULL;
+	size_t listsz = 0;
 
 	duk_get_global_string(ctx, name);
-
-	if (!(listsz = duk_get_length(ctx, -1))) {
-		duk_pop(ctx);
-		return NULL;
-	}
-
-	list = irc_util_calloc(listsz + 1, sizeof (char *));
-
 	duk_enum(ctx, -1, 0);
 
-	for (size_t i = 0; i < listsz && duk_next(ctx, -1, 1); ++i) {
+	for (size_t i = 0; duk_next(ctx, -1, 1); ++i) {
+		list = irc_util_reallocarray(list, ++listsz, sizeof (char *));
 		list[i] = irc_util_strdup(duk_to_string(ctx, -2));
 		duk_pop_n(ctx, 2);
 	}
 
 	duk_pop_n(ctx, 2);
 
+	/* Add a NULL sentinel value. */
+	list = irc_util_reallocarray(list, listsz + 1, sizeof (char *));
+	list[listsz] = NULL;
+
 	freelist(*ptable);
 	*ptable = list;
 
--- a/irccd/main.c	Wed Feb 03 20:45:00 2021 +0100
+++ b/irccd/main.c	Sun Feb 07 14:36:28 2021 +0100
@@ -46,7 +46,7 @@
 };
 
 static const char *config = IRCCD_SYSCONFDIR "/irccd.conf";
-static struct peers peers;
+static struct peers peers = LIST_HEAD_INITIALIZER();
 static int running = 1;
 
 /* conf.y */
@@ -118,10 +118,9 @@
 flush(const struct pollables *pb)
 {
 	struct peer *peer, *tmp;
-	struct pollfd *fd = pb->fds + pb->botsz;
+	struct pollfd *fd = pb->fds + pb->botsz + 1;
 
 	irc_bot_flush(pb->fds);
-	transport_flush(fd++);
 
 	LIST_FOREACH_SAFE(peer, &peers, link, tmp) {
 		if (peer_flush(peer, fd++) < 0) {
@@ -129,6 +128,13 @@
 			peer_finish(peer);
 		}
 	}
+
+	/*
+	 * Add a new client only now because we would iterate over a list
+	 * of pollfd that is smaller than the client list.
+	 */
+	if ((peer = transport_flush(pb->fds + pb->botsz)))
+		LIST_INSERT_HEAD(&peers, peer, link);
 }
 
 static inline void
--- a/irccd/peer.c	Wed Feb 03 20:45:00 2021 +0100
+++ b/irccd/peer.c	Sun Feb 07 14:36:28 2021 +0100
@@ -35,6 +35,10 @@
 
 #include "peer.h"
 
+typedef void (*plugin_set_fn)(struct irc_plugin *, const char *, const char *);
+typedef const char * (*plugin_get_fn)(struct irc_plugin *, const char *);
+typedef const char ** (*plugin_list_fn)(struct irc_plugin *);
+
 static size_t
 parse(char *line, const char **args, size_t max)
 {
@@ -63,7 +67,7 @@
 	return idx;
 }
 
-static struct irc_server *
+static inline struct irc_server *
 require_server(struct peer *p, const char *id)
 {
 	struct irc_server *s;
@@ -76,6 +80,19 @@
 	return s;
 }
 
+static inline struct irc_plugin *
+require_plugin(struct peer *p, const char *id)
+{
+	struct irc_plugin *plg;
+
+	if (!(plg = irc_bot_plugin_get(id))) {
+		peer_send(p, "plugin %s not found", id);
+		return NULL;
+	}
+
+	return plg;
+}
+
 static int
 ok(struct peer *p)
 {
@@ -84,8 +101,186 @@
 	return 0;
 }
 
+static int
+plugin_list_set(struct peer *p,
+                char *line,
+                plugin_set_fn set,
+                plugin_get_fn get,
+                plugin_list_fn list)
+{
+	const char *args[3] = {0}, *value, **keys;
+	char out[IRC_BUF_LEN];
+	FILE *fp;
+	struct irc_plugin *plg;
+	size_t argsz, keysz = 0;
+
+	if ((argsz = parse(line, args, 3)) < 1)
+		return EINVAL;
+	if (!(plg = require_plugin(p, args[0])))
+		return 0;
+
+	fp = fmemopen(out, sizeof (out) - 1, "w");
+
+	if (argsz == 3) {
+		set(plg, args[1], args[2]);
+		fprintf(fp, "OK");
+	} else if (argsz == 2) {
+		if ((value = get(plg, args[1])))
+			fprintf(fp, "OK 1\n%s", value);
+		else
+			fprintf(fp, "ERROR key not found");
+	} else {
+		keys = list(plg);
+
+		/* Compute the number of keys found. */
+		for (const char **key = keys; key && *key; ++key)
+			keysz++;
+
+		fprintf(fp, "OK %zu\n", keysz);
+
+		for (const char **key = keys; key && *key; ++key) {
+			value = get(plg, *key);
+			fprintf(fp, "%s=%s\n", *key, value ? value : "");
+		}
+	}
+
+	fclose(fp);
+	peer_send(p, out);
+
+	return 0;
+}
+
 /*
- * DISCONNECT [server]
+ * PLUGIN-CONFIG plugin [var [value]]
+ */
+static int
+cmd_plugin_config(struct peer *p, char *line)
+{
+	return plugin_list_set(p, line,
+	    irc_plugin_set_option, irc_plugin_get_option, irc_plugin_get_options);
+}
+
+/*
+ * PLUGIN-INFO plugin
+ */
+static int
+cmd_plugin_info(struct peer *p, char *line)
+{
+	struct irc_plugin *plg;
+	const char *args[1];
+
+	if (parse(line, args, 1) != 1)
+		return EINVAL;
+	if (!(plg = require_plugin(p, args[0])))
+		return 0;
+
+	peer_send(p, "OK %s\n%s\n%s\n%s\n%s", plg->name, plg->description,
+	    plg->version, plg->license, plg->author);
+
+	return 0;
+}
+
+/*
+ * PLUGIN-LOAD plugin
+ */
+static int
+cmd_plugin_load(struct peer *p, char *line)
+{
+	struct irc_plugin *plg;
+
+	if (!(plg = irc_bot_plugin_find(line, NULL)))
+		peer_send(p, "could not load plugin: %s", strerror(errno));
+
+	/* TODO: report error if fails to open. */
+	irc_bot_plugin_add(plg);
+
+	return ok(p);
+}
+
+/*
+ * PLUGIN-PATH plugin [var [value]]
+ */
+static int
+cmd_plugin_path(struct peer *p, char *line)
+{
+	return plugin_list_set(p, line,
+	    irc_plugin_set_path, irc_plugin_get_path, irc_plugin_get_paths);
+}
+
+/*
+ * PLUGIN-LIST
+ */
+static int
+cmd_plugin_list(struct peer *p, char *line)
+{
+	(void)line;
+
+	struct irc_plugin *plg;
+	FILE *fp;
+	char out[IRC_BUF_LEN];
+
+	fp = fmemopen(out, sizeof (out) - 1, "w");
+
+	fprintf(fp, "OK ");
+
+	LIST_FOREACH(plg, &irc.plugins, link) {
+		fprintf(fp, "%s", plg->name);
+
+		if (LIST_NEXT(plg, link))
+			fputc(' ', fp);
+	}
+
+	fclose(fp);
+	peer_send(p, out);
+
+	return 0;
+}
+
+/*
+ * PLUGIN-RELOAD plugin
+ */
+static int
+cmd_plugin_reload(struct peer *p, char *line)
+{
+	struct irc_plugin *plg;
+
+	if (!(plg = irc_bot_plugin_get(line)))
+		peer_send(p, "could not reload plugin: %s", strerror(errno));
+
+	/* TODO: report error if fails to reload. */
+
+	return ok(p);
+}
+
+/*
+ * PLUGIN-TEMPLATE plugin [var [value]]
+ */
+static int
+cmd_plugin_template(struct peer *p, char *line)
+{
+	return plugin_list_set(p, line,
+	    irc_plugin_set_template, irc_plugin_get_template, irc_plugin_get_templates);
+}
+
+/*
+ * PLUGIN-UNLOAD [plugin]
+ */
+static int
+cmd_plugin_unload(struct peer *p, char *line)
+{
+	const char *args[1] = {0};
+
+	/* TODO report error if plugin not found. */
+	if (parse(line, args, 1) == 0)
+		irc_bot_plugin_clear();
+	else
+		irc_bot_plugin_remove(args[0]);
+
+	return ok(p);
+}
+
+/*
+ * SERVER-DISCONNECT [server]
  */
 static int
 cmd_server_disconnect(struct peer *p, char *line)
@@ -105,7 +300,7 @@
 }
 
 /*
- * MESSAGE server channel message
+ * SERVER-MESSAGE server channel message
  */
 static int
 cmd_server_message(struct peer *p, char *line)
@@ -124,7 +319,7 @@
 }
 
 /*
- * ME server channel message
+ * SERVER-ME server channel message
  */
 static int
 cmd_server_me(struct peer *p, char *line)
@@ -143,7 +338,7 @@
 }
 
 /*
- * MODE server channel mode [limit] [user] [mask]
+ * SERVER-MODE server channel mode [limit] [user] [mask]
  */
 static int
 cmd_server_mode(struct peer *p, char *line)
@@ -166,7 +361,7 @@
 }
 
 /*
- * NOTICE server channel message
+ * SERVER-NOTICE server channel message
  */
 static int
 cmd_server_notice(struct peer *p, char *line)
@@ -185,7 +380,44 @@
 }
 
 /*
- * INVITE server channel target
+ * SERVER-INFO server
+ */
+static int
+cmd_server_info(struct peer *p, char *line)
+{
+	const char *args[1] = {0};
+	const struct irc_server *s;
+	const struct irc_channel *c;
+	char out[IRC_BUF_LEN];
+	FILE *fp;
+
+	if (parse(line, args, 1) != 1)
+		return EINVAL;
+	if (!(s = require_server(p, args[0])))
+		return 0;
+
+	fp = fmemopen(out, sizeof (out) - 1, "w");
+
+	fprintf(fp, "OK %s\n", s->name);
+	fprintf(fp, "%s %u%s\n", s->conn.hostname, s->conn.port,
+	    s->flags & IRC_SERVER_FLAGS_SSL ? " ssl" : "");
+	fprintf(fp, "%s %s %s\n", s->ident.nickname, s->ident.username, s->ident.realname);
+
+	LIST_FOREACH(c, &s->channels, link) {
+		fprintf(fp, "%s", c->name);
+
+		if (LIST_NEXT(c, link))
+			fputc(' ', fp);
+	}
+
+	fclose(fp);
+	peer_send(p, out);
+
+	return 0;
+}
+
+/*
+ * SERVER-INVITE server channel target
  */
 static int
 cmd_server_invite(struct peer *p, char *line)
@@ -204,7 +436,7 @@
 }
 
 /*
- * JOIN server channel [password]
+ * SERVER-JOIN server channel [password]
  */
 static int
 cmd_server_join(struct peer *p, char *line)
@@ -217,13 +449,13 @@
 	if (!(s = require_server(p, args[0])))
 		return 0;
 
-	irc_server_join(s, args[1], args[2][0] ? args[2] : NULL);
+	irc_server_join(s, args[1], args[2] ? args[2] : NULL);
 
 	return ok(p);
 }
 
 /*
- * KICK server channel target [reason]
+ * SERVER-KICK server channel target [reason]
  */
 static int
 cmd_server_kick(struct peer *p, char *line)
@@ -241,6 +473,9 @@
 	return ok(p);
 }
 
+/*
+ * SERVER-LIST
+ */
 static int
 cmd_server_list(struct peer *p, char *line)
 {
@@ -270,7 +505,7 @@
 }
 
 /*
- * PART server channel [reason]
+ * SERVER-PART server channel [reason]
  */
 static int
 cmd_server_part(struct peer *p, char *line)
@@ -289,7 +524,7 @@
 }
 
 /*
- * TOPIC server channel topic
+ * SERVER-TOPIC server channel topic
  */
 static int
 cmd_server_topic(struct peer *p, char *line)
@@ -321,7 +556,16 @@
 	const char *name;
 	int (*call)(struct peer *, char *);
 } cmds[] = {
+	{ "PLUGIN-CONFIG",      cmd_plugin_config       },
+	{ "PLUGIN-INFO",        cmd_plugin_info         },
+	{ "PLUGIN-LIST",        cmd_plugin_list         },
+	{ "PLUGIN-LOAD",        cmd_plugin_load         },
+	{ "PLUGIN-PATH",        cmd_plugin_path         },
+	{ "PLUGIN-RELOAD",      cmd_plugin_reload       },
+	{ "PLUGIN-TEMPLATE",    cmd_plugin_template     },
+	{ "PLUGIN-UNLOAD",      cmd_plugin_unload       },
 	{ "SERVER-DISCONNECT",  cmd_server_disconnect   },
+	{ "SERVER-INFO",        cmd_server_info         },
 	{ "SERVER-INVITE",      cmd_server_invite       },
 	{ "SERVER-JOIN",        cmd_server_join         },
 	{ "SERVER-KICK",        cmd_server_kick         },
@@ -336,18 +580,16 @@
 };
 
 static int
-cmp_cmd(const void *d1, const void *d2)
+cmp_cmd(const char *key, const struct cmd *cmd)
 {
-	const char *key = d1;
-	const struct cmd *cmd = d2;
-
 	return strncmp(key, cmd->name, strlen(cmd->name));
 }
 
 static const struct cmd *
 find(const char *line)
 {
-	return bsearch(line, cmds, IRC_UTIL_SIZE(cmds), sizeof (struct cmd), cmp_cmd);
+	return bsearch(line, cmds, IRC_UTIL_SIZE(cmds),
+	    sizeof (cmds[0]), (irc_cmp)cmp_cmd);
 }
 
 static void
--- a/irccdctl/main.c	Wed Feb 03 20:45:00 2021 +0100
+++ b/irccdctl/main.c	Sun Feb 07 14:36:28 2021 +0100
@@ -22,6 +22,7 @@
 #include <sys/time.h>
 #include <sys/un.h>
 #include <assert.h>
+#include <ctype.h>
 #include <err.h>
 #include <errno.h>
 #include <limits.h>
@@ -125,13 +126,365 @@
 	}
 }
 
-static void
+static char *
 ok(void)
 {
-	const char *response = poll();
+	char *response = poll();
+
+	if (strncmp(response, "OK", 2) != 0)
+		errx(1, "abort: %s", response);
+
+	/* Skip "OK". */
+	response += 2;
+
+	while (*response && isspace(*response))
+		response++;
+
+	return response;
+}
+
+static void
+show_connect(char *line)
+{
+	const char *args[2] = {0};
+
+	if (irc_util_split(line, args, 2) == 2) {
+		printf("%-16s: %s\n", "event", "onConnect");
+		printf("%-16s: %s\n", "server", args[0]);
+	}
+}
+
+static void
+show_disconnect(char *line)
+{
+	const char *args[2] = {0};
+
+	if (irc_util_split(line, args, 2) == 2) {
+		printf("%-16s: %s\n", "event", "onDisonnect");
+		printf("%-16s: %s\n", "server", args[0]);
+	}
+}
+
+static void
+show_invite(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) == 5) {
+		printf("%-16s: %s\n", "event", "onInvite");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "nickname", args[4]);
+	}
+}
+
+static void
+show_join(char *line)
+{
+	const char *args[4] = {0};
+
+	if (irc_util_split(line, args, 4) == 4) {
+		printf("%-16s: %s\n", "event", "onJoin");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+	}
+}
+
+static void
+show_kick(char *line)
+{
+	const char *args[6] = {0};
+
+	if (irc_util_split(line, args, 6) >= 5) {
+		printf("%-16s: %s\n", "event", "onKick");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "target", args[4]);
+		printf("%-16s: %s\n", "reason", args[5] ? args[5] : "");
+	}
+}
+
+static void
+show_me(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) == 5) {
+		printf("%-16s: %s\n", "event", "onMe");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "message", args[4]);
+	}
+}
+
+static void
+show_message(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) == 5) {
+		printf("%-16s: %s\n", "event", "onMessage");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "message", args[4]);
+	}
+}
+
+static void
+show_mode(char *line)
+{
+	const char *args[8] = {0};
+
+	if (irc_util_split(line, args, 8) >= 5) {
+		printf("%-16s: %s\n", "event", "onMode");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "mode", args[4]);
+		printf("%-16s: %s\n", "limit", (args[5] ? args[5] : ""));
+		printf("%-16s: %s\n", "user", (args[6] ? args[6] : ""));
+		printf("%-16s: %s\n", "mask", (args[7] ? args[7] : ""));
+	}
+}
+
+static void
+show_nick(char *line)
+{
+	const char *args[4] = {0};
+
+	if (irc_util_split(line, args, 4) == 4) {
+		printf("%-16s: %s\n", "event", "onNick");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "nickname", args[3]);
+	}
+}
+
+static void
+show_notice(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) == 5) {
+		printf("%-16s: %s\n", "event", "onNotice");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "message", args[4]);
+	}
+}
+
+static void
+show_part(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) >= 4) {
+		printf("%-16s: %s\n", "event", "onPart");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "reason", (args[4] ? args[4] : ""));
+	}
+}
+
+static void
+show_topic(char *line)
+{
+	const char *args[5] = {0};
+
+	if (irc_util_split(line, args, 5) >= 4) {
+		printf("%-16s: %s\n", "event", "onTopic");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "origin", args[2]);
+		printf("%-16s: %s\n", "channel", args[3]);
+		printf("%-16s: %s\n", "topic", args[4]);
+	}
+}
 
-	if (strcmp(response, "OK") != 0)
-		errx(1, "abort: %s", response);
+static void
+show_whois(char *line)
+{
+	const char *args[6] = {0};
+	//char *p, *token;
+
+	if (irc_util_split(line, args, 6) >= 4) {
+		printf("%-16s: %s\n", "event", "onWhois");
+		printf("%-16s: %s\n", "server", args[1]);
+		printf("%-16s: %s\n", "nickname", args[2]);
+		printf("%-16s: %s\n", "username", args[3]);
+		printf("%-16s: %s\n", "hostname", args[4]);
+		printf("%-16s: %s\n", "username", args[5]);
+		//printf("channels:  %s\n", args[6]);
+	}
+}
+
+static const struct {
+	const char *event;
+	void (*show)(char *);
+} watchtable[] = {
+	{ "EVENT-CONNECT",      show_connect    },
+	{ "EVENT-DISCONNECT",   show_disconnect },
+	{ "EVENT-INVITE",       show_invite     },
+	{ "EVENT-JOIN",         show_join       },
+	{ "EVENT-KICK",         show_kick       },
+	{ "EVENT-MESSAGE",      show_message    },
+	{ "EVENT-ME",           show_me         },
+	{ "EVENT-MODE",         show_mode       },
+	{ "EVENT-NICK",         show_nick       },
+	{ "EVENT-NOTICE",       show_notice     },
+	{ "EVENT-PART",         show_part       },
+	{ "EVENT-TOPIC",        show_topic      },
+	{ "EVENT-WHOIS",        show_whois      }
+};
+
+static void
+show(char *ev)
+{
+	for (size_t i = 0; i < IRC_UTIL_SIZE(watchtable); ++i) {
+		if (strncmp(watchtable[i].event, ev, strlen(watchtable[i].event)) == 0) {
+			watchtable[i].show(ev);
+			printf("\n");
+			break;
+		}
+	}
+}
+
+static void
+plugin_list_set(int argc, char **argv, const char *cmd)
+{
+	char *line, *p;
+	size_t num = 0;
+
+	if (argc == 3) {
+		req("%s %s %s %s", cmd, argv[0], argv[1], argv[2]);
+		ok();
+		return;
+	}
+
+	if (argc == 2)
+		req("%s %s %s", cmd, argv[0], argv[1]);
+	else
+		req("%s %s", cmd, argv[0]);
+
+	if (sscanf(line = ok(), "%zu", &num) != 1)
+		errx(1, "could not retrieve list");
+
+	if (argc == 2)
+		puts(poll());
+	else {
+		while (num-- != 0 && (line = poll())) {
+			if (!(p = strchr(line, '=')))
+				continue;
+
+			*p = '\0';
+			printf("%-16s: %s\n", line, p + 1);
+		}
+	}
+
+}
+
+static void
+cmd_plugin_config(int argc, char **argv)
+{
+	return plugin_list_set(argc, argv, "PLUGIN-CONFIG");
+}
+
+/*
+ * Response:
+ *
+ *     OK name
+ *     summary
+ *     version
+ *     license
+ *     author
+ */
+static void
+cmd_plugin_info(int argc, char **argv)
+{
+	(void)argc;
+
+	const char *response;
+
+	req("PLUGIN-INFO %s", argv[0]);
+
+	if (strncmp((response = poll()), "OK ", 3) != 0)
+		errx(1, "failed to retrieve plugin information");
+
+	printf("%-16s: %s\n", "name", response + 3);
+	printf("%-16s: %s\n", "summary", poll());
+	printf("%-16s: %s\n", "version", poll());
+	printf("%-16s: %s\n", "license", poll());
+	printf("%-16s: %s\n", "author", poll());
+}
+
+static void
+cmd_plugin_list(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	char *list;
+
+	req("PLUGIN-LIST");
+
+	if (strncmp(list = poll(), "OK ", 3) != 0)
+		errx(1, "failed to retrieve plugin list");
+
+	list += 3;
+
+	for (char *p; (p = strchr(list, ' ')); )
+		*p = '\n';
+
+	puts(list);
+}
+
+static void
+cmd_plugin_load(int argc, char **argv)
+{
+	(void)argc;
+
+	req("PLUGIN-LOAD %s", argv[0]);
+	ok();
+}
+
+static void
+cmd_plugin_path(int argc, char **argv)
+{
+	return plugin_list_set(argc, argv, "PLUGIN-PATH");
+}
+
+static void
+cmd_plugin_reload(int argc, char **argv)
+{
+	if (argc == 1)
+		req("PLUGIN-RELOAD %s", argv[0]);
+	else
+		req("PLUGIN-RELOAD");
+
+	ok();
+}
+
+static void
+cmd_plugin_template(int argc, char **argv)
+{
+	return plugin_list_set(argc, argv, "PLUGIN-TEMPLATE");
+}
+
+static void
+cmd_plugin_unload(int argc, char **argv)
+{
+	if (argc == 1)
+		req("PLUGIN-UNLOAD %s", argv[0]);
+	else
+		req("PLUGIN-UNLOAD");
+
+	ok();
 }
 
 static void
@@ -145,6 +498,58 @@
 	ok();
 }
 
+/*
+ * Response:
+ *
+ *     OK name
+ *     hostname port [ssl]
+ *     nickname username realname
+ *     chan1 chan2 chanN
+ */
+static void
+cmd_server_info(int argc, char **argv)
+{
+	(void)argc;
+
+	char *list;
+	const char *args[16] = {0};
+
+	req("SERVER-INFO %s", argv[0]);
+
+	if (strncmp(list = poll(), "OK ", 3) != 0)
+		errx(1, "failed to retrieve server information");
+
+	printf("%-16s: %s\n", "name", list + 3);
+
+	if (irc_util_split((list = poll()), args, 3) < 2)
+		errx(1, "malformed server connection");
+
+	printf("%-16s: %s\n", "hostname", args[0]);
+	printf("%-16s: %s\n", "port", args[1]);
+
+	if (args[2])
+		printf("%-16s: %s\n", "ssl", "true");
+
+	if (irc_util_split((list = poll()), args, 3) != 3)
+		errx(1, "malformed server ident");
+
+	printf("%-16s: %s\n", "nickname", args[0]);
+	printf("%-16s: %s\n", "username", args[0]);
+	printf("%-16s: %s\n", "realname", args[0]);
+	printf("%-16s: %s\n", "channels", poll());
+}
+
+static void
+cmd_server_join(int argc, char **argv)
+{
+	if (argc >= 3)
+		req("SERVER-JOIN %s %s %s", argv[0], argv[1], argv[2]);
+	else
+		req("SERVER-JOIN %s %s", argv[0], argv[1]);
+
+	ok();
+}
+
 static void
 cmd_server_list(int argc, char **argv)
 {
@@ -239,218 +644,6 @@
 }
 
 static void
-show_connect(char *line)
-{
-	const char *args[2] = {0};
-
-	if (irc_util_split(line, args, 2) == 2) {
-		printf("event:     onConnect\n");
-		printf("server:    %s\n", args[0]);
-	}
-}
-
-static void
-show_disconnect(char *line)
-{
-	const char *args[2] = {0};
-
-	if (irc_util_split(line, args, 2) == 2) {
-		printf("event:     onDisonnect\n");
-		printf("server:    %s\n", args[0]);
-	}
-}
-
-static void
-show_invite(char *line)
-{
-	const char *args[5] = {0};
-
-	if (irc_util_split(line, args, 5) == 5) {
-		printf("event:     onInvite\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("nickname:  %s\n", args[4]);
-	}
-}
-
-static void
-show_join(char *line)
-{
-	const char *args[4] = {0};
-
-	if (irc_util_split(line, args, 4) == 4) {
-		printf("event:     onJoin\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-	}
-}
-
-static void
-show_kick(char *line)
-{
-	const char *args[6] = {0};
-
-	if (irc_util_split(line, args, 6) >= 5) {
-		printf("event:     onKick\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("target:    %s\n", args[4]);
-		printf("reason:    %s\n", args[5] ? args[5] : "");
-	}
-}
-
-static void
-show_me(char *line)
-{
-	const char *args[5] = {0};
-
-	if (irc_util_split(line, args, 5) == 5) {
-		printf("event:     onMe\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("message:   %s\n", args[4]);
-	}
-}
-
-static void
-show_message(char *line)
-{
-	const char *args[5] = {0};
-
-	if (irc_util_split(line, args, 5) == 5) {
-		printf("event:     onMessage\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("message:   %s\n", args[4]);
-	}
-}
-
-static void
-show_mode(char *line)
-{
-	const char *args[8] = {0};
-
-	if (irc_util_split(line, args, 8) >= 5) {
-		printf("event:     onMode\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("mode:      %s\n", args[4]);
-		printf("limit:     %s\n", (args[5] ? args[5] : ""));
-		printf("user:      %s\n", (args[6] ? args[6] : ""));
-		printf("mask:      %s\n", (args[7] ? args[7] : ""));
-	}
-}
-
-static void
-show_nick(char *line)
-{
-	const char *args[4] = {0};
-
-	if (irc_util_split(line, args, 4) == 4) {
-		printf("event:     onNick\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("nickname:  %s\n", args[3]);
-	}
-}
-
-static void
-show_notice(char *line)
-{
-	const char *args[5] = {0};
-
-	if (irc_util_split(line, args, 5) == 5) {
-		printf("event:     onNotice\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("message:   %s\n", args[4]);
-	}
-}
-
-static void
-show_part(char *line)
-{
-	const char *args[5] = {0};
-
-	if (irc_util_split(line, args, 5) >= 4) {
-		printf("event:     onPart\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("reason:    %s\n", (args[4] ? args[4] : ""));
-	}
-}
-
-static void
-show_topic(char *line)
-{
-	const char *args[5] = {0};
-
-	if (irc_util_split(line, args, 5) >= 4) {
-		printf("event:     onTopic\n");
-		printf("server:    %s\n", args[1]);
-		printf("origin:    %s\n", args[2]);
-		printf("channel:   %s\n", args[3]);
-		printf("topic:     %s\n", args[4]);
-	}
-}
-
-static void
-show_whois(char *line)
-{
-	const char *args[6] = {0};
-	//char *p, *token;
-
-	if (irc_util_split(line, args, 6) >= 4) {
-		printf("event:     onWhois\n");
-		printf("server:    %s\n", args[1]);
-		printf("nickname:  %s\n", args[2]);
-		printf("username:  %s\n", args[3]);
-		printf("hostname:  %s\n", args[4]);
-		printf("username:  %s\n", args[5]);
-		//printf("channels:  %s\n", args[6]);
-	}
-}
-
-static const struct {
-	const char *event;
-	void (*show)(char *);
-} watchtable[] = {
-	{ "EVENT-CONNECT",      show_connect    },
-	{ "EVENT-DISCONNECT",   show_disconnect },
-	{ "EVENT-INVITE",       show_invite     },
-	{ "EVENT-JOIN",         show_join       },
-	{ "EVENT-KICK",         show_kick       },
-	{ "EVENT-MESSAGE",      show_message    },
-	{ "EVENT-ME",           show_me         },
-	{ "EVENT-MODE",         show_mode       },
-	{ "EVENT-NICK",         show_nick       },
-	{ "EVENT-NOTICE",       show_notice     },
-	{ "EVENT-PART",         show_part       },
-	{ "EVENT-TOPIC",        show_topic      },
-	{ "EVENT-WHOIS",        show_whois      }
-};
-
-static void
-show(char *ev)
-{
-	for (size_t i = 0; i < IRC_UTIL_SIZE(watchtable); ++i) {
-		if (strncmp(watchtable[i].event, ev, strlen(watchtable[i].event)) == 0) {
-			watchtable[i].show(ev);
-			printf("\n");
-			break;
-		}
-	}
-}
-
-static void
 cmd_watch(int argc, char **argv)
 {
 	(void)argc;
@@ -478,7 +671,17 @@
 	void (*exec)(int, char **);
 } cmds[] = {
 	/* name                 min     max     exec                   */
+	{ "plugin-config",      1,      3,      cmd_plugin_config       },
+	{ "plugin-info",        1,      1,      cmd_plugin_info         },
+	{ "plugin-list",        0,      0,      cmd_plugin_list         },
+	{ "plugin-load",        1,      1,      cmd_plugin_load         },
+	{ "plugin-path",        0,      3,      cmd_plugin_path         },
+	{ "plugin-reload",      0,      1,      cmd_plugin_reload       },
+	{ "plugin-template",    1,      3,      cmd_plugin_template     },
+	{ "plugin-unload",      0,      1,      cmd_plugin_unload       },
 	{ "server-disconnect",  0,      1,      cmd_server_disconnect   },
+	{ "server-info",        1,      1,      cmd_server_info         },
+	{ "server-join",        2,      3,      cmd_server_join         },
 	{ "server-list",        0,      0,      cmd_server_list         },
 	{ "server-me",          3,      3,      cmd_server_me           },
 	{ "server-message",     3,      3,      cmd_server_message      },
--- a/lib/irccd/limits.h	Wed Feb 03 20:45:00 2021 +0100
+++ b/lib/irccd/limits.h	Sun Feb 07 14:36:28 2021 +0100
@@ -31,6 +31,8 @@
 #define IRC_USERMODES_LEN       8               /* Number of modes (e.g. ohv). */
 #define IRC_CHANTYPES_LEN       8               /* Channel types. */
 #define IRC_CMDCHAR_LEN         4               /* Prefix for plugin commands (e.g. !). */
+#define IRC_CHARSET_LEN         16              /* Charset name max. */
+#define IRC_CASEMAPPING_LEN     16              /* Maximum case mapping length. */
 #define IRC_MESSAGE_LEN         512             /* Official length per message. */
 #define IRC_ARGS_MAX            32              /* Own supported number of arguments per message. */
 
--- a/lib/irccd/server.c	Wed Feb 03 20:45:00 2021 +0100
+++ b/lib/irccd/server.c	Sun Feb 07 14:36:28 2021 +0100
@@ -106,8 +106,8 @@
 static void
 remove_channel(struct irc_channel *ch)
 {
+	LIST_REMOVE(ch, link);
 	irc_channel_finish(ch);
-	LIST_REMOVE(ch, link);
 }
 
 static int
@@ -156,10 +156,6 @@
 		for (size_t i = 0; i < IRC_UTIL_SIZE(s->params.prefixes) && *pm && *tk; ++i) {
 			s->params.prefixes[i].mode = *pm++;
 			s->params.prefixes[i].token = *tk++;
-
-			irc_log_info("server %s: supports prefix %c=%c", s->name,
-			    s->params.prefixes[i].mode,
-			    s->params.prefixes[i].token);
 		}
 	}
 }
@@ -168,7 +164,6 @@
 read_support_chantypes(struct irc_server *s, const char *value)
 {
 	strlcpy(s->params.chantypes, value, sizeof (s->params.chantypes));
-	irc_log_info("server %s: supports channel types: %s", s->name, s->params.chantypes);
 }
 
 static void
@@ -194,6 +189,7 @@
 	s->state = IRC_SERVER_STATE_NONE;
 	ev->type = IRC_EVENT_DISCONNECT;
 	ev->server = s;
+
 	irc_log_info("server %s: connection lost", s->name);
 }
 
@@ -209,10 +205,44 @@
 		if (sscanf(msg->args[i], "%63[^=]=%63s", key, value) != 2)
 			continue;
 
-		if (strcmp(key, "PREFIX") == 0)
+		if (strcmp(key, "PREFIX") == 0) {
 			read_support_prefix(s, value);
-		if (strcmp(key, "CHANTYPES") == 0)
+			irc_log_info("server %s: prefixes:           %s",
+			    s->name, value);
+		} else if (strcmp(key, "CHANTYPES") == 0) {
 			read_support_chantypes(s, value);
+			irc_log_info("server %s: channel types:      %s",
+			    s->name, value);
+		} else if (strcmp(key, "CHANNELLEN") == 0) {
+			s->params.chanlen = atoi(value);
+			irc_log_info("server %s: channel name limit: %u",
+			    s->name, s->params.chanlen);
+		} else if (strcmp(key, "NICKLEN") == 0) {
+			s->params.nicklen = atoi(value);
+			irc_log_info("server %s: nickname limit:     %u",
+			    s->name, s->params.nicklen);
+		} else if (strcmp(key, "TOPICLEN") == 0) {
+			s->params.topiclen = atoi(value);
+			irc_log_info("server %s: topic limit:        %u",
+			    s->name, s->params.topiclen);
+		} else if (strcmp(key, "AWAYLEN") == 0) {
+			s->params.awaylen = atoi(value);
+			irc_log_info("server %s: away message limit: %u",
+			    s->name, s->params.awaylen);
+		} else if (strcmp(key, "KICKLEN") == 0) {
+			s->params.kicklen = atoi(value);
+			irc_log_info("server %s: kick reason limit:  %u",
+			    s->name, s->params.kicklen);
+		}
+		else if (strcmp(key, "CHARSET") == 0) {
+			strlcpy(s->params.charset, value, sizeof (s->params.charset));
+			irc_log_info("server %s: charset:            %s",
+			    s->name, s->params.charset);
+		} else if (strcmp(key, "CASEMAPPING") == 0) {
+			strlcpy(s->params.casemapping, value, sizeof (s->params.casemapping));
+			irc_log_info("server %s: case mapping:       %s",
+			    s->name, s->params.casemapping);
+		}
 	}
 }
 
@@ -299,7 +329,7 @@
 
 	ch = add_channel(s, ev->part.channel, NULL, 1);
 
-	if (is_self(s, ev->part.origin) == 0) {
+	if (is_self(s, ev->part.origin)) {
 		remove_channel(ch);
 		irc_log_info("server %s: leaving channel %s", s->name, ev->part.channel);
 	} else
--- a/lib/irccd/server.h	Wed Feb 03 20:45:00 2021 +0100
+++ b/lib/irccd/server.h	Sun Feb 07 14:36:28 2021 +0100
@@ -64,6 +64,13 @@
 
 struct irc_server_params {
 	char chantypes[IRC_CHANTYPES_LEN];
+	char charset[IRC_CHARSET_LEN];
+	char casemapping[IRC_CASEMAPPING_LEN];
+	unsigned int chanlen;
+	unsigned int nicklen;
+	unsigned int topiclen;
+	unsigned int awaylen;
+	unsigned int kicklen;
 	struct {
 		char mode;              /* Mode (e.g. ov). */
 		char token;             /* Symbol used (e.g. @+). */