changeset 938:7b74df7e8913

irccd: native plugin support
author David Demelier <markand@malikania.fr>
date Mon, 11 Jan 2021 21:25:58 +0100
parents ffe985308567
children a62c56c8b5ca
files .hgignore Makefile config.mk extern/libcompat/Makefile extern/libduktape/Makefile irccd/event.h irccd/limits.h irccd/log.c irccd/log.h irccd/main.c irccd/server.c irccd/server.h irccd/subst.c irccd/subst.h irccd/util.c irccd/util.h lib/irccd/dl-plugin.c lib/irccd/dl-plugin.h lib/irccd/event.h lib/irccd/limits.h lib/irccd/log.c lib/irccd/log.h lib/irccd/plugin.c lib/irccd/plugin.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 tests/example-dl-plugin.c tests/test-dl-plugin.c
diffstat 32 files changed, 3216 insertions(+), 2261 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Jan 11 10:28:49 2021 +0100
+++ b/.hgignore	Mon Jan 11 21:25:58 2021 +0100
@@ -24,8 +24,11 @@
 \.a$
 \.o$
 \.d$
+\.so$
+\.dylib$
 
 # tests.
+^tests/test-dl-plugin$
 ^tests/test-log$
 ^tests/test-subst$
 ^tests/test-util$
--- a/Makefile	Mon Jan 11 10:28:49 2021 +0100
+++ b/Makefile	Mon Jan 11 21:25:58 2021 +0100
@@ -16,58 +16,107 @@
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #
 
-.POSIX:
-
 .SUFFIXES:
 .SUFFIXES: .o .c
 
 include config.mk
 
-IRCCD=          irccd/irccd
-IRCCD_SRCS=     extern/libduktape/duktape.c     \
-                irccd/log.c                     \
-                irccd/server.c                  \
-                irccd/subst.c                   \
-                irccd/util.c
-IRCCD_OBJS=     ${IRCCD_SRCS:.c=.o}
-IRCCD_DEPS=     ${IRCCD_SRCS:.c=.d}
+IRCCD=                  irccd/irccd
+IRCCD_SRCS=             irccd/main.c
+IRCCD_OBJS=             ${IRCCD_SRCS:.c=.o}
+IRCCD_DEPS=             ${IRCCD_SRCS:.c=.d}
+
+LIBCOMPAT=              extern/libcompat/libirccd-compat.a
+
+ifeq (${WITH_JS},yes)
+LIBDUKTAPE=             extern/libduktape/libirccd-duktape.a
+endif
+
+LIBIRCCD=               lib/libirccd.a
+LIBIRCCD_SRCS=          lib/irccd/dl-plugin.c
+LIBIRCCD_SRCS+=         lib/irccd/log.c
+LIBIRCCD_SRCS+=         lib/irccd/plugin.c
+LIBIRCCD_SRCS+=         lib/irccd/server.c
+LIBIRCCD_SRCS+=         lib/irccd/subst.c
+LIBIRCCD_SRCS+=         lib/irccd/util.c
+LIBIRCCD_OBJS=          ${LIBIRCCD_SRCS:.c=.o}
+LIBIRCCD_DEPS=          ${LIBIRCCD_SRCS:.c=.d}
 
-TESTS=          tests/test-log.c                \
-                tests/test-util.c               \
-                tests/test-subst.c
-TESTS_OBJS=     ${TESTS:.c=}
+TESTS=                  tests/test-dl-plugin.c
+TESTS+=                 tests/test-log.c
+TESTS+=                 tests/test-util.c
+TESTS+=                 tests/test-subst.c
+TESTS_OBJS=             ${TESTS:.c=}
+
+DEFINES=                -D_BSD_SOURCE
+DEFINES+=               -DSOURCEDIR=\"`pwd`\"
 
-FLAGS=          -D_BSD_SOURCE                   \
-                -I extern/libduktape            \
-                -I extern/libgreatest           \
-                -I extern/libcompat/include     \
-                -I .
+INCS=                   -I extern/libcompat/include
+ifeq (${WITH_JS},yes)
+INCS+=                  -I extern/libduktape
+endif
+INCS+=                  -I extern/libgreatest
+INCS+=                  -I lib
+
+LIBS=                   -L extern/libcompat
+ifeq (${WITH_JS},yes)
+LIBS+=                  -L extern/libduktape
+endif
+LIBS+=                  -L lib
+
+LIBS+=                  -l irccd-compat
+ifeq (${WITH_JS},yes)
+LIBS+=                  -l irccd-duktape
+endif
+LIBS+=                  -l irccd
 
 all: ${IRCCD}
 
 .c.o:
-	${CC} -MMD ${FLAGS} ${CFLAGS} -c $< -o $@
+	${CMD.cc}
 
-.c:
-	${CC} ${FLAGS} ${CFLAGS} $< -o $@ extern/libcompat/libcompat.a ${IRCCD_OBJS} ${LDFLAGS}
-
+-include ${LIBIRCCD_DEPS}
 -include ${IRCCD_DEPS}
 
-extern/libcompat/libcompat.a:
-	${MAKE} -C extern/libcompat
+${LIBCOMPAT}:
+	${MAKE} CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" -C extern/libcompat
 
-${IRCCD_OBJS}: extern/libcompat/libcompat.a
+ifeq (${WITH_JS},yes)
+${LIBDUKTAPE}:
+	${MAKE} CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" -C extern/libduktape
+endif
+
+${LIBIRCCD_OBJS}: ${LIBCOMPAT}
+
+${LIBIRCCD}: ${LIBIRCCD_OBJS}
+	${CMD.ar}
 
-${IRCCD}: irccd/main.o ${IRCCD_OBJS}
-	${CC} -o $@ extern/libcompat/libcompat.a irccd/main.o ${IRCCD_OBJS} ${LDFLAGS}
+${IRCCD}: ${IRCCD_OBJS} ${LIBCOMPAT} ${LIBDUKTAPE} ${LIBIRCCD}
+	${CMD.ccld}
 
-clean:
-	${MAKE} -C extern/libcompat clean
-	rm -f irccd/main.o irccd/main.d ${IRCCD} ${IRCCD_OBJS} ${IRCCD_DEPS}
+# Unit tests.
+tests/test-%.o: tests/test-%.c
+	${CMD.cc}
+tests/test-%: tests/test-%.o ${LIBCOMPAT} ${IRCCD_OBJS}
+	${CMD.ccld}
 
-${TESTS_OBJS}: ${IRCCD_OBJS}
+${TESTS_OBJS}: ${LIBIRCCD}
+
+# Sample plugin for test-dl-plugin.
+tests/example-dl-plugin${EXT.shared}: tests/example-dl-plugin.o
+	${CMD.ld-shared}
+
+tests/test-dl-plugin: tests/example-dl-plugin${EXT.shared}
 
 tests: ${TESTS_OBJS}
 	for t in ${TESTS_OBJS}; do ./$$t; done
 
+clean:
+	${MAKE} -C extern/libcompat clean
+	${MAKE} -C extern/libduktape clean
+	rm -f ${LIBIRCCD} ${LIBIRCCD_OBJS} ${LIBIRCCD_DEPS}
+	rm -f ${IRCCD} ${IRCCD_OBJS} ${IRCCD_DEPS}
+	rm -f tests/example-dl-plugin${EXT.shared} tests/example-dl-plugin.o tests/example-dl-plugin.d
+	rm -f ${TESTS_OBJS}
+
 .PHONY: all clean tests
--- a/config.mk	Mon Jan 11 10:28:49 2021 +0100
+++ b/config.mk	Mon Jan 11 21:25:58 2021 +0100
@@ -16,8 +16,30 @@
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #
 
-CC=             cc
+# Build tools.
+CC=                     cc
+AR=                     ar
+
+# Installation paths.
+PREFIX=                 /usr/local
+BINDIR=                 bin
+
+# User options.
+WITH_JS=                yes
+
+# System dependant macros.
+OS:=                    $(shell uname -s)
 
-# Disable in release.
-CFLAGS=         -Wall -Wextra -g -O0 -fsanitize=address,undefined
-LDFLAGS=        -fsanitize=address,undefined
+# Standard commands.
+CMD.cc=                 ${CC} ${DEFINES} ${INCS} ${CFLAGS} -MMD -c $< -o $@
+CMD.ccld=               ${CC} ${DEFINES} ${INCS} ${CFLAGS} -o $@ $< ${LIBS} ${LDFLAGS}
+CMD.ar=                 ${AR} -rc $@ $^
+
+# Determine shared library extension and command to generate.
+ifeq (Darwin,${OS})
+EXT.shared=             .dylib
+CMD.ld-shared=          ${CC} -dynamiclib -o $@ $^ ${LDFLAGS}
+else
+EXT.shared=             .so
+CMD.ld-shared=          ${CC} -shared -o $@ $^ ${LDFLAGS}
+endif
--- a/extern/libcompat/Makefile	Mon Jan 11 10:28:49 2021 +0100
+++ b/extern/libcompat/Makefile	Mon Jan 11 21:25:58 2021 +0100
@@ -57,7 +57,7 @@
 FUNCTIONS_OBJS= ${FUNCTIONS_SRCS:.c=.o}
 FUNCTIONS_HDRS= ${FUNCTIONS_SRCS:.c=.h}
 
-LIB=            libcompat.a
+LIB=            libirccd-compat.a
 
 all: ${LIB}
 
@@ -69,7 +69,7 @@
 	@CC="${CC}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" ./trycompile -c ${*F}
 
 .c.h:
-	@CC="${CC}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" ./trycompile ${*F}
+	CC="${CC}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" ./trycompile ${*F}
 
 ${FUNCTIONS_HDRS} ${FUNCTIONS_OBJS}: trycompile
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extern/libduktape/Makefile	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,32 @@
+#
+# Makefile -- POSIX makefile for irccd
+#
+# 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 ../../config.mk
+
+all: libirccd-duktape.a
+
+duktape.o: duktape.c duktape.h duk_config.h
+	${CC} ${CFLAGS} -c $< -o $@
+
+libirccd-duktape.a: duktape.o
+	${CMD.ar}
+
+clean:
+	rm -f libirccd-duktape.a duktape.o
+
+.PHONY: clean
--- a/irccd/event.h	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-/*
- * event.h -- 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.
- */
-
-#ifndef IRCCD_EVENT_H
-#define IRCCD_EVENT_H
-
-#include <stddef.h>
-
-#include "limits.h"
-
-struct irc_server;
-
-enum irc_event_type {
-	IRC_EVENT_UNKNOWN,
-	IRC_EVENT_CONNECT,
-	IRC_EVENT_DISCONNECT,
-	IRC_EVENT_INVITE,
-	IRC_EVENT_JOIN,
-	IRC_EVENT_KICK,
-	IRC_EVENT_ME,
-	IRC_EVENT_MESSAGE,
-	IRC_EVENT_MODE,
-	IRC_EVENT_NAMES,
-	IRC_EVENT_NICK,
-	IRC_EVENT_NOTICE,
-	IRC_EVENT_PART,
-	IRC_EVENT_TOPIC,
-	IRC_EVENT_WHOIS
-};
-
-struct irc_event {
-	enum irc_event_type type;
-	struct irc_server *server;
-
-	/*
-	 * Raw arguments.
-	 *   [0]: prefix
-	 */
-	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 {
-			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;
-	};
-};
-
-#endif /* !IRCCD_EVENT_H */
--- a/irccd/limits.h	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-/*
- * limits.h -- irccd limits
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_LIMITS_H
-#define IRCCD_LIMITS_H
-
-/* IRC limits. */
-#define IRC_NICKNAME_MAX        32
-#define IRC_USERNAME_MAX        32
-#define IRC_REALNAME_MAX        64
-#define IRC_CHANNEL_MAX         64
-#define IRC_PASSWORD_MAX        64
-#define IRC_CTCPVERSION_MAX     128
-#define IRC_USERMODES_MAX       16
-
-#define IRC_MESSAGE_MAX         512
-#define IRC_ARGS_MAX            16
-
-/* Network limits. */
-#define IRC_HOST_MAX            32
-#define IRC_BUF_MAX             8192
-
-/* Types limits. */
-#define IRC_NAME_MAX            16
-
-#endif /* !IRCCD_LIMITS_H */
--- a/irccd/log.c	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,194 +0,0 @@
-/*
- * log.c -- loggers
- *
- * 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 <stdarg.h>
-#include <stdio.h>
-#include <string.h>
-#include <syslog.h>
-
-#include "log.h"
-
-enum level {
-	LEVEL_INFO,
-	LEVEL_WARN,
-	LEVEL_DEBUG
-};
-
-static FILE *out, *err;
-static bool verbosity;
-
-static void
-handler_files(enum level level, const char *fmt, va_list ap)
-{
-	switch (level) {
-	case LEVEL_WARN:
-		vfprintf(err, fmt, ap);
-		putc('\n', err);
-		fflush(err);
-		break;
-	default:
-		vfprintf(out, fmt, ap);
-		putc('\n', out);
-		fflush(out);
-		break;
-	}
-}
-
-static void
-finalizer_files(void)
-{
-	if (out)
-		fclose(out);
-	if (err)
-		fclose(err);
-}
-
-static void
-handler_syslog(enum level level, const char *fmt, va_list ap)
-{
-	static const int table[] = {
-		[LEVEL_INFO] = LOG_INFO,
-		[LEVEL_WARN] = LOG_WARNING,
-		[LEVEL_DEBUG] = LOG_DEBUG
-	};
-
-	/* TODO: add compatibility shim for vsyslog. */
-	vsyslog(table[level], fmt, ap);
-}
-
-static void
-finalizer_syslog(void)
-{
-	closelog();
-}
-
-static void (*handler)(enum level, const char *, va_list);
-static void (*finalizer)(void);
-
-void
-irc_log_to_syslog(void)
-{
-	irc_log_finish();
-
-	openlog("irccd", 0, LOG_DAEMON);
-
-	handler = handler_syslog;
-	finalizer = finalizer_syslog;
-}
-
-void
-irc_log_to_console(void)
-{
-	irc_log_finish();
-
-	out = stdout;
-	err = stderr;
-
-	handler = handler_files;
-	finalizer = NULL;
-}
-
-void
-irc_log_to_files(const char *stdout, const char *stderr)
-{
-	irc_log_finish();
-
-	if (!(out = fopen(stdout, "a")))
-		irc_log_warn("%s: %s", stdout, strerror(errno));
-	if (!(err = fopen(stderr, "a")))
-		irc_log_warn("%s: %s", stdout, strerror(errno));
-
-	handler = handler_files;
-	finalizer = finalizer_files;
-}
-
-void
-irc_log_to_null(void)
-{
-	irc_log_finish();
-
-	handler = NULL;
-	finalizer = NULL;
-}
-
-void
-irc_log_set_verbose(bool mode)
-{
-	verbosity = mode;
-}
-
-void
-irc_log_info(const char *fmt, ...)
-{
-	assert(fmt);
-
-	va_list ap;
-
-	va_start(ap, fmt);
-
-	if (verbosity && handler)
-		handler(LEVEL_INFO, fmt, ap);
-
-	va_end(ap);
-}
-
-void
-irc_log_warn(const char *fmt, ...)
-{
-	assert(fmt);
-
-	va_list ap;
-
-	va_start(ap, fmt);
-
-	if (handler)
-		handler(LEVEL_WARN, fmt, ap);
-
-	va_end(ap);
-}
-
-void
-irc_log_debug(const char *fmt, ...)
-{
-#if !defined(NDBEUG)
-	assert(fmt);
-
-	va_list ap;
-
-	va_start(ap, fmt);
-
-	if (handler)
-		handler(LEVEL_DEBUG, fmt, ap);
-
-	va_end(ap);
-#else
-	(void)fmt;
-#endif
-}
-
-void
-irc_log_finish(void)
-{
-	if (finalizer)
-		finalizer();
-
-	handler = NULL;
-	finalizer = NULL;
-}
--- a/irccd/log.h	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/*
- * log.h -- loggers
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_LOG_H
-#define IRCCD_LOG_H
-
-#include <stdbool.h>
-
-void
-irc_log_to_syslog(void);
-
-void
-irc_log_to_console(void);
-
-void
-irc_log_to_files(const char *, const char *);
-
-void
-irc_log_to_null(void);
-
-void
-irc_log_set_verbose(bool);
-
-void
-irc_log_info(const char *, ...);
-
-void
-irc_log_warn(const char *, ...);
-
-void
-irc_log_debug(const char *, ...);
-
-void
-irc_log_finish(void);
-
-#endif /* !IRCCD_LOG_H */
--- a/irccd/main.c	Mon Jan 11 10:28:49 2021 +0100
+++ b/irccd/main.c	Mon Jan 11 21:25:58 2021 +0100
@@ -20,8 +20,8 @@
 #include <stdio.h>
 #include <err.h>
 
-#include "event.h"
-#include "server.h"
+#include <irccd/event.h>
+#include <irccd/server.h>
 
 int
 main(int argc, char **argv)
@@ -59,6 +59,8 @@
 				printf("me, origin=%s,channel=%s,message=%s\n",
 				    ev.me.origin,ev.me.channel, ev.me.message);
 				break;
+			default:
+				break;
 			}
 		}
 	}
--- a/irccd/server.c	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,955 +0,0 @@
-/*
- * server.c -- an IRC server
- *
- * 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 <sys/types.h>
-#include <sys/socket.h>
-#include <assert.h>
-#include <ctype.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <netdb.h>
-#include <poll.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#if defined(IRCCD_HAVE_SSL)
-#       include <openssl/err.h>
-#endif
-
-#include "event.h"
-#include "log.h"
-#include "server.h"
-#include "util.h"
-
-struct origin {
-	char nickname[IRC_NICKNAME_MAX];
-	char username[IRC_USERNAME_MAX];
-	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
-compare_chan(const void *d1, const void *d2)
-{
-	return strcmp(
-		((const struct irc_server_channel *)d1)->name,
-		((const struct irc_server_channel *)d2)->name
-	);
-}
-
-static const struct origin *
-parse_origin(const char *prefix)
-{
-	static struct origin origin;
-	char fmt[128];
-
-	memset(&origin, 0, sizeof (origin));
-	snprintf(fmt, sizeof (fmt), "%%%zu[^!]!%%%zu[^@]@%%%zus",
-	    sizeof (origin.nickname) - 1,
-	    sizeof (origin.username) - 1,
-	    sizeof (origin.host) - 1);
-	sscanf(prefix, fmt, origin.nickname, origin.username, origin.host);
-
-	return &origin;
-}
-
-static inline void
-sort(struct irc_server *s)
-{
-	qsort(s->channels, s->channelsz, sizeof (*s->channels), compare_chan);
-}
-
-static struct irc_server_channel *
-add_channel(struct irc_server *s, const char *name, const char *password, bool joined)
-{
-	struct irc_server_channel ch = {
-		.joined = joined
-	};
-
-	strlcpy(ch.name, name, sizeof (ch.name));
-
-	if (password)
-		strlcpy(ch.password, password, sizeof (ch.password));
-
-	s->channels = irc_util_reallocarray(s->channels, ++s->channelsz, sizeof (ch));
-
-	memcpy(&s->channels[s->channelsz - 1], &ch, sizeof (ch));
-	sort(s);
-
-	return irc_server_find(s, name);
-}
-
-static void
-remove_channel(struct irc_server *s, struct irc_server_channel *ch)
-{
-	/* Null channel name will be moved at the end. */
-	memset(ch, 0, sizeof (*ch));
-
-	s->channels = irc_util_reallocarray(s->channels, --s->channelsz, sizeof (*ch));
-	sort(s);
-}
-
-static void
-read_support_prefix(struct irc_server *s, const char *value)
-{
-	char modes[16 + 1] = { 0 };
-	char tokens[16 + 1] = { 0 };
-
-	if (sscanf(value, "(%16[^)])%16s", modes, tokens) == 2) {
-		char *pm = modes;
-		char *tk = tokens;
-
-		for (size_t i = 0; i < 16 && *pm && *tk; ++i) {
-			s->prefixes[i].mode = *pm++;
-			s->prefixes[i].token = *tk++;
-		}
-	}
-}
-
-static void
-read_support_chantypes(struct irc_server *s, const char *value)
-{
-	strlcpy(s->chantypes, value, sizeof (s->chantypes));
-}
-
-static void
-convert_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);
-}
-
-static void
-convert_support(struct irc_server *s, struct irc_event *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)
-			continue;
-
-		if (strcmp(key, "PREFIX") == 0)
-			read_support_prefix(s, value);
-		if (strcmp(key, "CHANTYPES") == 0)
-			read_support_chantypes(s, value);
-	}
-}
-
-static void
-convert_join(struct irc_server *s, struct irc_event *ev)
-{
-	const struct origin *origin = parse_origin(ev->args[0]);
-	struct irc_server_channel *ch;
-
-	ev->type = IRC_EVENT_JOIN;
-	ev->server = s;
-	ev->join.origin = ev->args[0];
-	ev->join.channel = ev->args[2];
-
-	/* Also add a channel if the bot joined. */
-	if (strcmp(s->nickname, origin->nickname)) {
-		if ((ch = irc_server_find(s, ev->args[2])))
-			ch->joined = true;
-		else
-			add_channel(s, ev->args[2], NULL, true);
-	}
-}
-
-static void
-convert_kick(struct irc_server *s, struct irc_event *ev)
-{
-	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];
-
-	/*
-	 * 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) {
-		struct irc_server_channel *ch = irc_server_find(s, ev->args[2]);
-
-		if (ch) {
-			ch->joined = false;
-
-			if (s->flags & IRC_SERVER_FLAGS_AUTO_REJOIN)
-				irc_server_join(s, ch->name, ch->password);
-		}
-	}
-}
-
-static void
-convert_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]);
-	}
-
-#if 0
-	if (strcmp(m->args[0], s->nickname) == 0) {
-		/* Own user modes. */
-		strlcpy(s->usermodes, m->args[1], sizeof (s->usermodes);
-	} else {
-		/* TODO: channel modes. */
-	}
-#endif
-}
-
-static void
-convert_part(struct irc_server *s, struct irc_event *ev)
-{
-	const struct origin *origin = parse_origin(ev->args[0]);
-	struct irc_server_channel *ch = irc_server_find(s, ev->args[2]);
-
-	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)
-{
-	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];
-
-	/*
-	 * Detect CTCP commands which are PRIVMSG with a special boundaries.
-	 *
-	 * Example:
-	 * PRIVMSG jean :\001ACTION I'm eating\001.
-	 */
-	if (is_ctcp(ev->args[3])) {
-		ev->type = IRC_EVENT_ME;
-		ev->message.message = ctcp(ev->args[3] + 1);
-	}
-}
-
-static void
-convert_nick(struct irc_server *s, struct irc_event *ev)
-{
-	const struct origin *origin = parse_origin(ev->args[0]);
-
-	/* Update nickname if it is myself. */
-	if (strcmp(origin->nickname, s->nickname) == 0)
-		strlcpy(s->nickname, ev->args[2], 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];
-}
-
-static void
-convert_topic(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];
-}
-
-static void
-convert_ping(struct irc_server *s, struct irc_event *ev)
-{
-	irc_server_send(s, "PONG %s", ev->args[0]);
-}
-
-static void
-convert_names(struct irc_server *s, struct irc_event *ev)
-{
-	(void)s;
-	(void)ev;
-#if 0
-	struct irc_server_channel *chan;
-	char *p, *n;
-
-	if (m->argsz < 3 || !(chan = irc_server_find(s, m->args[2])))
-		return;
-
-	/*
-	 * Message arguments are as following:
-	 * 0------- 1 2------- 3--------------------
-	 * yourself = #channel nick1 nick2 nick3 ...
-	 */
-	for (p = m->args[3]; p; p = n ? n + 1 : NULL) {
-		if ((n = strpbrk(p, " ")))
-			*n = 0;
-
-		channel_add(chan, s, p);
-	}
-#endif
-}
-
-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            },
-	{ "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            }
-};
-
-static int
-compare_converter(const void *d1, const void *d2)
-{
-	return strcmp(d1, ((const struct convert *)d2)->command);
-}
-
-static void
-convert(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));
-
-	if (c)
-		c->convert(s, ev);
-}
-
-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++;
-
-	/* Skip optional spaces. */
-	while (*p && isspace(*p))
-		++p;
-
-	if (i >= IRC_MESSAGE_MAX)
-		return false;
-
-	*line = p;
-	ev->argsz++;
-
-	return true;
-}
-
-static void
-parse(struct irc_server *s, struct irc_event *ev, const char *line)
-{
-	if (!*line || *line++ != ':')
-		return;
-	if (!scan(ev, &line))   /* Prefix */
-		return;
-	if (!scan(ev, &line))   /* Command */
-		return;
-
-	/* 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);
-}
-
-static void
-clear(struct irc_server *s)
-{
-	s->state = IRC_SERVER_STATE_DISCONNECTED;
-
-	if (s->fd != 0) {
-		close(s->fd);
-		s->fd = 0;
-	}
-
-	if (s->ai) {
-		freeaddrinfo(s->ai);
-		s->ai = NULL;
-		s->aip = NULL;
-	}
-
-#if defined(IRCCD_HAVE_SSL)
-	if (s->ssl) {
-		SSL_free(s->ssl);
-		s->ssl = NULL;
-	}
-	if (s->ctx) {
-		SSL_CTX_free(s->ctx);
-		s->ctx = NULL;
-	}
-#endif
-}
-
-static bool
-lookup(struct irc_server *s)
-{
-	struct addrinfo hints = {
-		.ai_socktype = SOCK_STREAM,
-	};
-	char service[16];
-	int ret;
-
-	snprintf(service, sizeof (service), "%hu", s->port);
-
-	if ((ret = getaddrinfo(s->host, service, &hints, &s->ai)) != 0)
-		irc_log_warn("server %s: %s", s->name, gai_strerror(ret));
-
-	s->aip = s->ai;
-
-	return true;
-}
-
-static void
-auth(struct irc_server *s)
-{
-	s->state = IRC_SERVER_STATE_CONNECTED;
-
-	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_HAVE_SSL)
-
-static void
-secure_update(struct irc_server *s, int ret)
-{
-	(void)s;
-	(void)ret;
-
-	assert(s);
-
-	int r;
-
-	if (!(s->flags & SERVER_FL_SSL))
-		return;
-
-	switch ((r = SSL_get_error(s->ssl, ret))) {
-	case SSL_ERROR_WANT_READ:
-		s->ssl_state = SERVER_SSL_NEED_READ;
-		break;
-	case SSL_ERROR_WANT_WRITE:
-		s->ssl_state = SERVER_SSL_NEED_WRITE;
-		break;
-	case SSL_ERROR_SSL:
-		clear(s);
-		break;
-	default:
-		s->ssl_state = SERVER_SSL_NONE;
-		break;
-	}
-}
-
-#endif
-
-static void
-handshake(struct irc_server *s)
-{
-	assert(s);
-
-	if (!(s->flags & IRC_SERVER_FLAGS_SSL))
-		auth(s);
-	else {
-#if defined(IRCCD_HAVE_SSL)
-		int r;
-
-		s->state = SERVER_ST_HANDSHAKING;
-
-		if ((r = SSL_do_handshake(s->ssl)) > 0)
-			auth(s);
-
-		secure_update(s, r);
-#endif
-	}
-}
-
-static void
-secure_connect(struct irc_server *s)
-{
-	assert(s);
-
-	if (!(s->flags & IRC_SERVER_FLAGS_SSL))
-		handshake(s);
-	else {
-#if defined(IRCCD_HAVE_SSL)
-		int r;
-
-		if (!s->ctx)
-			s->ctx = SSL_CTX_new(TLS_method());
-		if (!s->ssl) {
-			s->ssl = SSL_new(s->ctx);
-			SSL_set_fd(s->ssl, s->fd);
-		}
-
-		if ((r = SSL_connect(s->ssl)) > 0)
-			ssl_handshake(s);
-
-		secure_update(s, r);
-#endif
-	}
-}
-
-static void
-dial(struct irc_server *s)
-{
-	/* No more address available. */
-	if (s->aip == NULL) {
-		clear(s);
-		return;
-	}
-
-	for (; s->aip; s->aip = s->aip->ai_next) {
-		/* We may need to close a socket that was open earlier. */
-		if (s->fd != 0)
-			close(s->fd);
-
-		s->fd = socket(s->aip->ai_family, s->aip->ai_socktype,
-		    s->aip->ai_protocol);
-
-		if (s->fd < 0) {
-			irc_log_warn("server %s: %s", s->name, strerror(errno));
-			continue;
-		}
-
-		/* TODO: is F_GETFL required before? */
-		fcntl(s->fd, F_SETFL, O_NONBLOCK);
-
-		/*
-		 * With some luck, the connection completes immediately,
-		 * otherwise we will need to wait until the socket is writable.
-		 */
-		if (connect(s->fd, s->aip->ai_addr, s->aip->ai_addrlen) == 0) {
-			secure_connect(s);
-			break;
-		}
-
-		/* Connect failed, check why. */
-		switch (errno) {
-		case EINPROGRESS:
-		case EAGAIN:
-			/* Let the writable state to determine. */
-			return;
-		default:
-			irc_log_warn("server %s: %s", s->name, strerror(errno));
-			break;
-		}
-	}
-}
-
-static void
-input(struct irc_server *s)
-{
-	char buf[IRC_MESSAGE_MAX] = {0};
-	ssize_t nr = 0;
-
-	if (s->flags & IRC_SERVER_FLAGS_SSL) {
-#if defined(IRCCD_HAVE_SSL)
-		nr = SSL_read(s->ssl, buf, sizeof (buf) - 1);
-		secure_update(s, nr);
-#endif
-	} else {
-		if ((nr = recv(s->fd, buf, sizeof (buf) - 1, 0)) < 0)
-			clear(s);
-	}
-
-	if (nr > 0) {
-		if (strlcat(s->in, buf, sizeof (s->in)) >= sizeof (s->in)) {
-			irc_log_warn("server %s: input buffer too long", s->name);
-			clear(s);
-		}
-	}
-}
-
-static void
-output(struct irc_server *s)
-{
-	ssize_t ns = 0;
-
-	if (s->flags & IRC_SERVER_FLAGS_SSL) {
-#if defined(IRCCD_HAVE_SSL)
-		ns = SSL_write(s->ssl, s->out.data, s->out.size);
-		secure_update(s, ns);
-#endif
-	} else if ((ns = send(s->fd, s->out, strlen(s->out), 0)) <= 0)
-		clear(s);
-
-	if (ns > 0) {
-		/* Optimize if everything was sent. */
-		if ((size_t)ns >= sizeof (s->out))
-			s->out[0] = '\0';
-		else
-			memmove(s->out, s->out + ns, sizeof (s->out) - ns);
-	}
-}
-
-static void
-prepare_connecting(const struct irc_server *s, struct pollfd *pfd)
-{
-	(void)s;
-
-#if defined(IRCCD_HAVE_SSL)
-	if (s->flags & IRC_SERVER_FLAGS_SSL && s->ssl && s->ctx) {
-		switch (s->ssl_state) {
-		case IRC_SERVER_SSL_NEED_READ:
-			pfd->events |= POLLIN;
-			break;
-		case IRC_SERVER_SSL_NEED_WRITE:
-			pfd->events |= POLLOUT;
-			break;
-		default:
-			break;
-		}
-	} else
-#endif
-		pfd->events |= POLLOUT;
-}
-
-static void
-prepare_ready(const struct irc_server *s, struct pollfd *pfd)
-{
-#if defined(IRCCD_HAVE_SSL)
-	if (s->flags & IRC_SERVER_FLAGS_SSL && s->ssl_state) {
-		switch (s->ssl_state) {
-		case SERVER_SSL_NEED_READ:
-			pfd->events |= POLLIN;
-			break;
-		case SERVER_SSL_NEED_WRITE:
-			pfd->events |= POLLOUT;
-			break;
-		default:
-			break;
-		}
-	} else {
-#endif
-		pfd->events |= POLLIN;
-
-		if (s->out[0])
-			pfd->events |= POLLOUT;
-#if defined(IRCCD_HAVE_SSL)
-	}
-#endif
-}
-
-static void
-flush_connecting(struct irc_server *s, const struct pollfd *pfd)
-{
-	(void)pfd;
-
-	int res, err = -1;
-	socklen_t len = sizeof (int);
-
-	if ((res = getsockopt(s->fd, SOL_SOCKET, SO_ERROR, &err, &len)) < 0 || err) {
-		irc_log_warn("server %s: %s", s->name, strerror(res ? err : errno));
-		dial(s);
-	} else
-		secure_connect(s);
-}
-
-static void
-flush_handshaking(struct irc_server *s, const struct pollfd *pfd)
-{
-	(void)pfd;
-
-	handshake(s);
-}
-
-static void
-flush_ready(struct irc_server *s, const struct pollfd *pfd)
-{
-	if (pfd->revents & POLLERR || pfd->revents & POLLHUP)
-		clear(s);
-	if (pfd->revents & POLLIN)
-		input(s);
-	if (pfd->revents & POLLOUT)
-		output(s);
-}
-
-static const struct {
-	void (*prepare)(const struct irc_server *, struct pollfd *);
-	void (*flush)(struct irc_server *, const struct pollfd *);
-} io_table[] = {
-	[IRC_SERVER_STATE_CONNECTING] = {
-		prepare_connecting,
-		flush_connecting
-	},
-	[IRC_SERVER_STATE_HANDSHAKING] = {
-		prepare_ready,
-		flush_handshaking
-	},
-	[IRC_SERVER_STATE_CONNECTED] = {
-		prepare_ready,
-		flush_ready
-	},
-};
-
-void
-irc_server_connect(struct irc_server *s)
-{
-	assert(s);
-
-	s->state = IRC_SERVER_STATE_CONNECTING;
-
-	if (!lookup(s))
-		clear(s);
-	else
-		dial(s);
-}
-
-void
-irc_server_disconnect(struct irc_server *s)
-{
-	assert(s);
-
-	clear(s);
-}
-
-void
-irc_server_prepare(const struct irc_server *s, struct pollfd *pfd)
-{
-	pfd->fd = s->fd;
-	pfd->events = 0;
-
-	if (io_table[s->state].prepare)
-		io_table[s->state].prepare(s, pfd);
-}
-
-void
-irc_server_flush(struct irc_server *s, const struct pollfd *pfd)
-{
-	if (io_table[s->state].flush)
-		io_table[s->state].flush(s, pfd);
-}
-
-bool
-irc_server_poll(struct irc_server *s, struct irc_event *ev)
-{
-	assert(s);
-	assert(ev);
-
-	char *pos;
-	size_t length;
-
-	if (!(pos = strstr(s->in, "\r\n")))
-		return false;
-
-	/* Turn end of the string at delimiter. */
-	*pos = 0;
-	length = pos - s->in;
-
-	/* Clear event in case we don't understand this message. */
-	memset(ev, 0, sizeof (*ev));
-	ev->type = IRC_EVENT_UNKNOWN;
-
-	if (length > 0)
-		parse(s, ev, s->in);
-
-	memmove(s->in, pos + 2, sizeof (s->in) - (length + 2));
-
-	return true;
-}
-
-struct irc_server_channel *
-irc_server_find(struct irc_server *s, const char *name)
-{
-	assert(s);
-	assert(name);
-
-	struct irc_server_channel key = {0};
-
-	strlcpy(key.name, name, sizeof (key.name));
-
-	return bsearch(&key, s->channels, s->channelsz, sizeof (key), compare_chan);
-}
-
-bool
-irc_server_send(struct irc_server *s, const char *fmt, ...)
-{
-	assert(s);
-	assert(fmt);
-
-	char buf[sizeof (s->out)];
-	va_list ap;
-	size_t len, avail, required;
-
-	va_start(ap, fmt);
-	required = vsnprintf(buf, sizeof (buf), fmt, ap);
-	va_end(ap);
-
-	len = strlen(s->out);
-	avail = sizeof (s->out) - len;
-
-	/* Don't forget \r\n. */
-	if (required + 2 >= avail)
-		return false;
-
-	strlcat(s->out, buf, sizeof (s->out));
-	strlcat(s->out, "\r\n", sizeof (s->out));
-
-	return true;
-}
-
-bool
-irc_server_join(struct irc_server *s, const char *name, const char *pass)
-{
-	assert(s);
-	assert(name);
-
-	struct irc_server_channel *ch;
-	bool ret = true;
-
-	/*
-	 * Search if there is already a channel pending or joined. If the
-	 * server is connected we send a join command otherwise we put it there
-	 * and wait for connection.
-	 */
-	if (!(ch = irc_server_find(s, name)))
-		ch = add_channel(s, name, pass, false);
-
-	if (!ch->joined && s->state == IRC_SERVER_STATE_CONNECTED) {
-		if (pass)
-			ret = irc_server_send(s, "JOIN %s %s", name, pass);
-		else
-			ret = irc_server_send(s, "JOIN %s", name);
-	}
-
-	return ret;
-}
-
-bool
-irc_server_part(struct irc_server *s, const char *name, const char *reason)
-{
-	assert(s);
-	assert(name);
-
-	bool ret;
-
-	if (reason && strlen(reason) > 0)
-		ret = irc_server_send(s, "PART %s :%s", name, reason);
-	else
-		ret = irc_server_send(s, "PART %s", name);
-
-	return ret;
-}
-
-bool
-irc_server_topic(struct irc_server *s, const char *name, const char *topic)
-{
-	assert(s);
-	assert(name);
-	assert(topic);
-
-	return irc_server_send(s, "TOPIC %s :%s", name, topic);
-}
-
-bool
-irc_server_message(struct irc_server *s, const char *chan, const char *msg)
-{
-	assert(s);
-	assert(chan);
-	assert(msg);
-
-	return irc_server_send(s, "PRIVMSG %s :%s", chan, msg);
-}
-
-bool
-irc_server_me(struct irc_server *s, const char *chan, const char *message)
-{
-	assert(s);
-	assert(chan);
-	assert(message);
-
-	return irc_server_send(s, "PRIVMSG %s :\001ACTION %s\001", chan, message);
-}
-
-void
-irc_server_finish(struct irc_server *s)
-{
-	assert(s);
-
-	clear(s);
-	free(s->channels);
-	memset(s, 0, sizeof (*s));
-}
--- a/irccd/server.h	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/*
- * server.h -- an IRC server
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_SERVER_H
-#define IRCCD_SERVER_H
-
-#include <stdbool.h>
-#include <stddef.h>
-
-#if defined(IRC_HAVE_SSL)
-#       include <openssl/ssl.h>
-#endif
-
-#include "limits.h"
-
-struct pollfd;
-
-struct irc_event;
-
-struct irc_server_channel {
-	char name[IRC_CHANNEL_MAX];
-	char password[IRC_PASSWORD_MAX];
-	bool joined;
-};
-
-enum irc_server_state {
-	IRC_SERVER_STATE_DISCONNECTED,
-	IRC_SERVER_STATE_CONNECTING,
-	IRC_SERVER_STATE_HANDSHAKING,
-	IRC_SERVER_STATE_CONNECTED,
-	IRC_SERVER_STATE_WAITING,
-	IRC_SERVER_STATE_NUM
-};
-
-enum irc_server_flags {
-	IRC_SERVER_FLAGS_SSL           = (1 << 0),
-	IRC_SERVER_FLAGS_AUTO_REJOIN   = (1 << 1)
-};
-
-struct irc_server_prefix {
-	char mode;
-	char token;
-};
-
-#if defined(IRCCD_HAVE_SSL)
-
-enum irc_server_ssl_state {
-	IRC_SERVER_SSL_NONE,
-	IRC_SERVER_SSL_NEED_READ,
-	IRC_SERVER_SSL_NEED_WRITE,
-};
-
-#endif
-
-struct irc_server {
-	/* Connection settings. */
-	char name[IRC_NAME_MAX];
-	char host[IRC_HOST_MAX];
-	unsigned short port;
-	enum irc_server_flags flags;
-
-	/* IRC identity. */
-	char nickname[IRC_NICKNAME_MAX];
-	char username[IRC_USERNAME_MAX];
-	char realname[IRC_REALNAME_MAX];
-	char ctcpversion[IRC_CTCPVERSION_MAX];
-	char usermodes[IRC_USERMODES_MAX];
-
-	/* Joined channels. */
-	struct irc_server_channel *channels;
-	size_t channelsz;
-
-	/* Network connectivity. */
-	int fd;
-	struct addrinfo *ai;
-	struct addrinfo *aip;
-	char in[IRC_BUF_MAX];
-	char out[IRC_BUF_MAX];
-	enum irc_server_state state;
-
-	/* OpenSSL support. */
-#if defined(IRCCD_HAVE_SSL)
-	SSL_CTX *ctx;
-	SSL *ssl;
-	enum irc_server_ssl_state ssl_state;
-#endif
-
-	/* IRC server settings. */
-	char chantypes[8];
-	struct irc_server_prefix prefixes[16];
-};
-
-void
-irc_server_connect(struct irc_server *);
-
-void
-irc_server_disconnect(struct irc_server *);
-
-void
-irc_server_prepare(const struct irc_server *, struct pollfd *);
-
-void
-irc_server_flush(struct irc_server *, const struct pollfd *);
-
-bool
-irc_server_poll(struct irc_server *, struct irc_event *);
-
-struct irc_server_channel *
-irc_server_find(struct irc_server *, const char *);
-
-bool
-irc_server_send(struct irc_server *, const char *, ...);
-
-bool
-irc_server_join(struct irc_server *, const char *, const char *);
-
-bool
-irc_server_part(struct irc_server *, const char *, const char *);
-
-bool
-irc_server_topic(struct irc_server *, const char *, const char *);
-
-bool
-irc_server_message(struct irc_server *, const char *, const char *);
-
-bool
-irc_server_me(struct irc_server *, const char *, const char *);
-
-void
-irc_server_finish(struct irc_server *);
-
-#endif /* !IRCCD_SERVER_H */
--- a/irccd/subst.c	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,485 +0,0 @@
-/*
- * subst.c -- pattern substitution
- *
- * 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 <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "subst.h"
-
-struct pair {
-	const char *key;
-	const char *value;
-};
-
-struct attributes {
-	char fg[16];
-	char bg[16];
-	char attrs[4][16];
-	size_t attrsz;
-};
-
-static const struct pair irc_colors[] = {
-	{ "white",      "0"     },
-	{ "black",      "1"     },
-	{ "blue",       "2"     },
-	{ "green",      "3"     },
-	{ "red",        "4"     },
-	{ "brown",      "5"     },
-	{ "purple",     "6"     },
-	{ "orange",     "7"     },
-	{ "yellow",     "8"     },
-	{ "lightgreen", "9"     },
-	{ "cyan",       "10"    },
-	{ "lightcyan",  "11"    },
-	{ "lightblue",  "12"    },
-	{ "pink",       "13"    },
-	{ "grey",       "14"    },
-	{ "lightgrey",  "15"    },
-	{ NULL,         NULL    }
-};
-
-static const struct pair irc_attrs[] = {
-	{ "bold",       "\x02"  },
-	{ "italic",     "\x09"  },
-	{ "reverse",    "\x16"  },
-	{ "strike",     "\x13"  },
-	{ "underline",  "\x15"  },
-	{ "underline2", "\x1f"  },
-	{ NULL,         NULL    }
-};
-
-static const struct pair shell_fg[] = {
-	{ "black",      "30"    },
-	{ "red",        "31"    },
-	{ "green",      "32"    },
-	{ "orange",     "33"    },
-	{ "blue",       "34"    },
-	{ "purple",     "35"    },
-	{ "cyan",       "36"    },
-	{ "white",      "37"    },
-	{ "default",    "39"    },
-	{ NULL,         NULL    }
-};
-
-static const struct pair shell_bg[] = {
-	{ "black",      "40"    },
-	{ "red",        "41"    },
-	{ "green",      "42"    },
-	{ "orange",     "43"    },
-	{ "blue",       "44"    },
-	{ "purple",     "45"    },
-	{ "cyan",       "46"    },
-	{ "white",      "47"    },
-	{ "default",    "49"    },
-	{ NULL,         NULL    }
-};
-
-static const struct pair shell_attrs[] = {
-	{ "bold",       "1"     },
-	{ "dim",        "2"     },
-	{ "underline",  "4"     },
-	{ "blink",      "5"     },
-	{ "reverse",    "7"     },
-	{ "hidden",     "8"     },
-	{ NULL,         NULL    }
-};
-
-static inline bool
-is_reserved(char token)
-{
-	return token == '#' || token == '@' || token == '$' || token == '!';
-}
-
-static inline bool
-scat(char **out, size_t *outsz, const char *value)
-{
-	size_t written;
-
-	if ((written = strlcpy(*out, value, *outsz)) >= *outsz) {
-		errno = ENOMEM;
-		return false;
-	}
-
-	*out += written;
-	*outsz -= written;
-
-	return true;
-}
-
-static inline bool
-ccat(char **out, size_t *outsz, char c)
-{
-	if (*outsz == 0)
-		return false;
-
-	*(*out)++ = c;
-	*(outsz) -= 1;
-
-	return true;
-}
-
-static inline void
-attributes_parse(const char *key, struct attributes *attrs)
-{
-	char attributes[64] = {0};
-
-	memset(attrs, 0, sizeof (*attrs));
-	sscanf(key, "%15[^,],%15[^,],%63s", attrs->fg, attrs->bg, attributes);
-
-	for (char *attr = attributes; *attr; ) {
-		char *p = strchr(attr, ',');
-
-		if (p)
-			*p = 0;
-
-		strlcpy(attrs->attrs[attrs->attrsz++], attr, sizeof (attrs->attrs[0]));
-
-		if (p)
-			attr = p + 1;
-		else
-			*attr = '\0';
-	}
-}
-
-static inline const char *
-find(const struct pair *pairs, const char *key)
-{
-	for (const struct pair *pair = pairs; pair->key; ++pair)
-		if (strcmp(pair->key, key) == 0)
-			return pair->value;
-
-	return NULL;
-}
-
-static bool
-subst_date(char *out, size_t outsz, const char *input, const struct irc_subst *subst)
-{
-	struct tm *tm;
-
-	if (!(subst->flags & IRC_SUBST_DATE))
-		return true;
-
-	tm = localtime(&subst->time);
-
-	if (strftime(out, outsz, input, tm) == 0) {
-		errno = ENOMEM;
-		return false;
-	}
-
-	return true;
-}
-
-static bool
-subst_keyword(const char *key, char **out, size_t *outsz, const struct irc_subst *subst)
-{
-	const char *value = NULL;
-
-	for (size_t i = 0; i < subst->keywordsz; ++i) {
-		if (strcmp(subst->keywords[i].key, key) == 0) {
-			value = subst->keywords[i].value;
-			break;
-		}
-	}
-
-	if (!value)
-		return true;
-
-	return scat(out, outsz, value);
-}
-
-static bool
-subst_env(const char *key, char **out, size_t *outsz)
-{
-	const char *value;
-
-	if (!(value = getenv(key)))
-		return true;
-
-	return scat(out, outsz, value);
-}
-
-static bool
-subst_shell(const char *key, char **out, size_t *outsz)
-{
-	FILE *fp;
-	size_t written;
-
-	/* Accept silently. */
-	if (!(fp = popen(key, "r")))
-		return true;
-
-	/*
-	 * Since we cannot determine the number of bytes that must be read, read until the end of
-	 * the output string and cut at the number of bytes read if lesser.
-	 */
-	if ((written = fread(*out, 1, *outsz - 1, fp)) > 0) {
-		/* Remove '\r\n' */
-		char *end;
-
-		if ((end = memchr(*out, '\r', written)) || (end = memchr(*out, '\n', written)))
-			*end = '\0';
-		else
-			end = *out + written;
-
-		*outsz -= end - *out;
-		*out = end;
-	}
-
-	pclose(fp);
-
-	return true;
-}
-
-static bool
-subst_irc_attrs(const char *key, char **out, size_t *outsz)
-{
-	const char *value;
-	struct attributes attrs;
-
-	if (!key[0])
-		return ccat(out, outsz, '\x03');
-
-	attributes_parse(key, &attrs);
-
-	if (attrs.fg[0] || attrs.attrs[0]) {
-		if (!ccat(out, outsz, '\x03'))
-			return false;
-
-		/* Foreground. */
-		if ((value = find(irc_colors, attrs.fg)) && !scat(out, outsz, value))
-			return false;
-
-		/* Background. */
-		if (attrs.bg[0]) {
-			if (!ccat(out, outsz, ','))
-				return false;
-			if ((value = find(irc_colors, attrs.bg)) && !scat(out, outsz, value))
-				return false;
-		}
-
-		/* Attributes. */
-		for (size_t i = 0; i < attrs.attrsz; ++i)
-			if ((value = find(irc_attrs, attrs.attrs[i])) && !scat(out, outsz, value))
-				return false;
-	}
-
-	return true;
-}
-
-static bool
-subst_shell_attrs(char *key, char **out, size_t *outsz)
-{
-	const char *value;
-	struct attributes attrs;
-
-	/* Empty attributes means reset: @{}. */
-	if (!key[0])
-		return scat(out, outsz, "\033[0m");
-
-	attributes_parse(key, &attrs);
-
-	if (!scat(out, outsz, "\033["))
-		return false;
-
-	/* Attributes first. */
-	for (size_t i = 0; i < attrs.attrsz; ++i) {
-		if ((value = find(shell_attrs, attrs.attrs[i])) && !scat(out, outsz, value))
-			return false;
-
-		/* Need to append ; if we have still more attributes or colors next. */
-		if ((i < attrs.attrsz || attrs.fg[0] || attrs.bg[0]) && !ccat(out, outsz, ';'))
-			return false;
-	}
-
-	/* Foreground. */
-	if (attrs.fg[0]) {
-		if ((value = find(shell_fg, attrs.fg)) && !scat(out, outsz, value))
-			return false;
-		if (attrs.bg[0] && !ccat(out, outsz, ';'))
-			return false;
-	}
-
-	/* Background. */
-	if (attrs.bg[0]) {
-		if ((value = find(shell_bg, attrs.bg)) && !scat(out, outsz, value))
-			return false;
-	}
-
-	return ccat(out, outsz, 'm');
-}
-
-static bool
-subst_default(const char **p, char **out, size_t *outsz, const char *key)
-{
-	return ccat(out, outsz, (*p)[-2]) &&
-	       ccat(out, outsz, '{') &&
-	       scat(out, outsz, key) &&
-	       ccat(out, outsz, '}');
-}
-
-static bool
-substitute(const char **p, char **out, size_t *outsz, const struct irc_subst *subst)
-{
-	char key[64] = {0};
-	size_t keysz;
-	char *end;
-	bool replaced = true;
-
-	if (!**p)
-		return true;
-
-	/* Find end of construction. */
-	if (!(end = strchr(*p, '}'))) {
-		errno = EINVAL;
-		return false;
-	}
-
-	/* Copy key. */
-	if ((keysz = end - *p) >= sizeof (key)) {
-		errno = ENOMEM;
-		return false;
-	}
-
-	memcpy(key, *p, keysz);
-
-	switch ((*p)[-2]) {
-	case '@':
-		/* attributes */
-		if (subst->flags & IRC_SUBST_IRC_ATTRS) {
-			if (!subst_irc_attrs(key, out, outsz))
-				return false;
-		} else if (subst->flags & IRC_SUBST_SHELL_ATTRS) {
-			if (!subst_shell_attrs(key, out, outsz))
-				return false;
-		} else
-			replaced = false;
-		break;
-	case '#':
-		/* keyword */
-		if (subst->flags & IRC_SUBST_KEYWORDS) {
-			if (!subst_keyword(key, out, outsz, subst))
-				return false;
-		} else
-			replaced = false;
-		break;
-	case '$':
-		/* environment variable */
-		if (subst->flags & IRC_SUBST_ENV) {
-			if (!subst_env(key, out, outsz))
-				return false;
-		} else
-			replaced = false;
-		break;
-	case '!':
-		/* shell */
-		if (subst->flags & IRC_SUBST_SHELL) {
-			if (!subst_shell(key, out, outsz))
-				return false;
-		} else
-			replaced = false;
-		break;
-	default:
-		break;
-	}
-
-	/* If substitution was disabled, put the token verbatim. */
-	if (!replaced && !subst_default(p, out, outsz, key))
-		return false;
-
-	/* Move after '}' */
-	*p = end + 1;
-
-	return true;
-}
-
-ssize_t
-irc_subst(char *out, size_t outsz, const char *input, const struct irc_subst *subst)
-{
-	assert(out);
-	assert(subst);
-
-	char *o = out;
-
-	if (!outsz)
-		return true;
-
-	/* Always start with the date first. */
-	if (!subst_date(out, outsz, input, subst))
-		goto err;
-
-	for (const char *i = input; *i && outsz; ) {
-		/*
-		 * Check if this is a reserved character, if it isn't go to the next character to
-		 * see if it's valid otherwise we print it as last token.
-		 *
-		 * Example:
-		 *   "#{abc}" -> keyword sequence
-		 *   "abc #"  -> keyword sequence interrupted, kept as-is.
-		 */
-		if (!is_reserved(*i)) {
-			if (!ccat(&o, &outsz, *i++))
-				goto err;
-			continue;
-		}
-
-		/*
-		 * Test if after the reserved token we have the opening { construct. If it's the
-		 * case we start substitution.
-		 *
-		 * Otherwise depending on what's after:
-		 *   If it is the same reserved token, it is "escaped" and printed
-		 *   If it is something else, we print the token and skip iteration.
-		 *
-		 * Examples:
-		 *   ## => #
-		 *   #@ => #@
-		 *   ##{foo} => #{foo}
-		 *   #{foo} => value
-		 */
-		if (*++i == '{') {
-			/* Skip '{'. */
-			++i;
-
-			if (!substitute(&i, &o, &outsz, subst))
-				goto err;
-		} else {
-			if (*i == i[-1])
-				++i;
-			if (!ccat(&o, &outsz, i[-1]))
-				goto err;
-		}
-	}
-
-	if (outsz < 1) {
-		errno = ENOMEM;
-		goto err;
-	}
-
-	*o = '\0';
-
-	return o - out;
-
-err:
-	out[0] = '\0';
-
-	return -1;
-}
--- a/irccd/subst.h	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/*
- * subst.h -- pattern substitution
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_SUBST_H
-#define IRCCD_SUBST_H
-
-#include <sys/types.h>
-#include <stddef.h>
-#include <time.h>
-
-enum irc_subst_flags {
-	IRC_SUBST_DATE          = (1 << 0),
-	IRC_SUBST_KEYWORDS      = (1 << 1),
-	IRC_SUBST_ENV           = (1 << 2),
-	IRC_SUBST_SHELL         = (1 << 3),
-	IRC_SUBST_IRC_ATTRS     = (1 << 4),
-	IRC_SUBST_SHELL_ATTRS   = (1 << 5)
-};
-
-struct irc_subst_keyword {
-	const char *key;
-	const char *value;
-};
-
-struct irc_subst {
-	time_t time;
-	enum irc_subst_flags flags;
-	const struct irc_subst_keyword *keywords;
-	size_t keywordsz;
-};
-
-ssize_t
-irc_subst(char *, size_t, const char *, const struct irc_subst *);
-
-#endif /* !IRCCD_SUBST_H */
--- a/irccd/util.c	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * util.c -- miscellaneous utilities
- *
- * 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 <err.h>
-#include <libgen.h>
-#include <limits.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <compat.h>
-
-#include "util.h"
-
-void *
-irc_util_malloc(size_t size)
-{
-	void *ret;
-
-	if (!(ret = malloc(size)))
-		err(1, "malloc");
-
-	return ret;
-}
-
-void *
-irc_util_calloc(size_t n, size_t size)
-{
-	void *ret;
-
-	if (!(ret = calloc(n, size)))
-		err(1, "calloc");
-
-	return ret;
-}
-
-void *
-irc_util_realloc(void *ptr, size_t size)
-{
-	void *ret;
-
-	if (!(ret = realloc(ptr, size)) && ptr)
-		err(1, "realloc");
-
-	return ret;
-}
-
-void *
-irc_util_reallocarray(void *ptr, size_t n, size_t size)
-{
-	void *ret;
-
-	if (!(ret = reallocarray(ptr, n, size)))
-		err(1, "reallocarray");
-
-	return ret;
-}
-
-void *
-irc_util_memdup(const void *ptr, size_t size)
-{
-	void *ret;
-
-	if (!(ret = malloc(size)))
-		err(1, "malloc");
-
-	return memcpy(ret, ptr, size);
-}
-
-char *
-irc_util_strdup(const char *src)
-{
-	char *ret;
-
-	if (!(ret = strdup(src)))
-		err(1, "strdup");
-
-	return ret;
-}
-
-char *
-irc_util_basename(const char *str)
-{
-	static char ret[PATH_MAX];
-	char tmp[PATH_MAX];
-
-	strlcpy(tmp, str, sizeof (tmp));
-	strlcpy(ret, basename(tmp), sizeof (ret));
-
-	return ret;
-}
-
-char *
-irc_util_dirname(const char *str)
-{
-	static char ret[PATH_MAX];
-	char tmp[PATH_MAX];
-
-	strlcpy(tmp, str, sizeof (tmp));
-	strlcpy(ret, dirname(tmp), sizeof (ret));
-
-	return ret;
-}
--- a/irccd/util.h	Mon Jan 11 10:28:49 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/*
- * util.h -- miscellaneous utilities
- *
- * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_UTIL_H
-#define IRCCD_UTIL_H
-
-#include <stddef.h>
-
-#define IRC_UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
-
-void *
-irc_util_malloc(size_t);
-
-void *
-irc_util_calloc(size_t, size_t);
-
-void *
-irc_util_realloc(void *, size_t);
-
-void *
-irc_util_reallocarray(void *, size_t, size_t);
-
-void *
-irc_util_memdup(const void *, size_t);
-
-char *
-irc_util_strdup(const char *);
-
-char *
-irc_util_basename(const char *);
-
-char *
-irc_util_dirname(const char *);
-
-#endif /* !IRCCD_UTIL_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/dl-plugin.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,220 @@
+/*
+ * dl-plugin.c -- native C plugins for irccd
+ *
+ * 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 <ctype.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "dl-plugin.h"
+#include "log.h"
+#include "plugin.h"
+#include "util.h"
+
+#define INVOKE(pg, name, sig, ...)                                              \
+do {                                                                            \
+        struct self *self = pg->data;                                           \
+        sig fn;                                                                 \
+                                                                                \
+        if (self->handle && (fn = dlsym(self->handle, symbol(self, name))))     \
+                return fn(__VA_ARGS__);                                         \
+} while (0)
+
+struct self {
+	char prefix[32];
+	void *handle;
+};
+
+typedef const char *    (*get_option_fn)(const char *);
+typedef const char *    (*get_path_fn)(const char *);
+typedef const char *    (*get_template_fn)(const char *);
+typedef const char **   (*get_options_fn)(void);
+typedef const char **   (*get_paths_fn)(void);
+typedef const char **   (*get_templates_fn)(void);
+typedef void            (*event_fn)(const struct irc_event *);
+typedef void            (*load_fn)(void);
+typedef void            (*reload_fn)(void);
+typedef void            (*unload_fn)(void);
+typedef void            (*set_option_fn)(const char *, const char *);
+typedef void            (*set_path_fn)(const char *, const char *);
+typedef void            (*set_template_fn)(const char *, const char *);
+
+static const char *
+symbol(const struct self *self, const char *func)
+{
+	static char sym[128];
+
+	snprintf(sym, sizeof (sym), "%s_%s",self->prefix, func);
+
+	return sym;
+}
+
+static void
+set_template(struct irc_plugin *plg, const char *key, const char *value)
+{
+	INVOKE(plg, "set_template", set_template_fn, key, value);
+}
+
+static const char *
+get_template(struct irc_plugin *plg, const char *key)
+{
+	INVOKE(plg, "get_template", get_template_fn, key);
+
+	return NULL;
+}
+
+static const char **
+get_templates(struct irc_plugin *plg)
+{
+	INVOKE(plg, "get_templates", get_templates_fn);
+
+	return NULL;
+}
+
+static void
+set_path(struct irc_plugin *plg, const char *key, const char *value)
+{
+	INVOKE(plg, "set_path", set_path_fn, key, value);
+}
+
+static const char *
+get_path(struct irc_plugin *plg, const char *key)
+{
+	INVOKE(plg, "get_path", get_path_fn, key);
+
+	return NULL;
+}
+
+static const char **
+get_paths(struct irc_plugin *plg)
+{
+	INVOKE(plg, "get_paths", get_paths_fn);
+
+	return NULL;
+}
+
+static void
+set_option(struct irc_plugin *plg, const char *key, const char *value)
+{
+	INVOKE(plg, "set_option", set_option_fn, key, value);
+}
+
+static const char *
+get_option(struct irc_plugin *plg, const char *key)
+{
+	INVOKE(plg, "get_option", get_option_fn, key);
+
+	return NULL;
+}
+
+static const char **
+get_options(struct irc_plugin *plg)
+{
+	INVOKE(plg, "get_options", get_options_fn);
+
+	return NULL;
+}
+
+static void
+load(struct irc_plugin *plg)
+{
+	INVOKE(plg, "load", load_fn);
+}
+
+static void
+reload(struct irc_plugin *plg)
+{
+	INVOKE(plg, "reload", reload_fn);
+}
+
+static void
+unload(struct irc_plugin *plg)
+{
+	INVOKE(plg, "unload", unload_fn);
+}
+
+static void
+handle(struct irc_plugin *plg, const struct irc_event *ev)
+{
+	INVOKE(plg, "event", event_fn, ev);
+}
+
+static void
+finish(struct irc_plugin *plg)
+{
+	struct self *self = plg->data;
+
+	if (self->handle)
+		dlclose(self->handle);
+
+	memset(self, 0, sizeof (*self));
+}
+
+static bool
+init(struct self *self, const char *path)
+{
+	memset(self, 0, sizeof (*self));
+
+	if (!(self->handle = dlopen(path, RTLD_NOW))) {
+		irc_log_warn("plugin: %s: %s", strerror(errno));
+		return false;
+	}
+
+	/* Compute prefix name */
+	strlcpy(self->prefix, irc_util_basename(path), sizeof (self->prefix));
+
+	/* Remove plugin extension. */
+	self->prefix[strcspn(self->prefix, ".")] = '\0';
+
+	/* Remove every invalid identifiers. */
+	for (char *p = self->prefix; *p; ++p)
+		if (!isalnum(*p))
+			*p = '_';
+
+	return true;
+}
+
+bool
+irc_dl_plugin_open(struct irc_plugin *plg, const char *path)
+{
+	struct self self;
+
+	if (!init(&self, path))
+		return false;
+
+	/* Data and all callbacks. */
+	plg->data = irc_util_memdup(&self, sizeof (self));
+	plg->set_template = set_template;
+	plg->get_template = get_template;
+	plg->get_templates = get_templates;
+	plg->set_path = set_path;
+	plg->get_path = get_path;
+	plg->get_paths = get_paths;
+	plg->set_option = set_option;
+	plg->get_option = get_option;
+	plg->get_options = get_options;
+	plg->load = load;
+	plg->reload = reload;
+	plg->unload = unload;
+	plg->handle = handle;
+	plg->finish = finish;
+
+	return true;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/dl-plugin.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,41 @@
+/*
+ * dl-plugin.c -- native C plugins for irccd
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_DL_PLUGIN_H
+#define IRCCD_DL_PLUGIN_H
+
+#include <stdbool.h>
+
+struct irc_plugin;
+
+#if defined(_WIN32)
+#       define IRC_DL_EXPORT __declspec(dllexport)
+#else
+#       define IRC_DL_EXPORT
+#endif
+
+#if defined(__APPLE__)
+#       define IRC_DL_EXT ".dylib"
+#else
+#       define IRC_DL_EXT ".so"
+#endif
+
+bool
+irc_dl_plugin_open(struct irc_plugin *, const char *);
+
+#endif /* !IRCCD_DL_PLUGIN_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/event.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,131 @@
+/*
+ * event.h -- 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.
+ */
+
+#ifndef IRCCD_EVENT_H
+#define IRCCD_EVENT_H
+
+#include <stddef.h>
+
+#include "limits.h"
+
+struct irc_server;
+
+enum irc_event_type {
+	IRC_EVENT_UNKNOWN,
+	IRC_EVENT_CONNECT,
+	IRC_EVENT_DISCONNECT,
+	IRC_EVENT_INVITE,
+	IRC_EVENT_JOIN,
+	IRC_EVENT_KICK,
+	IRC_EVENT_ME,
+	IRC_EVENT_MESSAGE,
+	IRC_EVENT_MODE,
+	IRC_EVENT_NAMES,
+	IRC_EVENT_NICK,
+	IRC_EVENT_NOTICE,
+	IRC_EVENT_PART,
+	IRC_EVENT_TOPIC,
+	IRC_EVENT_WHOIS
+};
+
+struct irc_event {
+	enum irc_event_type type;
+	struct irc_server *server;
+
+	/*
+	 * Raw arguments.
+	 *   [0]: prefix
+	 */
+	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 {
+			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;
+	};
+};
+
+#endif /* !IRCCD_EVENT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/limits.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,41 @@
+/*
+ * limits.h -- irccd limits
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_LIMITS_H
+#define IRCCD_LIMITS_H
+
+/* IRC limits. */
+#define IRC_NICKNAME_MAX        32
+#define IRC_USERNAME_MAX        32
+#define IRC_REALNAME_MAX        64
+#define IRC_CHANNEL_MAX         64
+#define IRC_PASSWORD_MAX        64
+#define IRC_CTCPVERSION_MAX     128
+#define IRC_USERMODES_MAX       16
+
+#define IRC_MESSAGE_MAX         512
+#define IRC_ARGS_MAX            16
+
+/* Network limits. */
+#define IRC_HOST_MAX            32
+#define IRC_BUF_MAX             8192
+
+/* Types limits. */
+#define IRC_NAME_MAX            16
+
+#endif /* !IRCCD_LIMITS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/log.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,194 @@
+/*
+ * log.c -- loggers
+ *
+ * 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 <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+
+#include "log.h"
+
+enum level {
+	LEVEL_INFO,
+	LEVEL_WARN,
+	LEVEL_DEBUG
+};
+
+static FILE *out, *err;
+static bool verbosity;
+
+static void
+handler_files(enum level level, const char *fmt, va_list ap)
+{
+	switch (level) {
+	case LEVEL_WARN:
+		vfprintf(err, fmt, ap);
+		putc('\n', err);
+		fflush(err);
+		break;
+	default:
+		vfprintf(out, fmt, ap);
+		putc('\n', out);
+		fflush(out);
+		break;
+	}
+}
+
+static void
+finalizer_files(void)
+{
+	if (out)
+		fclose(out);
+	if (err)
+		fclose(err);
+}
+
+static void
+handler_syslog(enum level level, const char *fmt, va_list ap)
+{
+	static const int table[] = {
+		[LEVEL_INFO] = LOG_INFO,
+		[LEVEL_WARN] = LOG_WARNING,
+		[LEVEL_DEBUG] = LOG_DEBUG
+	};
+
+	/* TODO: add compatibility shim for vsyslog. */
+	vsyslog(table[level], fmt, ap);
+}
+
+static void
+finalizer_syslog(void)
+{
+	closelog();
+}
+
+static void (*handler)(enum level, const char *, va_list);
+static void (*finalizer)(void);
+
+void
+irc_log_to_syslog(void)
+{
+	irc_log_finish();
+
+	openlog("irccd", 0, LOG_DAEMON);
+
+	handler = handler_syslog;
+	finalizer = finalizer_syslog;
+}
+
+void
+irc_log_to_console(void)
+{
+	irc_log_finish();
+
+	out = stdout;
+	err = stderr;
+
+	handler = handler_files;
+	finalizer = NULL;
+}
+
+void
+irc_log_to_files(const char *stdout, const char *stderr)
+{
+	irc_log_finish();
+
+	if (!(out = fopen(stdout, "a")))
+		irc_log_warn("%s: %s", stdout, strerror(errno));
+	if (!(err = fopen(stderr, "a")))
+		irc_log_warn("%s: %s", stdout, strerror(errno));
+
+	handler = handler_files;
+	finalizer = finalizer_files;
+}
+
+void
+irc_log_to_null(void)
+{
+	irc_log_finish();
+
+	handler = NULL;
+	finalizer = NULL;
+}
+
+void
+irc_log_set_verbose(bool mode)
+{
+	verbosity = mode;
+}
+
+void
+irc_log_info(const char *fmt, ...)
+{
+	assert(fmt);
+
+	va_list ap;
+
+	va_start(ap, fmt);
+
+	if (verbosity && handler)
+		handler(LEVEL_INFO, fmt, ap);
+
+	va_end(ap);
+}
+
+void
+irc_log_warn(const char *fmt, ...)
+{
+	assert(fmt);
+
+	va_list ap;
+
+	va_start(ap, fmt);
+
+	if (handler)
+		handler(LEVEL_WARN, fmt, ap);
+
+	va_end(ap);
+}
+
+void
+irc_log_debug(const char *fmt, ...)
+{
+#if !defined(NDBEUG)
+	assert(fmt);
+
+	va_list ap;
+
+	va_start(ap, fmt);
+
+	if (handler)
+		handler(LEVEL_DEBUG, fmt, ap);
+
+	va_end(ap);
+#else
+	(void)fmt;
+#endif
+}
+
+void
+irc_log_finish(void)
+{
+	if (finalizer)
+		finalizer();
+
+	handler = NULL;
+	finalizer = NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/log.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,51 @@
+/*
+ * log.h -- loggers
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_LOG_H
+#define IRCCD_LOG_H
+
+#include <stdbool.h>
+
+void
+irc_log_to_syslog(void);
+
+void
+irc_log_to_console(void);
+
+void
+irc_log_to_files(const char *, const char *);
+
+void
+irc_log_to_null(void);
+
+void
+irc_log_set_verbose(bool);
+
+void
+irc_log_info(const char *, ...);
+
+void
+irc_log_warn(const char *, ...);
+
+void
+irc_log_debug(const char *, ...);
+
+void
+irc_log_finish(void);
+
+#endif /* !IRCCD_LOG_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/plugin.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,170 @@
+/*
+ * plugin.c -- abstract plugin
+ *
+ * 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 <stddef.h>
+
+#include "plugin.h"
+
+void
+irc_plugin_set_template(struct irc_plugin *plg, const char *key, const char *value)
+{
+	assert(plg);
+	assert(key);
+	assert(value);
+
+	if (plg->set_template)
+		plg->set_template(plg, key, value);
+}
+
+const char *
+irc_plugin_get_template(struct irc_plugin *plg, const char *key)
+{
+	assert(plg);
+	assert(key);
+
+	if (plg->get_template)
+		return plg->get_template(plg, key);
+
+	return NULL;
+}
+
+const char **
+irc_plugin_get_templates(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->get_templates)
+		return plg->get_templates(plg);
+
+	return NULL;
+}
+
+void
+irc_plugin_set_path(struct irc_plugin *plg, const char *key, const char *value)
+{
+	assert(plg);
+	assert(key);
+	assert(value);
+
+	if (plg->set_path)
+		plg->set_path(plg, key, value);
+}
+
+const char *
+irc_plugin_get_path(struct irc_plugin *plg, const char *key)
+{
+	assert(plg);
+	assert(key);
+
+	if (plg->get_path)
+		return plg->get_path(plg, key);
+
+	return NULL;
+}
+
+const char **
+irc_plugin_get_paths(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->get_paths)
+		return plg->get_paths(plg);
+
+	return NULL;
+}
+
+void
+irc_plugin_set_option(struct irc_plugin *plg, const char *key, const char *value)
+{
+	assert(plg);
+	assert(key);
+	assert(value);
+
+	if (plg->set_option)
+		plg->set_option(plg, key, value);
+}
+
+const char *
+irc_plugin_get_option(struct irc_plugin *plg, const char *key)
+{
+	assert(plg);
+	assert(key);
+
+	if (plg->get_option)
+		return plg->get_option(plg, key);
+
+	return NULL;
+}
+
+const char **
+irc_plugin_get_options(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->get_options)
+		return plg->get_options(plg);
+
+	return NULL;
+}
+
+void
+irc_plugin_load(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->load)
+		plg->load(plg);
+}
+
+void
+irc_plugin_reload(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->reload)
+		plg->reload(plg);
+}
+
+void
+irc_plugin_unload(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->unload)
+		plg->unload(plg);
+}
+
+void
+irc_plugin_handle(struct irc_plugin *plg, const struct irc_event *ev)
+{
+	assert(plg);
+	assert(ev);
+
+	if (plg->handle)
+		plg->handle(plg, ev);
+}
+
+void
+irc_plugin_finish(struct irc_plugin *plg)
+{
+	assert(plg);
+
+	if (plg->finish)
+		plg->finish(plg);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/plugin.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,96 @@
+/*
+ * plugin.h -- abstract plugin
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_PLUGIN_H
+#define IRCCD_PLUGIN_H
+
+#include <stdbool.h>
+
+struct irc_event;
+
+struct irc_plugin {
+	const char *name;
+	const char *license;
+	const char *version;
+	const char *author;
+	const char *description;
+	void *data;
+
+	void (*set_template)(struct irc_plugin *, const char *, const char *);
+	const char *(*get_template)(struct irc_plugin *, const char *);
+	const char **(*get_templates)(struct irc_plugin *);
+
+	void (*set_path)(struct irc_plugin *, const char *, const char *);
+	const char *(*get_path)(struct irc_plugin *, const char *);
+	const char **(*get_paths)(struct irc_plugin *);
+
+	void (*set_option)(struct irc_plugin *, const char *, const char *);
+	const char *(*get_option)(struct irc_plugin *, const char *);
+	const char **(*get_options)(struct irc_plugin *);
+
+	void (*load)(struct irc_plugin *);
+	void (*reload)(struct irc_plugin *);
+	void (*unload)(struct irc_plugin *);
+	void (*handle)(struct irc_plugin *, const struct irc_event *);
+
+	void (*finish)(struct irc_plugin *);
+};
+
+void
+irc_plugin_set_template(struct irc_plugin *, const char *, const char *);
+
+const char *
+irc_plugin_get_template(struct irc_plugin *, const char *);
+
+const char **
+irc_plugin_get_templates(struct irc_plugin *);
+
+void
+irc_plugin_set_path(struct irc_plugin *, const char *, const char *);
+
+const char *
+irc_plugin_get_path(struct irc_plugin *, const char *);
+
+const char **
+irc_plugin_get_paths(struct irc_plugin *);
+
+void
+irc_plugin_set_option(struct irc_plugin *, const char *, const char *);
+
+const char *
+irc_plugin_get_option(struct irc_plugin *, const char *);
+
+const char **
+irc_plugin_get_options(struct irc_plugin *);
+
+void
+irc_plugin_load(struct irc_plugin *);
+
+void
+irc_plugin_reload(struct irc_plugin *);
+
+void
+irc_plugin_unload(struct irc_plugin *);
+
+void
+irc_plugin_handle(struct irc_plugin *, const struct irc_event *);
+
+void
+irc_plugin_finish(struct irc_plugin *);
+
+#endif /* !IRCCD_PLUGIN_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,955 @@
+/*
+ * server.c -- an IRC server
+ *
+ * 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 <sys/types.h>
+#include <sys/socket.h>
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <poll.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#if defined(IRCCD_HAVE_SSL)
+#       include <openssl/err.h>
+#endif
+
+#include "event.h"
+#include "log.h"
+#include "server.h"
+#include "util.h"
+
+struct origin {
+	char nickname[IRC_NICKNAME_MAX];
+	char username[IRC_USERNAME_MAX];
+	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
+compare_chan(const void *d1, const void *d2)
+{
+	return strcmp(
+		((const struct irc_server_channel *)d1)->name,
+		((const struct irc_server_channel *)d2)->name
+	);
+}
+
+static const struct origin *
+parse_origin(const char *prefix)
+{
+	static struct origin origin;
+	char fmt[128];
+
+	memset(&origin, 0, sizeof (origin));
+	snprintf(fmt, sizeof (fmt), "%%%zu[^!]!%%%zu[^@]@%%%zus",
+	    sizeof (origin.nickname) - 1,
+	    sizeof (origin.username) - 1,
+	    sizeof (origin.host) - 1);
+	sscanf(prefix, fmt, origin.nickname, origin.username, origin.host);
+
+	return &origin;
+}
+
+static inline void
+sort(struct irc_server *s)
+{
+	qsort(s->channels, s->channelsz, sizeof (*s->channels), compare_chan);
+}
+
+static struct irc_server_channel *
+add_channel(struct irc_server *s, const char *name, const char *password, bool joined)
+{
+	struct irc_server_channel ch = {
+		.joined = joined
+	};
+
+	strlcpy(ch.name, name, sizeof (ch.name));
+
+	if (password)
+		strlcpy(ch.password, password, sizeof (ch.password));
+
+	s->channels = irc_util_reallocarray(s->channels, ++s->channelsz, sizeof (ch));
+
+	memcpy(&s->channels[s->channelsz - 1], &ch, sizeof (ch));
+	sort(s);
+
+	return irc_server_find(s, name);
+}
+
+static void
+remove_channel(struct irc_server *s, struct irc_server_channel *ch)
+{
+	/* Null channel name will be moved at the end. */
+	memset(ch, 0, sizeof (*ch));
+
+	s->channels = irc_util_reallocarray(s->channels, --s->channelsz, sizeof (*ch));
+	sort(s);
+}
+
+static void
+read_support_prefix(struct irc_server *s, const char *value)
+{
+	char modes[16 + 1] = { 0 };
+	char tokens[16 + 1] = { 0 };
+
+	if (sscanf(value, "(%16[^)])%16s", modes, tokens) == 2) {
+		char *pm = modes;
+		char *tk = tokens;
+
+		for (size_t i = 0; i < 16 && *pm && *tk; ++i) {
+			s->prefixes[i].mode = *pm++;
+			s->prefixes[i].token = *tk++;
+		}
+	}
+}
+
+static void
+read_support_chantypes(struct irc_server *s, const char *value)
+{
+	strlcpy(s->chantypes, value, sizeof (s->chantypes));
+}
+
+static void
+convert_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);
+}
+
+static void
+convert_support(struct irc_server *s, struct irc_event *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)
+			continue;
+
+		if (strcmp(key, "PREFIX") == 0)
+			read_support_prefix(s, value);
+		if (strcmp(key, "CHANTYPES") == 0)
+			read_support_chantypes(s, value);
+	}
+}
+
+static void
+convert_join(struct irc_server *s, struct irc_event *ev)
+{
+	const struct origin *origin = parse_origin(ev->args[0]);
+	struct irc_server_channel *ch;
+
+	ev->type = IRC_EVENT_JOIN;
+	ev->server = s;
+	ev->join.origin = ev->args[0];
+	ev->join.channel = ev->args[2];
+
+	/* Also add a channel if the bot joined. */
+	if (strcmp(s->nickname, origin->nickname)) {
+		if ((ch = irc_server_find(s, ev->args[2])))
+			ch->joined = true;
+		else
+			add_channel(s, ev->args[2], NULL, true);
+	}
+}
+
+static void
+convert_kick(struct irc_server *s, struct irc_event *ev)
+{
+	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];
+
+	/*
+	 * 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) {
+		struct irc_server_channel *ch = irc_server_find(s, ev->args[2]);
+
+		if (ch) {
+			ch->joined = false;
+
+			if (s->flags & IRC_SERVER_FLAGS_AUTO_REJOIN)
+				irc_server_join(s, ch->name, ch->password);
+		}
+	}
+}
+
+static void
+convert_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]);
+	}
+
+#if 0
+	if (strcmp(m->args[0], s->nickname) == 0) {
+		/* Own user modes. */
+		strlcpy(s->usermodes, m->args[1], sizeof (s->usermodes);
+	} else {
+		/* TODO: channel modes. */
+	}
+#endif
+}
+
+static void
+convert_part(struct irc_server *s, struct irc_event *ev)
+{
+	const struct origin *origin = parse_origin(ev->args[0]);
+	struct irc_server_channel *ch = irc_server_find(s, ev->args[2]);
+
+	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)
+{
+	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];
+
+	/*
+	 * Detect CTCP commands which are PRIVMSG with a special boundaries.
+	 *
+	 * Example:
+	 * PRIVMSG jean :\001ACTION I'm eating\001.
+	 */
+	if (is_ctcp(ev->args[3])) {
+		ev->type = IRC_EVENT_ME;
+		ev->message.message = ctcp(ev->args[3] + 1);
+	}
+}
+
+static void
+convert_nick(struct irc_server *s, struct irc_event *ev)
+{
+	const struct origin *origin = parse_origin(ev->args[0]);
+
+	/* Update nickname if it is myself. */
+	if (strcmp(origin->nickname, s->nickname) == 0)
+		strlcpy(s->nickname, ev->args[2], 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];
+}
+
+static void
+convert_topic(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];
+}
+
+static void
+convert_ping(struct irc_server *s, struct irc_event *ev)
+{
+	irc_server_send(s, "PONG %s", ev->args[0]);
+}
+
+static void
+convert_names(struct irc_server *s, struct irc_event *ev)
+{
+	(void)s;
+	(void)ev;
+#if 0
+	struct irc_server_channel *chan;
+	char *p, *n;
+
+	if (m->argsz < 3 || !(chan = irc_server_find(s, m->args[2])))
+		return;
+
+	/*
+	 * Message arguments are as following:
+	 * 0------- 1 2------- 3--------------------
+	 * yourself = #channel nick1 nick2 nick3 ...
+	 */
+	for (p = m->args[3]; p; p = n ? n + 1 : NULL) {
+		if ((n = strpbrk(p, " ")))
+			*n = 0;
+
+		channel_add(chan, s, p);
+	}
+#endif
+}
+
+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            },
+	{ "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            }
+};
+
+static int
+compare_converter(const void *d1, const void *d2)
+{
+	return strcmp(d1, ((const struct convert *)d2)->command);
+}
+
+static void
+convert(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));
+
+	if (c)
+		c->convert(s, ev);
+}
+
+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++;
+
+	/* Skip optional spaces. */
+	while (*p && isspace(*p))
+		++p;
+
+	if (i >= IRC_MESSAGE_MAX)
+		return false;
+
+	*line = p;
+	ev->argsz++;
+
+	return true;
+}
+
+static void
+parse(struct irc_server *s, struct irc_event *ev, const char *line)
+{
+	if (!*line || *line++ != ':')
+		return;
+	if (!scan(ev, &line))   /* Prefix */
+		return;
+	if (!scan(ev, &line))   /* Command */
+		return;
+
+	/* 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);
+}
+
+static void
+clear(struct irc_server *s)
+{
+	s->state = IRC_SERVER_STATE_DISCONNECTED;
+
+	if (s->fd != 0) {
+		close(s->fd);
+		s->fd = 0;
+	}
+
+	if (s->ai) {
+		freeaddrinfo(s->ai);
+		s->ai = NULL;
+		s->aip = NULL;
+	}
+
+#if defined(IRCCD_HAVE_SSL)
+	if (s->ssl) {
+		SSL_free(s->ssl);
+		s->ssl = NULL;
+	}
+	if (s->ctx) {
+		SSL_CTX_free(s->ctx);
+		s->ctx = NULL;
+	}
+#endif
+}
+
+static bool
+lookup(struct irc_server *s)
+{
+	struct addrinfo hints = {
+		.ai_socktype = SOCK_STREAM,
+	};
+	char service[16];
+	int ret;
+
+	snprintf(service, sizeof (service), "%hu", s->port);
+
+	if ((ret = getaddrinfo(s->host, service, &hints, &s->ai)) != 0)
+		irc_log_warn("server %s: %s", s->name, gai_strerror(ret));
+
+	s->aip = s->ai;
+
+	return true;
+}
+
+static void
+auth(struct irc_server *s)
+{
+	s->state = IRC_SERVER_STATE_CONNECTED;
+
+	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_HAVE_SSL)
+
+static void
+secure_update(struct irc_server *s, int ret)
+{
+	(void)s;
+	(void)ret;
+
+	assert(s);
+
+	int r;
+
+	if (!(s->flags & SERVER_FL_SSL))
+		return;
+
+	switch ((r = SSL_get_error(s->ssl, ret))) {
+	case SSL_ERROR_WANT_READ:
+		s->ssl_state = SERVER_SSL_NEED_READ;
+		break;
+	case SSL_ERROR_WANT_WRITE:
+		s->ssl_state = SERVER_SSL_NEED_WRITE;
+		break;
+	case SSL_ERROR_SSL:
+		clear(s);
+		break;
+	default:
+		s->ssl_state = SERVER_SSL_NONE;
+		break;
+	}
+}
+
+#endif
+
+static void
+handshake(struct irc_server *s)
+{
+	assert(s);
+
+	if (!(s->flags & IRC_SERVER_FLAGS_SSL))
+		auth(s);
+	else {
+#if defined(IRCCD_HAVE_SSL)
+		int r;
+
+		s->state = SERVER_ST_HANDSHAKING;
+
+		if ((r = SSL_do_handshake(s->ssl)) > 0)
+			auth(s);
+
+		secure_update(s, r);
+#endif
+	}
+}
+
+static void
+secure_connect(struct irc_server *s)
+{
+	assert(s);
+
+	if (!(s->flags & IRC_SERVER_FLAGS_SSL))
+		handshake(s);
+	else {
+#if defined(IRCCD_HAVE_SSL)
+		int r;
+
+		if (!s->ctx)
+			s->ctx = SSL_CTX_new(TLS_method());
+		if (!s->ssl) {
+			s->ssl = SSL_new(s->ctx);
+			SSL_set_fd(s->ssl, s->fd);
+		}
+
+		if ((r = SSL_connect(s->ssl)) > 0)
+			ssl_handshake(s);
+
+		secure_update(s, r);
+#endif
+	}
+}
+
+static void
+dial(struct irc_server *s)
+{
+	/* No more address available. */
+	if (s->aip == NULL) {
+		clear(s);
+		return;
+	}
+
+	for (; s->aip; s->aip = s->aip->ai_next) {
+		/* We may need to close a socket that was open earlier. */
+		if (s->fd != 0)
+			close(s->fd);
+
+		s->fd = socket(s->aip->ai_family, s->aip->ai_socktype,
+		    s->aip->ai_protocol);
+
+		if (s->fd < 0) {
+			irc_log_warn("server %s: %s", s->name, strerror(errno));
+			continue;
+		}
+
+		/* TODO: is F_GETFL required before? */
+		fcntl(s->fd, F_SETFL, O_NONBLOCK);
+
+		/*
+		 * With some luck, the connection completes immediately,
+		 * otherwise we will need to wait until the socket is writable.
+		 */
+		if (connect(s->fd, s->aip->ai_addr, s->aip->ai_addrlen) == 0) {
+			secure_connect(s);
+			break;
+		}
+
+		/* Connect failed, check why. */
+		switch (errno) {
+		case EINPROGRESS:
+		case EAGAIN:
+			/* Let the writable state to determine. */
+			return;
+		default:
+			irc_log_warn("server %s: %s", s->name, strerror(errno));
+			break;
+		}
+	}
+}
+
+static void
+input(struct irc_server *s)
+{
+	char buf[IRC_MESSAGE_MAX] = {0};
+	ssize_t nr = 0;
+
+	if (s->flags & IRC_SERVER_FLAGS_SSL) {
+#if defined(IRCCD_HAVE_SSL)
+		nr = SSL_read(s->ssl, buf, sizeof (buf) - 1);
+		secure_update(s, nr);
+#endif
+	} else {
+		if ((nr = recv(s->fd, buf, sizeof (buf) - 1, 0)) < 0)
+			clear(s);
+	}
+
+	if (nr > 0) {
+		if (strlcat(s->in, buf, sizeof (s->in)) >= sizeof (s->in)) {
+			irc_log_warn("server %s: input buffer too long", s->name);
+			clear(s);
+		}
+	}
+}
+
+static void
+output(struct irc_server *s)
+{
+	ssize_t ns = 0;
+
+	if (s->flags & IRC_SERVER_FLAGS_SSL) {
+#if defined(IRCCD_HAVE_SSL)
+		ns = SSL_write(s->ssl, s->out.data, s->out.size);
+		secure_update(s, ns);
+#endif
+	} else if ((ns = send(s->fd, s->out, strlen(s->out), 0)) <= 0)
+		clear(s);
+
+	if (ns > 0) {
+		/* Optimize if everything was sent. */
+		if ((size_t)ns >= sizeof (s->out))
+			s->out[0] = '\0';
+		else
+			memmove(s->out, s->out + ns, sizeof (s->out) - ns);
+	}
+}
+
+static void
+prepare_connecting(const struct irc_server *s, struct pollfd *pfd)
+{
+	(void)s;
+
+#if defined(IRCCD_HAVE_SSL)
+	if (s->flags & IRC_SERVER_FLAGS_SSL && s->ssl && s->ctx) {
+		switch (s->ssl_state) {
+		case IRC_SERVER_SSL_NEED_READ:
+			pfd->events |= POLLIN;
+			break;
+		case IRC_SERVER_SSL_NEED_WRITE:
+			pfd->events |= POLLOUT;
+			break;
+		default:
+			break;
+		}
+	} else
+#endif
+		pfd->events |= POLLOUT;
+}
+
+static void
+prepare_ready(const struct irc_server *s, struct pollfd *pfd)
+{
+#if defined(IRCCD_HAVE_SSL)
+	if (s->flags & IRC_SERVER_FLAGS_SSL && s->ssl_state) {
+		switch (s->ssl_state) {
+		case SERVER_SSL_NEED_READ:
+			pfd->events |= POLLIN;
+			break;
+		case SERVER_SSL_NEED_WRITE:
+			pfd->events |= POLLOUT;
+			break;
+		default:
+			break;
+		}
+	} else {
+#endif
+		pfd->events |= POLLIN;
+
+		if (s->out[0])
+			pfd->events |= POLLOUT;
+#if defined(IRCCD_HAVE_SSL)
+	}
+#endif
+}
+
+static void
+flush_connecting(struct irc_server *s, const struct pollfd *pfd)
+{
+	(void)pfd;
+
+	int res, err = -1;
+	socklen_t len = sizeof (int);
+
+	if ((res = getsockopt(s->fd, SOL_SOCKET, SO_ERROR, &err, &len)) < 0 || err) {
+		irc_log_warn("server %s: %s", s->name, strerror(res ? err : errno));
+		dial(s);
+	} else
+		secure_connect(s);
+}
+
+static void
+flush_handshaking(struct irc_server *s, const struct pollfd *pfd)
+{
+	(void)pfd;
+
+	handshake(s);
+}
+
+static void
+flush_ready(struct irc_server *s, const struct pollfd *pfd)
+{
+	if (pfd->revents & POLLERR || pfd->revents & POLLHUP)
+		clear(s);
+	if (pfd->revents & POLLIN)
+		input(s);
+	if (pfd->revents & POLLOUT)
+		output(s);
+}
+
+static const struct {
+	void (*prepare)(const struct irc_server *, struct pollfd *);
+	void (*flush)(struct irc_server *, const struct pollfd *);
+} io_table[] = {
+	[IRC_SERVER_STATE_CONNECTING] = {
+		prepare_connecting,
+		flush_connecting
+	},
+	[IRC_SERVER_STATE_HANDSHAKING] = {
+		prepare_ready,
+		flush_handshaking
+	},
+	[IRC_SERVER_STATE_CONNECTED] = {
+		prepare_ready,
+		flush_ready
+	},
+};
+
+void
+irc_server_connect(struct irc_server *s)
+{
+	assert(s);
+
+	s->state = IRC_SERVER_STATE_CONNECTING;
+
+	if (!lookup(s))
+		clear(s);
+	else
+		dial(s);
+}
+
+void
+irc_server_disconnect(struct irc_server *s)
+{
+	assert(s);
+
+	clear(s);
+}
+
+void
+irc_server_prepare(const struct irc_server *s, struct pollfd *pfd)
+{
+	pfd->fd = s->fd;
+	pfd->events = 0;
+
+	if (io_table[s->state].prepare)
+		io_table[s->state].prepare(s, pfd);
+}
+
+void
+irc_server_flush(struct irc_server *s, const struct pollfd *pfd)
+{
+	if (io_table[s->state].flush)
+		io_table[s->state].flush(s, pfd);
+}
+
+bool
+irc_server_poll(struct irc_server *s, struct irc_event *ev)
+{
+	assert(s);
+	assert(ev);
+
+	char *pos;
+	size_t length;
+
+	if (!(pos = strstr(s->in, "\r\n")))
+		return false;
+
+	/* Turn end of the string at delimiter. */
+	*pos = 0;
+	length = pos - s->in;
+
+	/* Clear event in case we don't understand this message. */
+	memset(ev, 0, sizeof (*ev));
+	ev->type = IRC_EVENT_UNKNOWN;
+
+	if (length > 0)
+		parse(s, ev, s->in);
+
+	memmove(s->in, pos + 2, sizeof (s->in) - (length + 2));
+
+	return true;
+}
+
+struct irc_server_channel *
+irc_server_find(struct irc_server *s, const char *name)
+{
+	assert(s);
+	assert(name);
+
+	struct irc_server_channel key = {0};
+
+	strlcpy(key.name, name, sizeof (key.name));
+
+	return bsearch(&key, s->channels, s->channelsz, sizeof (key), compare_chan);
+}
+
+bool
+irc_server_send(struct irc_server *s, const char *fmt, ...)
+{
+	assert(s);
+	assert(fmt);
+
+	char buf[sizeof (s->out)];
+	va_list ap;
+	size_t len, avail, required;
+
+	va_start(ap, fmt);
+	required = vsnprintf(buf, sizeof (buf), fmt, ap);
+	va_end(ap);
+
+	len = strlen(s->out);
+	avail = sizeof (s->out) - len;
+
+	/* Don't forget \r\n. */
+	if (required + 2 >= avail)
+		return false;
+
+	strlcat(s->out, buf, sizeof (s->out));
+	strlcat(s->out, "\r\n", sizeof (s->out));
+
+	return true;
+}
+
+bool
+irc_server_join(struct irc_server *s, const char *name, const char *pass)
+{
+	assert(s);
+	assert(name);
+
+	struct irc_server_channel *ch;
+	bool ret = true;
+
+	/*
+	 * Search if there is already a channel pending or joined. If the
+	 * server is connected we send a join command otherwise we put it there
+	 * and wait for connection.
+	 */
+	if (!(ch = irc_server_find(s, name)))
+		ch = add_channel(s, name, pass, false);
+
+	if (!ch->joined && s->state == IRC_SERVER_STATE_CONNECTED) {
+		if (pass)
+			ret = irc_server_send(s, "JOIN %s %s", name, pass);
+		else
+			ret = irc_server_send(s, "JOIN %s", name);
+	}
+
+	return ret;
+}
+
+bool
+irc_server_part(struct irc_server *s, const char *name, const char *reason)
+{
+	assert(s);
+	assert(name);
+
+	bool ret;
+
+	if (reason && strlen(reason) > 0)
+		ret = irc_server_send(s, "PART %s :%s", name, reason);
+	else
+		ret = irc_server_send(s, "PART %s", name);
+
+	return ret;
+}
+
+bool
+irc_server_topic(struct irc_server *s, const char *name, const char *topic)
+{
+	assert(s);
+	assert(name);
+	assert(topic);
+
+	return irc_server_send(s, "TOPIC %s :%s", name, topic);
+}
+
+bool
+irc_server_message(struct irc_server *s, const char *chan, const char *msg)
+{
+	assert(s);
+	assert(chan);
+	assert(msg);
+
+	return irc_server_send(s, "PRIVMSG %s :%s", chan, msg);
+}
+
+bool
+irc_server_me(struct irc_server *s, const char *chan, const char *message)
+{
+	assert(s);
+	assert(chan);
+	assert(message);
+
+	return irc_server_send(s, "PRIVMSG %s :\001ACTION %s\001", chan, message);
+}
+
+void
+irc_server_finish(struct irc_server *s)
+{
+	assert(s);
+
+	clear(s);
+	free(s->channels);
+	memset(s, 0, sizeof (*s));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,147 @@
+/*
+ * server.h -- an IRC server
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_SERVER_H
+#define IRCCD_SERVER_H
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#if defined(IRC_HAVE_SSL)
+#       include <openssl/ssl.h>
+#endif
+
+#include "limits.h"
+
+struct pollfd;
+
+struct irc_event;
+
+struct irc_server_channel {
+	char name[IRC_CHANNEL_MAX];
+	char password[IRC_PASSWORD_MAX];
+	bool joined;
+};
+
+enum irc_server_state {
+	IRC_SERVER_STATE_DISCONNECTED,
+	IRC_SERVER_STATE_CONNECTING,
+	IRC_SERVER_STATE_HANDSHAKING,
+	IRC_SERVER_STATE_CONNECTED,
+	IRC_SERVER_STATE_WAITING,
+	IRC_SERVER_STATE_NUM
+};
+
+enum irc_server_flags {
+	IRC_SERVER_FLAGS_SSL           = (1 << 0),
+	IRC_SERVER_FLAGS_AUTO_REJOIN   = (1 << 1)
+};
+
+struct irc_server_prefix {
+	char mode;
+	char token;
+};
+
+#if defined(IRCCD_HAVE_SSL)
+
+enum irc_server_ssl_state {
+	IRC_SERVER_SSL_NONE,
+	IRC_SERVER_SSL_NEED_READ,
+	IRC_SERVER_SSL_NEED_WRITE,
+};
+
+#endif
+
+struct irc_server {
+	/* Connection settings. */
+	char name[IRC_NAME_MAX];
+	char host[IRC_HOST_MAX];
+	unsigned short port;
+	enum irc_server_flags flags;
+
+	/* IRC identity. */
+	char nickname[IRC_NICKNAME_MAX];
+	char username[IRC_USERNAME_MAX];
+	char realname[IRC_REALNAME_MAX];
+	char ctcpversion[IRC_CTCPVERSION_MAX];
+	char usermodes[IRC_USERMODES_MAX];
+
+	/* Joined channels. */
+	struct irc_server_channel *channels;
+	size_t channelsz;
+
+	/* Network connectivity. */
+	int fd;
+	struct addrinfo *ai;
+	struct addrinfo *aip;
+	char in[IRC_BUF_MAX];
+	char out[IRC_BUF_MAX];
+	enum irc_server_state state;
+
+	/* OpenSSL support. */
+#if defined(IRCCD_HAVE_SSL)
+	SSL_CTX *ctx;
+	SSL *ssl;
+	enum irc_server_ssl_state ssl_state;
+#endif
+
+	/* IRC server settings. */
+	char chantypes[8];
+	struct irc_server_prefix prefixes[16];
+};
+
+void
+irc_server_connect(struct irc_server *);
+
+void
+irc_server_disconnect(struct irc_server *);
+
+void
+irc_server_prepare(const struct irc_server *, struct pollfd *);
+
+void
+irc_server_flush(struct irc_server *, const struct pollfd *);
+
+bool
+irc_server_poll(struct irc_server *, struct irc_event *);
+
+struct irc_server_channel *
+irc_server_find(struct irc_server *, const char *);
+
+bool
+irc_server_send(struct irc_server *, const char *, ...);
+
+bool
+irc_server_join(struct irc_server *, const char *, const char *);
+
+bool
+irc_server_part(struct irc_server *, const char *, const char *);
+
+bool
+irc_server_topic(struct irc_server *, const char *, const char *);
+
+bool
+irc_server_message(struct irc_server *, const char *, const char *);
+
+bool
+irc_server_me(struct irc_server *, const char *, const char *);
+
+void
+irc_server_finish(struct irc_server *);
+
+#endif /* !IRCCD_SERVER_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/subst.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,485 @@
+/*
+ * subst.c -- pattern substitution
+ *
+ * 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 <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "subst.h"
+
+struct pair {
+	const char *key;
+	const char *value;
+};
+
+struct attributes {
+	char fg[16];
+	char bg[16];
+	char attrs[4][16];
+	size_t attrsz;
+};
+
+static const struct pair irc_colors[] = {
+	{ "white",      "0"     },
+	{ "black",      "1"     },
+	{ "blue",       "2"     },
+	{ "green",      "3"     },
+	{ "red",        "4"     },
+	{ "brown",      "5"     },
+	{ "purple",     "6"     },
+	{ "orange",     "7"     },
+	{ "yellow",     "8"     },
+	{ "lightgreen", "9"     },
+	{ "cyan",       "10"    },
+	{ "lightcyan",  "11"    },
+	{ "lightblue",  "12"    },
+	{ "pink",       "13"    },
+	{ "grey",       "14"    },
+	{ "lightgrey",  "15"    },
+	{ NULL,         NULL    }
+};
+
+static const struct pair irc_attrs[] = {
+	{ "bold",       "\x02"  },
+	{ "italic",     "\x09"  },
+	{ "reverse",    "\x16"  },
+	{ "strike",     "\x13"  },
+	{ "underline",  "\x15"  },
+	{ "underline2", "\x1f"  },
+	{ NULL,         NULL    }
+};
+
+static const struct pair shell_fg[] = {
+	{ "black",      "30"    },
+	{ "red",        "31"    },
+	{ "green",      "32"    },
+	{ "orange",     "33"    },
+	{ "blue",       "34"    },
+	{ "purple",     "35"    },
+	{ "cyan",       "36"    },
+	{ "white",      "37"    },
+	{ "default",    "39"    },
+	{ NULL,         NULL    }
+};
+
+static const struct pair shell_bg[] = {
+	{ "black",      "40"    },
+	{ "red",        "41"    },
+	{ "green",      "42"    },
+	{ "orange",     "43"    },
+	{ "blue",       "44"    },
+	{ "purple",     "45"    },
+	{ "cyan",       "46"    },
+	{ "white",      "47"    },
+	{ "default",    "49"    },
+	{ NULL,         NULL    }
+};
+
+static const struct pair shell_attrs[] = {
+	{ "bold",       "1"     },
+	{ "dim",        "2"     },
+	{ "underline",  "4"     },
+	{ "blink",      "5"     },
+	{ "reverse",    "7"     },
+	{ "hidden",     "8"     },
+	{ NULL,         NULL    }
+};
+
+static inline bool
+is_reserved(char token)
+{
+	return token == '#' || token == '@' || token == '$' || token == '!';
+}
+
+static inline bool
+scat(char **out, size_t *outsz, const char *value)
+{
+	size_t written;
+
+	if ((written = strlcpy(*out, value, *outsz)) >= *outsz) {
+		errno = ENOMEM;
+		return false;
+	}
+
+	*out += written;
+	*outsz -= written;
+
+	return true;
+}
+
+static inline bool
+ccat(char **out, size_t *outsz, char c)
+{
+	if (*outsz == 0)
+		return false;
+
+	*(*out)++ = c;
+	*(outsz) -= 1;
+
+	return true;
+}
+
+static inline void
+attributes_parse(const char *key, struct attributes *attrs)
+{
+	char attributes[64] = {0};
+
+	memset(attrs, 0, sizeof (*attrs));
+	sscanf(key, "%15[^,],%15[^,],%63s", attrs->fg, attrs->bg, attributes);
+
+	for (char *attr = attributes; *attr; ) {
+		char *p = strchr(attr, ',');
+
+		if (p)
+			*p = 0;
+
+		strlcpy(attrs->attrs[attrs->attrsz++], attr, sizeof (attrs->attrs[0]));
+
+		if (p)
+			attr = p + 1;
+		else
+			*attr = '\0';
+	}
+}
+
+static inline const char *
+find(const struct pair *pairs, const char *key)
+{
+	for (const struct pair *pair = pairs; pair->key; ++pair)
+		if (strcmp(pair->key, key) == 0)
+			return pair->value;
+
+	return NULL;
+}
+
+static bool
+subst_date(char *out, size_t outsz, const char *input, const struct irc_subst *subst)
+{
+	struct tm *tm;
+
+	if (!(subst->flags & IRC_SUBST_DATE))
+		return true;
+
+	tm = localtime(&subst->time);
+
+	if (strftime(out, outsz, input, tm) == 0) {
+		errno = ENOMEM;
+		return false;
+	}
+
+	return true;
+}
+
+static bool
+subst_keyword(const char *key, char **out, size_t *outsz, const struct irc_subst *subst)
+{
+	const char *value = NULL;
+
+	for (size_t i = 0; i < subst->keywordsz; ++i) {
+		if (strcmp(subst->keywords[i].key, key) == 0) {
+			value = subst->keywords[i].value;
+			break;
+		}
+	}
+
+	if (!value)
+		return true;
+
+	return scat(out, outsz, value);
+}
+
+static bool
+subst_env(const char *key, char **out, size_t *outsz)
+{
+	const char *value;
+
+	if (!(value = getenv(key)))
+		return true;
+
+	return scat(out, outsz, value);
+}
+
+static bool
+subst_shell(const char *key, char **out, size_t *outsz)
+{
+	FILE *fp;
+	size_t written;
+
+	/* Accept silently. */
+	if (!(fp = popen(key, "r")))
+		return true;
+
+	/*
+	 * Since we cannot determine the number of bytes that must be read, read until the end of
+	 * the output string and cut at the number of bytes read if lesser.
+	 */
+	if ((written = fread(*out, 1, *outsz - 1, fp)) > 0) {
+		/* Remove '\r\n' */
+		char *end;
+
+		if ((end = memchr(*out, '\r', written)) || (end = memchr(*out, '\n', written)))
+			*end = '\0';
+		else
+			end = *out + written;
+
+		*outsz -= end - *out;
+		*out = end;
+	}
+
+	pclose(fp);
+
+	return true;
+}
+
+static bool
+subst_irc_attrs(const char *key, char **out, size_t *outsz)
+{
+	const char *value;
+	struct attributes attrs;
+
+	if (!key[0])
+		return ccat(out, outsz, '\x03');
+
+	attributes_parse(key, &attrs);
+
+	if (attrs.fg[0] || attrs.attrs[0]) {
+		if (!ccat(out, outsz, '\x03'))
+			return false;
+
+		/* Foreground. */
+		if ((value = find(irc_colors, attrs.fg)) && !scat(out, outsz, value))
+			return false;
+
+		/* Background. */
+		if (attrs.bg[0]) {
+			if (!ccat(out, outsz, ','))
+				return false;
+			if ((value = find(irc_colors, attrs.bg)) && !scat(out, outsz, value))
+				return false;
+		}
+
+		/* Attributes. */
+		for (size_t i = 0; i < attrs.attrsz; ++i)
+			if ((value = find(irc_attrs, attrs.attrs[i])) && !scat(out, outsz, value))
+				return false;
+	}
+
+	return true;
+}
+
+static bool
+subst_shell_attrs(char *key, char **out, size_t *outsz)
+{
+	const char *value;
+	struct attributes attrs;
+
+	/* Empty attributes means reset: @{}. */
+	if (!key[0])
+		return scat(out, outsz, "\033[0m");
+
+	attributes_parse(key, &attrs);
+
+	if (!scat(out, outsz, "\033["))
+		return false;
+
+	/* Attributes first. */
+	for (size_t i = 0; i < attrs.attrsz; ++i) {
+		if ((value = find(shell_attrs, attrs.attrs[i])) && !scat(out, outsz, value))
+			return false;
+
+		/* Need to append ; if we have still more attributes or colors next. */
+		if ((i < attrs.attrsz || attrs.fg[0] || attrs.bg[0]) && !ccat(out, outsz, ';'))
+			return false;
+	}
+
+	/* Foreground. */
+	if (attrs.fg[0]) {
+		if ((value = find(shell_fg, attrs.fg)) && !scat(out, outsz, value))
+			return false;
+		if (attrs.bg[0] && !ccat(out, outsz, ';'))
+			return false;
+	}
+
+	/* Background. */
+	if (attrs.bg[0]) {
+		if ((value = find(shell_bg, attrs.bg)) && !scat(out, outsz, value))
+			return false;
+	}
+
+	return ccat(out, outsz, 'm');
+}
+
+static bool
+subst_default(const char **p, char **out, size_t *outsz, const char *key)
+{
+	return ccat(out, outsz, (*p)[-2]) &&
+	       ccat(out, outsz, '{') &&
+	       scat(out, outsz, key) &&
+	       ccat(out, outsz, '}');
+}
+
+static bool
+substitute(const char **p, char **out, size_t *outsz, const struct irc_subst *subst)
+{
+	char key[64] = {0};
+	size_t keysz;
+	char *end;
+	bool replaced = true;
+
+	if (!**p)
+		return true;
+
+	/* Find end of construction. */
+	if (!(end = strchr(*p, '}'))) {
+		errno = EINVAL;
+		return false;
+	}
+
+	/* Copy key. */
+	if ((keysz = end - *p) >= sizeof (key)) {
+		errno = ENOMEM;
+		return false;
+	}
+
+	memcpy(key, *p, keysz);
+
+	switch ((*p)[-2]) {
+	case '@':
+		/* attributes */
+		if (subst->flags & IRC_SUBST_IRC_ATTRS) {
+			if (!subst_irc_attrs(key, out, outsz))
+				return false;
+		} else if (subst->flags & IRC_SUBST_SHELL_ATTRS) {
+			if (!subst_shell_attrs(key, out, outsz))
+				return false;
+		} else
+			replaced = false;
+		break;
+	case '#':
+		/* keyword */
+		if (subst->flags & IRC_SUBST_KEYWORDS) {
+			if (!subst_keyword(key, out, outsz, subst))
+				return false;
+		} else
+			replaced = false;
+		break;
+	case '$':
+		/* environment variable */
+		if (subst->flags & IRC_SUBST_ENV) {
+			if (!subst_env(key, out, outsz))
+				return false;
+		} else
+			replaced = false;
+		break;
+	case '!':
+		/* shell */
+		if (subst->flags & IRC_SUBST_SHELL) {
+			if (!subst_shell(key, out, outsz))
+				return false;
+		} else
+			replaced = false;
+		break;
+	default:
+		break;
+	}
+
+	/* If substitution was disabled, put the token verbatim. */
+	if (!replaced && !subst_default(p, out, outsz, key))
+		return false;
+
+	/* Move after '}' */
+	*p = end + 1;
+
+	return true;
+}
+
+ssize_t
+irc_subst(char *out, size_t outsz, const char *input, const struct irc_subst *subst)
+{
+	assert(out);
+	assert(subst);
+
+	char *o = out;
+
+	if (!outsz)
+		return true;
+
+	/* Always start with the date first. */
+	if (!subst_date(out, outsz, input, subst))
+		goto err;
+
+	for (const char *i = input; *i && outsz; ) {
+		/*
+		 * Check if this is a reserved character, if it isn't go to the next character to
+		 * see if it's valid otherwise we print it as last token.
+		 *
+		 * Example:
+		 *   "#{abc}" -> keyword sequence
+		 *   "abc #"  -> keyword sequence interrupted, kept as-is.
+		 */
+		if (!is_reserved(*i)) {
+			if (!ccat(&o, &outsz, *i++))
+				goto err;
+			continue;
+		}
+
+		/*
+		 * Test if after the reserved token we have the opening { construct. If it's the
+		 * case we start substitution.
+		 *
+		 * Otherwise depending on what's after:
+		 *   If it is the same reserved token, it is "escaped" and printed
+		 *   If it is something else, we print the token and skip iteration.
+		 *
+		 * Examples:
+		 *   ## => #
+		 *   #@ => #@
+		 *   ##{foo} => #{foo}
+		 *   #{foo} => value
+		 */
+		if (*++i == '{') {
+			/* Skip '{'. */
+			++i;
+
+			if (!substitute(&i, &o, &outsz, subst))
+				goto err;
+		} else {
+			if (*i == i[-1])
+				++i;
+			if (!ccat(&o, &outsz, i[-1]))
+				goto err;
+		}
+	}
+
+	if (outsz < 1) {
+		errno = ENOMEM;
+		goto err;
+	}
+
+	*o = '\0';
+
+	return o - out;
+
+err:
+	out[0] = '\0';
+
+	return -1;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/subst.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,50 @@
+/*
+ * subst.h -- pattern substitution
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_SUBST_H
+#define IRCCD_SUBST_H
+
+#include <sys/types.h>
+#include <stddef.h>
+#include <time.h>
+
+enum irc_subst_flags {
+	IRC_SUBST_DATE          = (1 << 0),
+	IRC_SUBST_KEYWORDS      = (1 << 1),
+	IRC_SUBST_ENV           = (1 << 2),
+	IRC_SUBST_SHELL         = (1 << 3),
+	IRC_SUBST_IRC_ATTRS     = (1 << 4),
+	IRC_SUBST_SHELL_ATTRS   = (1 << 5)
+};
+
+struct irc_subst_keyword {
+	const char *key;
+	const char *value;
+};
+
+struct irc_subst {
+	time_t time;
+	enum irc_subst_flags flags;
+	const struct irc_subst_keyword *keywords;
+	size_t keywordsz;
+};
+
+ssize_t
+irc_subst(char *, size_t, const char *, const struct irc_subst *);
+
+#endif /* !IRCCD_SUBST_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/util.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,117 @@
+/*
+ * util.c -- miscellaneous utilities
+ *
+ * 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 <err.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <compat.h>
+
+#include "util.h"
+
+void *
+irc_util_malloc(size_t size)
+{
+	void *ret;
+
+	if (!(ret = malloc(size)))
+		err(1, "malloc");
+
+	return ret;
+}
+
+void *
+irc_util_calloc(size_t n, size_t size)
+{
+	void *ret;
+
+	if (!(ret = calloc(n, size)))
+		err(1, "calloc");
+
+	return ret;
+}
+
+void *
+irc_util_realloc(void *ptr, size_t size)
+{
+	void *ret;
+
+	if (!(ret = realloc(ptr, size)) && ptr)
+		err(1, "realloc");
+
+	return ret;
+}
+
+void *
+irc_util_reallocarray(void *ptr, size_t n, size_t size)
+{
+	void *ret;
+
+	if (!(ret = reallocarray(ptr, n, size)))
+		err(1, "reallocarray");
+
+	return ret;
+}
+
+void *
+irc_util_memdup(const void *ptr, size_t size)
+{
+	void *ret;
+
+	if (!(ret = malloc(size)))
+		err(1, "malloc");
+
+	return memcpy(ret, ptr, size);
+}
+
+char *
+irc_util_strdup(const char *src)
+{
+	char *ret;
+
+	if (!(ret = strdup(src)))
+		err(1, "strdup");
+
+	return ret;
+}
+
+char *
+irc_util_basename(const char *str)
+{
+	static char ret[PATH_MAX];
+	char tmp[PATH_MAX];
+
+	strlcpy(tmp, str, sizeof (tmp));
+	strlcpy(ret, basename(tmp), sizeof (ret));
+
+	return ret;
+}
+
+char *
+irc_util_dirname(const char *str)
+{
+	static char ret[PATH_MAX];
+	char tmp[PATH_MAX];
+
+	strlcpy(tmp, str, sizeof (tmp));
+	strlcpy(ret, dirname(tmp), sizeof (ret));
+
+	return ret;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/util.h	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,50 @@
+/*
+ * util.h -- miscellaneous utilities
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_UTIL_H
+#define IRCCD_UTIL_H
+
+#include <stddef.h>
+
+#define IRC_UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
+
+void *
+irc_util_malloc(size_t);
+
+void *
+irc_util_calloc(size_t, size_t);
+
+void *
+irc_util_realloc(void *, size_t);
+
+void *
+irc_util_reallocarray(void *, size_t, size_t);
+
+void *
+irc_util_memdup(const void *, size_t);
+
+char *
+irc_util_strdup(const char *);
+
+char *
+irc_util_basename(const char *);
+
+char *
+irc_util_dirname(const char *);
+
+#endif /* !IRCCD_UTIL_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/example-dl-plugin.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,160 @@
+/*
+ * example-dl-plugin.c -- simple plugin for unit tests
+ *
+ * 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 <string.h>
+
+#include <irccd/dl-plugin.h>
+#include <irccd/event.h>
+#include <irccd/util.h>
+
+struct kw {
+	const char *key;
+	char value[256];
+};
+
+/*
+ * Options.
+ */
+static struct kw options[] = {
+	{ "option-1", "value-1" }
+};
+
+static const char *options_list[] = {
+	"option-1",
+	NULL
+};
+
+/*
+ * Templates.
+ */
+static struct kw templates[] = {
+	{ "template-1", "Welcome #{target}" }
+};
+
+static const char *templates_list[] = {
+	"template-1",
+	NULL
+};
+
+/*
+ * Paths.
+ */
+static struct kw paths[] = {
+	{ "path-1", "/usr/local/etc" }
+};
+
+static const char *paths_list[] = {
+	"path-1",
+	NULL
+};
+
+static void
+set(struct kw *table, size_t tablesz, const char *key, const char *value)
+{
+	for (size_t i = 0; i < tablesz; ++i) {
+		if (strcmp(table[i].key, key) == 0) {
+			strlcpy(table[i].value, value, sizeof (table[i].value));
+			break;
+		}
+	}
+}
+
+static const char *
+get(const struct kw *table, size_t tablesz, const char *key)
+{
+	for (size_t i = 0; i < tablesz; ++i)
+		if (strcmp(table[i].key, key) == 0)
+			return table[i].value;
+
+	return NULL;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_set_option(const char *key, const char *value)
+{
+	set(options, IRC_UTIL_SIZE(options), key, value);
+}
+
+IRC_DL_EXPORT const char *
+example_dl_plugin_get_option(const char *key)
+{
+	return get(options, IRC_UTIL_SIZE(options), key);
+}
+
+IRC_DL_EXPORT const char **
+example_dl_plugin_get_options(void)
+{
+	return options_list;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_set_template(const char *key, const char *value)
+{
+	set(templates, IRC_UTIL_SIZE(templates), key, value);
+}
+
+IRC_DL_EXPORT const char *
+example_dl_plugin_get_template(const char *key)
+{
+	return get(templates, IRC_UTIL_SIZE(templates), key);
+}
+
+IRC_DL_EXPORT const char **
+example_dl_plugin_get_templates(void)
+{
+	return templates_list;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_set_path(const char *key, const char *value)
+{
+	set(paths, IRC_UTIL_SIZE(paths), key, value);
+}
+
+IRC_DL_EXPORT const char *
+example_dl_plugin_get_path(const char *key)
+{
+	return get(paths, IRC_UTIL_SIZE(paths), key);
+}
+
+IRC_DL_EXPORT const char **
+example_dl_plugin_get_paths(void)
+{
+	return paths_list;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_event(const struct irc_event *ev)
+{
+	(void)ev;
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_load(void)
+{
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_reload(void)
+{
+}
+
+IRC_DL_EXPORT void
+example_dl_plugin_unload(void)
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-dl-plugin.c	Mon Jan 11 21:25:58 2021 +0100
@@ -0,0 +1,160 @@
+/*
+ * test-dl-plugin.c -- test dl-plugin.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/dl-plugin.h>
+#include <irccd/event.h>
+#include <irccd/plugin.h>
+
+static struct irc_plugin plugin;
+
+static void
+setup(void *udata)
+{
+	(void)udata;
+
+	/* TODO: No idea how to stop greatest from here. */
+	if (!irc_dl_plugin_open(&plugin, SOURCEDIR "/tests/example-dl-plugin" IRC_DL_EXT))
+		exit(1);
+}
+
+static void
+teardown(void *udata)
+{
+	(void)udata;
+
+	irc_plugin_finish(&plugin);
+}
+
+GREATEST_TEST
+options_set_get(void)
+{
+	irc_plugin_set_option(&plugin, "option-1", "new-value-1");
+	GREATEST_ASSERT_STR_EQ("new-value-1", irc_plugin_get_option(&plugin, "option-1"));
+	GREATEST_ASSERT(!irc_plugin_get_option(&plugin, "not-found"));
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+options_list(void)
+{
+	const char **options = irc_plugin_get_options(&plugin);
+
+	GREATEST_ASSERT_STR_EQ("option-1", options[0]);
+	GREATEST_ASSERT(!options[1]);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_options)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(options_set_get);
+	GREATEST_RUN_TEST(options_list);
+}
+
+GREATEST_TEST
+paths_set_get(void)
+{
+	irc_plugin_set_path(&plugin, "path-1", "new-value-1");
+	GREATEST_ASSERT_STR_EQ("new-value-1", irc_plugin_get_path(&plugin, "path-1"));
+	GREATEST_ASSERT(!irc_plugin_get_path(&plugin, "not-found"));
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+paths_list(void)
+{
+	const char **paths = irc_plugin_get_paths(&plugin);
+
+	GREATEST_ASSERT_STR_EQ("path-1", paths[0]);
+	GREATEST_ASSERT(!paths[1]);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_paths)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(paths_set_get);
+	GREATEST_RUN_TEST(paths_list);
+}
+
+GREATEST_TEST
+templates_set_get(void)
+{
+	irc_plugin_set_template(&plugin, "template-1", "new-value-1");
+	GREATEST_ASSERT_STR_EQ("new-value-1", irc_plugin_get_template(&plugin, "template-1"));
+	GREATEST_ASSERT(!irc_plugin_get_template(&plugin, "not-found"));
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+templates_list(void)
+{
+	const char **templates = irc_plugin_get_templates(&plugin);
+
+	GREATEST_ASSERT_STR_EQ("template-1", templates[0]);
+	GREATEST_ASSERT(!templates[1]);
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_templates)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(templates_set_get);
+	GREATEST_RUN_TEST(templates_list);
+}
+
+GREATEST_TEST
+calls_simple(void)
+{
+	struct irc_event ev = {0};
+
+	irc_plugin_load(&plugin);
+	irc_plugin_unload(&plugin);
+	irc_plugin_reload(&plugin);
+	irc_plugin_handle(&plugin, &ev);
+
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_calls)
+{
+	GREATEST_SET_SETUP_CB(setup, NULL);
+	GREATEST_SET_TEARDOWN_CB(teardown, NULL);
+	GREATEST_RUN_TEST(calls_simple);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_options);
+	GREATEST_RUN_SUITE(suite_paths);
+	GREATEST_RUN_SUITE(suite_templates);
+	GREATEST_RUN_SUITE(suite_calls);
+	GREATEST_MAIN_END();
+
+	return 0;
+}