changeset 953:ab43ba409f9d

irccd: add SSL, and cleanup events
author David Demelier <markand@malikania.fr>
date Wed, 20 Jan 2021 12:32:59 +0100
parents 2899474aefd7
children 30643d18a635
files irccd/main.c irccdctl/main.c lib/CMakeLists.txt lib/irccd/event.c lib/irccd/event.h lib/irccd/irccd.c lib/irccd/js-plugin.c lib/irccd/list.h lib/irccd/peer.c lib/irccd/peer.h lib/irccd/server.c lib/irccd/server.h lib/irccd/util.c lib/irccd/util.h tests/CMakeLists.txt tests/test-event.c
diffstat 16 files changed, 550 insertions(+), 333 deletions(-) [+]
line wrap: on
line diff
--- a/irccd/main.c	Sun Jan 17 19:05:39 2021 +0100
+++ b/irccd/main.c	Wed Jan 20 12:32:59 2021 +0100
@@ -24,6 +24,8 @@
 #include <irccd/server.h>
 #include <irccd/transport.h>
 #include <irccd/util.h>
+#include <irccd/plugin.h>
+#include <irccd/js-plugin.h>
 
 int
 main(int argc, char **argv)
@@ -37,8 +39,9 @@
 		.nickname = "circ",
 		.hostname = "malikania.fr",
 		.port = 6697,
-		.flags = IRC_SERVER_FLAGS_SSL
+		.flags = IRC_SERVER_FLAGS_SSL | IRC_SERVER_FLAGS_JOIN_INVITE
 	};
+	struct irc_plugin p = {0};
 
 	irc_log_set_verbose(true);
 	irc_bot_init();
@@ -46,5 +49,7 @@
 	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	Sun Jan 17 19:05:39 2021 +0100
+++ b/irccdctl/main.c	Wed Jan 20 12:32:59 2021 +0100
@@ -235,6 +235,74 @@
 	ok();
 }
 
+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", args[1]);
+		printf("origin:    %s", args[2]);
+		printf("channel:   %s", args[3]);
+		printf("nickname:  %s", 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 const struct {
+	const char *event;
+	void (*show)(char *);
+} watchtable[] = {
+	{ "EVENT-INVITE", show_invite },
+	{ "EVENT-MESSAGE", show_message }
+};
+
+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);
+			break;
+		}
+	}
+}
+
+static void
+cmd_watch(int argc, char **argv)
+{
+	(void)argc;
+	(void)argv;
+
+	struct timeval tv = {0};
+	char *ev;
+
+	/* Enable watch. */
+	req("WATCH");
+	ok();
+
+	/* Turn off timeout to receive indefinitely. */
+	if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)) < 0)
+		err(1, "setsockopt");
+
+	while ((ev = poll()))
+		show(ev);
+}
+
 static const struct cmd {
 	const char *name;
 	int minargs;
@@ -250,7 +318,8 @@
 	{ "server-nick",        2,      2,      cmd_server_nick         },
 	{ "server-notice",      3,      3,      cmd_server_notice       },
 	{ "server-part",        3,      3,      cmd_server_part         },
-	{ "server-topic",       3,      3,      cmd_server_topic        }
+	{ "server-topic",       3,      3,      cmd_server_topic        },
+	{ "watch",              0,      0,      cmd_watch               }
 };
 
 static int
--- a/lib/CMakeLists.txt	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/CMakeLists.txt	Wed Jan 20 12:32:59 2021 +0100
@@ -26,6 +26,7 @@
 	irccd/config.h.in
 	irccd/dl-plugin.c
 	irccd/dl-plugin.h
+	irccd/event.c
 	irccd/event.h
 	irccd/irccd.c
 	irccd/irccd.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/event.c	Wed Jan 20 12:32:59 2021 +0100
@@ -0,0 +1,162 @@
+/*
+ * event.c -- IRC event
+ *
+ * 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.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "event.h"
+#include "server.h"
+
+static inline void
+scan(char **line, char **str)
+{
+	char *p = strchr(*line, ' ');
+
+	if (p)
+		*p = '\0';
+
+	*str = *line;
+	*line = p ? p + 1 : strchr(*line, '\0');
+}
+
+bool
+irc_event_is_ctcp(const char *line)
+{
+	size_t length;
+
+	if (!line)
+		return false;
+	if ((length = strlen(line)) < 2)
+		return false;
+
+	return line[0] == 0x1 && line[length - 1] == 0x1;
+}
+
+char *
+irc_event_ctcp(char *line)
+{
+	/* Skip first \001. */
+	if (*line == '\001')
+		line++;
+
+	/* Remove last \001. */
+	line[strcspn(line, "\001")] = '\0';
+
+	if (strncmp(line, "ACTION ", 7) == 0)
+		line += 7;
+
+	return line;
+}
+
+bool
+irc_event_parse(struct irc_event_msg *ev, const char *line)
+{
+	assert(ev);
+	assert(line);
+
+	char *ptr = ev->buf;
+	size_t a;
+
+	memset(ev, 0, sizeof (*ev));
+	strlcpy(ev->buf, line, sizeof (ev->buf));
+
+	/*
+	 * IRC message is defined as following:
+	 *
+	 * [:prefix] command arg1 arg2 [:last-argument]
+	 */
+	if (*line == ':')
+		scan((++ptr, &ptr), &ev->prefix);     /* prefix */
+
+	scan(&ptr, &ev->cmd);                         /* command */
+
+	/* And finally arguments. */
+	for (a = 0; *ptr && a < IRC_ARGS_MAX; ++a) {
+		if (*ptr == ':') {
+			ev->args[a] = ptr + 1;
+			ptr = strchr(ptr, '\0');
+		} else
+			scan(&ptr, &ev->args[a]);
+	}
+
+	if (a >= IRC_ARGS_MAX)
+		return errno = EMSGSIZE, false;
+	if (ev->cmd == NULL)
+		return errno = EBADMSG, false;
+
+	return true;
+}
+
+bool
+irc_event_str(const struct irc_event *ev, char *str, size_t strsz)
+{
+	assert(ev);
+	assert(str);
+
+	int written = 0;
+
+	switch (ev->type) {
+	case IRC_EVENT_INVITE:
+		written = snprintf(str, strsz, "EVENT-INVITE %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		break;
+	case IRC_EVENT_JOIN:
+		written = snprintf(str, strsz, "EVENT-JOIN %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0]);
+		break;
+	case IRC_EVENT_KICK:
+		written = snprintf(str, strsz, "EVENT-KICK %s %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1], ev->msg.args[2]);
+		break;
+	case IRC_EVENT_ME:
+		written = snprintf(str, strsz, "EVENT-ME %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		break;
+	case IRC_EVENT_MESSAGE:
+		written = snprintf(str, strsz, "EVENT-MESSAGE %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		break;
+	case IRC_EVENT_MODE:
+		written = snprintf(str, strsz, "EVENT-MODE %s %s %s %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1], ev->msg.args[2],
+		    ev->msg.args[3], ev->msg.args[4]);
+		break;
+	case IRC_EVENT_NICK:
+		written = snprintf(str, strsz, "EVENT-NICK %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0]);
+		break;
+	case IRC_EVENT_NOTICE:
+		written = snprintf(str, strsz, "EVENT-NOTICE %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		break;
+	case IRC_EVENT_PART:
+		written = snprintf(str, strsz, "EVENT-PART %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		break;
+	case IRC_EVENT_TOPIC:
+		written = snprintf(str, strsz, "EVENT-TOPIC %s %s %s %s", ev->server->name,
+		    ev->msg.prefix, ev->msg.args[0], ev->msg.args[1]);
+		break;
+	default:
+		break;
+	}
+
+	return written > 0;
+}
--- a/lib/irccd/event.h	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/event.h	Wed Jan 20 12:32:59 2021 +0100
@@ -19,6 +19,7 @@
 #ifndef IRCCD_EVENT_H
 #define IRCCD_EVENT_H
 
+#include <stdbool.h>
 #include <stddef.h>
 
 #include "limits.h"
@@ -43,95 +44,29 @@
 	IRC_EVENT_WHOIS
 };
 
+struct irc_event_msg {
+	char *prefix;
+	char *cmd;
+	char *args[IRC_ARGS_MAX];
+	char buf[IRC_MESSAGE_MAX];
+};
+
 struct irc_event {
 	enum irc_event_type type;
 	struct irc_server *server;
-
-	/*
-	 * Raw arguments.
-	 *   [0]: prefix
-	 *   [1]: origin
-	 *   [2 to argsz]: arguments
-	 */
-	char args[IRC_ARGS_MAX][IRC_MESSAGE_MAX];
-	size_t argsz;
-
-	/* Conveniently organized union depending on the type. */
-	union {
-		struct {
-			char *origin;
-			char *channel;
-			char *nickname;
-		} invite;
-
-		struct {
-			char *origin;
-			char *channel;
-		} join;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *target;
-			char *reason;
-		} kick;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *message;
-		} me;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *message;
-		} message;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *mode;
-			char *limit;
-			char *user;
-			char *mask;
-		} mode;
-
-		struct {
-			char *origin;
-			char *nickname;
-		} nick;
-
-		struct {
-			struct irc_channel *channel;
-		} names;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *message;
-		} notice;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *reason;
-		} part;
-
-		struct {
-			char *origin;
-			char *channel;
-			char *topic;
-		} topic;
-
-		struct {
-			char *nick;
-			char *user;
-			char *hostname;
-			char *realname;
-			char **channels;
-		} whois;
-	};
+	struct irc_event_msg msg;
 };
 
+bool
+irc_event_is_ctcp(const char *);
+
+char *
+irc_event_ctcp(char *);
+
+bool
+irc_event_parse(struct irc_event_msg *, const char *);
+
+bool
+irc_event_str(const struct irc_event *, char *, size_t);
+
 #endif /* !IRCCD_EVENT_H */
--- a/lib/irccd/irccd.c	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/irccd.c	Wed Jan 20 12:32:59 2021 +0100
@@ -90,7 +90,20 @@
 	return pkg;
 }
 
-static void
+static inline void
+broadcast(const struct irc_event *ev)
+{
+	char buf[IRC_MESSAGE_MAX];
+
+	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);
+}
+
+static inline void
 invoke(const struct irc_event *ev)
 {
 	for (size_t i = 0; i < irc.pluginsz; ++i)
@@ -118,7 +131,7 @@
 	struct irc_peer peer;
 	struct irc_event ev;
 
-	if (poll(pkg->fds, pkg->fdsz, 1000) < 0)
+	if (poll(pkg->fds, pkg->fdsz, 1000) < 0 && errno != EINTR)
 		err(1, "poll");
 
 	/*
@@ -150,9 +163,12 @@
 	 * 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)
-		while (irc_server_poll(s, &ev))
+	IRC_LIST_FOREACH(irc.servers, s) {
+		while (irc_server_poll(s, &ev)) {
+			broadcast(&ev);
 			invoke(&ev);
+		}
+	}
 }
 
 static void
--- a/lib/irccd/js-plugin.c	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/js-plugin.c	Wed Jan 20 12:32:59 2021 +0100
@@ -40,6 +40,7 @@
 #include "jsapi-unicode.h"
 #include "log.h"
 #include "plugin.h"
+#include "server.h"
 #include "util.h"
 
 struct self {
@@ -84,8 +85,10 @@
 }
 
 static void
-push_names(duk_context *ctx, const struct irc_channel *ch)
+push_names(duk_context *ctx, const struct irc_event *ev)
 {
+	const struct irc_channel *ch = irc_server_find(ev->server, ev->msg.args[0]);
+
 	duk_push_array(ctx);
 
 	for (size_t i = 0; i < ch->usersz; ++i) {
@@ -271,6 +274,9 @@
 static void
 handle(struct irc_plugin *plg, const struct irc_event *ev)
 {
+	(void)plg;
+	(void)ev;
+
 	switch (ev->type) {
 	case IRC_EVENT_CONNECT:
 		call(plg, "onConnect", "S", ev->server);
@@ -279,53 +285,52 @@
 		call(plg, "onDisconnect", "S", ev->server);
 		break;
 	case IRC_EVENT_INVITE:
-		call(plg, "onInvite", "Ssss", ev->server, ev->invite.origin,
-		    ev->invite.channel, ev->invite.nickname);
+		call(plg, "onInvite", "Ss s", ev->server, ev->msg.prefix,
+		     ev->msg.args[1]);
 		break;
 	case IRC_EVENT_JOIN:
-		call(plg, "onJoin", "Sss", ev->server, ev->join.origin,
-		    ev->join.channel);
+		call(plg, "onJoin", "Ss s", ev->server, ev->msg.prefix,
+		    ev->msg.args[0]);
 		break;
 	case IRC_EVENT_KICK:
-		call(plg, "onKick", "Sssss", ev->server, ev->kick.origin, ev->kick.channel,
-		    ev->kick.target, ev->kick.reason);
+		call(plg, "onKick", "Ss sss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1], ev->msg.args[2]);
 		break;
 	case IRC_EVENT_ME:
-		call(plg, "onMe", "Ssss", ev->server, ev->me.origin, ev->me.channel,
-		    ev->me.message);
+		call(plg, "onMe", "Ss ss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1]);
 		break;
 	case IRC_EVENT_MESSAGE:
-		call(plg, "onMessage", "Ssss", ev->server, ev->message.origin,
-		    ev->message.channel, ev->message.message);
+		call(plg, "onMessage", "Ss ss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1]);
 		break;
 	case IRC_EVENT_MODE:
-		call(plg, "onMode", "Sssssss", ev->server, ev->mode.origin, ev->mode.channel,
-		    ev->mode.mode, ev->mode.limit, ev->mode.user, ev->mode.mask);
+		call(plg, "onMode", "Ss sss ss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1], ev->msg.args[2],
+		    ev->msg.args[3], ev->msg.args[4]);
 		break;
 	case IRC_EVENT_NAMES:
-		call(plg, "onNames", "Ssx", ev->server, ev->names.channel->name,
-		    push_names, ev->names.channel);
+		call(plg, "onNames", "Ss x", ev->server, ev->msg.args[1],
+		    push_names);
 		break;
 	case IRC_EVENT_NICK:
-		call(plg, "onNick", "Sss", ev->server, ev->nick.origin, ev->nick.nickname);
+		call(plg, "onNick", "Ss s", ev->server, ev->msg.prefix,
+		    ev->msg.args[0]);
 		break;
 	case IRC_EVENT_NOTICE:
-		call(plg, "onNotice", "Ssss", ev->server, ev->notice.origin,
-		    ev->notice.channel, ev->notice.message);
+		call(plg, "onNotice", "Ss ss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1]);
 		break;
 	case IRC_EVENT_PART:
-		call(plg, "onPart", "Ssss", ev->server, ev->part.origin, ev->part.channel,
-		    ev->part.reason);
+		call(plg, "onPart", "Ss ss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1]);
 		break;
 	case IRC_EVENT_TOPIC:
-		call(plg, "onTopic", "Ssss", ev->server, ev->topic.origin,
-		    ev->topic.channel, ev->topic.topic);
+		call(plg, "onTopic", "Ss ss", ev->server, ev->msg.prefix,
+		    ev->msg.args[0], ev->msg.args[1]);
 		break;
-#if 0
 	case IRC_EVENT_WHOIS:
-		call(plg, "onWhois", "Sx", ev->topic.server, js_event_whois_push, &ev->whois.whois);
 		break;
-#endif
 	default:
 		break;
 	}
--- a/lib/irccd/list.h	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/list.h	Wed Jan 20 12:32:59 2021 +0100
@@ -16,8 +16,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef IRCD_LIST_H
-#define IRCD_LIST_H
+#ifndef IRCCD_LIST_H
+#define IRCCD_LIST_H
 
 #define IRC_LIST_ADD(h, o)                                              \
 do {                                                                    \
@@ -38,11 +38,10 @@
         (o)->next = (o)->prev = NULL;                                   \
 } while (0)
 
-
 #define IRC_LIST_FOREACH(h, s)                                          \
-	for ((s) = (h); (s); (s) = (s)->next)
+        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 /* IRC_LIST_H */
+#endif /* IRCCD_LIST_H */
--- a/lib/irccd/peer.c	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/peer.c	Wed Jan 20 12:32:59 2021 +0100
@@ -298,6 +298,16 @@
 	return ok(p);
 }
 
+static int
+cmd_watch(struct irc_peer *p, char *line)
+{
+	(void)line;
+
+	p->is_watching = true;
+
+	return ok(p);
+}
+
 static const struct cmd {
 	const char *name;
 	int (*call)(struct irc_peer *, char *);
@@ -313,6 +323,7 @@
 	{ "SERVER-NOTICE",      cmd_server_notice       },
 	{ "SERVER-PART",        cmd_server_part         },
 	{ "SERVER-TOPIC",       cmd_server_topic        },
+	{ "WATCH",              cmd_watch               }
 };
 
 static int
--- a/lib/irccd/peer.h	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/peer.h	Wed Jan 20 12:32:59 2021 +0100
@@ -27,6 +27,7 @@
 
 struct irc_peer {
 	int fd;
+	bool is_watching;
 	char in[IRC_BUF_MAX];
 	char out[IRC_BUF_MAX];
 };
--- a/lib/irccd/server.c	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/server.c	Wed Jan 20 12:32:59 2021 +0100
@@ -49,33 +49,6 @@
 	char host[IRC_HOST_MAX];
 };
 
-static bool
-is_ctcp(const char *line)
-{
-	assert(line);
-
-	const size_t length = strlen(line);
-
-	if (length < 2)
-		return false;
-
-	return line[0] == 0x1 && line[length - 1] == 0x1;
-}
-
-static char *
-ctcp(char *line)
-{
-	assert(line);
-
-	/* Remove last \001. */
-	line[strcspn(line, "\001")] = 0;
-
-	if (strncmp(line, "ACTION ", 7) == 0)
-		line += 7;
-
-	return line;
-}
-
 static int
 cmp_channel(const struct irc_channel *c1, const struct irc_channel *c2)
 {
@@ -117,16 +90,21 @@
 static struct irc_channel *
 add_channel(struct irc_server *s, const char *name, const char *password, bool joined)
 {
-	struct irc_channel ch = {
-		.joined = joined
-	};
+	struct irc_channel chnew = {0}, *ch;
 
-	strlcpy(ch.name, name, sizeof (ch.name));
+	if ((ch = irc_server_find(s, name))) {
+		ch->joined = joined;
+		return ch;
+	}
+
+	strlcpy(chnew.name, name, sizeof (chnew.name));
 
 	if (password)
-		strlcpy(ch.password, password, sizeof (ch.password));
+		strlcpy(chnew.password, password, sizeof (chnew.password));
 
-	IRC_SET_ALLOC_PUSH(&s->channels, &s->channelsz, &ch, cmp_channel);
+	chnew.joined = joined;
+
+	IRC_SET_ALLOC_PUSH(&s->channels, &s->channelsz, &chnew, cmp_channel);
 
 	return irc_server_find(s, name);
 }
@@ -134,14 +112,15 @@
 static void
 remove_channel(struct irc_server *s, struct irc_channel *ch)
 {
+	irc_channel_clear(ch);
 	IRC_SET_ALLOC_REMOVE(&s->channels, &s->channelsz, ch);
 }
 
 static void
 read_support_prefix(struct irc_server *s, const char *value)
 {
-	char modes[16 + 1] = { 0 };
-	char tokens[16 + 1] = { 0 };
+	char modes[16 + 1] = {0};
+	char tokens[16 + 1] = {0};
 
 	if (sscanf(value, "(%16[^)])%16s", modes, tokens) == 2) {
 		char *pm = modes;
@@ -161,26 +140,27 @@
 }
 
 static void
-convert_connect(struct irc_server *s, struct irc_event *ev)
+handle_connect(struct irc_server *s, struct irc_event *ev)
 {
 	s->state = IRC_SERVER_STATE_CONNECTED;
 
-	ev->type = IRC_EVENT_CONNECT;
-	ev->server = s;
-
 	/* 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);
+
+	ev->type = IRC_EVENT_CONNECT;
 }
 
 static void
-convert_support(struct irc_server *s, struct irc_event *ev)
+handle_support(struct irc_server *s, struct irc_event *ev)
 {
+	(void)ev;
+
 	char key[64];
 	char value[64];
 
-	for (size_t i = 4; i < ev->argsz; ++i) {
-		if (sscanf(ev->args[i], "%63[^=]=%63s", key, value) != 2)
+	for (size_t i = 2; ev->msg.args[i]; ++i) {
+		if (sscanf(ev->msg.args[i], "%63[^=]=%63s", key, value) != 2)
 			continue;
 
 		if (strcmp(key, "PREFIX") == 0)
@@ -191,85 +171,68 @@
 }
 
 static void
-convert_join(struct irc_server *s, struct irc_event *ev)
+handle_invite(struct irc_server *s, struct irc_event *ev)
 {
-	const struct origin *origin = parse_origin(ev->args[0]);
-	struct irc_channel *ch = NULL;
+	if (strcmp(ev->msg.args[0], s->nickname) == 0 && s->flags & IRC_SERVER_FLAGS_JOIN_INVITE)
+		irc_server_join(s, ev->msg.args[1], NULL);
 
-	ev->type = IRC_EVENT_JOIN;
-	ev->server = s;
-	ev->join.origin = ev->args[0];
-	ev->join.channel = ev->args[2];
-
-	if (!(ch = irc_server_find(s, ev->args[2])))
-		ch = add_channel(s, ev->args[2], NULL, true);
-	else
-		ch->joined = true;
-
-	irc_channel_add(ch, origin->nickname, 0);
+	ev->type = IRC_EVENT_INVITE;
 }
 
 static void
-convert_kick(struct irc_server *s, struct irc_event *ev)
+handle_join(struct irc_server *s, struct irc_event *ev)
 {
-	struct irc_channel *ch = irc_server_find(s, ev->args[2]);
+	add_channel(s, ev->msg.args[0], NULL, true);
 
-	ev->type = IRC_EVENT_KICK;
-	ev->server = s;
-	ev->kick.origin = ev->args[0];
-	ev->kick.channel = ev->args[2];
-	ev->kick.target = ev->args[3];
-	ev->kick.reason = ev->args[4];
+	ev->type = IRC_EVENT_JOIN;
+}
+
+static void
+handle_kick(struct irc_server *s, struct irc_event *ev)
+{
+	struct irc_channel *ch = add_channel(s, ev->msg.args[0], NULL, true);
 
 	/*
 	 * If the bot was kicked itself mark the channel as not joined and
 	 * rejoin it automatically if the option is set.
 	 */
-	if (strcmp(ev->args[3], s->nickname) == 0 && ch) {
+	if (strcmp(ev->msg.args[1], s->nickname) == 0) {
 		ch->joined = false;
 		irc_channel_clear(ch);
 
 		if (s->flags & IRC_SERVER_FLAGS_AUTO_REJOIN)
 			irc_server_join(s, ch->name, ch->password);
 	} else
-		irc_channel_remove(ch, ev->args[3]);
+		irc_channel_remove(ch, ev->msg.args[1]);
+
+	ev->type = IRC_EVENT_KICK;
 }
 
 static void
-convert_mode(struct irc_server *s, struct irc_event *ev)
+handle_mode(struct irc_server *s, struct irc_event *ev)
 {
 	(void)s;
 	(void)ev;
 
-	for (size_t i = 0; i < ev->argsz; ++i) {
-		printf("MODE: %zu=%s\n", i, ev->args[i]);
-	}
+	ev->type = IRC_EVENT_MODE;
 }
 
 static void
-convert_part(struct irc_server *s, struct irc_event *ev)
+handle_part(struct irc_server *s, struct irc_event *ev)
 {
-	const struct origin *origin = parse_origin(ev->args[0]);
-	struct irc_channel *ch = irc_server_find(s, ev->args[2]);
+	const struct origin *origin = parse_origin(ev->msg.prefix);
+	struct irc_channel *ch = add_channel(s, ev->msg.args[0], NULL, true);
+
+	if (strcmp(origin->nickname, s->nickname) == 0)
+		remove_channel(s, ch);
 
 	ev->type = IRC_EVENT_PART;
-	ev->server = s;
-	ev->part.origin = ev->args[0];
-	ev->part.channel = ev->args[2];
-	ev->part.reason = ev->args[3];
-
-	if (ch && strcmp(origin->nickname, s->nickname) == 0)
-		remove_channel(s, ch);
 }
 
 static void
-convert_msg(struct irc_server *s, struct irc_event *ev)
+handle_msg(struct irc_server *s, struct irc_event *ev)
 {
-	ev->type = IRC_EVENT_MESSAGE;
-	ev->server = s;
-	ev->message.origin = ev->args[0];
-	ev->message.channel = ev->args[2];
-	ev->message.message = ev->args[3];
+	(void)s;
 
 	/*
 	 * Detect CTCP commands which are PRIVMSG with a special boundaries.
@@ -277,164 +240,104 @@
 	 * Example:
 	 * PRIVMSG jean :\001ACTION I'm eating\001.
 	 */
-	if (is_ctcp(ev->args[3])) {
+	if (irc_event_is_ctcp(ev->msg.args[1])) {
 		ev->type = IRC_EVENT_ME;
-		ev->message.message = ctcp(ev->args[3] + 1);
-	}
+		ev->msg.args[1] = irc_event_ctcp(ev->msg.args[1]);
+	} else
+		ev->type = IRC_EVENT_MESSAGE;
 }
 
 static void
-convert_nick(struct irc_server *s, struct irc_event *ev)
+handle_nick(struct irc_server *s, struct irc_event *ev)
 {
-	const struct origin *origin = parse_origin(ev->args[0]);
+	const struct origin *origin = parse_origin(ev->msg.prefix);
 
 	/* Update nickname if it is myself. */
 	if (strcmp(origin->nickname, s->nickname) == 0)
-		strlcpy(s->nickname, ev->args[2], sizeof (s->nickname));
-}
+		strlcpy(s->nickname, ev->msg.args[0], sizeof (s->nickname));
 
-static void
-convert_notice(struct irc_server *s, struct irc_event *ev)
-{
-	ev->type = IRC_EVENT_NOTICE;
-	ev->server = s;
-	ev->notice.origin = ev->args[0];
-	ev->notice.channel = ev->args[2];
-	ev->notice.message = ev->args[3];
+	ev->type = IRC_EVENT_NICK;
 }
 
 static void
-convert_topic(struct irc_server *s, struct irc_event *ev)
+handle_notice(struct irc_server *s, struct irc_event *ev)
 {
-	ev->type = IRC_EVENT_TOPIC;
-	ev->server = s;
-	ev->topic.origin = ev->args[0];
-	ev->topic.channel = ev->args[2];
-	ev->topic.topic = ev->args[3];
-}
+	(void)s;
 
-static void
-convert_ping(struct irc_server *s, struct irc_event *ev)
-{
-	irc_server_send(s, "PONG %s", ev->args[0]);
+	ev->type = IRC_EVENT_NOTICE;
 }
 
 static void
-convert_names(struct irc_server *s, struct irc_event *ev)
+handle_topic(struct irc_server *s, struct irc_event *ev)
 {
-	struct irc_channel *ch;
-	char *p, *n;
-
-	if (ev->argsz < 6)
-		return;
-
-	/* TODO: create if not exist */
-	ch = irc_server_find(s, ev->args[4]);
+	(void)s;
 
-	for (p = ev->args[5]; p; ) {
-		n = strchr(p, ' ');
+	ev->type = IRC_EVENT_TOPIC;
+}
 
-		if (n)
-			*n = '\0';
-		if (strlen(p) > 0)
-			add_nick(s, ch, p);
-
-		p = n ? n + 1 : NULL;
-	}
+static void
+handle_ping(struct irc_server *s, struct irc_event *ev)
+{
+	irc_server_send(s, "PONG %s", ev->msg.args[0]);
 }
 
 static void
-convert_endofnames(struct irc_server *s, struct irc_event *ev)
+handle_names(struct irc_server *s, struct irc_event *ev)
 {
-	for (size_t i = 0; i < ev->argsz; ++i)
-		printf("%zd: %s\n", i, ev->args[i]);
-	ev->type = IRC_EVENT_NAMES;
-	ev->server = s;
-	ev->names.channel = irc_server_find(s, ev->args[3]);
-}
+	struct irc_channel *ch;
+	char *p, *token;
 
-static const struct convert {
-	const char *command;
-	void (*convert)(struct irc_server *, struct irc_event *);
-} converters[] = {
-	/* Must be kept ordered. */
-	{ "001",        convert_connect          },
-	{ "005",        convert_support          },
-	{ "353",        convert_names            },
-	{ "366",        convert_endofnames       },
-	{ "JOIN",       convert_join             },
-	{ "KICK",       convert_kick             },
-	{ "MODE",       convert_mode             },
-	{ "NICK",       convert_nick             },
-	{ "NOTICE",     convert_notice           },
-	{ "PART",       convert_part             },
-	{ "PING",       convert_ping             },
-	{ "PRIVMSG",    convert_msg              },
-	{ "TOPIC",      convert_topic            }
-};
+	ch = add_channel(s, ev->msg.args[2], NULL, true);
 
-static int
-compare_converter(const void *d1, const void *d2)
-{
-	return strcmp(d1, ((const struct convert *)d2)->command);
+	/* TODO: libcompat for strtok_r. */
+	for (p = ev->msg.args[3]; (token = strtok_r(p, " ", &p)); )
+		if (strlen(token) > 0)
+			add_nick(s, ch, token);
 }
 
 static void
-convert(struct irc_server *s, struct irc_event *ev)
+handle_endofnames(struct irc_server *s, struct irc_event *ev)
 {
-	const struct convert *c = bsearch(ev->args[1], converters, IRC_UTIL_SIZE(converters),
-	    sizeof (*c), &(compare_converter));
+	(void)s;
 
-	if (c)
-		c->convert(s, ev);
+	ev->type = IRC_EVENT_NAMES;
 }
 
-static inline bool
-scan(struct irc_event *ev, const char **line)
-{
-	const char *p = *line;
-	size_t i = 0;
-
-	/* Copy argument. */
-	while (i < IRC_MESSAGE_MAX && *p && !isspace(*p))
-		ev->args[ev->argsz][i++] = *p++;
+static const struct handler {
+	const char *command;
+	void (*handle)(struct irc_server *, struct irc_event *);
+} handlers[] = {
+	/* Must be kept ordered. */
+	{ "001",        handle_connect          },
+	{ "005",        handle_support          },
+	{ "353",        handle_names            },
+	{ "366",        handle_endofnames       },
+	{ "INVITE",     handle_invite           },
+	{ "JOIN",       handle_join             },
+	{ "KICK",       handle_kick             },
+	{ "MODE",       handle_mode             },
+	{ "NICK",       handle_nick             },
+	{ "NOTICE",     handle_notice           },
+	{ "PART",       handle_part             },
+	{ "PING",       handle_ping             },
+	{ "PRIVMSG",    handle_msg              },
+	{ "TOPIC",      handle_topic            }
+};
 
-	/* Skip optional spaces. */
-	while (*p && isspace(*p))
-		++p;
-
-	if (i >= IRC_MESSAGE_MAX)
-		return false;
-
-	*line = p;
-	ev->argsz++;
-
-	return true;
+static int
+compare_handler(const void *d1, const void *d2)
+{
+	return strcmp(d1, ((const struct handler *)d2)->command);
 }
 
 static void
-parse(struct irc_server *s, struct irc_event *ev, const char *line)
+handle(struct irc_server *s, struct irc_event *ev)
 {
-	if (!*line || *line++ != ':')
-		return;
-	if (!scan(ev, &line))   /* Prefix */
-		return;
-	if (!scan(ev, &line))   /* Command */
-		return;
+	const struct handler *c = bsearch(ev->msg.cmd, handlers, IRC_UTIL_SIZE(handlers),
+	    sizeof (*c), &(compare_handler));
 
-	/* Arguments. */
-	while (*line && ev->argsz < IRC_ARGS_MAX) {
-		/* Last argument: read until end. */
-		if (*line == ':') {
-			strlcpy(ev->args[ev->argsz++], ++line, IRC_MESSAGE_MAX);
-			break;
-		}
-
-		if (!scan(ev, &line))
-			return;
-	}
-
-	convert(s, ev);
+	if (c)
+		c->handle(s, ev);
 }
 
 static void
@@ -489,10 +392,12 @@
 {
 	s->state = IRC_SERVER_STATE_CONNECTED;
 
+	if (s->password[0])
+		irc_server_send(s, "PASS %s", s->password);
+
 	irc_server_send(s, "NICK %s", s->nickname);
 	irc_server_send(s, "USER %s %s %s :%s", s->username,
 		s->username, s->username, s->realname);
-	/* TODO: server password as well. */
 }
 
 #if defined(IRCCD_WITH_SSL)
@@ -756,8 +661,11 @@
 static void
 flush_ready(struct irc_server *s, const struct pollfd *pfd)
 {
-	if (pfd->revents & POLLERR || pfd->revents & POLLHUP)
+	if (pfd->revents & POLLERR || pfd->revents & POLLHUP) {
 		clear(s);
+		return;
+	}
+
 	if (pfd->revents & POLLIN)
 		input(s);
 	if (pfd->revents & POLLOUT)
@@ -843,8 +751,8 @@
 	memset(ev, 0, sizeof (*ev));
 	ev->type = IRC_EVENT_UNKNOWN;
 
-	if (length > 0)
-		parse(s, ev, s->in);
+	if (length > 0 && irc_event_parse(&ev->msg, s->in))
+		handle(s, ev);
 
 	memmove(s->in, pos + 2, sizeof (s->in) - (length + 2));
 
--- a/lib/irccd/server.h	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/server.h	Wed Jan 20 12:32:59 2021 +0100
@@ -47,8 +47,9 @@
 enum irc_server_flags {
 	IRC_SERVER_FLAGS_SSL           = (1 << 0),
 	IRC_SERVER_FLAGS_AUTO_REJOIN   = (1 << 1),
-	IRC_SERVER_FLAGS_IPV4          = (1 << 2),
-	IRC_SERVER_FLAGS_IPV6          = (1 << 3)
+	IRC_SERVER_FLAGS_JOIN_INVITE   = (1 << 2),
+	IRC_SERVER_FLAGS_IPV4          = (1 << 3),
+	IRC_SERVER_FLAGS_IPV6          = (1 << 4)
 };
 
 struct irc_server_prefix {
@@ -70,6 +71,7 @@
 	/* Connection settings. */
 	char name[IRC_NAME_MAX];
 	char hostname[IRC_HOST_MAX];
+	char password[IRC_PASSWORD_MAX];
 	unsigned short port;
 	enum irc_server_flags flags;
 
--- a/lib/irccd/util.c	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/util.c	Wed Jan 20 12:32:59 2021 +0100
@@ -115,3 +115,27 @@
 
 	return ret;
 }
+
+size_t
+irc_util_split(char *line, const char **args, size_t max)
+{
+	size_t idx;
+
+	if (!*line)
+		return 0;
+
+	for (idx = 0; idx < max; ++idx) {
+		char *sp = strchr(line, ' ');
+
+		if (!sp || idx + 1 >= max) {
+			args[idx++] = line;
+			break;
+		}
+
+		*sp = '\0';
+		args[idx] = line;
+		line = sp + 1;
+	}
+
+	return idx;
+}
--- a/lib/irccd/util.h	Sun Jan 17 19:05:39 2021 +0100
+++ b/lib/irccd/util.h	Wed Jan 20 12:32:59 2021 +0100
@@ -47,4 +47,7 @@
 char *
 irc_util_dirname(const char *);
 
+size_t
+irc_util_split(char *, const char **, size_t);
+
 #endif /* !IRCCD_UTIL_H */
--- a/tests/CMakeLists.txt	Sun Jan 17 19:05:39 2021 +0100
+++ b/tests/CMakeLists.txt	Wed Jan 20 12:32:59 2021 +0100
@@ -22,6 +22,7 @@
 	TESTS
 	test-bot
 	test-channel
+	test-event
 	#test-dl-plugin
 	test-log
 	test-rule
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-event.c	Wed Jan 20 12:32:59 2021 +0100
@@ -0,0 +1,75 @@
+/*
+ * test-event.c -- test event.h functions
+ *
+ * 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.
+ */
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/event.h>
+#include <irccd/server.h>
+
+GREATEST_TEST
+basics_parse_simple(void)
+{
+	/* This is a TOPIC message. */
+	char str[] = ":malikania.fr 332 boris #test :Welcome to #test :: a testing channel";
+	struct irc_event_msg msg;
+
+	irc_event_parse(&msg, str);
+
+	GREATEST_ASSERT_STR_EQ("malikania.fr", msg.prefix);
+	GREATEST_ASSERT_STR_EQ("332", msg.cmd);
+	GREATEST_ASSERT_STR_EQ("boris", msg.args[0]);
+	GREATEST_ASSERT_STR_EQ("#test", msg.args[1]);
+	GREATEST_ASSERT_STR_EQ("Welcome to #test :: a testing channel", msg.args[2]);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_parse_noprefix(void)
+{
+	/* Ping messages usually don't have a prefix. */
+	char str[] = "PING :malikania.fr";
+	struct irc_event_msg msg;
+
+	irc_event_parse(&msg, str);
+
+	GREATEST_ASSERT(!msg.prefix);
+	GREATEST_ASSERT_STR_EQ("PING", msg.cmd);
+	GREATEST_ASSERT_STR_EQ("malikania.fr", msg.args[0]);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_RUN_TEST(basics_parse_simple);
+	GREATEST_RUN_TEST(basics_parse_noprefix);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_MAIN_END();
+
+	return 0;
+}