changeset 809:8460b4a34191

misc: reorganize namespaces, closes #952 @4h
author David Demelier <markand@malikania.fr>
date Fri, 16 Nov 2018 12:25:00 +0100
parents 80bccab4a093
children 9277f19b28dd
files CMakeLists.txt cmake/export/CMakeLists.txt cmake/export/libirccd-core.pc cmake/export/libirccd-daemon.pc cmake/export/libirccd.pc cmake/function/IrccdDefinePlugin.cmake doc/doxygen/CMakeLists.txt doc/doxygen/mainpage.cpp doc/doxygen/modules.cpp doc/doxygen/page-overview-irccd.cpp doc/doxygen/page-overview-plugins.cpp doc/doxygen/page-overview-rules.cpp doc/doxygen/page-overview-servers.cpp doc/doxygen/page-overview-transports.cpp doc/doxygen/page-overview.cpp doc/doxygen/pages.cpp irccd-test/CMakeLists.txt irccd-test/main.cpp irccd/CMakeLists.txt irccd/main.cpp irccdctl/cli.cpp irccdctl/main.cpp libirccd-core/CMakeLists.txt libirccd-core/irccd/acceptor.hpp libirccd-core/irccd/config.cpp libirccd-core/irccd/config.hpp libirccd-core/irccd/connector.hpp libirccd-core/irccd/fs_util.cpp libirccd-core/irccd/fs_util.hpp libirccd-core/irccd/ini.cpp libirccd-core/irccd/ini.hpp libirccd-core/irccd/ini_util.hpp libirccd-core/irccd/json_util.cpp libirccd-core/irccd/json_util.hpp libirccd-core/irccd/options.cpp libirccd-core/irccd/options.hpp libirccd-core/irccd/stream.hpp libirccd-core/irccd/string_util.cpp libirccd-core/irccd/string_util.hpp libirccd-core/irccd/system.cpp libirccd-core/irccd/system.hpp libirccd-core/irccd/xdg.hpp libirccd-ctl/CMakeLists.txt libirccd-ctl/irccd/ctl/controller.cpp libirccd-daemon/CMakeLists.txt libirccd-daemon/irccd/daemon/bot.cpp libirccd-daemon/irccd/daemon/bot.hpp libirccd-daemon/irccd/daemon/command.cpp libirccd-daemon/irccd/daemon/command.hpp libirccd-daemon/irccd/daemon/dynlib_plugin.cpp libirccd-daemon/irccd/daemon/dynlib_plugin.hpp libirccd-daemon/irccd/daemon/irc.cpp libirccd-daemon/irccd/daemon/irc.hpp libirccd-daemon/irccd/daemon/logger.cpp libirccd-daemon/irccd/daemon/logger.hpp libirccd-daemon/irccd/daemon/plugin.cpp libirccd-daemon/irccd/daemon/plugin.hpp libirccd-daemon/irccd/daemon/plugin_service.cpp libirccd-daemon/irccd/daemon/plugin_service.hpp libirccd-daemon/irccd/daemon/rule.cpp libirccd-daemon/irccd/daemon/rule.hpp libirccd-daemon/irccd/daemon/rule_service.cpp libirccd-daemon/irccd/daemon/rule_service.hpp libirccd-daemon/irccd/daemon/rule_util.cpp libirccd-daemon/irccd/daemon/rule_util.hpp libirccd-daemon/irccd/daemon/server.cpp libirccd-daemon/irccd/daemon/server.hpp libirccd-daemon/irccd/daemon/server_service.cpp libirccd-daemon/irccd/daemon/server_service.hpp libirccd-daemon/irccd/daemon/server_util.cpp libirccd-daemon/irccd/daemon/server_util.hpp libirccd-daemon/irccd/daemon/transport_client.cpp libirccd-daemon/irccd/daemon/transport_client.hpp libirccd-daemon/irccd/daemon/transport_server.cpp libirccd-daemon/irccd/daemon/transport_server.hpp libirccd-daemon/irccd/daemon/transport_service.cpp libirccd-daemon/irccd/daemon/transport_service.hpp libirccd-daemon/irccd/daemon/transport_util.cpp libirccd-daemon/irccd/daemon/transport_util.hpp libirccd-js/CMakeLists.txt libirccd-js/irccd/js/directory_js_api.cpp libirccd-js/irccd/js/directory_js_api.hpp libirccd-js/irccd/js/elapsed_timer_js_api.cpp libirccd-js/irccd/js/elapsed_timer_js_api.hpp libirccd-js/irccd/js/file_js_api.cpp libirccd-js/irccd/js/file_js_api.hpp libirccd-js/irccd/js/irccd_js_api.cpp libirccd-js/irccd/js/irccd_js_api.hpp libirccd-js/irccd/js/js_api.hpp libirccd-js/irccd/js/js_plugin.cpp libirccd-js/irccd/js/js_plugin.hpp libirccd-js/irccd/js/logger_js_api.cpp libirccd-js/irccd/js/logger_js_api.hpp libirccd-js/irccd/js/plugin_js_api.cpp libirccd-js/irccd/js/plugin_js_api.hpp libirccd-js/irccd/js/server_js_api.cpp libirccd-js/irccd/js/server_js_api.hpp libirccd-js/irccd/js/system_js_api.cpp libirccd-js/irccd/js/system_js_api.hpp libirccd-js/irccd/js/timer_js_api.cpp libirccd-js/irccd/js/timer_js_api.hpp libirccd-js/irccd/js/unicode_js_api.cpp libirccd-js/irccd/js/unicode_js_api.hpp libirccd-js/irccd/js/util_js_api.cpp libirccd-js/irccd/js/util_js_api.hpp libirccd-test/CMakeLists.txt libirccd-test/irccd/test/cli_fixture.cpp libirccd-test/irccd/test/cli_fixture.hpp libirccd-test/irccd/test/command_fixture.cpp libirccd-test/irccd/test/debug_server.hpp libirccd-test/irccd/test/irccd_fixture.cpp libirccd-test/irccd/test/irccd_fixture.hpp libirccd-test/irccd/test/js_fixture.cpp libirccd-test/irccd/test/js_plugin_fixture.cpp libirccd-test/irccd/test/js_plugin_fixture.hpp libirccd-test/irccd/test/mock_plugin.cpp libirccd-test/irccd/test/mock_plugin.hpp libirccd-test/irccd/test/mock_server.hpp libirccd/CMakeLists.txt libirccd/irccd/acceptor.hpp libirccd/irccd/config.cpp libirccd/irccd/config.hpp libirccd/irccd/connector.hpp libirccd/irccd/daemon/command.cpp libirccd/irccd/daemon/command.hpp libirccd/irccd/daemon/dynlib_plugin.cpp libirccd/irccd/daemon/dynlib_plugin.hpp libirccd/irccd/daemon/irc.cpp libirccd/irccd/daemon/irc.hpp libirccd/irccd/daemon/irccd.cpp libirccd/irccd/daemon/irccd.hpp libirccd/irccd/daemon/logger.cpp libirccd/irccd/daemon/logger.hpp libirccd/irccd/daemon/plugin.cpp libirccd/irccd/daemon/plugin.hpp libirccd/irccd/daemon/plugin_service.cpp libirccd/irccd/daemon/plugin_service.hpp libirccd/irccd/daemon/rule.cpp libirccd/irccd/daemon/rule.hpp libirccd/irccd/daemon/rule_service.cpp libirccd/irccd/daemon/rule_service.hpp libirccd/irccd/daemon/rule_util.cpp libirccd/irccd/daemon/rule_util.hpp libirccd/irccd/daemon/server.cpp libirccd/irccd/daemon/server.hpp libirccd/irccd/daemon/server_service.cpp libirccd/irccd/daemon/server_service.hpp libirccd/irccd/daemon/server_util.cpp libirccd/irccd/daemon/server_util.hpp libirccd/irccd/daemon/transport_client.cpp libirccd/irccd/daemon/transport_client.hpp libirccd/irccd/daemon/transport_server.cpp libirccd/irccd/daemon/transport_server.hpp libirccd/irccd/daemon/transport_service.cpp libirccd/irccd/daemon/transport_service.hpp libirccd/irccd/daemon/transport_util.cpp libirccd/irccd/daemon/transport_util.hpp libirccd/irccd/fs_util.cpp libirccd/irccd/fs_util.hpp libirccd/irccd/ini.cpp libirccd/irccd/ini.hpp libirccd/irccd/ini_util.hpp libirccd/irccd/json_util.cpp libirccd/irccd/json_util.hpp libirccd/irccd/options.cpp libirccd/irccd/options.hpp libirccd/irccd/stream.hpp libirccd/irccd/string_util.cpp libirccd/irccd/string_util.hpp libirccd/irccd/system.cpp libirccd/irccd/system.hpp libirccd/irccd/xdg.hpp plugins/links/links.cpp plugins/links/links.hpp plugins/links/requester.cpp plugins/links/requester.hpp tests/CMakeLists.txt tests/src/irccdctl/cli-plugin-config/CMakeLists.txt tests/src/irccdctl/cli-plugin-config/main.cpp tests/src/irccdctl/cli-plugin-info/CMakeLists.txt tests/src/irccdctl/cli-plugin-info/main.cpp tests/src/irccdctl/cli-plugin-list/CMakeLists.txt tests/src/irccdctl/cli-plugin-list/main.cpp tests/src/irccdctl/cli-plugin-load/CMakeLists.txt tests/src/irccdctl/cli-plugin-load/main.cpp tests/src/irccdctl/cli-plugin-reload/CMakeLists.txt tests/src/irccdctl/cli-plugin-reload/main.cpp tests/src/irccdctl/cli-plugin-unload/CMakeLists.txt tests/src/irccdctl/cli-plugin-unload/main.cpp tests/src/irccdctl/cli-rule-add/CMakeLists.txt tests/src/irccdctl/cli-rule-edit/CMakeLists.txt tests/src/irccdctl/cli-rule-edit/main.cpp tests/src/irccdctl/cli-rule-info/CMakeLists.txt tests/src/irccdctl/cli-rule-info/main.cpp tests/src/irccdctl/cli-rule-list/CMakeLists.txt tests/src/irccdctl/cli-rule-list/main.cpp tests/src/irccdctl/cli-rule-move/CMakeLists.txt tests/src/irccdctl/cli-rule-move/main.cpp tests/src/irccdctl/cli-rule-remove/CMakeLists.txt tests/src/irccdctl/cli-rule-remove/main.cpp tests/src/irccdctl/cli-server-disconnect/CMakeLists.txt tests/src/irccdctl/cli-server-disconnect/main.cpp tests/src/irccdctl/cli-server-info/CMakeLists.txt tests/src/irccdctl/cli-server-invite/CMakeLists.txt tests/src/irccdctl/cli-server-join/CMakeLists.txt tests/src/irccdctl/cli-server-kick/CMakeLists.txt tests/src/irccdctl/cli-server-list/CMakeLists.txt tests/src/irccdctl/cli-server-list/main.cpp tests/src/irccdctl/cli-server-me/CMakeLists.txt tests/src/irccdctl/cli-server-message/CMakeLists.txt tests/src/irccdctl/cli-server-mode/CMakeLists.txt tests/src/irccdctl/cli-server-nick/CMakeLists.txt tests/src/irccdctl/cli-server-notice/CMakeLists.txt tests/src/irccdctl/cli-server-part/CMakeLists.txt tests/src/irccdctl/cli-server-reconnect/CMakeLists.txt tests/src/irccdctl/cli-server-reconnect/main.cpp tests/src/irccdctl/cli-server-topic/CMakeLists.txt tests/src/libirccd-core/CMakeLists.txt tests/src/libirccd-core/fs-util/CMakeLists.txt tests/src/libirccd-core/fs-util/main.cpp tests/src/libirccd-core/stream/CMakeLists.txt tests/src/libirccd-core/stream/main.cpp tests/src/libirccd-core/string-util/CMakeLists.txt tests/src/libirccd-core/string-util/main.cpp tests/src/libirccd-daemon/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-config/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-config/main.cpp tests/src/libirccd-daemon/command-plugin-info/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-info/main.cpp tests/src/libirccd-daemon/command-plugin-list/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-list/main.cpp tests/src/libirccd-daemon/command-plugin-load/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-load/main.cpp tests/src/libirccd-daemon/command-plugin-reload/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-reload/main.cpp tests/src/libirccd-daemon/command-plugin-unload/CMakeLists.txt tests/src/libirccd-daemon/command-plugin-unload/main.cpp tests/src/libirccd-daemon/command-rule-add/CMakeLists.txt tests/src/libirccd-daemon/command-rule-add/main.cpp tests/src/libirccd-daemon/command-rule-edit/CMakeLists.txt tests/src/libirccd-daemon/command-rule-edit/main.cpp tests/src/libirccd-daemon/command-rule-info/CMakeLists.txt tests/src/libirccd-daemon/command-rule-info/main.cpp tests/src/libirccd-daemon/command-rule-list/CMakeLists.txt tests/src/libirccd-daemon/command-rule-list/main.cpp tests/src/libirccd-daemon/command-rule-move/CMakeLists.txt tests/src/libirccd-daemon/command-rule-move/main.cpp tests/src/libirccd-daemon/command-rule-remove/CMakeLists.txt tests/src/libirccd-daemon/command-rule-remove/main.cpp tests/src/libirccd-daemon/command-server-connect/CMakeLists.txt tests/src/libirccd-daemon/command-server-connect/main.cpp tests/src/libirccd-daemon/command-server-disconnect/CMakeLists.txt tests/src/libirccd-daemon/command-server-disconnect/main.cpp tests/src/libirccd-daemon/command-server-info/CMakeLists.txt tests/src/libirccd-daemon/command-server-info/main.cpp tests/src/libirccd-daemon/command-server-invite/CMakeLists.txt tests/src/libirccd-daemon/command-server-invite/main.cpp tests/src/libirccd-daemon/command-server-join/CMakeLists.txt tests/src/libirccd-daemon/command-server-join/main.cpp tests/src/libirccd-daemon/command-server-kick/CMakeLists.txt tests/src/libirccd-daemon/command-server-kick/main.cpp tests/src/libirccd-daemon/command-server-list/CMakeLists.txt tests/src/libirccd-daemon/command-server-list/main.cpp tests/src/libirccd-daemon/command-server-me/CMakeLists.txt tests/src/libirccd-daemon/command-server-me/main.cpp tests/src/libirccd-daemon/command-server-message/CMakeLists.txt tests/src/libirccd-daemon/command-server-message/main.cpp tests/src/libirccd-daemon/command-server-mode/CMakeLists.txt tests/src/libirccd-daemon/command-server-mode/main.cpp tests/src/libirccd-daemon/command-server-nick/CMakeLists.txt tests/src/libirccd-daemon/command-server-nick/main.cpp tests/src/libirccd-daemon/command-server-notice/CMakeLists.txt tests/src/libirccd-daemon/command-server-notice/main.cpp tests/src/libirccd-daemon/command-server-part/CMakeLists.txt tests/src/libirccd-daemon/command-server-part/main.cpp tests/src/libirccd-daemon/command-server-reconnect/CMakeLists.txt tests/src/libirccd-daemon/command-server-reconnect/main.cpp tests/src/libirccd-daemon/command-server-topic/CMakeLists.txt tests/src/libirccd-daemon/command-server-topic/main.cpp tests/src/libirccd-daemon/dynlib-plugin/CMakeLists.txt tests/src/libirccd-daemon/dynlib-plugin/main.cpp tests/src/libirccd-daemon/dynlib-plugin/test_plugin.cpp tests/src/libirccd-daemon/irc/CMakeLists.txt tests/src/libirccd-daemon/irc/main.cpp tests/src/libirccd-daemon/logger/CMakeLists.txt tests/src/libirccd-daemon/logger/main.cpp tests/src/libirccd-daemon/rule-util/CMakeLists.txt tests/src/libirccd-daemon/rule-util/error-invalid-action.conf tests/src/libirccd-daemon/rule-util/main.cpp tests/src/libirccd-daemon/rule-util/simple.conf tests/src/libirccd-daemon/rules/CMakeLists.txt tests/src/libirccd-daemon/rules/main.cpp tests/src/libirccd-daemon/server-util/CMakeLists.txt tests/src/libirccd-daemon/server-util/full.conf tests/src/libirccd-daemon/server-util/full.json tests/src/libirccd-daemon/server-util/main.cpp tests/src/libirccd-daemon/server-util/ssl.conf tests/src/libirccd-js/js-api-logger/main.cpp tests/src/libirccd-js/js-api-timer/main.cpp tests/src/libirccd-js/js-plugin/main.cpp tests/src/libirccd/CMakeLists.txt tests/src/libirccd/command-plugin-config/CMakeLists.txt tests/src/libirccd/command-plugin-config/main.cpp tests/src/libirccd/command-plugin-info/CMakeLists.txt tests/src/libirccd/command-plugin-info/main.cpp tests/src/libirccd/command-plugin-list/CMakeLists.txt tests/src/libirccd/command-plugin-list/main.cpp tests/src/libirccd/command-plugin-load/CMakeLists.txt tests/src/libirccd/command-plugin-load/main.cpp tests/src/libirccd/command-plugin-reload/CMakeLists.txt tests/src/libirccd/command-plugin-reload/main.cpp tests/src/libirccd/command-plugin-unload/CMakeLists.txt tests/src/libirccd/command-plugin-unload/main.cpp tests/src/libirccd/command-rule-add/CMakeLists.txt tests/src/libirccd/command-rule-add/main.cpp tests/src/libirccd/command-rule-edit/CMakeLists.txt tests/src/libirccd/command-rule-edit/main.cpp tests/src/libirccd/command-rule-info/CMakeLists.txt tests/src/libirccd/command-rule-info/main.cpp tests/src/libirccd/command-rule-list/CMakeLists.txt tests/src/libirccd/command-rule-list/main.cpp tests/src/libirccd/command-rule-move/CMakeLists.txt tests/src/libirccd/command-rule-move/main.cpp tests/src/libirccd/command-rule-remove/CMakeLists.txt tests/src/libirccd/command-rule-remove/main.cpp tests/src/libirccd/command-server-connect/CMakeLists.txt tests/src/libirccd/command-server-connect/main.cpp tests/src/libirccd/command-server-disconnect/CMakeLists.txt tests/src/libirccd/command-server-disconnect/main.cpp tests/src/libirccd/command-server-info/CMakeLists.txt tests/src/libirccd/command-server-info/main.cpp tests/src/libirccd/command-server-invite/CMakeLists.txt tests/src/libirccd/command-server-invite/main.cpp tests/src/libirccd/command-server-join/CMakeLists.txt tests/src/libirccd/command-server-join/main.cpp tests/src/libirccd/command-server-kick/CMakeLists.txt tests/src/libirccd/command-server-kick/main.cpp tests/src/libirccd/command-server-list/CMakeLists.txt tests/src/libirccd/command-server-list/main.cpp tests/src/libirccd/command-server-me/CMakeLists.txt tests/src/libirccd/command-server-me/main.cpp tests/src/libirccd/command-server-message/CMakeLists.txt tests/src/libirccd/command-server-message/main.cpp tests/src/libirccd/command-server-mode/CMakeLists.txt tests/src/libirccd/command-server-mode/main.cpp tests/src/libirccd/command-server-nick/CMakeLists.txt tests/src/libirccd/command-server-nick/main.cpp tests/src/libirccd/command-server-notice/CMakeLists.txt tests/src/libirccd/command-server-notice/main.cpp tests/src/libirccd/command-server-part/CMakeLists.txt tests/src/libirccd/command-server-part/main.cpp tests/src/libirccd/command-server-reconnect/CMakeLists.txt tests/src/libirccd/command-server-reconnect/main.cpp tests/src/libirccd/command-server-topic/CMakeLists.txt tests/src/libirccd/command-server-topic/main.cpp tests/src/libirccd/dynlib-plugin/CMakeLists.txt tests/src/libirccd/dynlib-plugin/main.cpp tests/src/libirccd/dynlib-plugin/test_plugin.cpp tests/src/libirccd/fs-util/CMakeLists.txt tests/src/libirccd/fs-util/main.cpp tests/src/libirccd/irc/CMakeLists.txt tests/src/libirccd/irc/main.cpp tests/src/libirccd/logger/CMakeLists.txt tests/src/libirccd/logger/main.cpp tests/src/libirccd/rule-util/CMakeLists.txt tests/src/libirccd/rule-util/error-invalid-action.conf tests/src/libirccd/rule-util/main.cpp tests/src/libirccd/rule-util/simple.conf tests/src/libirccd/rules/CMakeLists.txt tests/src/libirccd/rules/main.cpp tests/src/libirccd/server-util/CMakeLists.txt tests/src/libirccd/server-util/full.conf tests/src/libirccd/server-util/full.json tests/src/libirccd/server-util/main.cpp tests/src/libirccd/server-util/ssl.conf tests/src/libirccd/stream/CMakeLists.txt tests/src/libirccd/stream/main.cpp tests/src/libirccd/string-util/CMakeLists.txt tests/src/libirccd/string-util/main.cpp tests/src/plugins/CMakeLists.txt tests/src/plugins/ask/main.cpp tests/src/plugins/auth/main.cpp tests/src/plugins/hangman/main.cpp tests/src/plugins/history/main.cpp tests/src/plugins/joke/main.cpp tests/src/plugins/logger/main.cpp tests/src/plugins/plugin/main.cpp tests/src/plugins/tictactoe/main.cpp
diffstat 388 files changed, 24892 insertions(+), 24491 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -37,11 +37,11 @@
 #
 # doc                       - The documentation process.
 # extern                    - External libraries.
-# libirccd-core             - Common code.
+# libirccd                  - Common code.
 # libirccd-ctl              - The irccdctl library.
 # libirccd-js               - Javascript bindings library.
 # libirccd-test             - Helpers for unit tests.
-# libirccd                  - The irccd library.
+# libirccd-daemon           - The irccd bot library.
 # irccd                     - The irccd executable.
 # irccdctl                  - The irccdctl utility.
 # plugins                   - Official irccd plugins.
@@ -86,8 +86,8 @@
 
 add_subdirectory(extern/json)
 add_subdirectory(doc)
-add_subdirectory(libirccd-core)
 add_subdirectory(libirccd)
+add_subdirectory(libirccd-daemon)
 add_subdirectory(libirccd-ctl)
 add_subdirectory(libirccd-test)
 
--- a/cmake/export/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/cmake/export/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -50,7 +50,7 @@
 		set(IRCCD_EXTRA_LIBS "-lssl -lcrypto")
 	endif ()
 
-	foreach (pkg libirccd-core libirccd libirccd-ctl libirccd-js libirccd-test)
+	foreach (pkg libirccd libirccd-daemon libirccd-ctl libirccd-js libirccd-test)
 		configure_file(
 			${CMAKE_CURRENT_SOURCE_DIR}/${pkg}.pc
 			${CMAKE_CURRENT_BINARY_DIR}/${pkg}.pc
--- a/cmake/export/libirccd-core.pc	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-prefix=@CMAKE_INSTALL_PREFIX@
-exec_prefix=@CMAKE_INSTALL_PREFIX@
-includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
-libdir=@CMAKE_INSTALL_FULL_LIBDIR@
-
-Name: libirccd-core
-Description: irccd (core library)
-Version: @IRCCD_VERSION@
-Cflags: -I${includedir} -I${includedir}/irccd/extern
-Libs: -L${libdir} -lirccd-core -lboost_system -lboost_filesystem -pthread @IRCCD_EXTRA_LIBS@
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmake/export/libirccd-daemon.pc	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,11 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=@CMAKE_INSTALL_PREFIX@
+includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
+libdir=@CMAKE_INSTALL_FULL_LIBDIR@
+
+Name: libirccd-daemon
+Description: irccd (daemon library)
+Version: @IRCCD_VERSION@
+Cflags: -I${includedir} -I${includedir}/irccd/extern
+Libs: -L${libdir} -lirccd-daemon
+Requires: libirccd
--- a/cmake/export/libirccd.pc	Thu Nov 15 13:19:17 2018 +0100
+++ b/cmake/export/libirccd.pc	Fri Nov 16 12:25:00 2018 +0100
@@ -4,8 +4,7 @@
 libdir=@CMAKE_INSTALL_FULL_LIBDIR@
 
 Name: libirccd
-Description: irccd (daemon library)
+Description: irccd (core library)
 Version: @IRCCD_VERSION@
 Cflags: -I${includedir} -I${includedir}/irccd/extern
-Libs: -L${libdir} -lirccd
-Requires: libirccd-core
+Libs: -L${libdir} -lirccd -lboost_system -lboost_filesystem -pthread @IRCCD_EXTRA_LIBS@
--- a/cmake/function/IrccdDefinePlugin.cmake	Thu Nov 15 13:19:17 2018 +0100
+++ b/cmake/function/IrccdDefinePlugin.cmake	Fri Nov 16 12:25:00 2018 +0100
@@ -89,7 +89,7 @@
 	endif ()
 
 	add_library(plugin-${PLG_NAME} MODULE ${PLG_SOURCES} ${PLG_OUTPUT_DOC} ${PLG_DOCS})
-	target_link_libraries(plugin-${PLG_NAME} libirccd ${PLG_LIBRARIES})
+	target_link_libraries(plugin-${PLG_NAME} libirccd-daemon ${PLG_LIBRARIES})
 	target_include_directories(plugin-${PLG_NAME} PRIVATE ${PLG_INCLUDES})
 
 	# Change output name.
--- a/doc/doxygen/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/doc/doxygen/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -26,7 +26,7 @@
 
 set(DOXYGEN_STRIP_FROM_PATH
 	libirccd/irccd
-	libirccd-core/irccd
+	libirccd-daemon/irccd
 	libirccd-ctl/irccd
 	libirccd-js/irccd
 	libirccd-test/irccd
@@ -35,14 +35,10 @@
 doxygen_add_docs(
 	docs-doxygen
 	${CMAKE_CURRENT_SOURCE_DIR}/mainpage.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/page-overview-irccd.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/page-overview-plugins.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/page-overview-rules.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/page-overview-servers.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/page-overview-transports.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/page-overview.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/modules.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/pages.cpp
 	${CMAKE_SOURCE_DIR}/libirccd
-	${CMAKE_SOURCE_DIR}/libirccd-core
+	${CMAKE_SOURCE_DIR}/libirccd-daemon
 	${CMAKE_SOURCE_DIR}/libirccd-ctl
 	${CMAKE_SOURCE_DIR}/libirccd-js
 	${CMAKE_SOURCE_DIR}/libirccd-test
--- a/doc/doxygen/mainpage.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/doc/doxygen/mainpage.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -12,13 +12,13 @@
  *
  * The irccd libraries are split and described as following:
  *
- * | Library       | Description                                     | Dependencies         |
- * |---------------|-------------------------------------------------|----------------------|
- * | libirccd-core | Common utilities                                | libjson              |
- * | libirccd-ctl  | Classes to connect to irccd instance            | libirccd             |
- * | libirccd-test | Tools to create unit tests                      | libirccd,libirccd-js |
- * | libirccd-js   | Libraries to create Javascript APIs and plugins | libirccd, libduktape |
- * | libirccd      | Everything related to irccd instance            | libirccd-core        |
+ * | Library         | Description                                     | Dependencies                 |
+ * |-----------------|-------------------------------------------------|------------------------------|
+ * | libirccd        | Common utilities                                | libjson                      |
+ * | libirccd-ctl    | Classes to connect to irccd instance            | libirccd-daemon              |
+ * | libirccd-test   | Tools to create unit tests                      | libirccd-daemon, libirccd-js |
+ * | libirccd-js     | Libraries to create Javascript APIs and plugins | libirccd-daemon, libduktape  |
+ * | libirccd-daemon | Everything related to irccd instance            | libirccd                     |
  *
  * There is also external libraries shipped with irccd:
  *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/doxygen/modules.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,135 @@
+/*
+ * modules.cpp -- doxygen modules page
+ *
+ * Copyright (c) 2013-2018 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.
+ */
+
+/**
+ * \defgroup daemon irccd::daemon
+ * \brief Module libirccd-daemon
+ */
+
+/**
+ * \defgroup daemon-transports transports
+ * \ingroup daemon
+ * \brief Servers, clients and commands.
+ */
+
+/**
+ * \defgroup daemon-servers servers
+ * \ingroup daemon
+ * \brief IRC server, events and functions.
+ */
+
+/**
+ * \defgroup daemon-plugins plugins
+ * \ingroup daemon
+ * \brief Plugin objects and functions.
+ */
+
+/**
+ * \defgroup daemon-loggers loggers
+ * \ingroup daemon
+ * \brief Log mechanism.
+ */
+
+/**
+ * \defgroup daemon-loggers-sinks sinks
+ * \ingroup daemon-loggers
+ * \brief Predefined logger sinks.
+ */
+
+/**
+ * \brief Specialized loggable traits.
+ * \defgroup daemon-loggers-traits traits
+ * \ingroup daemon-loggers
+ */
+
+
+/**
+ * \defgroup daemon-rules rules
+ * \ingroup daemon
+ * \brief Rule objects.
+ */
+
+/**
+ * \defgroup daemon-utilites utilities
+ * \ingroup daemon
+ * \brief Utilities.
+ */
+
+/**
+ * \defgroup daemon-services services
+ * \ingroup daemon
+ * \brief Irccd services.
+ */
+
+/**
+ * \defgroup ctl irccd::ctl
+ * \brief Module libirccd-ctl
+ */
+
+/**
+ * \defgroup core irccd
+ * \brief Module libirccd
+ */
+
+/**
+ * \defgroup core-networking networking
+ * \ingroup core
+ * \brief Networking support.
+ *
+ * Each irccd instance is controllable via sockets using JSON messages.
+ *
+ * This mechanism is offered via the triplet stream/acceptor/connector. Irccd
+ * uses different acceptors to wait for clients to connect and then construct
+ * a stream of it. Once ready, streams are ready to receive and send messages.
+ *
+ * On the client side (e.g. irccdctl), a generic connector is created to connect
+ * to the irccd instance. Once ready, a stream is also created and ready to
+ * perform the same receive and send messages.
+ *
+ * By default, irccd provides predefined implementations for TCP/IP, local unix
+ * sockets and optionally TLS over those.
+ */
+
+/**
+ * \defgroup core-streams streams
+ * \ingroup core-networking
+ * \brief Generic I/O streams.
+ */
+
+/**
+ * \defgroup core-acceptors acceptors
+ * \ingroup core-networking
+ * \brief Generic I/O acceptors.
+ */
+
+/**
+ * \defgroup core-connectors connectors
+ * \ingroup core-networking
+ * \brief Generic I/O connectors.
+ */
+
+/**
+ * \defgroup js irccd::js
+ * \brief Javascript support.
+ */
+
+/**
+ * \defgroup js-api api
+ * \ingroup js
+ * \brief Javascript APIs.
+ */
--- a/doc/doxygen/page-overview-irccd.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-/**
- * \page overview-irccd Irccd overview
- * \brief Irccd architecture overview.
- *
- * The irccd daemon runs different services to deliver the bot functionalities.
- * The main loop is controlled with a `boost::asio::io_context` and completely
- * mono-threaded.
- *
- * ~~~
- *                      +--------------+
- *                      |              |
- *                      | rule_service |
- *                      |              |
- *                      +-------^------+
- *                              |
- *                              |
- *                              |
- * +----------------+       +---+---+       +-------------------+
- * |                |       |       |       |                   |
- * | plugin_service <-------+ irccd +-------> transport_service |
- * |                |       |       |       |                   |
- * +----------------+       +---+---+       +-------------------+
- *                              |
- *                              |
- *                              |
- *                      +-------v--------+
- *                      |                |
- *                      | server_service |
- *                      |                |
- *                      +----------------+
- * ~~~
- *
- * Look at the different subpages for more information.
- *
- * - \subpage overview-plugins
- * - \subpage overview-rules
- * - \subpage overview-servers
- * - \subpage overview-transports
- */
--- a/doc/doxygen/page-overview-plugins.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * \page overview-plugins Plugins
- * \brief Plugins overview
- *
- * The plugins are the essential part of irccd, they are called for each IRC
- * events for each IRC servers.
- *
- * Plugins can be written in Javascript or in C++.
- *
- * ~~~
- *                    uses                      uses
- * +----------------+       +----------------+         +---------------+
- * |                |       |                |    0..* |               |
- * | server_service +-------> plugin_service +--+------+ plugin_loader |
- * |                |       |                |  |      |               |
- * +----------------+       +----------------+  |      +-------+-------+
- *                                              |              |
- *                                              | invoke       | find or open
- *                                              |              |
- *                                              |          +---v----+
- *                                              |     0..* |        |
- *                                              +----------+ plugin |
- *                                                         |        |
- *                                                         +----^---+
- *                                                              |
- *                                                              | inherits
- *                                                              |
- *                                                        +-----+-----+
- *                                                        |           |
- *                                                        | js_plugin |
- *                                                        |           |
- *                                                        +-----------+
- * ~~~
- *
- * ## The plugin_service
- *
- * The plugin_service class opens, loads, reload or unload plugins. It also
- * invoke plugins for IRC events from server_service.
- *
- * It also uses plugin_loaders objects to find new plugins once requested or
- * opens them.
- *
- * ## The plugin_loader class
- *
- * This abstract class is responsible of searching and opening plugins. It has a
- * convenient predefined function that will search for standard path according
- * to a file extension.
- *
- * ## The plugin class
- *
- * The abstract plugin class is the user point of customization. It contains
- * various virtual functions to be redefined for IRC events the user is
- * interested in.
- *
- * ## The js_plugin class
- *
- * If built with Javascript support and linked against libirccd-js, one can use
- * js_plugin to load Javscript plugin files.
- *
- * This class will call global functions defined in the file. More information
- * in the official Javascript API.
- */
--- a/doc/doxygen/page-overview-rules.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/**
- * \page overview-rules Rules
- * \brief Rules overview
- *
- * The rules is the mechanism in irccd that accept/forbid plugin commands
- * invocations depending on user criterias. It's a kind of plugin firewall.
- *
- * It's usage is pretty simple.
- *
- * ~~~
- *  +----------------+   asks    +--------------+
- *  |                |           |              |
- *  | server_service +-----------> rule_service |
- *  |                |           |              |
- *  +----------------+           +-------+------+
- *                                       |
- *                                       |
- *                                       | 0..*
- *                                    +------+
- *                                    |      |
- *                                    | rule |
- *                                    |      |
- *                                    +------+
- * ~~~
- *
- * ## The rule_service class
- *
- * Owns a set of rule and provide functions to check if a rule will match
- * depending on the following criterias:
- *
- * - the server name
- * - the origin user
- * - the channel name
- * - the plugin name
- * - the event name
- *
- * Then, if the rule match, its action is considered (accept or drop).
- *
- * ## The rule class
- *
- * A simple data that contains all criterias.
- *
- * ## Notes
- *
- * You may wonder why it's server_service that uses rule_service. It's because
- * the server_service is the only one that knows all criterias, some IRC events
- * don't have those.
- *
- * This may change in the future.
- */
--- a/doc/doxygen/page-overview-servers.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-/**
- * \page overview-servers Servers
- * \brief Servers overview
- *
- * This page explains the overview related to IRC servers.
- *
- * ~~~
- * +----------------+    invoke    +----------------+
- * |                |              |                |
- * | server_service +------+-------> plugin_service |
- * |                |      |       |                |
- * +-------+--------+      |       +----------------+
- *         |               |
- *         |               | dispatches
- *         | 0..*          |
- *     +---+----+          |       +-------------------+
- *     |        |          |       |                   |
- *     | server |          +-------> transport_service |
- *     |        |                  |                   |
- *     +---+----+                  +-------------------+
- *         |
- *         |
- *         | 1
- *   +-----+------+
- *   |            |
- *   | connection |
- *   |            |
- *   +------------+
- * ~~~
- *
- * ## The server_service class
- *
- * This class is responsible of servers, it receives messages from them and then
- * invoke plugins and dispatches IRC events to all irccdctl clients connected.
- *
- * ## The server class
- *
- * The server class is higher level than connection. It stores all options, user
- * information, settings and messages queues for IRC.
- *
- * It also does authentication and has various IRC commands predefined.
- *
- * ## The connection class
- *
- * The connection class is the lowest part of the IRC connection, it only
- * receives and sends messages with appropriate parsing.
- */
--- a/doc/doxygen/page-overview-transports.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-/**
- * \page overview-transports Transports
- * \brief Transports overview
- *
- * The transports feature is dedicated into irccd to irccdctl dialogs. It
- * allows:
- *
- * - Requests from irccdctl,
- * - Events from irccd to all irccdctl,
- * - Different type of protocols (TCP/IP, local and TLS).
- *
- * It consists of different parts.
- *
- * ~~~
- * +-------------------+         +---------+
- * |                   |    0..* |         |
- * | transport_service +---------+ command |
- * |                   |         |         |
- * +---------+---------+         +----^----+
- *           |                        |
- *           |                        | executes
- *           | 0..*                   |
- * +---------+--------+          +----+-------------+
- * |                  | 1   0..* |                  |
- * | transport_server +----------+ transport_client |
- * |                  |          |                  |
- * +------------------+          +------------------+
- * ~~~
- *
- * ## The transport_service class
- *
- * This class owns several transport_server, it will wait for a new client in
- * each of those servers.
- *
- * It's also dedicated to broadcast messages to all connected transport_clients.
- *
- * ## The transport_server class
- *
- * This class has only one purpose, to accept a new client. It's abstract and
- * the underlying implementation is responsible of doing its own operation.
- *
- * ## The transport_client class
- *
- * This stateful class represent a direct connection to a irccdctl client.
- *
- * It does authentication if required and process input messages. It does not
- * call commands directly but dispatch that to the transport_service.
- *
- * ## The command class
- *
- * This abstract class defines an operation to perform.
- */
--- a/doc/doxygen/page-overview.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-/**
- * \page overview Overview
- * \brief Architecture overview.
- *
- * \subpage overview-irccd
- */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/doxygen/pages.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,280 @@
+
+/*
+ * pages.cpp -- doxygen related pages
+ *
+ * Copyright (c) 2013-2018 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.
+ */
+
+/**
+ * \page overview Overview
+ * \brief Architecture overview.
+ *
+ * \subpage overview-irccd
+ */
+
+/**
+ * \page overview-irccd Irccd overview
+ * \brief Irccd architecture overview.
+ *
+ * The irccd daemon runs different services to deliver the bot functionalities.
+ * The main loop is controlled with a `boost::asio::io_context` and completely
+ * mono-threaded.
+ *
+ * ~~~
+ *                      +--------------+
+ *                      |              |
+ *                      | rule_service |
+ *                      |              |
+ *                      +-------^------+
+ *                              |
+ *                              |
+ *                              |
+ * +----------------+       +---+---+       +-------------------+
+ * |                |       |       |       |                   |
+ * | plugin_service <-------+ irccd +-------> transport_service |
+ * |                |       |       |       |                   |
+ * +----------------+       +---+---+       +-------------------+
+ *                              |
+ *                              |
+ *                              |
+ *                      +-------v--------+
+ *                      |                |
+ *                      | server_service |
+ *                      |                |
+ *                      +----------------+
+ * ~~~
+ *
+ * Look at the different subpages for more information.
+ *
+ * - \subpage overview-plugins
+ * - \subpage overview-rules
+ * - \subpage overview-servers
+ * - \subpage overview-transports
+ */
+
+/**
+ * \page overview-plugins Plugins
+ * \brief Plugins overview
+ *
+ * The plugins are the essential part of irccd, they are called for each IRC
+ * events for each IRC servers.
+ *
+ * Plugins can be written in Javascript or in C++.
+ *
+ * ~~~
+ *                    uses                      uses
+ * +----------------+       +----------------+         +---------------+
+ * |                |       |                |    0..* |               |
+ * | server_service +-------> plugin_service +--+------+ plugin_loader |
+ * |                |       |                |  |      |               |
+ * +----------------+       +----------------+  |      +-------+-------+
+ *                                              |              |
+ *                                              | invoke       | find or open
+ *                                              |              |
+ *                                              |          +---v----+
+ *                                              |     0..* |        |
+ *                                              +----------+ plugin |
+ *                                                         |        |
+ *                                                         +----^---+
+ *                                                              |
+ *                                                              | inherits
+ *                                                              |
+ *                                                        +-----+-----+
+ *                                                        |           |
+ *                                                        | js_plugin |
+ *                                                        |           |
+ *                                                        +-----------+
+ * ~~~
+ *
+ * ## The plugin_service
+ *
+ * The plugin_service class opens, loads, reload or unload plugins. It also
+ * invoke plugins for IRC events from server_service.
+ *
+ * It also uses plugin_loaders objects to find new plugins once requested or
+ * opens them.
+ *
+ * ## The plugin_loader class
+ *
+ * This abstract class is responsible of searching and opening plugins. It has a
+ * convenient predefined function that will search for standard path according
+ * to a file extension.
+ *
+ * ## The plugin class
+ *
+ * The abstract plugin class is the user point of customization. It contains
+ * various virtual functions to be redefined for IRC events the user is
+ * interested in.
+ *
+ * ## The js_plugin class
+ *
+ * If built with Javascript support and linked against libirccd-js, one can use
+ * js_plugin to load Javscript plugin files.
+ *
+ * This class will call global functions defined in the file. More information
+ * in the official Javascript API.
+ */
+
+/**
+ * \page overview-rules Rules
+ * \brief Rules overview
+ *
+ * The rules is the mechanism in irccd that accept/forbid plugin commands
+ * invocations depending on user criterias. It's a kind of plugin firewall.
+ *
+ * It's usage is pretty simple.
+ *
+ * ~~~
+ *  +----------------+   asks    +--------------+
+ *  |                |           |              |
+ *  | server_service +-----------> rule_service |
+ *  |                |           |              |
+ *  +----------------+           +-------+------+
+ *                                       |
+ *                                       |
+ *                                       | 0..*
+ *                                    +------+
+ *                                    |      |
+ *                                    | rule |
+ *                                    |      |
+ *                                    +------+
+ * ~~~
+ *
+ * ## The rule_service class
+ *
+ * Owns a set of rule and provide functions to check if a rule will match
+ * depending on the following criterias:
+ *
+ * - the server name
+ * - the origin user
+ * - the channel name
+ * - the plugin name
+ * - the event name
+ *
+ * Then, if the rule match, its action is considered (accept or drop).
+ *
+ * ## The rule class
+ *
+ * A simple data that contains all criterias.
+ *
+ * ## Notes
+ *
+ * You may wonder why it's server_service that uses rule_service. It's because
+ * the server_service is the only one that knows all criterias, some IRC events
+ * don't have those.
+ *
+ * This may change in the future.
+ */
+
+/**
+ * \page overview-servers Servers
+ * \brief Servers overview
+ *
+ * This page explains the overview related to IRC servers.
+ *
+ * ~~~
+ * +----------------+    invoke    +----------------+
+ * |                |              |                |
+ * | server_service +------+-------> plugin_service |
+ * |                |      |       |                |
+ * +-------+--------+      |       +----------------+
+ *         |               |
+ *         |               | dispatches
+ *         | 0..*          |
+ *     +---+----+          |       +-------------------+
+ *     |        |          |       |                   |
+ *     | server |          +-------> transport_service |
+ *     |        |                  |                   |
+ *     +---+----+                  +-------------------+
+ *         |
+ *         |
+ *         | 1
+ *   +-----+------+
+ *   |            |
+ *   | connection |
+ *   |            |
+ *   +------------+
+ * ~~~
+ *
+ * ## The server_service class
+ *
+ * This class is responsible of servers, it receives messages from them and then
+ * invoke plugins and dispatches IRC events to all irccdctl clients connected.
+ *
+ * ## The server class
+ *
+ * The server class is higher level than connection. It stores all options, user
+ * information, settings and messages queues for IRC.
+ *
+ * It also does authentication and has various IRC commands predefined.
+ *
+ * ## The connection class
+ *
+ * The connection class is the lowest part of the IRC connection, it only
+ * receives and sends messages with appropriate parsing.
+ */
+
+/**
+ * \page overview-transports Transports
+ * \brief Transports overview
+ *
+ * The transports feature is dedicated into irccd to irccdctl dialogs. It
+ * allows:
+ *
+ * - Requests from irccdctl,
+ * - Events from irccd to all irccdctl,
+ * - Different type of protocols (TCP/IP, local and TLS).
+ *
+ * It consists of different parts.
+ *
+ * ~~~
+ * +-------------------+         +---------+
+ * |                   |    0..* |         |
+ * | transport_service +---------+ command |
+ * |                   |         |         |
+ * +---------+---------+         +----^----+
+ *           |                        |
+ *           |                        | executes
+ *           | 0..*                   |
+ * +---------+--------+          +----+-------------+
+ * |                  | 1   0..* |                  |
+ * | transport_server +----------+ transport_client |
+ * |                  |          |                  |
+ * +------------------+          +------------------+
+ * ~~~
+ *
+ * ## The transport_service class
+ *
+ * This class owns several transport_server, it will wait for a new client in
+ * each of those servers.
+ *
+ * It's also dedicated to broadcast messages to all connected transport_clients.
+ *
+ * ## The transport_server class
+ *
+ * This class has only one purpose, to accept a new client. It's abstract and
+ * the underlying implementation is responsible of doing its own operation.
+ *
+ * ## The transport_client class
+ *
+ * This stateful class represent a direct connection to a irccdctl client.
+ *
+ * It does authentication if required and process input messages. It does not
+ * call commands directly but dispatch that to the transport_service.
+ *
+ * ## The command class
+ *
+ * This abstract class defines an operation to perform.
+ */
--- a/irccd-test/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/irccd-test/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -27,7 +27,7 @@
 
 irccd_define_executable(
 	TARGET irccd-test
-	LIBRARIES libirccd libirccd-test ${LIBRARIES}
+	LIBRARIES libirccd-test ${LIBRARIES}
 	INCLUDES ${INCLUDES}
 	DESCRIPTION "Plugin tester"
 	SOURCES main.cpp
--- a/irccd-test/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/irccd-test/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -36,7 +36,7 @@
 #include <irccd/string_util.hpp>
 
 #include <irccd/daemon/dynlib_plugin.hpp>
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 #include <irccd/daemon/server_service.hpp>
 
@@ -50,15 +50,25 @@
 using boost::format;
 using boost::str;
 
-namespace irccd::test {
+using irccd::string_util::split;
 
-namespace su = string_util;
+using irccd::daemon::bot;
+using irccd::daemon::names_event;
+using irccd::daemon::plugin;
+using irccd::daemon::server;
+using irccd::daemon::whois_event;
+using irccd::daemon::dynlib_plugin_loader;
+
+using irccd::js::js_plugin_loader;
+using irccd::js::js_api;
+
+namespace irccd::test {
 
 namespace {
 
 boost::asio::io_service io;
 
-std::unique_ptr<irccd> daemon;
+std::unique_ptr<bot> daemon;
 std::shared_ptr<plugin> plugin;
 
 // {{{ function table
@@ -162,7 +172,7 @@
  */
 void on_command(const std::string& data)
 {
-	const auto args = su::split(data, " ", 4);
+	const auto args = split(data, " ", 4);
 
 	plugin->handle_command(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -181,7 +191,7 @@
  */
 void on_connect(const std::string& data)
 {
-	const auto args = su::split(data, " ");
+	const auto args = split(data, " ");
 
 	plugin->handle_connect(*daemon, {get_server(get_arg(args, 0))});
 }
@@ -195,7 +205,7 @@
  */
 void on_invite(const std::string& data)
 {
-	const auto args = su::split(data, " ");
+	const auto args = split(data, " ");
 
 	plugin->handle_invite(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -214,7 +224,7 @@
  */
 void on_join(const std::string& data)
 {
-	const auto args = su::split(data, " ");
+	const auto args = split(data, " ");
 
 	plugin->handle_join(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -232,7 +242,7 @@
  */
 void on_kick(const std::string& data)
 {
-	const auto args = su::split(data, " ", 5);
+	const auto args = split(data, " ", 5);
 
 	plugin->handle_kick(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -264,7 +274,7 @@
  */
 void on_me(const std::string& data)
 {
-	const auto args = su::split(data, " ", 4);
+	const auto args = split(data, " ", 4);
 
 	plugin->handle_me(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -283,7 +293,7 @@
  */
 void on_message(const std::string& data)
 {
-	const auto args = su::split(data, " ", 4);
+	const auto args = split(data, " ", 4);
 
 	plugin->handle_message(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -302,7 +312,7 @@
  */
 void on_mode(const std::string& data)
 {
-	const auto args = su::split(data, " ", 7);
+	const auto args = split(data, " ", 7);
 
 	plugin->handle_mode(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -324,7 +334,7 @@
  */
 void on_names(const std::string& data)
 {
-	const auto args = su::split(data, " ");
+	const auto args = split(data, " ");
 
 	names_event ev;
 
@@ -346,7 +356,7 @@
  */
 void on_nick(const std::string& data)
 {
-	const auto args = su::split(data, " ");
+	const auto args = split(data, " ");
 
 	plugin->handle_nick(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -364,7 +374,7 @@
  */
 void on_notice(const std::string& data)
 {
-	const auto args = su::split(data, " ", 4);
+	const auto args = split(data, " ", 4);
 
 	plugin->handle_notice(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -383,7 +393,7 @@
  */
 void on_part(const std::string& data)
 {
-	const auto args = su::split(data, " ", 4);
+	const auto args = split(data, " ", 4);
 
 	plugin->handle_part(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -414,7 +424,7 @@
  */
 void on_topic(const std::string& data)
 {
-	const auto args = su::split(data, " ", 4);
+	const auto args = split(data, " ", 4);
 
 	plugin->handle_topic(*daemon, {
 		get_server(get_arg(args, 0)),
@@ -445,7 +455,7 @@
  */
 void on_whois(const std::string& data)
 {
-	const auto args = su::split(data, " ");
+	const auto args = split(data, " ");
 
 	whois_event ev;
 
@@ -509,7 +519,7 @@
 auto complete(EditLine* el, int) -> unsigned char
 {
 	const auto* lf = el_line(el);
-	const auto args = su::split(std::string(lf->buffer, lf->cursor), " ");
+	const auto args = split(std::string(lf->buffer, lf->cursor), " ");
 
 	if (args.size() == 0U)
 		return CC_REFRESH;
@@ -631,13 +641,13 @@
 
 void load(int argc, char** argv)
 {
-	daemon = std::make_unique<irccd>(io);
+	daemon = std::make_unique<bot>(io);
 	daemon->plugins().add_loader(std::make_unique<dynlib_plugin_loader>());
 
 #if defined(IRCCD_HAVE_JS)
-	auto loader = std::make_unique<js::js_plugin_loader>(*daemon);
+	auto loader = std::make_unique<js_plugin_loader>(*daemon);
 
-	for (const auto& f : js::js_api::registry)
+	for (const auto& f : js_api::registry)
 		loader->get_modules().push_back(f());
 
 	daemon->plugins().add_loader(std::move(loader));
--- a/irccd/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/irccd/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -25,8 +25,8 @@
 irccd_define_executable(
 	TARGET irccd
 	EXPORT
-	DESCRIPTION "The main irccd daemon."
+	DESCRIPTION "The main irccd bot."
 	SOURCES CMakeLists.txt main.cpp
 	INCLUDES ${irccd_SOURCE_DIR}
-	LIBRARIES libirccd ${LIBRARIES}
+	LIBRARIES libirccd-daemon ${LIBRARIES}
 )
--- a/irccd/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/irccd/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 
 #include <irccd/daemon/command.hpp>
 #include <irccd/daemon/dynlib_plugin.hpp>
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/logger.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 #include <irccd/daemon/transport_service.hpp>
@@ -39,11 +39,11 @@
 #	include <irccd/js/js_plugin.hpp>
 #endif
 
-namespace irccd {
+namespace irccd::daemon {
 
 namespace {
 
-std::unique_ptr<irccd> instance;
+std::unique_ptr<bot> instance;
 
 // {{{ usage
 
@@ -163,16 +163,17 @@
 
 } // !namespace
 
-} // !irccd
+} // !irccd::daemon
 
 int main(int argc, char** argv)
 {
 	using namespace irccd;
+	using namespace irccd::daemon;
 
 	boost::asio::io_service service;
 	boost::asio::signal_set sigs(service, SIGINT, SIGTERM);
 
-	instance = std::make_unique<class irccd>(service);
+	instance = std::make_unique<bot>(service);
 
 	init(argc, argv);
 
--- a/irccdctl/cli.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/irccdctl/cli.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -33,6 +33,8 @@
 using irccd::json_util::deserializer;
 using irccd::json_util::pretty;
 
+using irccd::daemon::rule_error;
+
 namespace irccd::ctl {
 
 // {{{ helpers
--- a/irccdctl/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/irccdctl/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -42,6 +42,8 @@
 using boost::format;
 using boost::str;
 
+using irccd::daemon::transport_error;
+
 namespace irccd::ctl {
 
 namespace {
--- a/libirccd-core/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-project(libirccd-core)
-
-find_package(Boost 1.60 REQUIRED QUIET COMPONENTS filesystem system)
-
-set(
-	SOURCES
-	${libirccd-core_SOURCE_DIR}/irccd/acceptor.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/config.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/config.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/connector.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/fs_util.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/fs_util.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/ini.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/ini.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/ini_util.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/json_util.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/json_util.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/options.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/options.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/stream.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/string_util.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/string_util.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/system.cpp
-	${libirccd-core_SOURCE_DIR}/irccd/system.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/xdg.hpp
-)
-
-set(
-	LIBRARIES
-	${CMAKE_DL_LIBS}
-	libjson
-	Threads::Threads
-	Boost::filesystem
-	Boost::system
-)
-
-if (CMAKE_SYSTEM_NAME MATCHES Windows)
-	list(APPEND LIBRARIES mswsock shlwapi ws2_32)
-elseif (CMAKE_SYSTEM_NAME MATCHES "Linux")
-	#
-	# Disable epoll in boost until it get fixed:
-	# https://github.com/boostorg/asio/issues/150
-	#
-	list(APPEND FLAGS "BOOST_ASIO_DISABLE_EPOLL")
-elseif (APPLE)
-	list(APPEND LIBRARIES resolv)
-endif ()
-
-if (IRCCD_HAVE_SSL)
-	list(APPEND LIBRARIES OpenSSL::Crypto OpenSSL::SSL)
-endif ()
-
-irccd_define_library(
-	TARGET libirccd-core
-	EXPORT
-	FLAGS ${FLAGS}
-	HEADERS ${libirccd-core_SOURCE_DIR}/irccd/
-	SOURCES ${SOURCES}
-	LIBRARIES ${LIBRARIES}
-	PUBLIC_INCLUDES
-		$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}>
-		$<BUILD_INTERFACE:${libirccd-core_SOURCE_DIR}>
-		$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
-)
--- a/libirccd-core/irccd/acceptor.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,522 +0,0 @@
-/*
- * acceptor.hpp -- abstract stream acceptor interface
- *
- * Copyright (c) 2013-2018 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_ACCEPTOR_HPP
-#define IRCCD_ACCEPTOR_HPP
-
-/**
- * \file acceptor.hpp
- * \brief Abstract stream acceptor interface.
- */
-
-/**
- * \defgroup acceptors Generic I/O acceptors
- * \ingroup networking
- * \brief Generic I/O acceptors.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <cassert>
-#include <functional>
-#include <memory>
-#include <system_error>
-
-#include <boost/asio.hpp>
-#include <boost/filesystem/path.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-#	include <boost/asio/ssl.hpp>
-#endif
-
-#include "stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief Abstract stream acceptor interface.
- * \ingroup acceptors
- *
- * This class is used to wait a new client in an asynchronous manner. Derived
- * classes must implement a non-blocking accept function.
- */
-class acceptor {
-public:
-	/**
-	 * \brief Accept completion handler.
-	 */
-	using handler = std::function<void (std::error_code, std::shared_ptr<stream>)>;
-
-public:
-	/**
-	 * Default constructor.
-	 */
-	acceptor() = default;
-
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~acceptor() = default;
-
-	/**
-	 * Start asynchronous accept.
-	 *
-	 * Once the client is accepted, the original acceptor must be kept until it
-	 * is destroyed.
-	 *
-	 * \pre another accept operation must not be running
-	 * \pre handler != nullptr
-	 * \param handler the handler
-	 */
-	virtual void accept(handler handler) = 0;
-};
-
-// {{{ basic_socket_acceptor
-
-/**
- * \brief Convenient acceptor owner.
- * \ingroup acceptors
- */
-template <typename Acceptor>
-class basic_socket_acceptor : public acceptor {
-public:
-	/**
-	 * Underlying socket type.
-	 */
-	using socket_type = typename Acceptor::protocol_type::socket;
-
-private:
-#if !defined(NDEBUG)
-	bool is_accepting_{false};
-#endif
-
-protected:
-	/**
-	 * \brief The I/O context.
-	 */
-	boost::asio::io_context& service_;
-
-	/**
-	 * \brief The underlying acceptor.
-	 */
-	Acceptor acceptor_;
-
-public:
-	/**
-	 * Construct a basic_socket_acceptor.
-	 *
-	 * \param service the I/O context
-	 */
-	basic_socket_acceptor(boost::asio::io_context& service);
-
-	/**
-	 * Construct a basic_socket_acceptor with a already bound native
-	 * acceptor.
-	 *
-	 * \param service the I/O context
-	 * \param acceptor the acceptor
-	 */
-	basic_socket_acceptor(boost::asio::io_context& service, Acceptor acceptor) noexcept;
-
-	/**
-	 * Get the I/O context.
-	 *
-	 * \return the context
-	 */
-	auto get_service() const noexcept -> const boost::asio::io_context&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the context
-	 */
-	auto get_service() noexcept -> boost::asio::io_context&;
-
-	/**
-	 * Get the underlying acceptor.
-	 *
-	 * \return the acceptor
-	 */
-	auto get_acceptor() const noexcept -> const Acceptor&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the acceptor
-	 */
-	auto get_acceptor() noexcept -> Acceptor&;
-
-	/**
-	 * Accept a new client.
-	 *
-	 * \pre another accept call must not be running
-	 * \param sc the socket type
-	 * \param handler the handler
-	 * \note implemented for SocketAcceptor concept
-	 */
-	template <typename Socket, typename Handler>
-	void accept(Socket& sc, Handler handler);
-};
-
-template <typename Acceptor>
-inline basic_socket_acceptor<Acceptor>::basic_socket_acceptor(boost::asio::io_context& service)
-	: service_(service)
-	, acceptor_(service)
-{
-}
-
-template <typename Acceptor>
-inline basic_socket_acceptor<Acceptor>::basic_socket_acceptor(boost::asio::io_context& service, Acceptor acceptor) noexcept
-	: service_(service)
-	, acceptor_(std::move(acceptor))
-{
-}
-
-template <typename Acceptor>
-inline auto basic_socket_acceptor<Acceptor>::get_service() const noexcept -> const boost::asio::io_context&
-{
-	return service_;
-}
-
-template <typename Acceptor>
-inline auto basic_socket_acceptor<Acceptor>::get_service() noexcept -> boost::asio::io_context&
-{
-	return service_;
-}
-
-template <typename Acceptor>
-inline auto basic_socket_acceptor<Acceptor>::get_acceptor() const noexcept -> const Acceptor&
-{
-	return acceptor_;
-}
-
-template <typename Acceptor>
-inline auto basic_socket_acceptor<Acceptor>::get_acceptor() noexcept -> Acceptor&
-{
-	return acceptor_;
-}
-
-template <typename Acceptor>
-template <typename Socket, typename Handler>
-inline void basic_socket_acceptor<Acceptor>::accept(Socket& sc, Handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_accepting_);
-
-	is_accepting_ = true;
-#endif
-
-	assert(acceptor_.is_open());
-
-	acceptor_.async_accept(sc, [this, handler] (auto code) {
-#if !defined(NDEBUG)
-		is_accepting_ = false;
-#endif
-		(void)this;
-		handler(std::move(code));
-	});
-}
-
-// }}}
-
-// {{{ ip_acceptor
-
-/**
- * \brief TCP/IP acceptor.
- * \ingroup acceptors
- */
-class ip_acceptor : public basic_socket_acceptor<boost::asio::ip::tcp::acceptor> {
-private:
-	void open(bool ipv4, bool ipv6);
-	void set(bool ipv4, bool ipv6);
-	void bind(const std::string& address, std::uint16_t port, bool ipv4, bool ipv6);
-
-public:
-	/**
-	 * Construct a TCP/IP acceptor.
-	 *
-	 * If both ipv4 and ipv6 are set, the acceptor will listen on the two
-	 * protocols.
-	 *
-	 * To listen to any address, you can use "*" as address argument.
-	 *
-	 * \pre at least ipv4 or ipv6 must be true
-	 * \param service the I/O service
-	 * \param address the address to bind or * for any
-	 * \param port the port number
-	 * \param ipv4 enable ipv4
-	 * \param ipv6 enable ipv6
-	 */
-	ip_acceptor(boost::asio::io_context& service,
-	            std::string address,
-	            std::uint16_t port,
-	            bool ipv4 = true,
-	            bool ipv6 = true);
-
-	/**
-	 * Inherited constructors.
-	 */
-	using basic_socket_acceptor::basic_socket_acceptor;
-
-	/**
-	 * Inherited functions.
-	 */
-	using basic_socket_acceptor::accept;
-
-	/**
-	 * \copydoc acceptor::accept
-	 */
-	void accept(handler handler) override;
-};
-
-inline void ip_acceptor::open(bool ipv4, bool ipv6)
-{
-	using boost::asio::ip::tcp;
-
-	if (ipv6)
-		acceptor_.open(tcp::v6());
-	else
-		acceptor_.open(tcp::v4());
-}
-
-inline void ip_acceptor::set(bool ipv4, bool ipv6)
-{
-	using boost::asio::socket_base;
-	using boost::asio::ip::v6_only;
-
-	if (ipv6)
-		acceptor_.set_option(v6_only(!ipv4));
-
-	acceptor_.set_option(socket_base::reuse_address(true));
-}
-
-inline void ip_acceptor::bind(const std::string& address, std::uint16_t port, bool ipv4, bool ipv6)
-{
-	using boost::asio::ip::make_address_v4;
-	using boost::asio::ip::make_address_v6;
-	using boost::asio::ip::tcp;
-
-	tcp::endpoint ep;
-
-	if (address == "*")
-		ep = tcp::endpoint(ipv6 ? tcp::v6() : tcp::v4(), port);
-	else if (ipv6)
-		ep = tcp::endpoint(make_address_v6(address), port);
-	else
-		ep = tcp::endpoint(make_address_v4(address), port);
-
-	acceptor_.bind(ep);
-	acceptor_.listen();
-}
-
-inline ip_acceptor::ip_acceptor(boost::asio::io_context& service,
-                                std::string address,
-                                std::uint16_t port,
-                                bool ipv4,
-                                bool ipv6)
-	: basic_socket_acceptor(service)
-{
-	open(ipv4, ipv6);
-	set(ipv4, ipv6);
-	bind(address, port, ipv4, ipv6);
-}
-
-inline void ip_acceptor::accept(handler handler)
-{
-	auto stream = std::make_shared<ip_stream>(service_);
-
-	basic_socket_acceptor::accept(stream->get_socket(), [handler, stream] (auto code) {
-		if (code)
-			handler(std::move(code), nullptr);
-		else
-			handler(std::move(code), std::move(stream));
-	});
-}
-
-// }}}
-
-// {{{ local_acceptor
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-
-/**
- * \brief Local acceptor.
- * \ingroup acceptors
- * \note Only available if BOOST_ASIO_HAS_LOCAL_SOCKETS is defined
- */
-class local_acceptor : public basic_socket_acceptor<boost::asio::local::stream_protocol::acceptor> {
-public:
-	/**
-	 * Construct a local acceptor.
-	 *
-	 * \param service the I/O service
-	 * \param path the unix socket file
-	 */
-	local_acceptor(boost::asio::io_context& service,
-	               const boost::filesystem::path& path);
-
-	/**
-	 * Inherited constructors.
-	 */
-	using basic_socket_acceptor::basic_socket_acceptor;
-
-	/**
-	 * Inherited functions.
-	 */
-	using basic_socket_acceptor::accept;
-
-	/**
-	 * \copydoc acceptor::accept
-	 */
-	void accept(handler handler) override;
-};
-
-inline local_acceptor::local_acceptor(boost::asio::io_context& service,
-                                      const boost::filesystem::path& path)
-	: basic_socket_acceptor(service)
-{
-	using boost::asio::socket_base;
-
-	std::remove(path.string().c_str());
-
-	acceptor_.open();
-	acceptor_.set_option(socket_base::reuse_address(true));
-	acceptor_.bind({ path.string() });
-	acceptor_.listen();
-}
-
-inline void local_acceptor::accept(handler handler)
-{
-	auto stream = std::make_shared<local_stream>(service_);
-
-	basic_socket_acceptor::accept(stream->get_socket(), [handler, stream] (auto code) {
-		if (code)
-			handler(std::move(code), nullptr);
-		else
-			handler(std::move(code), std::move(stream));
-	});
-}
-
-#endif
-
-// }}}
-
-// {{{ tls_acceptor
-
-#if defined(IRCCD_HAVE_SSL)
-
-/**
- * \brief TLS/SSL acceptors.
- * \ingroup acceptors
- * \tparam SocketAcceptor the socket connector (e.g. ip_acceptor)
- *
- * Wrap a SocketAcceptor object.
- *
- * The SocketAcceptor object must have the following types:
- *
- * ```cpp
- * using socket_type = implementation-defined
- * ```
- *
- * The following function:
- *
- * ```cpp
- * template <typename Handler>
- * void accept(socket_type& sc, Handler handler);
- *
- * auto get_context() -> boost::asio::io_context&
- * ```
- *
- * The Handler callback must have the signature
- * `void f(const std::error_code&)`.
- */
-template <typename SocketAcceptor>
-class tls_acceptor : public acceptor {
-private:
-	using socket_type = typename SocketAcceptor::socket_type;
-
-	std::shared_ptr<boost::asio::ssl::context> context_;
-	SocketAcceptor acceptor_;
-
-public:
-	/**
-	 * Construct a secure layer transport server.
-	 *
-	 * \param context the SSL context
-	 * \param args the arguments to SocketAcceptor constructor
-	 */
-	template <typename... Args>
-	tls_acceptor(boost::asio::ssl::context context, Args&&... args);
-
-	/**
-	 * \copydoc acceptor::accept
-	 */
-	void accept(handler handler) override;
-};
-
-template <typename SocketAcceptor>
-template <typename... Args>
-inline tls_acceptor<SocketAcceptor>::tls_acceptor(boost::asio::ssl::context context, Args&&... args)
-	: context_(std::make_shared<boost::asio::ssl::context>(std::move(context)))
-	, acceptor_(std::forward<Args>(args)...)
-{
-}
-
-template <typename SocketAcceptor>
-inline void tls_acceptor<SocketAcceptor>::accept(handler handler)
-{
-	auto client = std::make_shared<tls_stream<socket_type>>(acceptor_.get_service(), context_);
-
-	acceptor_.accept(client->get_socket().lowest_layer(), [handler, client] (auto code) {
-		using boost::asio::ssl::stream_base;
-
-		if (code) {
-			handler(std::move(code), nullptr);
-			return;
-		}
-
-		client->get_socket().async_handshake(stream_base::server, [handler, client] (auto code) {
-			if (code)
-				handler(std::move(code), nullptr);
-			else
-				handler(std::move(code), std::move(client));
-		});
-	});
-}
-
-/**
- * \brief Convenient alias.
- */
-using tls_ip_acceptor = tls_acceptor<ip_acceptor>;
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-
-/**
- * \brief Convenient alias.
- */
-using tls_local_acceptor = tls_acceptor<local_acceptor>;
-
-#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
-
-#endif // !IRCCD_HAVE_SSL
-
-// }}}
-
-} // !irccd
-
-#endif // !IRCCD_ACCEPTOR_HPP
--- a/libirccd-core/irccd/config.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/*
- * config.cpp -- irccd configuration loader
- *
- * Copyright (c) 2013-2018 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 <boost/filesystem.hpp>
-
-#include "config.hpp"
-#include "system.hpp"
-
-namespace irccd {
-
-auto config::search(std::string_view name) -> std::optional<config>
-{
-	for (const auto& path : sys::config_filenames(name)) {
-		boost::system::error_code ec;
-
-		if (boost::filesystem::exists(path, ec) && !ec)
-			return config(path);
-	}
-
-	return std::nullopt;
-}
-
-config::config(std::string path)
-	: document(path.empty() ? ini::document() : ini::read_file(path))
-	, path_(std::move(path))
-{
-}
-
-auto config::get_path() const noexcept -> const std::string&
-{
-	return path_;
-}
-
-} // !irccd
--- a/libirccd-core/irccd/config.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/*
- * config.hpp -- irccd configuration loader
- *
- * Copyright (c) 2013-2018 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_CONFIG_HPP
-#define IRCCD_CONFIG_HPP
-
-/**
- * \file config.hpp
- * \brief Read .ini configuration file for irccd
- */
-
-#include <optional>
-#include <string_view>
-
-#include "ini.hpp"
-
-namespace irccd {
-
-/**
- * \brief Read .ini configuration file for irccd
- */
-class config : public ini::document {
-private:
-	std::string path_;
-
-public:
-	/**
-	 * Search the configuration file into the standard defined paths.
-	 *
-	 * \param name the file name
-	 * \return the config or empty if not found
-	 */
-	static auto search(std::string_view name) -> std::optional<config>;
-
-	/**
-	 * Load the configuration from the specified path.
-	 *
-	 * \param path the path
-	 */
-	config(std::string path = "");
-
-	/**
-	 * Get the path to the configuration file.
-	 *
-	 * \return the path
-	 */
-	auto get_path() const noexcept -> const std::string&;
-};
-
-} // !irccd
-
-#endif // !IRCCD_CONFIG_HPP
--- a/libirccd-core/irccd/connector.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,452 +0,0 @@
-/*
- * connector.hpp -- abstract connection interface
- *
- * Copyright (c) 2013-2018 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_CONNECTOR_HPP
-#define IRCCD_CONNECTOR_HPP
-
-/**
- * \file connector.hpp
- * \brief Abstract connection interface.
- */
-
-/**
- * \defgroup connectors Generic I/O connectors
- * \ingroup networking
- * \brief Generic I/O connectors.
- */
-
-#include <cassert>
-#include <functional>
-#include <memory>
-#include <system_error>
-
-#include <boost/asio.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-#	include <boost/asio/ssl.hpp>
-#endif
-
-#include <boost/filesystem/path.hpp>
-
-#include "stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief Abstract connection interface.
- * \ingroup connectors
- *
- * This class is used to connect to a stream end point (usually sockets) in an
- * asynchronous manner.
- *
- * Derived class must implement non-blocking connect function.
- */
-class connector {
-public:
-	/**
-	 * \brief Connect completion handler.
-	 */
-	using handler = std::function<void (std::error_code, std::shared_ptr<stream>)>;
-
-	/**
-	 * Default constructor.
-	 */
-	connector() = default;
-
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~connector() = default;
-
-	/**
-	 * Start asynchronous connect.
-	 *
-	 * Once the client is connected, the original acceptor must be kept until it
-	 * is destroyed.
-	 *
-	 * \pre another connect operation must not be running
-	 * \pre handler != nullptr
-	 * \param handler the handler
-	 */
-	virtual void connect(handler handler) = 0;
-};
-
-// {{{ socket_connector_base
-
-/**
- * \brief Provide convenient functions for connectors.
- * \ingroup connectors
- */
-class socket_connector_base : public connector {
-protected:
-	/**
-	 * \brief The I/O service.
-	 */
-	boost::asio::io_context& service_;
-
-public:
-	/**
-	 * Construct the connector
-	 *
-	 * \param service the service
-	 */
-	socket_connector_base(boost::asio::io_context& service);
-
-	/**
-	 * Get the I/O service.
-	 *
-	 * \return the service
-	 */
-	auto get_service() const noexcept -> const boost::asio::io_context&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the service
-	 */
-	auto get_service() noexcept -> boost::asio::io_context&;
-};
-
-inline socket_connector_base::socket_connector_base(boost::asio::io_context& service)
-	: service_(service)
-{
-}
-
-inline auto socket_connector_base::get_service() const noexcept -> const boost::asio::io_context&
-{
-	return service_;
-}
-
-inline auto socket_connector_base::get_service() noexcept -> boost::asio::io_context&
-{
-	return service_;
-}
-
-// }}}
-
-// {{{ ip_connector
-
-/**
- * \brief TCP/IP connector.
- * \ingroup connectors
- */
-class ip_connector : public socket_connector_base {
-public:
-	/**
-	 * Underlying socket type.
-	 */
-	using socket_type = boost::asio::ip::tcp::socket;
-
-private:
-	boost::asio::ip::tcp::resolver resolver_;
-
-	std::string hostname_;
-	std::string port_;
-
-	bool ipv4_;
-	bool ipv6_;
-
-#if !defined(NDEBUG)
-	bool is_connecting_{false};
-#endif
-
-	template <typename Handler>
-	void resolve(Handler handler);
-
-public:
-	/**
-	 * Construct the TCP/IP connector.
-	 *
-	 * \pre at least ipv4 or ipv6 must be true
-	 * \param service the I/O context
-	 * \param hostname the hostname
-	 * \param port the port or service name
-	 * \param ipv4 enable IPv4
-	 * \param ipv6 enable IPv6
-	 */
-	ip_connector(boost::asio::io_context& service,
-	             std::string hostname,
-	             std::string port,
-	             bool ipv4 = true,
-	             bool ipv6 = true) noexcept;
-
-	/**
-	 * Connect to the given socket.
-	 *
-	 * \param sc the socket type
-	 * \param handler the handler
-	 * \note implemented for SocketConnector concept
-	 */
-	template <typename Socket, typename Handler>
-	void connect(Socket& sc, Handler handler);
-
-	/**
-	 * \copydoc connector::connect
-	 */
-	void connect(handler handler);
-};
-
-template <typename Handler>
-inline void ip_connector::resolve(Handler handler)
-{
-	using boost::asio::ip::tcp;
-
-	if (ipv6_ && ipv4_)
-		resolver_.async_resolve(hostname_, port_, handler);
-	else if (ipv6_)
-		resolver_.async_resolve(tcp::v6(), hostname_, port_, handler);
-	else
-		resolver_.async_resolve(tcp::v4(), hostname_, port_, handler);
-}
-
-inline ip_connector::ip_connector(boost::asio::io_context& service,
-                                  std::string hostname,
-                                  std::string port,
-                                  bool ipv4,
-                                  bool ipv6) noexcept
-	: socket_connector_base(service)
-	, resolver_(service)
-	, hostname_(std::move(hostname))
-	, port_(std::move(port))
-	, ipv4_(ipv4)
-	, ipv6_(ipv6)
-{
-	assert(!hostname_.empty());
-	assert(!port_.empty());
-	assert(ipv4 || ipv6);
-}
-
-template <typename Socket, typename Handler>
-inline void ip_connector::connect(Socket& sc, Handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_connecting_);
-
-	is_connecting_ = true;
-#endif
-
-	resolve([this, &sc, handler] (auto code, auto res) {
-#if !defined(NDEBUG)
-		is_connecting_ = false;
-#endif
-		(void)this;
-
-		if (code) {
-			handler(std::move(code));
-			return;
-		}
-
-		async_connect(sc, res, [handler] (auto code, auto) {
-			handler(std::move(code));
-		});
-	});
-}
-
-inline void ip_connector::connect(handler handler)
-{
-	auto stream = std::make_shared<ip_stream>(service_);
-
-	connect(stream->get_socket(), [handler, stream] (auto code) {
-		if (code)
-			handler(std::move(code), nullptr);
-		else
-			handler(std::move(code), std::move(stream));
-	});
-}
-
-// }}}
-
-// {{{ local_connector
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-
-/**
- * \brief Unix domain connector.
- * \ingroup connectors
- */
-class local_connector : public socket_connector_base {
-public:
-	/**
-	 * Underlying socket type.
-	 */
-	using socket_type = boost::asio::local::stream_protocol::socket;
-
-private:
-	boost::filesystem::path path_;
-
-#if !defined(NDEBUG)
-	bool is_connecting_{false};
-#endif
-
-public:
-	/**
-	 * Construct a local connector.
-	 *
-	 * \param service the service
-	 * \param path the path to the file
-	 */
-	local_connector(boost::asio::io_context& service,
-	                boost::filesystem::path path) noexcept;
-
-	/**
-	 * Connect to the given socket.
-	 *
-	 * \param sc the socket type
-	 * \param handler the handler
-	 * \note implemented for SocketConnector concept
-	 */
-	template <typename Socket, typename Handler>
-	void connect(Socket& sc, Handler handler) noexcept;
-
-	/**
-	 * \copydoc connector::connect
-	 */
-	void connect(handler handler);
-};
-
-inline local_connector::local_connector(boost::asio::io_context& service,
-                                        boost::filesystem::path path) noexcept
-	: socket_connector_base(service)
-	, path_(std::move(path))
-{
-}
-
-template <typename Socket, typename Handler>
-inline void local_connector::connect(Socket& sc, Handler handler) noexcept
-{
-#if !defined(NDEBUG)
-	assert(!is_connecting_);
-
-	is_connecting_ = true;
-#endif
-
-	sc.async_connect({ path_.string() }, [this, handler] (auto code) {
-#if !defined(NDEBUG)
-		is_connecting_ = false;
-#endif
-		(void)this;
-		handler(std::move(code));
-	});
-}
-
-inline void local_connector::connect(handler handler)
-{
-	auto stream = std::make_shared<local_stream>(service_);
-
-	connect(stream->get_socket(), [handler, stream] (auto code) {
-		if (code)
-			handler(std::move(code), nullptr);
-		else
-			handler(std::move(code), std::move(stream));
-	});
-}
-
-#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
-
-// }}}
-
-// {{{ tls_connector
-
-#if defined(IRCCD_HAVE_SSL)
-
-/**
- * \brief TLS/SSL connectors.
- * \ingroup connectors
- * \tparam SocketConnector the socket connector (e.g. ip_connector)
- */
-template <typename SocketConnector>
-class tls_connector : public connector {
-public:
-	/**
-	 * \brief the underlying socket type.
-	 */
-	using socket_type = typename SocketConnector::socket_type;
-
-private:
-	std::shared_ptr<boost::asio::ssl::context> context_;
-	SocketConnector connector_;
-
-public:
-	/**
-	 * Construct a secure layer transport server.
-	 *
-	 * \param context the SSL context
-	 * \param args the arguments to SocketConnector constructor
-	 */
-	template <typename... Args>
-	tls_connector(boost::asio::ssl::context context, Args&&... args);
-
-	/**
-	 * \copydoc connector::connect
-	 */
-	void connect(handler handler) override;
-};
-
-template <typename SocketConnector>
-template <typename... Args>
-inline tls_connector<SocketConnector>::tls_connector(boost::asio::ssl::context context, Args&&... args)
-	: context_(std::make_shared<boost::asio::ssl::context>(std::move(context)))
-	, connector_(std::forward<Args>(args)...)
-{
-}
-
-template <typename SocketConnector>
-inline void tls_connector<SocketConnector>::connect(handler handler)
-{
-	using boost::asio::ssl::stream_base;
-
-	assert(handler);
-
-	auto stream = std::make_shared<tls_stream<socket_type>>(connector_.get_service(), context_);
-
-	connector_.connect(stream->get_socket().lowest_layer(), [handler, stream] (auto code) {
-		if (code) {
-			handler(code, nullptr);
-			return;
-		}
-
-		stream->get_socket().async_handshake(stream_base::client, [handler, stream] (auto code) {
-			if (code)
-				handler(std::move(code), nullptr);
-			else
-				handler(std::move(code), std::move(stream));
-		});
-	});
-}
-
-/**
- * \brief Convenient alias.
- */
-using tls_ip_connector = tls_connector<ip_connector>;
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-
-/**
- * \brief Convenient alias.
- */
-using tls_local_connector = tls_connector<local_connector>;
-
-#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
-
-#endif // !IRCCD_HAVE_SSL
-
-// }}}
-
-} // !irccd
-
-#endif // !IRCCD_CONNECTOR_HPP
--- a/libirccd-core/irccd/fs_util.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/*
- * fs_util.cpp -- filesystem utilities
- *
- * Copyright (c) 2013-2018 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 "fs_util.hpp"
-
-namespace irccd::fs_util {
-
-// {{{ base_name
-
-auto base_name(const std::string& path) -> std::string
-{
-	return boost::filesystem::path(path).filename().string();
-}
-
-// }}}
-
-// {{{ dir_name
-
-auto dir_name(const std::string& path) -> std::string
-{
-	return boost::filesystem::path(path).parent_path().string();
-}
-
-// }}}
-
-// {{{ find
-
-auto find(const std::string& base, const std::string& name, bool recursive) -> std::string
-{
-	return find_if(base, recursive, [&] (const auto& entry) {
-		return entry.path().filename().string() == name;
-	});
-}
-
-auto find(const std::string& base, const std::regex& regex, bool recursive) -> std::string
-{
-	return find_if(base, recursive, [&] (const auto& entry) {
-		return std::regex_match(entry.path().filename().string(), regex);
-	});
-}
-
-// }}}
-
-} // !irccd::fs_util
--- a/libirccd-core/irccd/fs_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/*
- * fs_util.hpp -- filesystem utilities
- *
- * Copyright (c) 2013-2018 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_FS_UTIL_HPP
-#define IRCCD_FS_UTIL_HPP
-
-/**
- * \file fs_util.hpp
- * \brief Filesystem utilities.
- */
-
-#include <regex>
-#include <string>
-
-#include <boost/filesystem.hpp>
-
-/**
- * \brief Filesystem utilities.
- */
-namespace irccd::fs_util {
-
-// {{{ base_name
-
-/**
- * Get the base name from a path.
- *
- * Example, base_name("/etc/foo.conf") returns foo.conf
- *
- * \param path the path
- * \return the base name
- */
-auto base_name(const std::string& path) -> std::string;
-
-// }}}
-
-// {{{ dir_name
-
-/**
- * Get the parent directory from a path.
- *
- * Example, dir_name("/etc/foo.conf") returns /etc
- *
- * \param path the path
- * \return the parent directory
- */
-auto dir_name(const std::string& path) -> std::string;
-
-// }}}
-
-// {{{ find_if
-
-/**
- * Search an item recursively.
- *
- * The predicate must have the following signature:
- *  void f(const boost::filesystem::directory_entry& entry)
- *
- * Where:
- *   - base is the current parent directory in the tree
- *   - entry is the current entry
- *
- * \param base the base directory
- * \param predicate the predicate
- * \param recursive true to do recursive search
- * \return the full path name to the file or empty string if never found
- * \throw boost::system::system_error on errors
- */
-template <typename Predicate>
-auto find_if(const std::string& base, bool recursive, Predicate&& predicate) -> std::string
-{
-	const auto find = [&] (auto it) -> std::string {
-		for (const auto& entry : it)
-			if (predicate(entry))
-				return entry.path().string();
-
-		return "";
-	};
-
-	return recursive
-		? find(boost::filesystem::recursive_directory_iterator(base))
-		: find(boost::filesystem::directory_iterator(base));
-}
-
-// }}}
-
-// {{{ find
-
-/**
- * Find a file by name recursively.
- *
- * \param base the base directory
- * \param name the file name
- * \param recursive true to do recursive search
- * \return the full path name to the file or empty string if never found
- * \throw boost::system::system_error on errors
- */
-auto find(const std::string& base, const std::string& name, bool recursive = false) -> std::string;
-
-/**
- * Overload by regular expression.
- *
- * \param base the base directory
- * \param regex the regular expression
- * \param recursive true to do recursive search
- * \return the full path name to the file or empty string if never found
- * \throw boost::system::system_error on errors
- */
-auto find(const std::string& base, const std::regex& regex, bool recursive = false) -> std::string;
-
-// }}}
-
-} // !irccd::fs_util
-
-#endif // !IRCCD_FS_UTIL_HPP
--- a/libirccd-core/irccd/ini.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,638 +0,0 @@
-/*
- * ini.cpp -- extended .ini file parser
- *
- * Copyright (c) 2013-2018 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 <cassert>
-#include <cctype>
-#include <cstring>
-#include <iostream>
-#include <iterator>
-#include <fstream>
-#include <sstream>
-#include <stdexcept>
-
-// for PathIsRelative.
-#if defined(_WIN32)
-#  include <Shlwapi.h>
-#endif
-
-#include "ini.hpp"
-
-using namespace std::string_literals;
-
-namespace irccd::ini {
-
-namespace {
-
-using stream_iterator = std::istreambuf_iterator<char>;
-using token_iterator = std::vector<token>::const_iterator;
-
-auto is_absolute(const std::string& path) noexcept -> bool
-{
-#if defined(_WIN32)
-	return !PathIsRelative(path.c_str());
-#else
-	return path.size() > 0 && path[0] == '/';
-#endif
-}
-
-auto is_quote(char c) noexcept -> bool
-{
-	return c == '\'' || c == '"';
-}
-
-auto is_space(char c) noexcept -> bool
-{
-	// Custom version because std::isspace includes \n as space.
-	return c == ' ' || c == '\t';
-}
-
-auto is_list(char c) noexcept -> bool
-{
-	return c == '(' || c == ')' || c == ',';
-}
-
-auto is_reserved(char c) noexcept -> bool
-{
-	return is_list(c) || is_quote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '=';
-}
-
-void analyse_line(unsigned& line, unsigned& column, stream_iterator& it) noexcept
-{
-	assert(*it == '\n');
-
-	++ line;
-	++ it;
-	column = 0;
-}
-
-void analyse_comment(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
-{
-	assert(*it == '#');
-
-	while (it != end && *it != '\n') {
-		++ column;
-		++ it;
-	}
-}
-
-void analyse_spaces(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
-{
-	assert(is_space(*it));
-
-	while (it != end && is_space(*it)) {
-		++ column;
-		++ it;
-	}
-}
-
-void analyse_list(tokens& list, unsigned line, unsigned& column, stream_iterator& it) noexcept
-{
-	assert(is_list(*it));
-
-	switch (*it++) {
-	case '(':
-		list.emplace_back(token::list_begin, line, column++);
-		break;
-	case ')':
-		list.emplace_back(token::list_end, line, column++);
-		break;
-	case ',':
-		list.emplace_back(token::comma, line, column++);
-		break;
-	default:
-		break;
-	}
-}
-
-void analyse_section(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
-{
-	assert(*it == '[');
-
-	std::string value;
-	unsigned save = column;
-
-	// Read section name.
-	++ it;
-	while (it != end && *it != ']') {
-		if (*it == '\n')
-			throw exception(line, column, "section not terminated, missing ']'");
-		if (is_reserved(*it))
-			throw exception(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'");
-
-		++ column;
-		value += *it++;
-	}
-
-	if (it == end)
-		throw exception(line, column, "section name expected after '[', got <EOF>");
-	if (value.empty())
-		throw exception(line, column, "empty section name");
-
-	// Remove ']'.
-	++ it;
-
-	list.emplace_back(token::section, line, save, std::move(value));
-}
-
-void analyse_assign(tokens& list, unsigned& line, unsigned& column, stream_iterator& it)
-{
-	assert(*it == '=');
-
-	list.push_back({ token::assign, line, column++ });
-	++ it;
-}
-
-void analyse_quoted_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
-{
-	std::string value;
-	unsigned save = column;
-	char quote = *it++;
-
-	while (it != end && *it != quote) {
-		// TODO: escape sequence
-		++ column;
-		value += *it++;
-	}
-
-	if (it == end)
-		throw exception(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>");
-
-	// Remove quote.
-	++ it;
-
-	list.push_back({ token::quoted_word, line, save, std::move(value) });
-}
-
-void analyse_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
-{
-	assert(!is_reserved(*it));
-
-	std::string value;
-	unsigned save = column;
-
-	while (it != end && !std::isspace(*it) && !is_reserved(*it)) {
-		++ column;
-		value += *it++;
-	}
-
-	list.push_back({ token::word, line, save, std::move(value) });
-}
-
-void analyse_include(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
-{
-	assert(*it == '@');
-
-	std::string include;
-	unsigned save = column;
-
-	// Read include.
-	++ it;
-	while (it != end && !is_space(*it)) {
-		++ column;
-		include += *it++;
-	}
-
-	if (include == "include")
-		list.push_back({ token::include, line, save });
-	else if (include == "tryinclude")
-		list.push_back({ token::tryinclude, line, save });
-	else
-		throw exception(line, column, "expected include or tryinclude after '@' token");
-}
-
-void parse_option_value_simple(option& option, token_iterator& it)
-{
-	assert(it->get_type() == token::word || it->get_type() == token::quoted_word);
-
-	option.push_back((it++)->get_value());
-}
-
-void parse_option_value_list(option& option, token_iterator& it, token_iterator end)
-{
-	assert(it->get_type() == token::list_begin);
-
-	token_iterator save = it++;
-
-	while (it != end && it->get_type() != token::list_end) {
-		switch (it->get_type()) {
-		case token::comma:
-			// Previous must be a word.
-			if (it[-1].get_type() != token::word && it[-1].get_type() != token::quoted_word)
-				throw exception(it->get_line(), it->get_column(),
-				                "unexpected comma after '"s + it[-1].get_value() + "'");
-
-			++ it;
-			break;
-		case token::word:
-		case token::quoted_word:
-			option.push_back((it++)->get_value());
-			break;
-		default:
-			throw exception(it->get_line(), it->get_column(), "unexpected '"s + it[-1].get_value() + "' in list construct");
-			break;
-		}
-	}
-
-	if (it == end)
-		throw exception(save->get_line(), save->get_column(), "unterminated list construct");
-
-	// Remove ).
-	++ it;
-}
-
-void parse_option(section& sc, token_iterator& it, token_iterator end)
-{
-	option option(it->get_value());
-	token_iterator save(it);
-
-	// No '=' or something else?
-	if (++it == end)
-		throw exception(save->get_line(), save->get_column(), "expected '=' assignment, got <EOF>");
-	if (it->get_type() != token::assign)
-		throw exception(it->get_line(), it->get_column(), "expected '=' assignment, got " + it->get_value());
-
-	// Empty options are allowed so just test for words.
-	if (++it != end) {
-		if (it->get_type() == token::word || it->get_type() == token::quoted_word)
-			parse_option_value_simple(option, it);
-		else if (it->get_type() == token::list_begin)
-			parse_option_value_list(option, it, end);
-	}
-
-	sc.push_back(std::move(option));
-}
-
-void parse_include(document& doc, const std::string& path, token_iterator& it, token_iterator end, bool required)
-{
-	token_iterator save(it);
-
-	if (++it == end)
-		throw exception(save->get_line(), save->get_column(), "expected file name after '@include' statement, got <EOF>");
-	if (it->get_type() != token::word && it->get_type() != token::quoted_word)
-		throw exception(it->get_line(), it->get_column(),
-		                "expected file name after '@include' statement, got "s + it->get_value());
-
-	std::string value = (it++)->get_value();
-	std::string file;
-
-	if (!is_absolute(value)) {
-#if defined(_WIN32)
-		file = path + "\\" + value;
-#else
-		file = path + "/" + value;
-#endif
-	} else
-		file = value;
-
-	try {
-		/*
-		 * If required is set to true, we have @include, otherwise the non-fatal
-		 * @tryinclude keyword.
-		 */
-		for (const auto& sc : read_file(file))
-			doc.push_back(sc);
-	} catch (...) {
-		if (required)
-			throw;
-	}
-}
-
-void parse_section(document& doc, token_iterator& it, token_iterator end)
-{
-	section sc(it->get_value());
-
-	// Skip [section].
-	++ it;
-
-	// Read until next section.
-	while (it != end && it->get_type() != token::section) {
-		if (it->get_type() != token::word)
-			throw exception(it->get_line(), it->get_column(),
-			                "unexpected token '"s + it->get_value() + "' in section definition");
-
-		parse_option(sc, it, end);
-	}
-
-	doc.push_back(std::move(sc));
-}
-
-} // !namespace
-
-exception::exception(unsigned line, unsigned column, std::string msg) noexcept
-	: line_(line)
-	, column_(column)
-	, message_(std::move(msg))
-{
-}
-
-auto exception::line() const noexcept -> unsigned
-{
-	return line_;
-}
-
-auto exception::column() const noexcept -> unsigned
-{
-	return column_;
-}
-
-auto exception::what() const noexcept -> const char*
-{
-	return message_.c_str();
-}
-
-token::token(type type, unsigned line, unsigned column, std::string value) noexcept
-	: type_(type)
-	, line_(line)
-	, column_(column)
-{
-	switch (type) {
-	case include:
-		value_ = "@include";
-		break;
-	case tryinclude:
-		value_ = "@tryinclude";
-		break;
-	case section:
-	case word:
-	case quoted_word:
-		value_ = value;
-		break;
-	case assign:
-		value_ = "=";
-		break;
-	case list_begin:
-		value_ = "(";
-		break;
-	case list_end:
-		value_ = ")";
-		break;
-	case comma:
-		value_ = ",";
-		break;
-	default:
-		break;
-	}
-}
-
-auto token::get_type() const noexcept -> type
-{
-	return type_;
-}
-
-auto token::get_line() const noexcept -> unsigned
-{
-	return line_;
-}
-
-auto token::get_column() const noexcept -> unsigned
-{
-	return column_;
-}
-
-auto token::get_value() const noexcept -> const std::string&
-{
-	return value_;
-}
-
-option::option(std::string key) noexcept
-	: std::vector<std::string>()
-	, key_(std::move(key))
-{
-	assert(!key_.empty());
-}
-
-option::option(std::string key, std::string value) noexcept
-	: key_(std::move(key))
-{
-	assert(!key_.empty());
-
-	push_back(std::move(value));
-}
-
-option::option(std::string key, std::vector<std::string> values) noexcept
-	: std::vector<std::string>(std::move(values))
-	, key_(std::move(key))
-{
-	assert(!key_.empty());
-}
-
-auto option::get_key() const noexcept -> const std::string&
-{
-	return key_;
-}
-
-auto option::get_value() const noexcept -> const std::string&
-{
-	static std::string dummy;
-
-	return empty() ? dummy : (*this)[0];
-}
-
-section::section(std::string key) noexcept
-	: key_(std::move(key))
-{
-	assert(!key_.empty());
-}
-
-auto section::get_key() const noexcept -> const std::string&
-{
-	return key_;
-}
-
-auto section::contains(std::string_view key) const noexcept -> bool
-{
-	return find(key) != end();
-}
-
-auto section::get(std::string_view key) const noexcept -> option
-{
-	auto it = find(key);
-
-	if (it == end())
-		return option(std::string(key));
-
-	return *it;
-}
-
-auto section::find(std::string_view key) noexcept -> iterator
-{
-	return std::find_if(begin(), end(), [&] (const auto& o) {
-		return o.get_key() == key;
-	});
-}
-
-auto section::find(std::string_view key) const noexcept -> const_iterator
-{
-	return std::find_if(cbegin(), cend(), [&] (const auto& o) {
-		return o.get_key() == key;
-	});
-}
-
-auto section::operator[](std::string_view key) -> option&
-{
-	assert(contains(key));
-
-	return *find(key);
-}
-
-auto section::operator[](std::string_view key) const -> const option&
-{
-	assert(contains(key));
-
-	return *find(key);
-}
-
-auto document::contains(std::string_view key) const noexcept -> bool
-{
-	return find(key) != end();
-}
-
-auto document::get(std::string_view key) const noexcept -> section
-{
-	auto it = find(key);
-
-	if (it == end())
-		return section(std::string(key));
-
-	return *it;
-}
-
-auto document::find(std::string_view key) noexcept -> iterator
-{
-	return std::find_if(begin(), end(), [&] (const auto& o) {
-		return o.get_key() == key;
-	});
-}
-
-auto document::find(std::string_view key) const noexcept -> const_iterator
-{
-	return std::find_if(cbegin(), cend(), [&] (const auto& o) {
-		return o.get_key() == key;
-	});
-}
-
-auto document::operator[](std::string_view key) -> section&
-{
-	assert(contains(key));
-
-	return *find(key);
-}
-
-auto document::operator[](std::string_view key) const -> const section&
-{
-	assert(contains(key));
-
-	return *find(key);
-}
-
-tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end)
-{
-	tokens list;
-	unsigned line = 1;
-	unsigned column = 0;
-
-	while (it != end) {
-		if (*it == '\n')
-			analyse_line(line, column, it);
-		else if (*it == '#')
-			analyse_comment(column, it, end);
-		else if (*it == '[')
-			analyse_section(list, line, column, it, end);
-		else if (*it == '=')
-			analyse_assign(list, line, column, it);
-		else if (is_space(*it))
-			analyse_spaces(column, it, end);
-		else if (*it == '@')
-			analyse_include(list, line, column, it, end);
-		else if (is_quote(*it))
-			analyse_quoted_word(list, line, column, it, end);
-		else if (is_list(*it))
-			analyse_list(list, line, column, it);
-		else
-			analyse_word(list, line, column, it, end);
-	}
-
-	return list;
-}
-
-tokens analyse(std::istream& stream)
-{
-	return analyse(std::istreambuf_iterator<char>(stream), {});
-}
-
-document parse(const tokens& tokens, const std::string& path)
-{
-	document doc;
-	token_iterator it = tokens.cbegin();
-	token_iterator end = tokens.cend();
-
-	while (it != end) {
-		switch (it->get_type()) {
-		case token::include:
-			parse_include(doc, path, it, end, true);
-			break;
-		case token::tryinclude:
-			parse_include(doc, path, it, end, false);
-			break;
-		case token::section:
-			parse_section(doc, it, end);
-			break;
-		default:
-			throw exception(it->get_line(), it->get_column(),
-			                "unexpected '"s + it->get_value() + "' on root document");
-		}
-	}
-
-	return doc;
-}
-
-document read_file(const std::string& filename)
-{
-	// Get parent path.
-	auto parent = filename;
-	auto pos = parent.find_last_of("/\\");
-
-	if (pos != std::string::npos)
-		parent.erase(pos);
-	else
-		parent = ".";
-
-	std::ifstream input(filename);
-
-	if (!input)
-		throw exception(0, 0, std::strerror(errno));
-
-	return parse(analyse(input), parent);
-}
-
-document read_string(const std::string& buffer)
-{
-	std::istringstream iss(buffer);
-
-	return parse(analyse(iss));
-}
-
-void dump(const tokens& tokens)
-{
-	for (const token& token: tokens) {
-		// TODO: add better description
-		std::cout << token.get_line() << ":" << token.get_column() << ": " << token.get_value() << std::endl;
-	}
-}
-
-} // !irccd::ini
--- a/libirccd-core/irccd/ini.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,493 +0,0 @@
-/*
- * ini.hpp -- extended .ini file parser
- *
- * Copyright (c) 2013-2018 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_INI_HPP
-#define IRCCD_INI_HPP
-
-/**
- * \file ini.hpp
- * \brief Extended .ini file parser.
- * \author David Demelier <markand@malikania.fr>
- * \version 2.0.0
- */
-
-/**
- * \page Ini Ini
- * \brief Extended .ini file parser.
- * - \subpage ini-syntax
- */
-
-/**
- * \page ini-syntax Syntax
- * \brief File syntax.
- *
- * The syntax is similar to most of `.ini` implementations as:
- *
- * - a section is delimited by `[name]` can be redefined multiple times,
- * - an option **must** always be defined in a section,
- * - empty options must be surrounded by quotes,
- * - lists can not include trailing commas,
- * - include statements must always live at the beginning of files
- *   (in no sections),
- * - comments start with # until the end of line,
- * - options with spaces **must** use quotes.
- *
- * # Basic file
- *
- * ````ini
- * # This is a comment.
- * [section]
- * option1 = value1
- * option2 = "value 2 with spaces"	# comment is also allowed here
- * ````
- *
- * # Redefinition
- *
- * Sections can be redefined multiple times and are kept the order they are
- * seen.
- *
- * ````ini
- * [section]
- * value = "1"
- *
- * [section]
- * value = "2"
- * ````
- *
- * The ini::document object will contains two ini::section.
- *
- * # Lists
- *
- * Lists are defined using `()` and commas, like values, they may have quotes.
- *
- * ````ini
- * [section]
- * names = ( "x1", "x2" )
- *
- * # This is also allowed.
- * biglist = (
- *   "abc",
- *   "def"
- * )
- * ````
- *
- * # Include statement
- *
- * You can split a file into several pieces, if the include statement contains a
- * relative path, the path will be relative to the current file being parsed.
- *
- * You **must** use the include statement before any section.
- *
- * If the file contains spaces, use quotes.
- *
- * ````ini
- * # main.conf
- * @include "foo.conf"
- *
- * # foo.conf
- * [section]
- * option1 = value1
- * ````
- */
-
-#include <algorithm>
-#include <exception>
-#include <stdexcept>
-#include <string>
-#include <string_view>
-#include <vector>
-
-/**
- * \brief Namespace for ini related classes.
- */
-namespace irccd::ini {
-
-class document;
-
-/**
- * \brief exception in a file.
- */
-class exception : public std::exception {
-private:
-	unsigned line_;
-	unsigned column_;
-	std::string message_;
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param line the line
-	 * \param column the column
-	 * \param msg the message
-	 */
-	exception(unsigned line, unsigned column, std::string msg) noexcept;
-
-	/**
-	 * Get the line number.
-	 *
-	 * \return the line
-	 */
-	auto line() const noexcept -> unsigned;
-
-	/**
-	 * Get the column number.
-	 *
-	 * \return the column
-	 */
-	auto column() const noexcept -> unsigned;
-
-	/**
-	 * Return the raw exception message (no line and column shown).
-	 *
-	 * \return the exception message
-	 */
-	auto what() const noexcept -> const char* override;
-};
-
-/**
- * \brief Describe a token read in the .ini source.
- *
- * This class can be used when you want to parse a .ini file yourself.
- *
- * \see analyse
- */
-class token {
-public:
-	/**
-	 * \brief token type.
-	 */
-	enum type {
-		include,                //!< include statement
-		tryinclude,             //!< tryinclude statement
-		section,                //!< [section]
-		word,                   //!< word without quotes
-		quoted_word,            //!< word with quotes
-		assign,                 //!< = assignment
-		list_begin,             //!< begin of list (
-		list_end,               //!< end of list )
-		comma                   //!< list separation
-	};
-
-private:
-	type type_;
-	unsigned line_;
-	unsigned column_;
-	std::string value_;
-
-public:
-	/**
-	 * Construct a token.
-	 *
-	 * \param type the type
-	 * \param line the line
-	 * \param column the column
-	 * \param value the value
-	 */
-	token(type type, unsigned line, unsigned column, std::string value = "") noexcept;
-
-	/**
-	 * Get the type.
-	 *
-	 * \return the type
-	 */
-	auto get_type() const noexcept -> type;
-
-	/**
-	 * Get the line.
-	 *
-	 * \return the line
-	 */
-	auto get_line() const noexcept -> unsigned;
-
-	/**
-	 * Get the column.
-	 *
-	 * \return the column
-	 */
-	auto get_column() const noexcept -> unsigned;
-
-	/**
-	 * Get the value. For words, quoted words and section, the value is the
-	 * content. Otherwise it's the characters parsed.
-	 *
-	 * \return the value
-	 */
-	auto get_value() const noexcept -> const std::string&;
-};
-
-/**
- * List of tokens in order they are analyzed.
- */
-using tokens = std::vector<token>;
-
-/**
- * \brief option definition.
- */
-class option : public std::vector<std::string> {
-private:
-	std::string key_;
-
-public:
-	/**
-	 * Construct an empty option.
-	 *
-	 * \pre key must not be empty
-	 * \param key the key
-	 */
-	option(std::string key) noexcept;
-
-	/**
-	 * Construct a single option.
-	 *
-	 * \pre key must not be empty
-	 * \param key the key
-	 * \param value the value
-	 */
-	option(std::string key, std::string value) noexcept;
-
-	/**
-	 * Construct a list option.
-	 *
-	 * \pre key must not be empty
-	 * \param key the key
-	 * \param values the values
-	 */
-	option(std::string key, std::vector<std::string> values) noexcept;
-
-	/**
-	 * Get the option key.
-	 *
-	 * \return the key
-	 */
-	auto get_key() const noexcept -> const std::string&;
-
-	/**
-	 * Get the option value.
-	 *
-	 * \return the value
-	 */
-	auto get_value() const noexcept -> const std::string&;
-};
-
-/**
- * \brief Section that contains one or more options.
- */
-class section : public std::vector<option> {
-private:
-	std::string key_;
-
-public:
-	/**
-	 * Construct a section with its name.
-	 *
-	 * \pre key must not be empty
-	 * \param key the key
-	 */
-	section(std::string key) noexcept;
-
-	/**
-	 * Get the section key.
-	 *
-	 * \return the key
-	 */
-	auto get_key() const noexcept -> const std::string&;
-
-	/**
-	 * Check if the section contains a specific option.
-	 *
-	 * \param key the option key
-	 * \return true if the option exists
-	 */
-	auto contains(std::string_view key) const noexcept -> bool;
-
-	/**
-	 * Find an option or return an empty one if not found.
-	 *
-	 * \param key the key
-	 * \return the option or empty one if not found
-	 */
-	auto get(std::string_view key) const noexcept -> option;
-
-	/**
-	 * Find an option by key and return an iterator.
-	 *
-	 * \param key the key
-	 * \return the iterator or end() if not found
-	 */
-	auto find(std::string_view key) noexcept -> iterator;
-
-	/**
-	 * Find an option by key and return an iterator.
-	 *
-	 * \param key the key
-	 * \return the iterator or end() if not found
-	 */
-	auto find(std::string_view key) const noexcept -> const_iterator;
-
-	/**
-	 * Access an option at the specified key.
-	 *
-	 * \param key the key
-	 * \return the option
-	 * \pre contains(key) must return true
-	 */
-	auto operator[](std::string_view key) -> option&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \param key the key
-	 * \return the option
-	 * \pre contains(key) must return true
-	 */
-	auto operator[](std::string_view key) const -> const option&;
-
-	/**
-	 * Inherited operators.
-	 */
-	using std::vector<option>::operator[];
-};
-
-/**
- * \brief Ini document description.
- * \see read_file
- * \see read_string
- */
-class document : public std::vector<section> {
-public:
-	/**
-	 * Check if a document has a specific section.
-	 *
-	 * \param key the key
-	 * \return true if the document contains the section
-	 */
-	auto contains(std::string_view key) const noexcept -> bool;
-
-	/**
-	 * Find a section or return an empty one if not found.
-	 *
-	 * \param key the key
-	 * \return the section or empty one if not found
-	 */
-	auto get(std::string_view key) const noexcept -> section;
-
-	/**
-	 * Find a section by key and return an iterator.
-	 *
-	 * \param key the key
-	 * \return the iterator or end() if not found
-	 */
-	auto find(std::string_view key) noexcept -> iterator;
-
-	/**
-	 * Find a section by key and return an iterator.
-	 *
-	 * \param key the key
-	 * \return the iterator or end() if not found
-	 */
-	auto find(std::string_view key) const noexcept -> const_iterator;
-
-	/**
-	 * Access a section at the specified key.
-	 *
-	 * \param key the key
-	 * \return the section
-	 * \pre contains(key) must return true
-	 */
-	auto operator[](std::string_view key) -> section&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \param key the key
-	 * \return the section
-	 * \pre contains(key) must return true
-	 */
-	auto operator[](std::string_view key) const -> const section&;
-
-	/**
-	 * Inherited operators.
-	 */
-	using std::vector<section>::operator[];
-};
-
-/**
- * Analyse a stream and detect potential syntax errors. This does not parse the
- * file like including other files in include statement.
- *
- * It does only analysis, for example if an option is defined under no section,
- * this does not trigger an exception while it's invalid.
- *
- * \param it the iterator
- * \param end where to stop
- * \return the list of tokens
- * \throws exception on errors
- */
-auto analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end) -> tokens;
-
-/**
- * Overloaded function for stream.
- *
- * \param stream the stream
- * \return the list of tokens
- * \throws exception on errors
- */
-auto analyse(std::istream& stream) -> tokens;
-
-/**
- * Parse the produced tokens.
- *
- * \param tokens the tokens
- * \param path the parent path
- * \return the document
- * \throw exception on errors
- */
-auto parse(const tokens& tokens, const std::string& path = ".") -> document;
-
-/**
- * Parse a file.
- *
- * \param filename the file name
- * \return the document
- * \throw exception on errors
- */
-auto read_file(const std::string& filename) -> document;
-
-/**
- * Parse a string.
- *
- * If the string contains include statements, they are relative to the current
- * working directory.
- *
- * \param buffer the buffer
- * \return the document
- * \throw exception on exceptions
- */
-auto read_string(const std::string& buffer) -> document;
-
-/**
- * Show all tokens and their description.
- *
- * \param tokens the tokens
- */
-void dump(const tokens& tokens);
-
-} // !irccd::ini
-
-#endif // !IRCCD_INI_HPP
--- a/libirccd-core/irccd/ini_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-/*
- * ini_util.hpp -- ini utilities
- *
- * Copyright (c) 2013-2018 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_INI_UTIL_HPP
-#define IRCCD_INI_UTIL_HPP
-
-/**
- * \file ini_util.hpp
- * \brief Ini utilities.
- */
-
-#include <optional>
-
-#include "ini.hpp"
-#include "string_util.hpp"
-
-namespace irccd {
-
-/**
- * \brief Ini utilities.
- */
-namespace ini_util {
-
-/**
- * Get an unsigned integer from the configuration section.
- *
- * \param sc the section
- * \param name the option name
- * \return the value or none if not able to convert
- */
-template <typename Int>
-inline auto get_uint(const ini::section& sc, std::string_view name) noexcept -> std::optional<Int>
-{
-	return string_util::to_uint<Int>(sc.get(name).get_value());
-}
-
-/**
- * Get an optional string or the default value if not given.
- *
- * \param sc the section
- * \param name the option name
- * \param def the default value
- * \return the value or def if not found
- */
-inline auto optional_string(const ini::section& sc,
-                            std::string_view name,
-                            std::string_view def) noexcept -> std::string
-{
-	const auto it = sc.find(name);
-
-	if (it == sc.end())
-		return std::string(def);
-
-	return it->get_value();
-}
-
-/**
- * Get an optional unsigned integer from the configuration section.
- *
- * \param sc the section
- * \param name the option name
- * \param def the default value
- * \return the value or none if not able to convert
- */
-template <typename Int>
-inline auto optional_uint(const ini::section& sc,
-                          std::string_view name,
-                          Int def) noexcept -> std::optional<Int>
-{
-	const auto it = sc.find(name);
-
-	if (it == sc.end())
-		return def;
-
-	return string_util::to_uint<Int>(it->get_value());
-}
-
-} // !ini_util
-
-} // !irccd
-
-#endif // !IRCCD_INI_UTIL_HPP
--- a/libirccd-core/irccd/json_util.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-/*
- * json_util.cpp -- utilities for JSON
- *
- * Copyright (c) 2018 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 <limits>
-#include <type_traits>
-
-#include "json_util.hpp"
-
-using nlohmann::json;
-
-namespace irccd::json_util {
-
-namespace {
-
-template <typename Int>
-auto clampi(const json& value) noexcept -> std::optional<Int>
-{
-	static_assert(std::is_signed<Int>::value, "Int must be signed");
-
-	if (!value.is_number_integer())
-		return std::nullopt;
-
-	const auto ret = value.get<std::int64_t>();
-
-	if (ret < std::numeric_limits<Int>::min() || ret > std::numeric_limits<Int>::max())
-		return std::nullopt;
-
-	return static_cast<Int>(ret);
-}
-
-template <typename Int>
-auto clampu(const json& value) noexcept -> std::optional<Int>
-{
-	static_assert(std::is_unsigned<Int>::value, "Int must be unsigned");
-
-	if (!value.is_number_unsigned())
-		return std::nullopt;
-
-	const auto ret = value.get<std::uint64_t>();
-
-	if (ret > std::numeric_limits<Int>::max())
-		return std::nullopt;
-
-	return static_cast<Int>(ret);
-}
-
-} // !namespace
-
-auto type_traits<bool>::get(const json& value) noexcept -> std::optional<bool>
-{
-	if (!value.is_boolean())
-		return std::nullopt;
-
-	return value.get<bool>();
-}
-
-auto type_traits<double>::get(const json& value) noexcept -> std::optional<double>
-{
-	if (!value.is_number_float())
-		return std::nullopt;
-
-	return value.get<double>();
-}
-
-auto type_traits<std::string>::get(const json& value) -> std::optional<std::string>
-{
-	if (!value.is_string())
-		return std::nullopt;
-
-	return value.get<std::string>();
-}
-
-auto type_traits<std::int8_t>::get(const json& value) -> std::optional<std::int8_t>
-{
-	return clampi<std::int8_t>(value);
-}
-
-auto type_traits<std::int16_t>::get(const json& value) -> std::optional<std::int16_t>
-{
-	return clampi<std::int16_t>(value);
-}
-
-auto type_traits<std::int32_t>::get(const json& value) -> std::optional<std::int32_t>
-{
-	return clampi<std::int32_t>(value);
-}
-
-auto type_traits<std::int64_t>::get(const json& value) noexcept -> std::optional<std::int64_t>
-{
-	if (!value.is_number_integer())
-		return std::nullopt;
-
-	return value.get<std::int64_t>();
-}
-
-auto type_traits<std::uint8_t>::get(const json& value) -> std::optional<std::uint8_t>
-{
-	return clampu<std::uint8_t>(value);
-}
-
-auto type_traits<std::uint16_t>::get(const json& value) -> std::optional<std::uint16_t>
-{
-	return clampu<std::uint16_t>(value);
-}
-
-auto type_traits<std::uint32_t>::get(const json& value) -> std::optional<std::uint32_t>
-{
-	return clampu<std::uint32_t>(value);
-}
-
-auto type_traits<std::uint64_t>::get(const json& value) noexcept -> std::optional<std::uint64_t>
-{
-	if (!value.is_number_unsigned())
-		return std::nullopt;
-
-	return value.get<std::uint64_t>();
-}
-
-auto pretty(const json& value, int indent) -> std::string
-{
-	switch (value.type()) {
-	case json::value_t::null:
-		return "null";
-	case json::value_t::string:
-		return value.get<std::string>();
-	case json::value_t::boolean:
-		return value.get<bool>() ? "true" : "false";
-	case json::value_t::number_integer:
-		return std::to_string(value.get<std::int64_t>());
-	case json::value_t::number_unsigned:
-		return std::to_string(value.get<std::uint64_t>());
-	case json::value_t::number_float:
-		return std::to_string(value.get<double>());
-	default:
-		return value.dump(indent);
-	}
-}
-
-auto contains(const nlohmann::json& array, const nlohmann::json& value) noexcept -> bool
-{
-	for (const auto& v : array)
-		if (v == value)
-			return true;
-
-	return false;
-}
-
-} // !irccd::json_util
--- a/libirccd-core/irccd/json_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,295 +0,0 @@
-/*
- * json_util.hpp -- utilities for JSON
- *
- * Copyright (c) 2018 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_JSON_UTIL_HPP
-#define IRCCD_JSON_UTIL_HPP
-
-/**
- * \file json_util.hpp
- * \brief Utilities for JSON.
- */
-
-#include <cstdint>
-#include <optional>
-#include <string>
-
-#include <json.hpp>
-
-/**
- * \brief Utilities for JSON.
- */
-namespace irccd::json_util {
-
-/**
- * \brief Describe how to convert a JSON value.
- *
- * This traits must be specialized for every type you want to convert from JSON
- * to its native type.
- *
- * You only need to implement the get function with the following signature:
- *
- * ```cpp
- * static std::optional<T> get(const nlohmann::json& value);
- * ```
- *
- * The implementation should not throw an exception but return a null optional
- * instead.
- *
- * This traits is already specialized for the given types:
- *
- * - bool
- * - double
- * - std::uint(8, 16, 32, 64)_t
- * - std::string
- */
-template <typename T>
-struct type_traits;
-
-/**
- * \brief Specialization for `bool`.
- */
-template <>
-struct type_traits<bool> {
-	/**
-	 * Convert the JSON value to bool.
-	 *
-	 * \param value the value
-	 * \return the bool or empty if not a boolean type
-	 */
-	static auto get(const nlohmann::json& value) noexcept -> std::optional<bool>;
-};
-
-/**
- * \brief Specialization for `double`.
- */
-template <>
-struct type_traits<double> {
-	/**
-	 * Convert the JSON value to bool.
-	 *
-	 * \param value the value
-	 * \return the double or empty if not a double type
-	 */
-	static auto get(const nlohmann::json& value) noexcept -> std::optional<double>;
-};
-
-/**
- * \brief Specialization for `std::string`.
- */
-template <>
-struct type_traits<std::string> {
-	/**
-	 * Convert the JSON value to std::string.
-	 *
-	 * \param value the value
-	 * \return the string or empty if not a string type
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::string>;
-};
-
-/**
- * \brief Specialization for `std::int8_t`.
- */
-template <>
-struct type_traits<std::int8_t> {
-	/**
-	 * Convert the JSON value to std::int8_t.
-	 *
-	 * \param value the value
-	 * \return the value or empty if value does not fit between the range
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::int8_t>;
-};
-
-/**
- * \brief Specialization for `std::int16_t`.
- */
-template <>
-struct type_traits<std::int16_t> {
-	/**
-	 * Convert the JSON value to std::int16_t.
-	 *
-	 * \param value the value
-	 * \return the value or empty if value does not fit between the range
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::int16_t>;
-};
-
-/**
- * \brief Specialization for `std::int32_t`.
- */
-template <>
-struct type_traits<std::int32_t> {
-	/**
-	 * Convert the JSON value to std::int32_t.
-	 *
-	 * \param value the value
-	 * \return the value or empty if value does not fit between the range
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::int32_t>;
-};
-
-/**
- * \brief Specialization for `std::int64_t`.
- */
-template <>
-struct type_traits<std::int64_t> {
-	/**
-	 * Convert the JSON value to std::int64_t.
-	 *
-	 * \param value the value
-	 * \return the int or empty if not a int type
-	 */
-	static auto get(const nlohmann::json& value) noexcept -> std::optional<std::int64_t>;
-};
-
-/**
- * \brief Specialization for `std::uint8_t`.
- */
-template <>
-struct type_traits<std::uint8_t> {
-	/**
-	 * Convert the JSON value to std::uint8_t.
-	 *
-	 * \param value the value
-	 * \return the value or empty if value does not fit between the range
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::uint8_t>;
-};
-
-/**
- * \brief Specialization for `std::uint16_t`.
- */
-template <>
-struct type_traits<std::uint16_t> {
-	/**
-	 * Convert the JSON value to std::uint16_t.
-	 *
-	 * \param value the value
-	 * \return the value or empty if value does not fit between the range
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::uint16_t>;
-};
-
-/**
- * \brief Specialization for `std::int32_t`.
- */
-template <>
-struct type_traits<std::uint32_t> {
-	/**
-	 * Convert the JSON value to std::uint32_t.
-	 *
-	 * \param value the value
-	 * \return the value or empty if value does not fit between the range
-	 */
-	static auto get(const nlohmann::json& value) -> std::optional<std::uint32_t>;
-};
-
-/**
- * \brief Specialization for `std::uint64_t`.
- */
-template <>
-struct type_traits<std::uint64_t> {
-	/**
-	 * Convert the JSON value to std::uint64_t.
-	 *
-	 * \param value the value
-	 * \return the int or empty if not a int type
-	 */
-	static auto get(const nlohmann::json& value) noexcept -> std::optional<std::uint64_t>;
-};
-
-/**
- * \brief Convenient JSON object parser
- *
- * This class helps destructuring insecure JSON input by returning optional
- * values if they are not present or invalid.
- */
-class deserializer : public nlohmann::json {
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param obj the JSON object
-	 */
-	deserializer(const nlohmann::json& obj)
-		: nlohmann::json(obj)
-	{
-	}
-
-	/**
-	 * Get a value from the document object.
-	 *
-	 * \param key the property key
-	 * \return the value or std::nullopt if not found or not convertible
-	 */
-	template <typename Type>
-	auto get(const std::string& key) const noexcept -> std::optional<Type>
-	{
-		const auto it = find(key);
-
-		if (it == end())
-			return std::nullopt;
-
-		return type_traits<Type>::get(*it);
-	}
-
-	/**
-	 * Get an optional value from the document object.
-	 *
-	 * If the value is undefined, the default value is returned. Otherwise, if
-	 * the value is not in the given type, std::nullopt is returned.
-	 *
-	 * \param key the property key
-	 * \param def the default value if property is undefined
-	 * \return the value, std::nullopt or def
-	 */
-	template <typename Type, typename DefaultValue>
-	auto optional(const std::string& key, DefaultValue&& def) const noexcept -> std::optional<Type>
-	{
-		const auto it = find(key);
-
-		if (it == end())
-			return std::optional<Type>(std::forward<DefaultValue>(def));
-
-		return type_traits<Type>::get(*it);
-	}
-};
-
-/**
- * Print the value as human readable.
- *
- * \note This only works on flat objects.
- * \param value the value
- * \param indent the optional indent for objects/arrays
- * \return the string
- */
-auto pretty(const nlohmann::json& value, int indent = 4) -> std::string;
-
-/**
- * Check if a JSON array contains a specific value in any order.
- *
- * \param array the JSON array
- * \param value the JSON value
- * \return true if value is present
- */
-auto contains(const nlohmann::json& array, const nlohmann::json& value) noexcept -> bool;
-
-} // !irccd::json_util
-
-#endif // !IRCCD_JSON_UTIL_HPP
--- a/libirccd-core/irccd/options.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-/*
- * options.cpp -- parse Unix command line options
- *
- * Copyright (c) 2015-2018 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 <cassert>
-
-#include "options.hpp"
-
-namespace irccd {
-
-namespace option {
-
-namespace {
-
-using iterator = std::vector<std::string>::iterator;
-using args = std::vector<std::string>;
-
-inline bool is_option(const std::string& arg) noexcept
-{
-    return arg.size() >= 2 && arg[0] == '-';
-}
-
-inline bool is_long_option(const std::string& arg) noexcept
-{
-    assert(is_option(arg));
-
-    return arg.size() >= 3 && arg[1] == '-';
-}
-
-inline bool is_short_simple(const std::string& arg) noexcept
-{
-    assert(is_option(arg) && !is_long_option(arg));
-
-    return arg.size() == 2;
-}
-
-void parse_long_option(result& result, args& args, iterator& it, iterator& end, const options& definition)
-{
-    auto arg = *it++;
-    auto opt = definition.find(arg);
-
-    if (opt == definition.end())
-        throw invalid_option(arg);
-
-    // Need argument?
-    if (opt->second) {
-        if (it == end || is_option(*it))
-            throw missing_value(arg);
-
-        result.insert(std::make_pair(arg, *it++));
-        it = args.erase(args.begin(), it);
-        end = args.end();
-    } else {
-        result.insert(std::make_pair(arg, ""));
-        it = args.erase(args.begin());
-        end = args.end();
-    }
-}
-
-void parse_short_option_simple(result& result, args& args, iterator& it, iterator &end, const options& definition)
-{
-    /*
-     * Here two cases:
-     *
-     * -v (no option)
-     * -c value
-     */
-    auto arg = *it++;
-    auto opt = definition.find(arg);
-
-    if (opt == definition.end())
-        throw invalid_option(arg);
-
-    // Need argument?
-    if (opt->second) {
-        if (it == end || is_option(*it))
-            throw missing_value(arg);
-
-        result.insert(std::make_pair(arg, *it++));
-        it = args.erase(args.begin(), it);
-        end = args.end();
-    } else {
-        result.insert(std::make_pair(arg, ""));
-        it = args.erase(args.begin());
-        end = args.end();
-    }
-}
-
-void parse_short_option_compressed(result& result, args& args, iterator& it, iterator &end, const options& definition)
-{
-    /*
-     * Here multiple scenarios:
-     *
-     * 1. -abc (-a -b -c if all are simple boolean arguments)
-     * 2. -vc foo.conf (-v -c foo.conf if -c is argument dependant)
-     * 3. -vcfoo.conf (-v -c foo.conf also)
-     */
-    auto value = it->substr(1);
-    auto len = value.length();
-    int toremove = 1;
-
-    for (std::size_t i = 0; i < len; ++i) {
-        auto arg = std::string{'-'} + value[i];
-        auto opt = definition.find(arg);
-
-        if (opt == definition.end())
-            throw invalid_option(arg);
-
-        if (opt->second) {
-            if (i == (len - 1)) {
-                // End of string, get the next argument (see 2.).
-                if (++it == end || is_option(*it))
-                    throw missing_value(arg);
-
-                result.insert(std::make_pair(arg, *it));
-                toremove += 1;
-            } else {
-                result.insert(std::make_pair(arg, value.substr(i + 1)));
-                i = len;
-            }
-        } else
-            result.insert(std::make_pair(arg, ""));
-    }
-
-    it = args.erase(args.begin(), args.begin() + toremove);
-    end = args.end();
-}
-
-void parse_short_option(result& result, args& args, iterator& it, iterator &end, const options& definition)
-{
-    if (is_short_simple(*it))
-        parse_short_option_simple(result, args, it, end, definition);
-    else
-        parse_short_option_compressed(result, args, it, end, definition);
-}
-
-} // !namespace
-
-result read(std::vector<std::string>& args, const options& definition)
-{
-    result result;
-
-    auto it = args.begin();
-    auto end = args.end();
-
-    while (it != end) {
-        if (!is_option(*it))
-            break;
-
-        if (is_long_option(*it))
-            parse_long_option(result, args, it, end, definition);
-        else
-            parse_short_option(result, args, it, end, definition);
-    }
-
-    return result;
-}
-
-result read(int& argc, char**& argv, const options& definition)
-{
-    std::vector<std::string> args;
-
-    for (int i = 0; i < argc; ++i)
-        args.push_back(argv[i]);
-
-    auto before = args.size();
-    auto result = read(args, definition);
-
-    argc -= before - args.size();
-    argv += before - args.size();
-
-    return result;
-}
-
-} // !option
-
-} // !irccd
--- a/libirccd-core/irccd/options.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-/*
- * options.hpp -- parse Unix command line options
- *
- * Copyright (c) 2015-2018 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_COMMON_OPTIONS_HPP
-#define IRCCD_COMMON_OPTIONS_HPP
-
-/**
- * \file options.hpp
- * \brief Basic Unix options parser.
- */
-
-#include <exception>
-#include <map>
-#include <string>
-#include <utility>
-#include <vector>
-
-namespace irccd {
-
-/**
- * Namespace for options parsing.
- */
-namespace option {
-
-/**
- * \brief This exception is thrown when an invalid option has been found.
- */
-class invalid_option : public std::exception {
-private:
-    std::string message_;
-    std::string name_;
-
-public:
-    /**
-     * Construct the exception.
-     *
-     * \param name the argument missing
-     */
-    inline invalid_option(std::string name)
-        : name_(std::move(name))
-    {
-        message_ = std::string("invalid option: ") + name_;
-    }
-
-    /**
-     * Get the option name.
-     *
-     * \return the name
-     */
-    inline const std::string& name() const noexcept
-    {
-        return name_;
-    }
-
-    /**
-     * Get the error message.
-     *
-     * \return the error message
-     */
-    const char* what() const noexcept override
-    {
-        return message_.c_str();
-    }
-};
-
-/**
- * \brief This exception is thrown when an option requires a value and no value
- * has been given.
- */
-class missing_value : public std::exception {
-private:
-    std::string message_;
-    std::string name_;
-
-public:
-    /**
-     * Construct the exception.
-     *
-     * \param name the option that requires a value
-     */
-    inline missing_value(std::string name)
-        : name_(std::move(name))
-    {
-        message_ = std::string("missing argument for: ") + name_;
-    }
-
-    /**
-     * Get the option name.
-     *
-     * \return the name
-     */
-    inline const std::string& name() const noexcept
-    {
-        return name_;
-    }
-
-    /**
-     * Get the error message.
-     *
-     * \return the error message
-     */
-    const char* what() const noexcept override
-    {
-        return message_.c_str();
-    }
-};
-
-/**
- * Packed multimap of options.
- */
-using result = std::multimap<std::string, std::string>;
-
-/**
- * Define the allowed options.
- */
-using options = std::map<std::string, bool>;
-
-/**
- * Extract the command line options and return a result.
- *
- * \param args the arguments
- * \param definition
- * \warning the arguments vector is modified in place to remove parsed options
- * \throw missing_value
- * \throw invalid_option
- * \return the result
- */
-result read(std::vector<std::string>& args, const options& definition);
-
-/**
- * Overloaded function for usage with main() arguments.
- *
- * \param argc the number of arguments
- * \param argv the argument vector
- * \param definition
- * \note don't forget to remove the first argv[0] argument
- * \warning the argc and argv are modified in place to remove parsed options
- * \throw missing_value
- * \throw invalid_option
- * \return the result
- */
-result read(int& argc, char**& argv, const options& definition);
-
-} // !option
-
-} // !irccd
-
-#endif // !IRCCD_COMMON_OPTIONS_HPP
--- a/libirccd-core/irccd/stream.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,463 +0,0 @@
-/*
- * stream.hpp -- abstract stream interface
- *
- * Copyright (c) 2013-2018 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_STREAM_HPP
-#define IRCCD_STREAM_HPP
-
-/**
- * \file stream.hpp
- * \brief Abstract stream interface.
- */
-
-/**
- * \defgroup networking Networking
- * \brief Networking
- *
- * Each irccd instance is controllable via sockets using JSON messages.
- *
- * This mechanism is offered via the triplet stream/acceptor/connector. Irccd
- * uses different acceptors to wait for clients to connect and then construct
- * a stream of it. Once ready, streams are ready to receive and send messages.
- *
- * On the client side (e.g. irccdctl), a generic connector is created to connect
- * to the irccd instance. Once ready, a stream is also created and ready to
- * perform the same receive and send messages.
- *
- * By default, irccd provides predefined implementations for TCP/IP, local unix
- * sockets and optionally TLS over those.
- */
-
-/**
- * \defgroup streams Generic I/O streams
- * \ingroup networking
- * \brief Generic I/O streams.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <cassert>
-#include <cstddef>
-#include <functional>
-#include <ostream>
-#include <string>
-#include <system_error>
-#include <utility>
-
-#include <boost/asio.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-#	include <boost/asio/ssl.hpp>
-#endif
-
-#include "json.hpp"
-
-namespace irccd {
-
-/**
- * \brief Abstract stream interface
- * \ingroup streams
- *
- * Abstract I/O interface that allows reading/writing from a stream in an
- * asynchronous manner.
- *
- * The derived classes must implement non-blocking recv and send operations.
- */
-class stream {
-public:
-	/**
-	 * \brief Read completion handler.
-	 */
-	using recv_handler = std::function<void (std::error_code, nlohmann::json)>;
-
-	/**
-	 * \brief Write completion handler.
-	 */
-	using send_handler = std::function<void (std::error_code)>;
-
-	/**
-	 * Default constructor.
-	 */
-	stream() = default;
-
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~stream() = default;
-
-	/**
-	 * Start asynchronous read.
-	 *
-	 * \pre another read operation must not be running
-	 * \pre handler != nullptr
-	 * \param handler the handler
-	 */
-	virtual void recv(recv_handler handler) = 0;
-
-	/**
-	 * Start asynchronous write.
-	 *
-	 * \pre json.is_object()
-	 * \pre another write operation must not be running
-	 * \pre handler != nullptr
-	 * \param json the JSON message
-	 * \param handler the handler
-	 */
-	virtual void send(const nlohmann::json& json, send_handler handler) = 0;
-};
-
-// {{{ socket_stream_base
-
-/**
- * \brief Abstract base interface for Boost.Asio sockets.
- * \ingroup streams
- *
- * This class provides convenient functions for underlying sockets.
- *
- * \see basic_socket_stream
- */
-class socket_stream_base : public stream {
-private:
-	boost::asio::streambuf input_{2048};
-	boost::asio::streambuf output_;
-
-#if !defined(NDEBUG)
-	bool is_receiving_{false};
-	bool is_sending_{false};
-#endif
-
-	void handle_recv(boost::system::error_code, std::size_t, recv_handler);
-	void handle_send(boost::system::error_code, std::size_t, send_handler);
-
-protected:
-	/**
-	 * Convenient function for receiving for the underlying socket.
-	 *
-	 * \param sc the socket
-	 * \param handler the handler
-	 */
-	template <typename Socket>
-	void recv(Socket& sc, recv_handler handler);
-
-	/**
-	 * Convenient function for sending for the underlying socket.
-	 *
-	 * \param json the JSON object
-	 * \param sc the socket
-	 * \param handler the handler
-	 */
-	template <typename Socket>
-	void send(const nlohmann::json& json, Socket& sc, send_handler handler);
-};
-
-inline void socket_stream_base::handle_recv(boost::system::error_code code,
-                                            std::size_t xfer,
-                                            recv_handler handler)
-{
-#if !defined(NDEBUG)
-	is_receiving_ = false;
-#endif
-
-	if (code == boost::asio::error::not_found) {
-		handler(make_error_code(std::errc::argument_list_too_long), nullptr);
-		return;
-	}
-	if (code == boost::asio::error::eof || xfer == 0) {
-		handler(make_error_code(std::errc::connection_reset), nullptr);
-		return;
-	}
-	if (code) {
-		handler(std::move(code), nullptr);
-		return;
-	}
-
-	// 1. Convert the buffer safely.
-	std::string buffer;
-
-	try {
-		buffer = std::string(
-			boost::asio::buffers_begin(input_.data()),
-			boost::asio::buffers_begin(input_.data()) + xfer - /* \r\n\r\n */ 4
-		);
-
-		input_.consume(xfer);
-	} catch (const std::bad_alloc&) {
-		handler(make_error_code(std::errc::not_enough_memory), nullptr);
-		return;
-	}
-
-	// 2. Convert to JSON.
-	nlohmann::json doc;
-
-	try {
-		doc = nlohmann::json::parse(buffer);
-	} catch (const std::exception&) {
-		handler(make_error_code(std::errc::invalid_argument), nullptr);
-		return;
-	}
-
-	if (!doc.is_object())
-		handler(make_error_code(std::errc::invalid_argument), nullptr);
-	else
-		handler(std::error_code(), std::move(doc));
-}
-
-inline void socket_stream_base::handle_send(boost::system::error_code code,
-                                            std::size_t xfer,
-                                            send_handler handler)
-{
-#if !defined(NDEBUG)
-	is_sending_ = false;
-#endif
-
-	if (code == boost::asio::error::eof || xfer == 0) {
-		handler(make_error_code(std::errc::connection_reset));
-		return;
-	}
-	else
-		handler(std::move(code));
-}
-
-template <typename Socket>
-inline void socket_stream_base::recv(Socket& sc, recv_handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_receiving_);
-
-	is_receiving_ = true;
-#endif
-
-	assert(handler);
-
-	async_read_until(sc, input_, "\r\n\r\n", [this, handler] (auto code, auto xfer) {
-		handle_recv(code, xfer, handler);
-	});
-}
-
-template <typename Socket>
-inline void socket_stream_base::send(const nlohmann::json& json, Socket& sc, send_handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_sending_);
-
-	is_sending_ = true;
-#endif
-
-	assert(json.is_object());
-	assert(handler);
-
-	std::ostream out(&output_);
-
-	out << json.dump(0);
-	out << "\r\n\r\n";
-	out << std::flush;
-
-	async_write(sc, output_, [this, handler] (auto code, auto xfer) {
-		handle_send(code, xfer, handler);
-	});
-}
-
-// }}}
-
-// {{{ basic_socket_stream
-
-/**
- * \brief Complete implementation for basic sockets
- * \ingroup streams
- * \tparam Socket Boost.Asio socket (e.g. boost::asio::ip::tcp::socket)
- */
-template <typename Socket>
-class basic_socket_stream : public socket_stream_base {
-private:
-	Socket socket_;
-
-public:
-	/**
-	 * Construct a socket stream.
-	 *
-	 * \param service the I/O service
-	 */
-	basic_socket_stream(boost::asio::io_context& service);
-
-	/**
-	 * Get the underlying socket.
-	 *
-	 * \return the socket
-	 */
-	auto get_socket() const noexcept -> const Socket&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the socket
-	 */
-	auto get_socket() noexcept -> Socket&;
-
-	/**
-	 * \copydoc stream::recv
-	 */
-	void recv(recv_handler handler) override;
-
-	/**
-	 * \copydoc stream::send
-	 */
-	void send(const nlohmann::json& json, send_handler handler) override;
-};
-
-template <typename Socket>
-inline basic_socket_stream<Socket>::basic_socket_stream(boost::asio::io_context& ctx)
-	: socket_(ctx)
-{
-}
-
-template <typename Socket>
-inline auto basic_socket_stream<Socket>::get_socket() const noexcept -> const Socket&
-{
-	return socket_;
-}
-
-template <typename Socket>
-inline auto basic_socket_stream<Socket>::get_socket() noexcept -> Socket&
-{
-	return socket_;
-}
-
-template <typename Socket>
-inline void basic_socket_stream<Socket>::recv(recv_handler handler)
-{
-	socket_stream_base::recv(socket_, handler);
-}
-
-template <typename Socket>
-inline void basic_socket_stream<Socket>::send(const nlohmann::json& json, send_handler handler)
-{
-	socket_stream_base::send(json, socket_, handler);
-}
-
-// }}}
-
-// {{{ ip_stream
-
-/**
- * \brief Convenient alias for boost::asio::ip::tcp::socket
- * \ingroup streams
- */
-using ip_stream = basic_socket_stream<boost::asio::ip::tcp::socket>;
-
-// }}}
-
-// {{{ local_stream
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-
-/**
- * \brief Convenient alias for boost::asio::local::stream_protocol::socket
- * \ingroup streams
- */
-using local_stream = basic_socket_stream<boost::asio::local::stream_protocol::socket>;
-
-#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
-
-// }}}
-
-// {{{ tls_stream
-
-#if defined(IRCCD_HAVE_SSL)
-
-/**
- * \brief TLS/SSL streams.
- * \ingroup streams
- * \tparam Socket the Boost.Asio compatible socket.
- */
-template <typename Socket>
-class tls_stream : public socket_stream_base {
-private:
-	boost::asio::ssl::stream<Socket> socket_;
-	std::shared_ptr<boost::asio::ssl::context> context_;
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param service the I/O service
-	 * \param ctx the shared context
-	 */
-	tls_stream(boost::asio::io_context& service, std::shared_ptr<boost::asio::ssl::context> ctx);
-
-	/**
-	 * Get the SSL socket.
-	 *
-	 * \return the socket
-	 */
-	auto get_socket() const noexcept -> const boost::asio::ssl::stream<Socket>&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the socket
-	 */
-	auto get_socket() noexcept -> boost::asio::ssl::stream<Socket>&;
-
-	/**
-	 * \copydoc stream::recv
-	 */
-	void recv(recv_handler handler) override;
-
-	/**
-	 * \copydoc stream::send
-	 */
-	void send(const nlohmann::json& json, send_handler handler) override;
-};
-
-template <typename Socket>
-inline tls_stream<Socket>::tls_stream(boost::asio::io_context& service, std::shared_ptr<boost::asio::ssl::context> ctx)
-	: socket_(service, *ctx)
-	, context_(std::move(ctx))
-{
-}
-
-template <typename Socket>
-inline auto tls_stream<Socket>::get_socket() const noexcept -> const boost::asio::ssl::stream<Socket>&
-{
-	return socket_;
-}
-
-template <typename Socket>
-inline auto tls_stream<Socket>::get_socket() noexcept -> boost::asio::ssl::stream<Socket>&
-{
-	return socket_;
-}
-
-template <typename Socket>
-inline void tls_stream<Socket>::recv(recv_handler handler)
-{
-	socket_stream_base::recv(socket_, handler);
-}
-
-template <typename Socket>
-inline void tls_stream<Socket>::send(const nlohmann::json& json, send_handler handler)
-{
-	socket_stream_base::send(json, socket_, handler);
-}
-
-#endif // !IRCCD_HAVE_SSL
-
-// }}}
-
-} // !irccd
-
-#endif // !IRCCD_STREAM_HPP
--- a/libirccd-core/irccd/string_util.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,454 +0,0 @@
-/*
- * string_util.cpp -- string utilities
- *
- * Copyright (c) 2013-2018 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 <boost/predef/os.h>
-
-#include "sysconfig.hpp"
-
-#if defined(IRCCD_HAVE_POPEN)
-#	include <array>
-#	include <cerrno>
-#	include <cstring>
-#	include <functional>
-#	include <memory>
-#endif
-
-#include <cassert>
-#include <iomanip>
-#include <regex>
-
-#include "string_util.hpp"
-
-using namespace std::string_literals;
-
-namespace irccd::string_util {
-
-// {{{ subst
-
-namespace {
-
-const std::unordered_map<std::string_view, int> 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  }
-};
-
-const std::unordered_map<std::string_view, char> irc_attributes{
-	{ "bold",       '\x02'  },
-	{ "italic",     '\x09'  },
-	{ "strike",     '\x13'  },
-	{ "reset",      '\x0f'  },
-	{ "underline",  '\x15'  },
-	{ "underline2", '\x1f'  },
-	{ "reverse",    '\x16'  }
-};
-
-const std::unordered_map<std::string_view, unsigned> shell_colors{
-	{ "black",      30  },
-	{ "red",        31  },
-	{ "green",      32  },
-	{ "orange",     33  },
-	{ "blue",       34  },
-	{ "purple",     35  },
-	{ "cyan",       36  },
-	{ "white",      37  },
-	{ "default",    39  },
-};
-
-const std::unordered_map<std::string_view, unsigned> shell_attributes{
-	{ "bold",       1   },
-	{ "dim",        2   },
-	{ "underline",  4   },
-	{ "blink",      5   },
-	{ "reverse",    7   },
-	{ "hidden",     8   }
-};
-
-auto is_reserved(char token) noexcept -> bool
-{
-	return token == '#' || token == '@' || token == '$' || token == '!';
-}
-
-auto subst_date(const std::string& text, const subst& params) -> std::string
-{
-	std::ostringstream oss;
-
-#if defined(IRCCD_HAVE_STD_PUT_TIME)
-	oss << std::put_time(std::localtime(&params.time), text.c_str());
-#else
-	/*
-	 * Quick and dirty hack because old version of GCC does not have this
-	 * function.
-	 */
-	char buffer[4096];
-
-	std::strftime(buffer, sizeof (buffer) - 1, text.c_str(), std::localtime(&params.time));
-
-	oss << buffer;
-#endif
-
-	return oss.str();
-}
-
-auto subst_keywords(const std::string& content, const subst& params) -> std::string
-{
-	const auto value = params.keywords.find(std::string(content));
-
-	if (value != params.keywords.end())
-		return value->second;
-
-	return "";
-}
-
-auto subst_env(const std::string& content) -> std::string
-{
-	const auto value = std::getenv(content.c_str());
-
-	if (value != nullptr)
-		return value;
-
-	return "";
-}
-
-auto subst_irc_attrs(const std::string& content) -> std::string
-{
-	auto list = split(content, ",");
-
-	// @{} means reset.
-	if (list.empty())
-		return std::string(1, irc_attributes.at("reset"));
-
-	std::ostringstream oss;
-
-	// Remove useless spaces.
-	std::transform(list.begin(), list.end(), list.begin(), strip);
-
-	/*
-	 * 0: foreground
-	 * 1: background
-	 * 2-n: attributes
-	 */
-	auto foreground = list[0];
-	if (!foreground.empty() || list.size() >= 2) {
-		// Color sequence.
-		oss << '\x03';
-
-		// Foreground.
-		auto it = irc_colors.find(foreground);
-		if (it != irc_colors.end())
-			oss << it->second;
-
-		// Background.
-		if (list.size() >= 2 && (it = irc_colors.find(list[1])) != irc_colors.end())
-			oss << "," << it->second;
-
-		// Attributes.
-		for (std::size_t i = 2; i < list.size(); ++i) {
-			auto attribute = irc_attributes.find(list[i]);
-
-			if (attribute != irc_attributes.end())
-				oss << attribute->second;
-		}
-	}
-
-	return oss.str();
-}
-
-auto subst_shell_attrs(const std::string& content) -> std::string
-{
-#if !BOOST_OS_WINDOWS
-	auto list = split(content, ",");
-
-	if (list.empty())
-		return "\033[0m";
-	if (list.size() > 3)
-		return "";
-
-	std::vector<std::string> seq;
-
-	/*
-	 * Shell sequence looks like this:
-	 *
-	 * ^[[attributes;foreground;backgroundm
-	 */
-	if (list.size() >= 3) {
-		const auto it = shell_attributes.find(list[2]);
-
-		if (it != shell_attributes.end())
-			seq.push_back(std::to_string(it->second));
-		else
-			return "";
-	}
-	if (list.size() >= 1) {
-		const auto it = shell_colors.find(list[0]);
-
-		if (it != shell_colors.end())
-			seq.push_back(std::to_string(it->second));
-		else
-			return "";
-	}
-	if (list.size() >= 2) {
-		const auto it = shell_colors.find(list[1]);
-
-		if (it != shell_colors.end())
-			seq.push_back(std::to_string(it->second + 10));
-		else
-			return "";
-	}
-
-	std::ostringstream oss;
-
-	oss << "\033[";
-	oss << string_util::join(seq, ';');
-	oss << "m";
-
-	return oss.str();
-#else
-	return "";
-#endif
-}
-
-auto subst_shell(const std::string& command) -> std::string
-{
-#if defined(IRCCD_HAVE_POPEN)
-	std::unique_ptr<FILE, std::function<int (FILE*)>> fp(popen(command.c_str(), "r"), pclose);
-
-	if (fp == nullptr)
-		throw std::runtime_error(std::strerror(errno));
-
-	std::string result;
-	std::array<char, 128> buffer;
-	std::size_t n;
-
-	while ((n = std::fread(buffer.data(), 1, 128, fp.get())) > 0)
-		result.append(buffer.data(), n);
-	if (std::ferror(fp.get()))
-		throw std::runtime_error(std::strerror(errno));
-
-	// Erase final '\n'.
-	auto it = result.find('\n');
-	if (it != std::string::npos)
-		result.erase(it);
-
-	return result;
-#else
-	throw std::runtime_error("shell template not available");
-#endif
-}
-
-auto substitute(std::string::const_iterator& it,
-                std::string::const_iterator& end,
-                char token,
-                const subst& params) -> std::string
-{
-	assert(is_reserved(token));
-
-	std::string content, value;
-
-	if (it == end)
-		return "";
-
-	while (it != end && *it != '}')
-		content += *it++;
-
-	if (it == end || *it != '}')
-		throw std::invalid_argument("unclosed "s + token + " construct"s);
-
-	it++;
-
-	// Create default original value if flag is disabled.
-	value = std::string(1, token) + "{"s + content + "}"s;
-
-	switch (token) {
-	case '#':
-		if ((params.flags & subst_flags::keywords) == subst_flags::keywords)
-			value = subst_keywords(content, params);
-		break;
-	case '$':
-		if ((params.flags & subst_flags::env) == subst_flags::env)
-			value = subst_env(content);
-		break;
-	case '@':
-		if ((params.flags & subst_flags::irc_attrs) == subst_flags::irc_attrs)
-			value = subst_irc_attrs(content);
-		else if ((params.flags & subst_flags::shell_attrs) == subst_flags::shell_attrs)
-			value = subst_shell_attrs(content);
-		break;
-	case '!':
-		if ((params.flags & subst_flags::shell) == subst_flags::shell)
-			value = subst_shell(content);
-		break;
-	default:
-		break;
-	}
-
-	return value;
-}
-
-} // !namespace
-
-auto format(std::string text, const subst& params) -> std::string
-{
-	/*
-	 * Change the date format before anything else to avoid interpolation with
-	 * keywords and user input.
-	 */
-	if ((params.flags & subst_flags::date) == subst_flags::date)
-		text = subst_date(text, params);
-
-	std::ostringstream oss;
-
-	for (auto it = text.cbegin(), end = text.cend(); it != end; ) {
-		auto token = *it;
-
-		// Is the current character a reserved token or not?
-		if (!is_reserved(token)) {
-			oss << *it++;
-			continue;
-		}
-
-		// The token was at the end, just write it and return now.
-		if (++it == end) {
-			oss << token;
-			continue;
-		}
-
-		// The token is declaring a template variable, substitute it.
-		if (*it == '{') {
-			oss << substitute(++it, end, token, params);
-			continue;
-		}
-
-		/*
-		 * If the next token is different from the previous one, just let the
-		 * next iteration parse the string because we can have the following
-		 * constructs.
-		 *
-		 * "@#{var}" -> "@value"
-		 */
-		if (*it != token) {
-			oss << token;
-			continue;
-		}
-
-		/*
-		 * Write the token only if it's not a variable because at this step we
-		 * may have the following constructs.
-		 *
-		 * "##" -> "##"
-		 * "##hello" -> "##hello"
-		 * "##{hello}" -> "#{hello}"
-		 */
-		if (++it == end)
-			oss << token << token;
-		else if (*it == '{')
-			oss << token;
-	}
-
-	return oss.str();
-}
-
-// }}}
-
-// {{{ strip
-
-auto strip(std::string str) noexcept -> std::string
-{
-	const auto test = [] (auto c) noexcept { return !std::isspace(c); };
-
-	str.erase(str.begin(), std::find_if(str.begin(), str.end(), test));
-	str.erase(std::find_if(str.rbegin(), str.rend(), test).base(), str.end());
-
-	return str;
-}
-
-// }}}
-
-// {{{ split
-
-auto split(std::string_view list, const std::string& delimiters, int max) -> std::vector<std::string>
-{
-	std::vector<std::string> result;
-	std::size_t next = -1, current;
-	int count = 1;
-	bool finished = false;
-
-	if (list.empty())
-		return result;
-
-	do {
-		std::string val;
-
-		current = next + 1;
-		next = list.find_first_of(delimiters, current);
-
-		// split max, get until the end.
-		if (max >= 0 && count++ >= max) {
-			val = list.substr(current, std::string::npos);
-			finished = true;
-		} else {
-			val = list.substr(current, next - current);
-			finished = next == std::string::npos;
-		}
-
-		result.push_back(val);
-	} while (!finished);
-
-	return result;
-}
-
-// }}}
-
-// {{{ is_identifier
-
-auto is_identifier(std::string_view name) noexcept -> bool
-{
-	static const std::regex regex("[A-Za-z0-9-_]+");
-
-	return std::regex_match(std::string(name), regex);
-}
-
-// }}}
-
-// {{{ is_boolean
-
-auto is_boolean(std::string value) noexcept -> bool
-{
-	std::transform(value.begin(), value.end(), value.begin(), [] (auto c) noexcept {
-		return toupper(c);
-	});
-
-	return value == "1" || value == "YES" || value == "TRUE" || value == "ON";
-}
-
-// }}}
-
-} // !util::string_util
--- a/libirccd-core/irccd/string_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,402 +0,0 @@
-/*
- * string_util.hpp -- string utilities
- *
- * Copyright (c) 2013-2018 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_STRING_UTIL_HPP
-#define IRCCD_STRING_UTIL_HPP
-
-/**
- * \file string_util.hpp
- * \brief String utilities.
- */
-
-#include "sysconfig.hpp"
-
-#include <ctime>
-#include <initializer_list>
-#include <limits>
-#include <optional>
-#include <sstream>
-#include <stdexcept>
-#include <string>
-#include <string_view>
-#include <type_traits>
-#include <unordered_map>
-#include <vector>
-
-/**
- * \brief String utilities.
- */
-namespace irccd::string_util {
-
-// {{{ subst
-
-/**
- * \brief Disable or enable some features.
- */
-enum class subst_flags : unsigned {
-	date            = (1 << 0),      //!< date templates
-	keywords        = (1 << 1),      //!< keywords
-	env             = (1 << 2),      //!< environment variables
-	shell           = (1 << 3),      //!< command line command
-	irc_attrs       = (1 << 4),      //!< IRC escape codes
-	shell_attrs     = (1 << 5)       //!< shell attributes
-};
-
-/**
- * \cond IRCCD_ENUM_HIDDEN_SYMBOLS
- */
-
-/**
- * Apply bitwise XOR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator^(subst_flags v1, subst_flags v2) noexcept -> subst_flags
-{
-	return static_cast<subst_flags>(static_cast<unsigned>(v1) ^ static_cast<unsigned>(v2));
-}
-
-/**
- * Apply bitwise AND.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator&(subst_flags v1, subst_flags v2) noexcept -> subst_flags
-{
-	return static_cast<subst_flags>(static_cast<unsigned>(v1) & static_cast<unsigned>(v2));
-}
-
-/**
- * Apply bitwise OR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator|(subst_flags v1, subst_flags v2) noexcept -> subst_flags
-{
-	return static_cast<subst_flags>(static_cast<unsigned>(v1) | static_cast<unsigned>(v2));
-}
-
-/**
- * Apply bitwise NOT.
- *
- * \param v the value
- * \return the complement
- */
-inline auto operator~(subst_flags v) noexcept -> subst_flags
-{
-	return static_cast<subst_flags>(~static_cast<unsigned>(v));
-}
-
-/**
- * Assign bitwise OR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator|=(subst_flags& v1, subst_flags v2) noexcept -> subst_flags&
-{
-	return v1 = v1 | v2;
-}
-
-/**
- * Assign bitwise AND.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator&=(subst_flags& v1, subst_flags v2) noexcept -> subst_flags&
-{
-	return v1 = v1 & v2;
-}
-
-/**
- * Assign bitwise XOR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator^=(subst_flags& v1, subst_flags v2) noexcept -> subst_flags&
-{
-	return v1 = v1 ^ v2;
-}
-
-/**
- * \endcond
- */
-
-/**
- * \brief Used for format() function.
- */
-class subst {
-public:
-	/**
-	 * Flags for selecting templates.
-	 */
-	subst_flags flags{
-		subst_flags::date |
-		subst_flags::keywords |
-		subst_flags::env |
-		subst_flags::irc_attrs
-	};
-
-	/**
-	 * Fill that field if you want a date.
-	 */
-	std::time_t time{std::time(nullptr)};
-
-	/**
-	 * Fill that map if you want to replace keywords.
-	 */
-	std::unordered_map<std::string, std::string> keywords;
-};
-
-/**
- * Format a string and update all templates.
- *
- * ## Syntax
- *
- * The syntax is <strong>?{}</strong> where <strong>?</strong> is replaced by
- * one of the token defined below. Braces are mandatory and cannot be ommited.
- *
- * To write a literal template construct, prepend the token twice.
- *
- * ## Availables templates
- *
- * The following templates are available:
- *
- * - <strong>\#{name}</strong>: name will be substituted from the keywords in
- *   params,
- * - <strong>\${name}</strong>: name will be substituted from the environment
- *   variable,
- * - <strong>\@{attributes}</strong>: the attributes will be substituted to IRC
- *   or shell colors (see below),
- * - <strong>%</strong>, any format accepted by strftime(3).
- *
- * ## Attributes
- *
- * The attribute format is composed of three parts, foreground, background and
- * modifiers, each separated by a comma.
- *
- * **Note:** you cannot omit parameters, to specify the background, you must
- * specify the foreground.
- *
- * ## Examples
- *
- * ### Valid constructs
- *
- * - <strong>\#{target}, welcome</strong>: if target is set to "irccd",
- *   becomes "irccd, welcome",
- * - <strong>\@{red}\#{target}</strong>: if target is specified, it is written
- *   in red,
- *
- * ### Invalid or literals constructs
- *
- * - <strong>\#\#{target}</strong>: will output "\#{target}",
- * - <strong>\#\#</strong>: will output "\#\#",
- * - <strong>\#target</strong>: will output "\#target",
- * - <strong>\#{target</strong>: will throw std::invalid_argument.
- *
- * ### Colors & attributes
- *
- * - <strong>\@{red,blue}</strong>: will write text red on blue background,
- * - <strong>\@{default,yellow}</strong>: will write default color text on
- *   yellow background,
- * - <strong>\@{white,black,bold,underline}</strong>: will write white text on
- *   black in both bold and underline.
- *
- * \param text the text to format
- * \param params the additional options
- * \return the modified text
- */
-auto format(std::string text, const subst& params = {}) -> std::string;
-
-// }}}
-
-// {{{ strip
-
-/**
- * Remove leading and trailing spaces.
- *
- * \param str the string
- * \return the removed white spaces
- */
-auto strip(std::string str) noexcept -> std::string;
-
-// }}}
-
-// {{{ split
-
-/**
- * Split a string by delimiters.
- *
- * \param list the string to split
- * \param delimiters a list of delimiters
- * \param max max number of split
- * \return a list of string splitted
- */
-auto split(std::string_view list, const std::string& delimiters, int max = -1) -> std::vector<std::string>;
-
-// }}}
-
-// {{{ join
-
-/**
- * Join values by a separator and return a string.
- *
- * \param first the first iterator
- * \param last the last iterator
- * \param delim the optional delimiter
- * \return the string
- */
-template <typename InputIt, typename DelimType = char>
-auto join(InputIt first, InputIt last, DelimType delim = ':') -> std::string
-{
-	std::ostringstream oss;
-
-	if (first != last) {
-		oss << *first;
-
-		while (++first != last)
-			oss << delim << *first;
-	}
-
-	return oss.str();
-}
-
-/**
- * Overloaded function that takes a container.
- *
- * \param c the container
- * \param delim the optional delimiter
- * \return the string
- */
-template <typename Container, typename DelimType = char>
-auto join(const Container& c, DelimType delim = ':') -> std::string
-{
-	return join(c.begin(), c.end(), delim);
-}
-
-/**
- * Convenient overload.
- *
- * \param list the initializer list
- * \param delim the delimiter
- * \return the string
- */
-template <typename T, typename DelimType = char>
-auto join(std::initializer_list<T> list, DelimType delim = ':') -> std::string
-{
-	return join(list.begin(), list.end(), delim);
-}
-
-// }}}
-
-// {{{ is_identifier
-
-/**
- * Check if a string is a valid irccd identifier.
- *
- * \param name the identifier name
- * \return true if is valid
- */
-auto is_identifier(std::string_view name) noexcept -> bool;
-
-// }}}
-
-// {{{ is_boolean
-
-/**
- * Check if the value is a boolean, 1, yes and true are accepted.
- *
- * \param value the value
- * \return true if is boolean
- * \note this function is case-insensitive
- */
-auto is_boolean(std::string value) noexcept -> bool;
-
-// }}}
-
-// {{{ to_int
-
-/**
- * Convert the given string into a signed integer.
- *
- * \param str the string to convert
- * \param min the minimum value allowed
- * \param max the maximum value allowed
- * \return the value or boost::none if not convertible
- */
-template <typename T = int>
-auto to_int(const std::string& str,
-            T min = std::numeric_limits<T>::min(),
-            T max = std::numeric_limits<T>::max()) noexcept -> std::optional<T>
-{
-	static_assert(std::is_signed<T>::value, "must be signed");
-
-	char* end;
-	auto v = std::strtoll(str.c_str(), &end, 10);
-
-	if (*end != '\0' || v < min || v > max)
-		return std::nullopt;
-
-	return static_cast<T>(v);
-}
-
-// }}}
-
-// {{{ to_uint
-
-/**
- * Convert the given string into a unsigned integer.
- *
- * \note invalid numbers are valid as well
- * \param str the string to convert
- * \param min the minimum value allowed
- * \param max the maximum value allowed
- * \return the value or boost::none if not convertible
- */
-template <typename T = unsigned>
-auto to_uint(const std::string& str,
-             T min = std::numeric_limits<T>::min(),
-             T max = std::numeric_limits<T>::max()) noexcept -> std::optional<T>
-{
-	static_assert(std::is_unsigned<T>::value, "must be unsigned");
-
-	char* end;
-	auto v = std::strtoull(str.c_str(), &end, 10);
-
-	if (*end != '\0' || v < min || v > max)
-		return std::nullopt;
-
-	return static_cast<T>(v);
-}
-
-// }}}
-
-} // !irccd::string_util
-
-#endif // !IRCCD_STRING_UTIL_HPP
--- a/libirccd-core/irccd/system.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,491 +0,0 @@
-/*
- * system.cpp -- platform dependent functions for system inspection
- *
- * Copyright (c) 2013-2018 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 <cassert>
-#include <cerrno>
-#include <cstdlib>
-#include <cstring>
-#include <ctime>
-#include <stdexcept>
-#include <string>
-
-#include <boost/dll.hpp>
-#include <boost/filesystem.hpp>
-#include <boost/predef/os.h>
-
-#include "sysconfig.hpp"
-
-#if BOOST_OS_WINDOWS
-#	include <sys/timeb.h>
-#	include <shlobj.h>
-#else
-#	include <sys/utsname.h>
-#	include <sys/types.h>
-#	include <sys/param.h>
-#	include <sys/time.h>
-#	include <unistd.h>
-#endif
-
-#if BOOST_OS_LINUX
-#	include <sys/sysinfo.h>
-#endif
-
-#if BOOST_OS_MACOS
-#	include <sys/sysctl.h>
-#	include <libproc.h>
-#endif
-
-#include "system.hpp"
-#include "string_util.hpp"
-#include "xdg.hpp"
-
-namespace irccd::sys {
-
-namespace {
-
-// {{{ base_directory
-
-/*
- * base_directory
- * ------------------------------------------------------------------
- *
- * Get the base program directory.
- *
- * If irccd has been compiled with relative paths, the base directory is
- * evaluated by climbing the `bindir' directory from the executable path.
- *
- * Otherwise, use the installation prefix.
- */
-auto base_directory() -> boost::filesystem::path
-{
-	static const boost::filesystem::path bindir(IRCCD_INSTALL_BINDIR);
-	static const boost::filesystem::path prefix(IRCCD_INSTALL_PREFIX);
-
-	boost::filesystem::path path(".");
-
-	if (bindir.is_relative()) {
-		try {
-			path = boost::dll::program_location();
-			path = path.parent_path();
-		} catch (...) {
-			path = ".";
-		}
-
-		// Compute relative base directory.
-		for (auto len = std::distance(bindir.begin(), bindir.end()); len > 0; len--)
-			path = path.parent_path();
-		if (path.empty())
-			path = ".";
-	} else
-		path = prefix;
-
-	return path;
-}
-
-// }}}
-
-// {{{ system_directory
-
-/*
- * system_directory
- * ------------------------------------------------------------------
- *
- * Compute the system directory path for the given component.
- *
- * Referenced by:
- *
- * - cachedir,
- * - datadir,
- * - sysconfigdir,
- * - plugindir.
- */
-auto system_directory(const std::string& component) -> boost::filesystem::path
-{
-	boost::filesystem::path path(component);
-
-	if (path.is_relative())
-		path = base_directory() / component;
-
-	return path.string();
-}
-
-// }}}
-
-// {{{ user_config_directory
-
-/*
- * user_config_directory
- * ------------------------------------------------------------------
- *
- * Get user configuration directory.
- *
- * Referenced by:
- *
- * - config_filenames.
- *
- * Requires:
- *
- * - Windows:
- *   - <shlobj.h>
- */
-auto user_config_directory() -> boost::filesystem::path
-{
-	boost::filesystem::path path;
-
-#if BOOST_OS_WINDOWS
-	char folder[MAX_PATH] = {0};
-
-	if (SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, folder) == S_OK) {
-		path /= folder;
-		path /= "\\irccd\\config";
-	} else
-		path = ".";
-#else
-	try {
-		path = xdg().get_config_home();
-	} catch (...) {
-		path = sys::env("HOME");
-		path /= ".config";
-	}
-
-	path /= "irccd";
-#endif
-
-	return path;
-}
-
-// }}}
-
-// {{{ user_plugin_directory
-
-/*
- * user_plugin_directory
- * ------------------------------------------------------------------
- *
- * Referenced by:
- *
- * - plugin_filenames.
- *
- * Requires:
- *
- * - Windows:
- *   - <shlobj.h>
- *
- * Like add user_config_directory but for plugins.
- */
-auto user_plugin_directory() -> boost::filesystem::path
-{
-	boost::filesystem::path path;
-
-#if BOOST_OS_WINDOWS
-	char folder[MAX_PATH] = {0};
-
-	if (SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, folder) == S_OK) {
-		path /= folder;
-		path /= "\\irccd\\share";
-	}
-#else
-	try {
-		path = xdg().get_data_home();
-	} catch (...) {
-		path = sys::env("HOME");
-		path /= ".local/share";
-	}
-
-	path /= "irccd";
-#endif
-
-	return path / "plugins";
-}
-
-// }}}
-
-} // !namespace
-
-// {{{ set_program_name
-
-void set_program_name(std::string name) noexcept
-{
-#if defined(IRCCD_HAVE_SETPROGNAME)
-	static std::string save = name;
-
-	setprogname(save.c_str());
-#else
-	(void)name;
-#endif
-}
-
-// }}}
-
-// {{{ name
-
-auto name() -> std::string
-{
-#if BOOST_OS_LINUX
-	return "Linux";
-#elif BOOST_OS_WINDOWS
-	return "Windows";
-#elif BOOST_OS_BSD_FREE
-	return "FreeBSD";
-#elif BOOST_OS_BSD_DRAGONFLY
-	return "DragonFlyBSD";
-#elif BOOST_OS_BSD_OPEN
-	return "OpenBSD";
-#elif BOOST_OS_BSD_NET
-	return "NetBSD";
-#elif BOOST_OS_MACOS
-	return "macOS";
-#elif BOOST_OS_ANDROID
-	return "Android";
-#elif BOOST_OS_AIX
-	return "Aix";
-#elif BOOST_OS_HAIKU
-	return "Haiku";
-#elif BOOST_OS_IOS
-	return "iOS";
-#elif BOOST_OS_SOLARIS
-	return "Solaris";
-#else
-	return "Unknown";
-#endif
-}
-
-// }}}
-
-// {{{ version
-
-/*
- * Requires:
- *
- * - Windows:
- *   - <windows.h>
- * - Others:
- *   - <sys/utsname.h>
- */
-auto version() -> std::string
-{
-#if BOOST_OS_WINDOWS
-	const auto version = GetVersion();
-	const auto major = (DWORD)(LOBYTE(LOWORD(version)));
-	const auto minor = (DWORD)(HIBYTE(LOWORD(version)));
-
-	return std::to_string(major) + "." + std::to_string(minor);
-#else
-	struct utsname uts;
-
-	if (::uname(&uts) < 0)
-		throw std::runtime_error(std::strerror(errno));
-
-	return std::string(uts.release);
-#endif
-}
-
-// }}}
-
-// {{{ uptime
-
-/*
- * Requires:
- *
- * - Windows:
- *   - <windows.h>
- * - Linux:
- *   - <sys/sysinfo.h>
- * - Mac:
- *   - <sys/types.h>
- *   - <sys/sysctl.h>
- * - Others:
- *   - <ctime>
- */
-auto uptime() -> std::uint64_t
-{
-#if BOOST_OS_WINDOWS
-	return ::GetTickCount64() / 1000;
-#elif BOOST_OS_LINUX
-	struct sysinfo info;
-
-	if (sysinfo(&info) < 0)
-		throw std::runtime_error(std::strerror(errno));
-
-	return info.uptime;
-#elif BOOST_OS_MACOS
-	struct timeval boottime;
-	size_t length = sizeof (boottime);
-	int mib[2] = { CTL_KERN, KERN_BOOTTIME };
-
-	if (sysctl(mib, 2, &boottime, &length, nullptr, 0) < 0)
-		throw std::runtime_error(std::strerror(errno));
-
-	time_t bsec = boottime.tv_sec, csec = time(nullptr);
-
-	return difftime(csec, bsec);
-#else
-	struct timespec ts;
-
-	if (clock_gettime(CLOCK_UPTIME, &ts) < 0)
-		throw std::runtime_error(std::strerror(errno));
-
-	return ts.tv_sec;
-#endif
-}
-
-// }}}
-
-// {{{ ticks
-
-/*
- * Requires:
- *
- * - Windows:
- *   - <sys/timeb.h>
- * - Others:
- *   - <sys/times.h>
- */
-auto ticks() -> std::uint64_t
-{
-#if BOOST_OS_WINDOWS
-	_timeb tp;
-
-	_ftime(&tp);
-
-	return tp.time * 1000LL + tp.millitm;
-#else
-	struct timeval tp;
-
-	gettimeofday(&tp, NULL);
-
-	return tp.tv_sec * 1000LL + tp.tv_usec / 1000;
-#endif
-}
-
-// }}}
-
-// {{{ home
-
-/*
- * Requires:
- *
- * - Windows:
- *   - <shlobj.h>
- */
-auto home() -> std::string
-{
-#if BOOST_OS_WINDOWS
-	char path[MAX_PATH];
-
-	if (SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, path) != S_OK)
-		return "";
-
-	return std::string(path);
-#else
-	return env("HOME");
-#endif
-}
-
-// }}}
-
-// {{{ env
-
-/*
- * Requires:
- *
- * - <cstdlib>
- */
-auto env(const std::string& var) -> std::string
-{
-	const auto value = std::getenv(var.c_str());
-
-	if (value == nullptr)
-		return "";
-
-	return value;
-}
-
-// }}}
-
-// {{{ cachedir
-
-auto cachedir() -> boost::filesystem::path
-{
-	return system_directory(IRCCD_INSTALL_LOCALSTATEDIR) / "cache/irccd";
-}
-
-// }}}
-
-// {{{ datadir
-
-auto datadir() -> boost::filesystem::path
-{
-	return system_directory(IRCCD_INSTALL_DATADIR);
-}
-
-// }}}
-
-// {{{ sysconfdir
-
-auto sysconfdir() -> boost::filesystem::path
-{
-	return system_directory(IRCCD_INSTALL_SYSCONFDIR) / "irccd";
-}
-
-// }}}
-
-// {{{ plugindir
-
-auto plugindir() -> boost::filesystem::path
-{
-	return system_directory(IRCCD_INSTALL_LIBDIR) / "irccd";
-}
-
-// }}}
-
-// {{{ config_filenames
-
-auto config_filenames(std::string_view file) -> std::vector<std::string>
-{
-	// TODO: remove this once we can use std::filesystem.
-	const std::string filename(file);
-
-	return {
-		(user_config_directory() / filename).string(),
-		(sysconfdir() / filename).string()
-	};
-}
-
-// }}}
-
-// {{{ plugin_filenames
-
-auto plugin_filenames(const std::string& name,
-                      const std::vector<std::string>& extensions) -> std::vector<std::string>
-{
-	assert(!extensions.empty());
-
-	std::vector<std::string> result;
-
-	for (const auto& ext : extensions)
-		result.push_back((user_plugin_directory() / (name + ext)).string());
-	for (const auto& ext : extensions)
-		result.push_back((plugindir() / (name + ext)).string());
-
-	return result;
-}
-
-// }}}
-
-} // !irccd::sys
--- a/libirccd-core/irccd/system.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,165 +0,0 @@
-/*
- * system.hpp -- platform dependent functions for system inspection
- *
- * Copyright (c) 2013-2018 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_SYSTEM_HPP
-#define IRCCD_SYSTEM_HPP
-
-/**
- * \file system.hpp
- * \brief System dependant functions
- */
-
-#include <cstdint>
-#include <string>
-#include <string_view>
-#include <vector>
-
-#include <boost/filesystem.hpp>
-
-#include "sysconfig.hpp"
-
-/**
- * \brief Namespace for system functions.
- */
-namespace irccd::sys {
-
-/**
- * Set the program name, needed for some functions or some systems.
- *
- * \param name the program name
- */
-void set_program_name(std::string name) noexcept;
-
-/**
- * Get the system name.
- *
- * \return the name
- */
-auto name() -> std::string;
-
-/**
- * Get the system version.
- *
- * \return the version
- */
-auto version() -> std::string;
-
-/**
- * Get the number of seconds elapsed since the boottime.
- *
- * \return the number of seconds
- */
-auto uptime() -> std::uint64_t;
-
-/**
- * Get the milliseconds elapsed since the application
- * startup.
- *
- * \return the milliseconds
- */
-auto ticks() -> std::uint64_t;
-
-/**
- * Get an environment variable.
- *
- * \param var the environment variable
- * \return the value or empty string
- */
-auto env(const std::string& var) -> std::string;
-
-/**
- * Get home directory usually /home/foo
- *
- * \return the home directory
- */
-auto home() -> std::string;
-
-/**
- * Get the cache directory as specified as compile time option
- * IRCCD_INSTALL_LOCALSTATEDIR, if the value is absolute, it is returned as-is.
- *
- * If the component is relative, it is evaluated using the binary executable
- * path.
- *
- * \return the evaluated cache directory.
- * \see datadir
- * \see configdir
- */
-auto cachedir() -> boost::filesystem::path;
-
-/**
- * Like cachedir but for IRCCD_INSTALL_DATADIR.
- *
- * \return the evaluated data directory.
- * \see cachedir
- * \see datadir
- */
-auto datadir() -> boost::filesystem::path;
-
-/**
- * Like cachedir but for IRCCD_INSTALL_SYSCONFDIR.
- *
- * \return the evaluated config directory.
- * \see cachedir
- * \see datadir
- * \note use config_filenames for irccd.conf, irccdctl.conf files
- */
-auto sysconfdir() -> boost::filesystem::path;
-
-/**
- * Like cachedir but for IRCCD_INSTALL_LIBDIR.
- *
- * \return the evaluated system plugin directory.
- * \see cachedir
- * \see datadir
- */
-auto plugindir() -> boost::filesystem::path;
-
-/**
- * Get user account login or empty if not available.
- *
- * \return the user account name
- */
-auto username() -> std::string;
-
-/**
- * Construct a list of paths to read configuration files from.
- *
- * This function does not test the presence of the files as a condition race
- * may occur.
- *
- * The caller is responsible of opening files for each path.
- *
- * \param file the filename to append for convenience
- * \return the list of paths to check in order
- */
-auto config_filenames(std::string_view file) -> std::vector<std::string>;
-
-/**
- * Construct a list of paths for reading plugins.
- *
- * \param name the plugin id (without extension)
- * \param extensions the list of extensions supported
- * \return the list of filenames to check
- */
-auto plugin_filenames(const std::string& name,
-                      const std::vector<std::string>& extensions) -> std::vector<std::string>;
-
-} // !irccd::sys
-
-#endif // !IRCCD_SYSTEM_HPP
--- a/libirccd-core/irccd/xdg.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,192 +0,0 @@
-/*
- * xdg.hpp -- XDG directory specifications
- *
- * Copyright (c) 2013-2018 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_XDG_HPP
-#define IRCCD_XDG_HPP
-
-/**
- * \file xdg.hpp
- * \brief XDG directory specifications.
- * \author David Demelier <markand@malikana.fr>
- */
-
-#include <cstdlib>
-#include <sstream>
-#include <stdexcept>
-#include <string>
-#include <vector>
-
-namespace irccd {
-
-/**
- * \brief XDG directory specifications.
- *
- * Read and get XDG directories.
- *
- * This file should compiles on Windows to facilitate portability but its
- * functions must not be used.
- */
-class xdg {
-private:
-	std::string config_home_;
-	std::string data_home_;
-	std::string cache_home_;
-	std::string runtime_dir_;
-	std::vector<std::string> config_dirs_;
-	std::vector<std::string> data_dirs_;
-
-	auto is_absolute(const std::string& path) const noexcept -> bool
-	{
-		return path.length() > 0 && path[0] == '/';
-	}
-
-	auto split(const std::string& arg) const -> std::vector<std::string>
-	{
-		std::stringstream iss(arg);
-		std::string item;
-		std::vector<std::string> elems;
-
-		while (std::getline(iss, item, ':')) {
-			if (is_absolute(item))
-				elems.push_back(item);
-		}
-
-		return elems;
-	}
-
-	auto env_or_home(const std::string& var, const std::string& repl) const -> std::string
-	{
-		auto value = std::getenv(var.c_str());
-
-		if (value == nullptr || !is_absolute(value)) {
-			auto home = std::getenv("HOME");
-
-			if (home == nullptr)
-				throw std::runtime_error("could not get home directory");
-
-			return std::string(home) + "/" + repl;
-		}
-
-		return value;
-	}
-
-	auto list_or_defaults(const std::string& var,
-	                      const std::vector<std::string>& list) const -> std::vector<std::string>
-	{
-		const auto value = std::getenv(var.c_str());
-
-		if (!value)
-			return list;
-
-		// No valid item at all? Use defaults.
-		if (const auto result = split(value); !result.empty())
-			return result;
-
-		return list;
-	}
-
-public:
-	/**
-	 * Open an xdg instance and load directories.
-	 *
-	 * \throw std::runtime_error on failures
-	 */
-	xdg()
-		: config_home_(env_or_home("XDG_CONFIG_HOME", ".config"))
-		, data_home_(env_or_home("XDG_DATA_HOME", ".local/share"))
-		, cache_home_(env_or_home("XDG_CACHE_HOME", ".cache"))
-		, config_dirs_(list_or_defaults("XDG_CONFIG_DIRS", { "/etc/xdg" }))
-		, data_dirs_(list_or_defaults("XDG_DATA_DIRS", { "/usr/local/share", "/usr/share" }))
-	{
-
-		/*
-		 * Runtime directory is a special case and does not have a replacement,
-		 * the application should manage this by itself.
-		 */
-		if (const auto runtime = std::getenv("XDG_RUNTIME_DIR"); runtime && is_absolute(runtime))
-			runtime_dir_ = runtime;
-	}
-
-	/**
-	 * Get the config directory. ${XDG_CONFIG_HOME} or ${HOME}/.config
-	 *
-	 * \return the config directory
-	 */
-	auto get_config_home() const noexcept -> const std::string&
-	{
-		return config_home_;
-	}
-
-	/**
-	 * Get the data directory. ${XDG_DATA_HOME} or ${HOME}/.local/share
-	 *
-	 * \return the data directory
-	 */
-	auto get_data_home() const noexcept -> const std::string&
-	{
-		return data_home_;
-	}
-
-	/**
-	 * Get the cache directory. ${XDG_CACHE_HOME} or ${HOME}/.cache
-	 *
-	 * \return the cache directory
-	 */
-	auto get_cache_home() const noexcept -> const std::string&
-	{
-		return cache_home_;
-	}
-
-	/**
-	 * Get the runtime directory.
-	 *
-	 * There is no replacement for XDG_RUNTIME_DIR, if it is not set, an empty
-	 * value is returned and the user is responsible of using something else.
-	 *
-	 * \return the runtime directory
-	 */
-	auto get_runtime_dir() const noexcept -> const std::string&
-	{
-		return runtime_dir_;
-	}
-
-	/**
-	 * Get the standard config directories. ${XDG_CONFIG_DIRS} or { "/etc/xdg" }
-	 *
-	 * \return the list of config directories
-	 */
-	auto get_config_dirs() const noexcept -> const std::vector<std::string>&
-	{
-		return config_dirs_;
-	}
-
-	/**
-	 * Get the data directories. ${XDG_DATA_DIRS} or { "/usr/local/share",
-	 * "/usr/share" }
-	 *
-	 * \return the list of data directories
-	 */
-	auto get_data_dirs() const noexcept -> const std::vector<std::string>&
-	{
-		return data_dirs_;
-	}
-};
-
-} // !irccd
-
-#endif // !IRCCD_XDG_HPP
--- a/libirccd-ctl/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-ctl/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -30,7 +30,7 @@
 	HEADERS ${libirccd-ctl_SOURCE_DIR}/irccd/ctl
 	SOURCES ${SOURCES}
 	LIBRARIES
-		libirccd
+		libirccd-daemon
 	PUBLIC_INCLUDES
 		$<BUILD_INTERFACE:${libirccd-ctl_SOURCE_DIR}>
 )
--- a/libirccd-ctl/irccd/ctl/controller.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-ctl/irccd/ctl/controller.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,7 @@
 #include <irccd/sysconfig.hpp>
 #include <irccd/json_util.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 #include <irccd/daemon/plugin.hpp>
 #include <irccd/daemon/rule.hpp>
@@ -30,6 +30,11 @@
 
 using irccd::json_util::deserializer;
 
+using irccd::daemon::bot_error;
+using irccd::daemon::plugin_error;
+using irccd::daemon::rule_error;
+using irccd::daemon::server_error;
+
 namespace irccd::ctl {
 
 void controller::authenticate(connect_handler handler, nlohmann::json info)
@@ -64,9 +69,9 @@
 		const auto major = doc.get<int>("major");
 
 		if (!program && *program != "irccd")
-			handler(irccd_error::not_irccd, std::move(message));
+			handler(bot_error::not_irccd, std::move(message));
 		else if (major && *major != IRCCD_VERSION_MAJOR)
-			handler(irccd_error::incompatible_version, std::move(message));
+			handler(bot_error::incompatible_version, std::move(message));
 		else {
 			if (!password_.empty())
 				authenticate(std::move(handler), message);
@@ -126,7 +131,7 @@
 
 		if (e && c) {
 			if (*c == "irccd")
-				code = make_error_code(static_cast<irccd_error::error>(*e));
+				code = make_error_code(static_cast<bot_error::error>(*e));
 			else if (*c == "server")
 				code = make_error_code(static_cast<server_error::error>(*e));
 			else if (*c == "plugin")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,66 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+project(libirccd-daemon)
+
+set(
+	SOURCES
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/bot.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/bot.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/command.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/command.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/dynlib_plugin.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/dynlib_plugin.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/irc.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/irc.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/logger.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/logger.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/plugin.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/plugin.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/plugin_service.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/plugin_service.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/rule.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/rule.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/rule_service.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/rule_service.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/rule_util.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/rule_util.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/server.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/server.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/server_service.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/server_service.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/server_util.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/server_util.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_client.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_client.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_server.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_server.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_service.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_service.hpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_util.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_util.hpp
+)
+
+irccd_define_library(
+	TARGET libirccd-daemon
+	EXPORT
+	HEADERS ${libirccd-daemon_SOURCE_DIR}/irccd/daemon
+	SOURCES ${SOURCES}
+	LIBRARIES libirccd
+	PUBLIC_INCLUDES $<BUILD_INTERFACE:${libirccd-daemon_SOURCE_DIR}>
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/bot.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,324 @@
+/*
+ * bot.cpp -- main bot class
+ *
+ * Copyright (c) 2013-2018 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 <fstream>
+
+#include <boost/predef/os.h>
+
+#include <irccd/string_util.hpp>
+#include <irccd/system.hpp>
+
+#include "bot.hpp"
+#include "logger.hpp"
+#include "plugin_service.hpp"
+#include "rule_service.hpp"
+#include "server_service.hpp"
+#include "transport_service.hpp"
+
+namespace irccd::daemon {
+
+namespace {
+
+class format_filter : public logger::filter {
+private:
+	std::string info_;
+	std::string warning_;
+	std::string debug_;
+
+	auto convert(const std::string&,
+	             std::string_view,
+	             std::string_view,
+	             std::string_view) const -> std::string;
+
+public:
+	format_filter(std::string info, std::string warning, std::string debug) noexcept;
+
+	auto pre_debug(std::string_view,
+	               std::string_view,
+	               std::string_view) const -> std::string override;
+
+	auto pre_info(std::string_view,
+	              std::string_view,
+	              std::string_view) const -> std::string override;
+
+	auto pre_warning(std::string_view,
+	                 std::string_view,
+	                 std::string_view) const -> std::string override;
+};
+
+auto format_filter::convert(const std::string& tmpl,
+                            std::string_view category,
+                            std::string_view component,
+                            std::string_view message) const -> std::string
+{
+	if (tmpl.empty())
+		return pre(category, component, message);
+
+	string_util::subst params;
+
+	params.flags &= ~(string_util::subst_flags::irc_attrs);
+	params.flags |= string_util::subst_flags::shell_attrs;
+	params.keywords.emplace("category", std::string(category));
+	params.keywords.emplace("component", std::string(component));
+	params.keywords.emplace("message", std::string(message));
+
+	return string_util::format(tmpl, params);
+}
+
+format_filter::format_filter(std::string info, std::string warning, std::string debug) noexcept
+	: info_(std::move(info))
+	, warning_(std::move(warning))
+	, debug_(std::move(debug))
+{
+}
+
+auto format_filter::pre_debug(std::string_view category,
+                              std::string_view component,
+                              std::string_view message) const -> std::string
+{
+	return convert(debug_, category, component, message);
+}
+
+auto format_filter::pre_info(std::string_view category,
+                             std::string_view component,
+                             std::string_view message) const -> std::string
+{
+	return convert(info_, category, component, message);
+}
+
+auto format_filter::pre_warning(std::string_view category,
+                                std::string_view component,
+                                std::string_view message) const -> std::string
+{
+	return convert(warning_, category, component, message);
+}
+
+} // !namespace
+
+void bot::load_logs_file(const ini::section& sc)
+{
+	/*
+	 * TODO: improve that with CMake options.
+	 */
+#if BOOST_OS_WINDOWS
+	std::string normal = "log.txt";
+	std::string errors = "errors.txt";
+#else
+	std::string normal = "/var/log/irccd/log.txt";
+	std::string errors = "/var/log/irccd/errors.txt";
+#endif
+
+	ini::section::const_iterator it;
+
+	if ((it = sc.find("path-logs")) != sc.end())
+		normal = it->get_value();
+	if ((it = sc.find("path-errors")) != sc.end())
+		errors = it->get_value();
+
+	try {
+		sink_ = std::make_unique<logger::file_sink>(std::move(normal), std::move(errors));
+	} catch (const std::exception& ex) {
+		sink_->warning("logs", "") << ex.what() << std::endl;
+	}
+}
+
+void bot::load_logs_syslog()
+{
+#if defined(IRCCD_HAVE_SYSLOG)
+	sink_ = std::make_unique<logger::syslog_sink>();
+#else
+	sink_->warning("logs", "") << "logs: syslog is not available on this platform" << std::endl;
+#endif // !IRCCD_HAVE_SYSLOG
+}
+
+void bot::load_logs()
+{
+	const auto sc = config_.get("logs");
+
+	if (sc.empty())
+		return;
+
+	sink_->set_verbose(string_util::is_identifier(sc.get("verbose").get_value()));
+
+	const auto type = sc.get("type").get_value();
+
+	if (!type.empty()) {
+		// Console is the default, no test case.
+		if (type == "file")
+			load_logs_file(sc);
+		else if (type == "syslog")
+			load_logs_syslog();
+		else if (type != "console")
+			sink_->warning("logs", "") << "invalid log type '" << type << std::endl;
+	}
+}
+
+void bot::load_formats()
+{
+	const auto sc = config_.get("format");
+
+	if (sc.empty())
+		return;
+
+	sink_->set_filter(std::make_unique<format_filter>(
+		sc.get("info").get_value(),
+		sc.get("warning").get_value(),
+		sc.get("debug").get_value()
+	));
+}
+
+bot::bot(boost::asio::io_service& service, std::string config)
+	: config_(std::move(config))
+	, service_(service)
+	, sink_(std::make_unique<logger::console_sink>())
+	, server_service_(std::make_unique<server_service>(*this))
+	, tpt_service_(std::make_unique<transport_service>(*this))
+	, rule_service_(std::make_unique<rule_service>(*this))
+	, plugin_service_(std::make_unique<plugin_service>(*this))
+{
+}
+
+bot::~bot() = default;
+
+auto bot::get_config() const noexcept -> const config&
+{
+	return config_;
+}
+
+void bot::set_config(config cfg) noexcept
+{
+	config_ = std::move(cfg);
+}
+
+auto bot::get_service() const noexcept -> const boost::asio::io_service&
+{
+	return service_;
+}
+
+auto bot::get_service() noexcept -> boost::asio::io_service&
+{
+	return service_;
+}
+
+auto bot::get_log() const noexcept -> const logger::sink&
+{
+	return *sink_;
+}
+
+auto bot::get_log() noexcept -> logger::sink&
+{
+	return *sink_;
+}
+
+auto bot::servers() noexcept -> server_service&
+{
+	return *server_service_;
+}
+
+auto bot::transports() noexcept -> transport_service&
+{
+	return *tpt_service_;
+}
+
+auto bot::rules() noexcept -> rule_service&
+{
+	return *rule_service_;
+}
+
+auto bot::plugins() noexcept -> plugin_service&
+{
+	return *plugin_service_;
+}
+
+void bot::set_log(std::unique_ptr<logger::sink> sink) noexcept
+{
+	assert(sink);
+
+	sink_ = std::move(sink);
+}
+
+void bot::load() noexcept
+{
+	/*
+	 * Order matters, please be careful when changing this.
+	 *
+	 * 1. Open logs as early as possible to use the defined outputs on any
+	 *    loading errors.
+	 */
+
+	// [logs] and [format] sections.
+	load_logs();
+	load_formats();
+
+	if (!loaded_)
+		sink_->info("irccd", "") << "loading configuration from " << config_.get_path() << std::endl;
+	else
+		sink_->info("irccd", "") << "reloading configuration" << std::endl;
+
+	if (!loaded_)
+		tpt_service_->load(config_);
+
+	server_service_->load(config_);
+	plugin_service_->load(config_);
+	rule_service_->load(config_);
+
+	// Mark as loaded.
+	loaded_ = true;
+}
+
+auto bot_category() noexcept -> const std::error_category&
+{
+	static const class category : public std::error_category {
+	public:
+		auto name() const noexcept -> const char* override
+		{
+			return "irccd";
+		}
+
+		auto message(int e) const -> std::string override
+		{
+			switch (static_cast<bot_error::error>(e)) {
+			case bot_error::error::not_irccd:
+				return "daemon is not irccd instance";
+			case bot_error::error::incompatible_version:
+				return "major version is incompatible";
+			case bot_error::error::auth_required:
+				return "authentication is required";
+			case bot_error::error::invalid_auth:
+				return "invalid authentication";
+			case bot_error::error::invalid_message:
+				return "invalid message";
+			case bot_error::error::invalid_command:
+				return "invalid command";
+			case bot_error::error::incomplete_message:
+				return "command requires more arguments";
+			default:
+				return "no error";
+			}
+		}
+	} category;
+
+	return category;
+}
+
+auto make_error_code(bot_error::error e) noexcept -> std::error_code
+{
+	return { static_cast<int>(e), bot_category() };
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/bot.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,255 @@
+/*
+ * bot.hpp -- main bot class
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_BOT_HPP
+#define IRCCD_DAEMON_BOT_HPP
+
+/**
+ * \file bot.hpp
+ * \brief Base class for irccd front end.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <memory>
+#include <system_error>
+
+#include <boost/asio/io_service.hpp>
+
+#include <irccd/config.hpp>
+
+/**
+ * \brief Main irccd namespace
+ */
+namespace irccd::daemon {
+
+namespace logger {
+
+class sink;
+
+} // !logger
+
+class plugin_service;
+class rule_service;
+class server_service;
+class transport_service;
+
+/**
+ * \brief Irccd main instance.
+ */
+class bot {
+private:
+	// Configuration.
+	config config_;
+
+	// Main io service.
+	boost::asio::io_service& service_;
+
+	// Tells if the configuration has already been called.
+	bool loaded_{false};
+
+	// Custom logger.
+	std::unique_ptr<logger::sink> sink_;
+
+	// Services.
+	std::unique_ptr<server_service> server_service_;
+	std::unique_ptr<transport_service> tpt_service_;
+	std::unique_ptr<rule_service> rule_service_;
+	std::unique_ptr<plugin_service> plugin_service_;
+
+	// Not copyable and not movable because services have references.
+	bot(const bot&) = delete;
+	bot(bot&&) = delete;
+
+	void operator=(const bot&) = delete;
+	void operator=(bot&&) = delete;
+
+	// Load functions.
+	void load_logs_file(const ini::section&);
+	void load_logs_syslog();
+	void load_logs();
+	void load_formats();
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * This only create a barebone irccd instance.
+	 *
+	 * \param service the service
+	 * \param config the optional path to the configuration.
+	 * \see load_all
+	 * \see load_config
+	 */
+	bot(boost::asio::io_service& service, std::string config = "");
+
+	/**
+	 * Default destructor.
+	 */
+	~bot();
+
+	/**
+	 * Get the current configuration.
+	 *
+	 * \return the configuration
+	 */
+	auto get_config() const noexcept -> const config&;
+
+	/**
+	 * Set the configuration.
+	 *
+	 * \param cfg the new config
+	 */
+	void set_config(config cfg) noexcept;
+
+	/**
+	 * Get the underlying io service.
+	 *
+	 * \return the service
+	 */
+	auto get_service() const noexcept -> const boost::asio::io_service&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the service
+	 */
+	auto get_service() noexcept -> boost::asio::io_service&;
+
+	/**
+	 * Access the logger.
+	 *
+	 * \return the logger
+	 */
+	auto get_log() const noexcept -> const logger::sink&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the logger
+	 */
+	auto get_log() noexcept -> logger::sink&;
+
+	/**
+	 * Set the logger.
+	 *
+	 * \pre sink != nullptr
+	 * \param sink the new sink
+	 */
+	void set_log(std::unique_ptr<logger::sink> sink) noexcept;
+
+	/**
+	 * Access the server service.
+	 *
+	 * \return the service
+	 */
+	auto servers() noexcept -> server_service&;
+
+	/**
+	 * Access the transport service.
+	 *
+	 * \return the service
+	 */
+	auto transports() noexcept -> transport_service&;
+
+	/**
+	 * Access the rule service.
+	 *
+	 * \return the service
+	 */
+	auto rules() noexcept -> rule_service&;
+
+	/**
+	 * Access the plugin service.
+	 *
+	 * \return the service
+	 */
+	auto plugins() noexcept -> plugin_service&;
+
+	/**
+	 * Load and re-apply the configuration to the daemon.
+	 */
+	void load() noexcept;
+};
+
+/**
+ * \brief Irccd error.
+ */
+class bot_error : public std::system_error {
+public:
+	/**
+	 * \brief Irccd related errors.
+	 */
+	enum error {
+		//!< No error.
+		no_error = 0,
+
+		//!< The connected peer is not irccd.
+		not_irccd,
+
+		//!< The irccd version is too different.
+		incompatible_version,
+
+		//!< Authentication was required but not issued.
+		auth_required,
+
+		//!< Authentication was invalid.
+		invalid_auth,
+
+		//!< The message was not a valid JSON object.
+		invalid_message,
+
+		//!< The specified command does not exist.
+		invalid_command,
+
+		//!< The specified command requires more arguments.
+		incomplete_message,
+	};
+
+	/**
+	 * Inherited constructors.
+	 */
+	using system_error::system_error;
+};
+
+/**
+ * Get the irccd error category singleton.
+ *
+ * \return the singleton
+ */
+auto bot_category() noexcept -> const std::error_category&;
+
+/**
+ * Create a std::error_code from bot_error::error enum.
+ *
+ * \param e the error code
+ * \return the error code
+ */
+auto make_error_code(bot_error::error e) noexcept -> std::error_code;
+
+} // !irccd::daemon
+
+namespace std {
+
+template <>
+struct is_error_code_enum<irccd::daemon::bot_error::error> : public std::true_type {
+};
+
+} // !std
+
+#endif // !IRCCD_DAEMON_IRCCD_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/command.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,856 @@
+/*
+ * command.cpp -- remote command
+ *
+ * Copyright (c) 2013-2018 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 <irccd/string_util.hpp>
+
+#include "bot.hpp"
+#include "command.hpp"
+#include "plugin.hpp"
+#include "plugin_service.hpp"
+#include "rule.hpp"
+#include "rule_service.hpp"
+#include "rule_util.hpp"
+#include "server.hpp"
+#include "server_service.hpp"
+#include "server_util.hpp"
+#include "transport_client.hpp"
+
+using namespace std::string_literals;
+
+namespace irccd::daemon {
+
+namespace {
+
+void exec_set(transport_client& client, plugin& plugin, const nlohmann::json& args)
+{
+	assert(args.count("value") > 0);
+
+	const auto var = args.find("variable");
+	const auto value = args.find("value");
+
+	if (var == args.end() || !var->is_string())
+		throw bot_error(bot_error::error::incomplete_message);
+	if (value == args.end() || !value->is_string())
+		throw bot_error(bot_error::error::incomplete_message);
+
+	auto config = plugin.get_options();
+
+	config[*var] = *value;
+	plugin.set_options(config);
+	client.success("plugin-config");
+}
+
+void exec_get(transport_client& client, plugin& plugin, const nlohmann::json& args)
+{
+	auto variables = nlohmann::json::object();
+	auto var = args.find("variable");
+
+	if (var != args.end() && var->is_string())
+		variables[var->get<std::string>()] = plugin.get_options()[*var];
+	else
+		for (const auto& pair : plugin.get_options())
+			variables[pair.first] = pair.second;
+
+	/*
+	 * Don't put all variables into the response, put them into a sub
+         * property 'variables' instead.
+	 *
+	 * It's easier for the client to iterate over all.
+	 */
+	client.write({
+		{ "command",    "plugin-config" },
+		{ "variables",  variables       }
+	});
+}
+
+template <typename T>
+auto bind() noexcept -> command::constructor
+{
+	return [] () noexcept {
+		return std::make_unique<T>();
+	};
+}
+
+} // !namespace
+
+const std::vector<command::constructor> command::registry{
+	bind<plugin_config_command>(),
+	bind<plugin_info_command>(),
+	bind<plugin_list_command>(),
+	bind<plugin_load_command>(),
+	bind<plugin_reload_command>(),
+	bind<plugin_unload_command>(),
+	bind<rule_add_command>(),
+	bind<rule_edit_command>(),
+	bind<rule_info_command>(),
+	bind<rule_info_command>(),
+	bind<rule_list_command>(),
+	bind<rule_move_command>(),
+	bind<rule_remove_command>(),
+	bind<server_connect_command>(),
+	bind<server_disconnect_command>(),
+	bind<server_info_command>(),
+	bind<server_invite_command>(),
+	bind<server_join_command>(),
+	bind<server_kick_command>(),
+	bind<server_list_command>(),
+	bind<server_me_command>(),
+	bind<server_message_command>(),
+	bind<server_mode_command>(),
+	bind<server_nick_command>(),
+	bind<server_notice_command>(),
+	bind<server_part_command>(),
+	bind<server_reconnect_command>(),
+	bind<server_topic_command>(),
+};
+
+// {{{ plugin_config_command
+
+auto plugin_config_command::get_name() const noexcept -> std::string_view
+{
+	return "plugin-config";
+}
+
+void plugin_config_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("plugin");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw plugin_error(plugin_error::invalid_identifier);
+
+	const auto plugin = bot.plugins().require(*id);
+
+	if (args.count("value") > 0)
+		exec_set(client, *plugin, args);
+	else
+		exec_get(client, *plugin, args);
+}
+
+// }}}
+
+// {{{ plugin_info_command
+
+auto plugin_info_command::get_name() const noexcept -> std::string_view
+{
+	return "plugin-info";
+}
+
+void plugin_info_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("plugin");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw plugin_error(plugin_error::invalid_identifier);
+
+	const auto plugin = bot.plugins().require(*id);
+
+	client.write({
+		{ "command",    "plugin-info"                           },
+		{ "author",     std::string(plugin->get_author())       },
+		{ "license",    std::string(plugin->get_license())      },
+		{ "summary",    std::string(plugin->get_summary())      },
+		{ "version",    std::string(plugin->get_version())      }
+	});
+}
+
+// }}}
+
+// {{{ plugin_list_command
+
+auto plugin_list_command::get_name() const noexcept -> std::string_view
+{
+	return "plugin-list";
+}
+
+void plugin_list_command::exec(bot& bot, transport_client& client, const document&)
+{
+	auto list = nlohmann::json::array();
+
+	for (const auto& plg : bot.plugins().list())
+		list += plg->get_id();
+
+	client.write({
+		{ "command",    "plugin-list"   },
+		{ "list",       list            }
+	});
+}
+
+// }}}
+
+// {{{ plugin_load_command
+
+auto plugin_load_command::get_name() const noexcept -> std::string_view
+{
+	return "plugin-load";
+}
+
+void plugin_load_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("plugin");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw plugin_error(plugin_error::invalid_identifier);
+
+	bot.plugins().load(*id, "");
+	client.success("plugin-load");
+}
+
+// }}}
+
+// {{{ plugin_reload_command
+
+auto plugin_reload_command::get_name() const noexcept -> std::string_view
+{
+	return "plugin-reload";
+}
+
+void plugin_reload_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("plugin");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw plugin_error(plugin_error::invalid_identifier);
+
+	bot.plugins().reload(*id);
+	client.success("plugin-reload");
+}
+
+// }}}
+
+// {{{ plugin_unload_command
+
+auto plugin_unload_command::get_name() const noexcept -> std::string_view
+{
+	return "plugin-unload";
+}
+
+void plugin_unload_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("plugin");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw plugin_error(plugin_error::invalid_identifier);
+
+	bot.plugins().unload(*id);
+	client.success("plugin-unload");
+}
+
+// }}}
+
+// {{{ rule_add_command
+
+auto rule_add_command::get_name() const noexcept -> std::string_view
+{
+	return "rule-add";
+}
+
+void rule_add_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto index = args.optional<unsigned>("index", bot.rules().list().size());
+
+	if (!index || *index > bot.rules().list().size())
+		throw rule_error(rule_error::error::invalid_index);
+
+	bot.rules().insert(rule_util::from_json(args), *index);
+	client.success("rule-add");
+}
+
+// }}}
+
+// {{{ rule_edit_command
+
+auto rule_edit_command::get_name() const noexcept -> std::string_view
+{
+	return "rule-edit";
+}
+
+void rule_edit_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	static const auto updateset = [] (auto& set, auto args, const auto& key) {
+		for (const auto& v : args["remove-"s + key]) {
+			if (v.is_string())
+				set.erase(v.template get<std::string>());
+		}
+		for (const auto& v : args["add-"s + key]) {
+			if (v.is_string())
+				set.insert(v.template get<std::string>());
+		}
+	};
+
+	const auto index = args.get<unsigned>("index");
+
+	if (!index)
+		throw rule_error(rule_error::invalid_index);
+
+	// Create a copy to avoid incomplete edition in case of errors.
+	auto rule = bot.rules().require(*index);
+
+	updateset(rule.channels, args, "channels");
+	updateset(rule.events, args, "events");
+	updateset(rule.plugins, args, "plugins");
+	updateset(rule.servers, args, "servers");
+
+	auto action = args.find("action");
+
+	if (action != args.end()) {
+		if (!action->is_string())
+			throw rule_error(rule_error::error::invalid_action);
+
+		if (action->get<std::string>() == "accept")
+			rule.action = rule::action_type::accept;
+		else if (action->get<std::string>() == "drop")
+			rule.action = rule::action_type::drop;
+		else
+			throw rule_error(rule_error::invalid_action);
+	}
+
+	// All done, sync the rule.
+	bot.rules().require(*index) = rule;
+	client.success("rule-edit");
+}
+
+// }}}
+
+// {{{ rule_info_command
+
+auto rule_info_command::get_name() const noexcept -> std::string_view
+{
+	return "rule-info";
+}
+
+void rule_info_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto index = args.get<unsigned>("index");
+
+	if (!index)
+		throw rule_error(rule_error::invalid_index);
+
+	auto json = rule_util::to_json(bot.rules().require(*index));
+
+	json.push_back({"command", "rule-info"});
+	client.write(std::move(json));
+}
+
+// }}}
+
+// {{{ rule_list_command
+
+auto rule_list_command::get_name() const noexcept -> std::string_view
+{
+	return "rule-list";
+}
+
+void rule_list_command::exec(bot& bot, transport_client& client, const document&)
+{
+	auto array = nlohmann::json::array();
+
+	for (const auto& rule : bot.rules().list())
+		array.push_back(rule_util::to_json(rule));
+
+	client.write({
+		{ "command",    "rule-list"             },
+		{ "list",       std::move(array)        }
+	});
+}
+
+// }}}
+
+// {{{ rule_move_command
+
+auto rule_move_command::get_name() const noexcept -> std::string_view
+{
+	return "rule-move";
+}
+
+void rule_move_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto from = args.get<unsigned>("from");
+	const auto to = args.get<unsigned>("to");
+
+	if (!from || !to)
+		throw rule_error(rule_error::invalid_index);
+
+	/*
+	 * Examples of moves
+	 * --------------------------------------------------------------
+	 *
+	 * Before: [0] [1] [2]
+	 *
+	 * from = 0
+	 * to   = 2
+	 *
+	 * After:  [1] [2] [0]
+	 *
+	 * --------------------------------------------------------------
+	 *
+	 * Before: [0] [1] [2]
+	 *
+	 * from = 2
+	 * to   = 0
+	 *
+	 * After:  [2] [0] [1]
+	 *
+	 * --------------------------------------------------------------
+	 *
+	 * Before: [0] [1] [2]
+	 *
+	 * from = 0
+	 * to   = 123
+	 *
+	 * After:  [1] [2] [0]
+	 */
+
+	// Ignore dumb input.
+	if (*from == *to) {
+		client.success("rule-move");
+		return;
+	}
+
+	if (*from >= bot.rules().list().size())
+		throw rule_error(rule_error::error::invalid_index);
+
+	const auto save = bot.rules().list()[*from];
+
+	bot.rules().remove(*from);
+	bot.rules().insert(save, *to > bot.rules().list().size() ? bot.rules().list().size() : *to);
+	client.success("rule-move");
+}
+
+// }}}
+
+// {{{ rule_remove_command
+
+auto rule_remove_command::get_name() const noexcept -> std::string_view
+{
+	return "rule-remove";
+}
+
+void rule_remove_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto index = args.get<unsigned>("index");
+
+	if (!index || *index >= bot.rules().list().size())
+		throw rule_error(rule_error::invalid_index);
+
+	bot.rules().remove(*index);
+	client.success("rule-remove");
+}
+
+// }}}
+
+// {{{ server_connect_command
+
+auto server_connect_command::get_name() const noexcept -> std::string_view
+{
+	return "server-connect";
+}
+
+void server_connect_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	auto server = server_util::from_json(bot.get_service(), args);
+
+	if (bot.servers().has(server->get_id()))
+		throw server_error(server_error::already_exists);
+
+	bot.servers().add(std::move(server));
+	client.success("server-connect");
+}
+
+// }}}
+
+// {{{ server_disconnect_command
+
+auto server_disconnect_command::get_name() const noexcept -> std::string_view
+{
+	return "server-disconnect";
+}
+
+void server_disconnect_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto it = args.find("server");
+
+	if (it == args.end())
+		bot.servers().clear();
+	else {
+		if (!it->is_string() || !string_util::is_identifier(it->get<std::string>()))
+			throw server_error(server_error::invalid_identifier);
+
+		const auto name = it->get<std::string>();
+
+		bot.servers().require(name);
+		bot.servers().remove(name);
+	}
+
+	client.success("server-disconnect");
+}
+
+// }}}
+
+// {{{ server_info_command
+
+auto server_info_command::get_name() const noexcept -> std::string_view
+{
+	return "server-info";
+}
+
+void server_info_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+
+	const auto server = bot.servers().require(*id);
+
+	// Construct the JSON response.
+	auto response = document::object();
+
+	// General stuff.
+	response.push_back({"command", "server-info"});
+	response.push_back({"name", server->get_id()});
+	response.push_back({"hostname", server->get_hostname()});
+	response.push_back({"port", server->get_port()});
+	response.push_back({"nickname", server->get_nickname()});
+	response.push_back({"username", server->get_username()});
+	response.push_back({"realname", server->get_realname()});
+	response.push_back({"channels", server->get_channels()});
+
+	// Optional stuff.
+	response.push_back({"ipv4", static_cast<bool>(server->get_options() & server::options::ipv4)});
+	response.push_back({"ipv6", static_cast<bool>(server->get_options() & server::options::ipv6)});
+	response.push_back({"ssl", static_cast<bool>(server->get_options() & server::options::ssl)});
+
+	client.write(response);
+}
+
+// }}}
+
+// {{{ server_invite_command
+
+auto server_invite_command::get_name() const noexcept -> std::string_view
+{
+	return "server-invite";
+}
+
+void server_invite_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto target = args.get<std::string>("target");
+	const auto channel = args.get<std::string>("channel");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!target || target->empty())
+		throw server_error(server_error::invalid_nickname);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+
+	bot.servers().require(*id)->invite(*target, *channel);
+	client.success("server-invite");
+}
+
+// }}}
+
+// {{{ server_join_command
+
+auto server_join_command::get_name() const noexcept -> std::string_view
+{
+	return "server-join";
+}
+
+void server_join_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("channel");
+	const auto password = args.optional<std::string>("password", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!password)
+		throw server_error(server_error::invalid_password);
+
+	bot.servers().require(*id)->join(*channel, *password);
+	client.success("server-join");
+}
+
+// }}}
+
+// {{{ server_kick_command
+
+auto server_kick_command::get_name() const noexcept -> std::string_view
+{
+	return "server-kick";
+}
+
+void server_kick_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto target = args.get<std::string>("target");
+	const auto channel = args.get<std::string>("channel");
+	const auto reason = args.optional<std::string>("reason", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!target || target->empty())
+		throw server_error(server_error::invalid_nickname);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!reason)
+		throw server_error(server_error::invalid_message);
+
+	bot.servers().require(*id)->kick(*target, *channel, *reason);
+	client.success("server-kick");
+}
+
+// }}}
+
+// {{{ server_list_command
+
+auto server_list_command::get_name() const noexcept -> std::string_view
+{
+	return "server-list";
+}
+
+void server_list_command::exec(bot& bot, transport_client& client, const document&)
+{
+	auto json = nlohmann::json::object();
+	auto list = nlohmann::json::array();
+
+	for (const auto& server : bot.servers().list())
+		list.push_back(server->get_id());
+
+	client.write({
+		{ "command",    "server-list"   },
+		{ "list",       std::move(list) }
+	});
+}
+
+// }}}
+
+// {{{ server_me_command
+
+auto server_me_command::get_name() const noexcept -> std::string_view
+{
+	return "server-me";
+}
+
+void server_me_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("target");
+	const auto message = args.optional<std::string>("message", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!message)
+		throw server_error(server_error::invalid_message);
+
+	bot.servers().require(*id)->me(*channel, *message);
+	client.success("server-me");
+}
+
+// }}}
+
+// {{{ server_message_command
+
+auto server_message_command::get_name() const noexcept -> std::string_view
+{
+	return "server-message";
+}
+
+void server_message_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("target");
+	const auto message = args.optional<std::string>("message", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!message)
+		throw server_error(server_error::invalid_message);
+
+	bot.servers().require(*id)->message(*channel, *message);
+	client.success("server-message");
+}
+
+// }}}
+
+// {{{ server_mode_command
+
+auto server_mode_command::get_name() const noexcept -> std::string_view
+{
+	return "server-mode";
+}
+
+void server_mode_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("channel");
+	const auto mode = args.get<std::string>("mode");
+	const auto limit = args.optional<std::string>("limit", "");
+	const auto user = args.optional<std::string>("user", "");
+	const auto mask = args.optional<std::string>("mask", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!mode || mode->empty())
+		throw server_error(server_error::invalid_mode);
+	if (!limit || !user || !mask)
+		throw server_error(server_error::invalid_mode);
+
+	bot.servers().require(*id)->mode(*channel, *mode, *limit, *user, *mask);
+	client.success("server-mode");
+}
+
+// }}}
+
+// {{{ server_nick_command
+
+auto server_nick_command::get_name() const noexcept -> std::string_view
+{
+	return "server-nick";
+}
+
+void server_nick_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto nick = args.get<std::string>("nickname");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!nick || nick->empty())
+		throw server_error(server_error::invalid_nickname);
+
+	bot.servers().require(*id)->set_nickname(*nick);
+	client.success("server-nick");
+}
+
+// }}}
+
+// {{{ server_notice_command
+
+auto server_notice_command::get_name() const noexcept -> std::string_view
+{
+	return "server-notice";
+}
+
+void server_notice_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("target");
+	const auto message = args.optional<std::string>("message", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!message)
+		throw server_error(server_error::invalid_message);
+
+	bot.servers().require(*id)->notice(*channel, *message);
+	client.success("server-notice");
+}
+
+// }}}
+
+// {{{ server_part_command
+
+auto server_part_command::get_name() const noexcept -> std::string_view
+{
+	return "server-part";
+}
+
+void server_part_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("channel");
+	const auto reason = args.optional<std::string>("reason", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!reason)
+		throw server_error(server_error::invalid_message);
+
+	bot.servers().require(*id)->part(*channel, *reason);
+	client.success("server-part");
+}
+
+// }}}
+
+// {{{ server_reconnect_command
+
+auto server_reconnect_command::get_name() const noexcept -> std::string_view
+{
+	return "server-reconnect";
+}
+
+void server_reconnect_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto it = args.find("server");
+
+	if (it == args.end())
+		bot.servers().reconnect();
+	else {
+		if (!it->is_string() || !string_util::is_identifier(it->get<std::string>()))
+			throw server_error(server_error::invalid_identifier);
+
+		bot.servers().reconnect(it->get<std::string>());
+	}
+
+	client.success("server-reconnect");
+}
+
+// }}}
+
+// {{{ server_topic_command
+
+auto server_topic_command::get_name() const noexcept -> std::string_view
+{
+	return "server-topic";
+}
+
+void server_topic_command::exec(bot& bot, transport_client& client, const document& args)
+{
+	const auto id = args.get<std::string>("server");
+	const auto channel = args.get<std::string>("channel");
+	const auto topic = args.optional<std::string>("topic", "");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!channel || channel->empty())
+		throw server_error(server_error::invalid_channel);
+	if (!topic)
+		throw server_error(server_error::invalid_message);
+
+	bot.servers().require(*id)->topic(*channel, *topic);
+	client.success("server-topic");
+}
+
+// }}}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/command.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,794 @@
+/*
+ * command.hpp -- remote command
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_COMMAND_HPP
+#define IRCCD_DAEMON_COMMAND_HPP
+
+/**
+ * \file command.hpp
+ * \brief Remote commands.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <functional>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+#include <irccd/json_util.hpp>
+
+namespace irccd::daemon {
+
+class bot;
+class transport_client;
+
+// {{{ command
+
+/**
+ * \brief Server side remote command
+ * \ingroup transports
+ */
+class command {
+public:
+	/**
+	 * \brief Convenient alias.
+	 */
+	using document = json_util::deserializer;
+
+	/**
+	 * \brief Command constructor factory.
+	 */
+	using constructor = std::function<auto () -> std::unique_ptr<command>>;
+
+	/**
+	 * \brief Registry of all commands.
+	 */
+	static const std::vector<constructor> registry;
+
+	/**
+	 * Default destructor virtual.
+	 */
+	virtual ~command() = default;
+
+	/**
+	 * Return the command name, must not have spaces.
+	 *
+	 * \return the command name
+	 */
+	virtual auto get_name() const noexcept -> std::string_view = 0;
+
+	/**
+	 * Execute the command.
+	 *
+	 * If the command throw an exception, the error is sent to the client so be
+	 * careful about sensitive information.
+	 *
+	 * The implementation should use client.success() or client.error() to send
+	 * some data.
+	 *
+	 * \param bot the irccd instance
+	 * \param client the client
+	 * \param args the client arguments
+	 */
+	virtual void exec(bot& bot, transport_client& client, const document& args) = 0;
+};
+
+// }}}
+
+// {{{ plugin_config_command
+
+/**
+ * \brief Implementation of plugin-config transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - plugin_error::not_found
+ */
+class plugin_config_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ plugin_info_command
+
+/**
+ * \brief Implementation of plugin-info transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - plugin_error::not_found
+ */
+class plugin_info_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ plugin_list_command
+
+/**
+ * \brief Implementation of plugin-list transport command.
+ * \ingroup transports
+ */
+class plugin_list_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ plugin_load_command
+
+/**
+ * \brief Implementation of plugin-load transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - plugin_error::already_exists
+ * - plugin_error::not_found
+ * - plugin_error::exec_error
+ */
+class plugin_load_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ plugin_reload_command
+
+/**
+ * \brief Implementation of plugin-reload transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - plugin_error::not_found
+ * - plugin_error::exec_error
+ */
+class plugin_reload_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ plugin_unload_command
+
+/**
+ * \brief Implementation of plugin-unload transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - plugin_error::not_found
+ * - plugin_error::exec_error
+ */
+class plugin_unload_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ rule_add_command
+
+/**
+ * \brief Implementation of rule-add transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - rule_error::invalid_action
+ */
+class rule_add_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ rule_edit_command
+
+/**
+ * \brief Implementation of rule-edit transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - rule_error::invalid_index
+ * - rule_error::invalid_action
+ */
+class rule_edit_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ rule_info_command
+
+/**
+ * \brief Implementation of rule-info transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - rule_error::invalid_index
+ */
+class rule_info_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ rule_list_command
+
+/**
+ * \brief Implementation of rule-list transport command.
+ * \ingroup transports
+ */
+class rule_list_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ rule_move_command
+
+/**
+ * \brief Implementation of rule-move transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - rule_error::invalid_index
+ */
+class rule_move_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ rule_remove_command
+
+/**
+ * \brief Implementation of rule-remove transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - rule_error::invalid_index
+ */
+class rule_remove_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_connect_command
+
+/**
+ * \brief Implementation of server-connect transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::already_exists,
+ * - server_error::invalid_hostname,
+ * - server_error::invalid_identifier,
+ * - server_error::invalid_port_number,
+ * - server_error::ssl_disabled.
+ */
+class server_connect_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_disconnect_command
+
+/**
+ * \brief Implementation of server-disconnect transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_disconnect_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_info_command
+
+/**
+ * \brief Implementation of server-info transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_info_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_invite_command
+
+/**
+ * \brief Implementation of server-invite transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::invalid_nickname,
+ * - server_error::not_found.
+ */
+class server_invite_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_join_command
+
+/**
+ * \brief Implementation of server-join transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_join_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_kick_command
+
+/**
+ * \brief Implementation of server-kick transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::invalid_nickname,
+ * - server_error::not_found.
+ */
+class server_kick_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_list_command
+
+/**
+ * \brief Implementation of server-list transport command.
+ * \ingroup transports
+ */
+class server_list_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_me_command
+
+/**
+ * \brief Implementation of server-me transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_me_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_message_command
+
+/**
+ * \brief Implementation of server-message transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_message_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_mode_command
+
+/**
+ * \brief Implementation of server-mode transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::invalid_mode,
+ * - server_error::not_found.
+ */
+class server_mode_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_nick_command
+
+/**
+ * \brief Implementation of server-nick transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_identifier,
+ * - server_error::invalid_nickname,
+ * - server_error::not_found.
+ */
+class server_nick_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_notice_command
+
+/**
+ * \brief Implementation of server-notice transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_notice_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_part_command
+
+/**
+ * \brief Implementation of server-part transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_part_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_reconnect_command
+
+/**
+ * \brief Implementation of server-reconnect transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_reconnect_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+// {{{ server_topic_command
+
+/**
+ * \brief Implementation of server-topic transport command.
+ * \ingroup transports
+ *
+ * Replies:
+ *
+ * - server_error::invalid_channel,
+ * - server_error::invalid_identifier,
+ * - server_error::not_found.
+ */
+class server_topic_command : public command {
+public:
+	/**
+	 * \copydoc command::get_name
+	 */
+	auto get_name() const noexcept -> std::string_view override;
+
+	/**
+	 * \copydoc command::exec
+	 */
+	void exec(bot& bot, transport_client& client, const document& args) override;
+};
+
+// }}}
+
+} // !irccd::daemon
+
+#endif // !IRCCD_DAEMON_COMMAND_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/dynlib_plugin.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,104 @@
+/*
+ * dynlib_plugin.cpp -- native plugin implementation
+ *
+ * Copyright (c) 2013-2018 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 <algorithm>
+
+#include <boost/dll.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/predef/os.h>
+#include <boost/format.hpp>
+
+#define BOOST_DLL_FORCE_ALIAS_INSTANTIATION
+#include "dynlib_plugin.hpp"
+
+#if BOOST_OS_WINDOWS
+#	define DYNLIB_EXTENSION ".dll"
+#elif BOOST_OS_MACOS
+#	define DYNLIB_EXTENSION ".dylib"
+#else
+#	define DYNLIB_EXTENSION ".so"
+#endif
+
+using boost::format;
+using boost::str;
+
+namespace irccd::daemon {
+
+namespace {
+
+auto symbol(std::string_view path) -> std::pair<std::string, std::string>
+{
+	auto id = boost::filesystem::path(std::string(path)).stem().string();
+
+	// Remove forbidden characters.
+	id.erase(std::remove_if(id.begin(), id.end(), [] (auto c) {
+		return !isalnum(c) && c != '-' && c != '_';
+	}), id.end());
+
+	// Transform - to _.
+	std::transform(id.begin(), id.end(), id.begin(), [] (auto c) noexcept {
+		return c == '-' ? '_' : c;
+	});
+
+	return {
+		str(format("irccd_abi_%1%") % id),
+		str(format("irccd_init_%1%") % id)
+	};
+}
+
+} // !namespace
+
+dynlib_plugin_loader::dynlib_plugin_loader(std::vector<std::string> directories) noexcept
+	: plugin_loader(std::move(directories), { DYNLIB_EXTENSION })
+{
+}
+
+auto dynlib_plugin_loader::open(std::string_view id, std::string_view path) -> std::shared_ptr<plugin>
+{
+	const std::string idstr(id);
+	const std::string pathstr(path);
+
+	const auto [ abisym, initsym ] = symbol(pathstr);
+
+	using abisym_func_type = version ();
+	using initsym_func_type = std::unique_ptr<plugin> (std::string);
+
+	const auto abi = boost::dll::import_alias<abisym_func_type>(pathstr, abisym);
+	const auto init = boost::dll::import_alias<initsym_func_type>(pathstr, initsym);
+
+	// The abi version is reset after new major version, check for both.
+	const version current;
+
+	if (current.major != abi().major || current.abi != abi().abi)
+		throw plugin_error(plugin_error::exec_error, idstr, "incompatible version");
+
+	auto plg = init(idstr);
+
+	if (!plg)
+		throw plugin_error(plugin_error::exec_error, idstr, "invalid plugin");
+
+	/*
+	 * We need to keep a reference to `init' variable for the whole plugin
+	 * lifetime.
+	 */
+	return std::shared_ptr<plugin>(plg.release(), [init] (auto ptr) mutable {
+		delete ptr;
+	});
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/dynlib_plugin.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,55 @@
+/*
+ * dynlib_plugin.hpp -- native plugin implementation
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_DYNLIB_PLUGIN_HPP
+#define IRCCD_DAEMON_DYNLIB_PLUGIN_HPP
+
+/**
+ * \file dynlib_plugin.hpp
+ * \brief Native plugin implementation.
+ */
+
+#define BOOST_DLL_FORCE_ALIAS_INSTANTIATION
+#include <boost/dll.hpp>
+
+#include "plugin.hpp"
+
+namespace irccd::daemon {
+
+/**
+ * \ingroup plugins
+ * \brief Implementation for searching native plugins.
+ */
+class dynlib_plugin_loader : public plugin_loader {
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param directories optional directories to search, if empty use defaults.
+	 */
+	dynlib_plugin_loader(std::vector<std::string> directories = {}) noexcept;
+
+	/**
+	 * \copydoc plugin_loader::open
+	 */
+	auto open(std::string_view id, std::string_view file) -> std::shared_ptr<plugin> override;
+};
+
+} // !irccd::daemon
+
+#endif // !IRCCD_DAEMON_DYNLIB_PLUGIN_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/irc.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,300 @@
+/*
+ * irc.cpp -- low level IRC functions
+ *
+ * Copyright (c) 2013-2018 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 <cassert>
+#include <iterator>
+#include <sstream>
+
+#include "irc.hpp"
+
+using std::errc;
+using std::flush;
+using std::isspace;
+using std::istreambuf_iterator;
+using std::istringstream;
+using std::move;
+using std::ostream;
+using std::string;
+using std::string_view;
+using std::vector;
+
+using boost::asio::async_connect;
+using boost::asio::async_read_until;
+using boost::asio::async_write;
+using boost::asio::io_service;
+using boost::asio::ip::tcp;
+
+#if defined(IRCCD_HAVE_SSL)
+
+using boost::asio::ssl::stream_base;
+
+#endif
+
+namespace irccd::daemon::irc {
+
+auto message::get(unsigned short index) const noexcept -> const string&
+{
+	static const string dummy;
+
+	return (index >= args.size()) ? dummy : args[index];
+}
+
+auto message::is_ctcp(unsigned short index) const noexcept -> bool
+{
+	const auto a = get(index);
+
+	if (a.empty())
+		return false;
+
+	return a.front() == 0x01 && a.back() == 0x01;
+}
+
+auto message::ctcp(unsigned short index) const -> string
+{
+	assert(is_ctcp(index));
+
+	return args[index].substr(1, args[index].size() - 1);
+}
+
+auto message::parse(const string& line) -> message
+{
+	istringstream iss(line);
+	string prefix;
+
+	if (line.empty())
+		return {};
+
+	// Prefix.
+	if (line[0] == ':') {
+		iss.ignore(1);
+		iss >> prefix;
+		iss.ignore(1);
+	}
+
+	// Command.
+	string command;
+	iss >> command;
+	iss.ignore(1);
+
+	// Arguments.
+	vector<std::string> args;
+	istreambuf_iterator<char> it(iss), end;
+
+	while (it != end) {
+		std::string arg;
+
+		if (*it == ':')
+			arg = string(++it, end);
+		else {
+			while (!isspace(*it) && it != end)
+				arg.push_back(*it++);
+
+			// Skip space after param.
+			if (it != end)
+				++it;
+		}
+
+		args.push_back(move(arg));
+	}
+
+	return { move(prefix), move(command), move(args) };
+}
+
+auto user::parse(string_view line) -> user
+{
+	if (line.empty())
+		return { "", "" };
+
+	const auto pos = line.find("!");
+
+	if (pos == string::npos)
+		return { string(line), "" };
+
+	return { string(line.substr(0, pos)), string(line.substr(pos + 1)) };
+}
+
+void connection::handshake(const connect_handler& handler)
+{
+	if (!ssl_) {
+		handler({});
+		return;
+	}
+
+#if defined(IRCCD_HAVE_SSL)
+	ssl_socket_.async_handshake(stream_base::client, [handler] (auto code) {
+		handler(std::move(code));
+	});
+#endif
+}
+
+void connection::connect(const tcp::resolver::results_type& endpoints, const connect_handler& handler)
+{
+	async_connect(socket_, endpoints, [this, handler] (auto code, auto) {
+		if (code) {
+			handler(move(code));
+			return;
+		}
+
+		handshake(handler);
+	});
+}
+
+void connection::resolve(string_view hostname, string_view port, const connect_handler& handler)
+{
+	auto chain = [this, handler] (auto code, auto eps) {
+		if (code)
+			handler(std::move(code));
+		else
+			connect(eps, std::move(handler));
+	};
+
+	if (ipv6_ && ipv4_)
+		resolver_.async_resolve(hostname, port, move(chain));
+	else if (ipv6_)
+		resolver_.async_resolve(tcp::v6(), hostname, port, move(chain));
+	else
+		resolver_.async_resolve(tcp::v4(), hostname, port, move(chain));
+}
+
+connection::connection(io_service& service)
+	: service_(service)
+	, resolver_(service)
+{
+}
+
+void connection::use_ipv4(bool enable) noexcept
+{
+	ipv4_ = enable;
+}
+
+void connection::use_ipv6(bool enable) noexcept
+{
+	ipv6_ = enable;
+}
+
+void connection::use_ssl(bool enable) noexcept
+{
+	ssl_ = enable;
+}
+
+void connection::connect(string_view hostname, string_view service, connect_handler handler)
+{
+#if !defined(IRCCD_HAVE_SSL)
+	assert(!ssl_);
+#endif
+#if !defined(NDEBUG)
+	assert(!is_connecting_);
+
+	is_connecting_ = true;
+#endif
+	assert(handler);
+	assert(ipv4_ || ipv6_);
+
+	auto chain = [this, handler] (auto code) {
+#if !defined(NDEBUG)
+		is_connecting_ = false;
+#endif
+		(void)this;
+
+		handler(move(code));
+	};
+
+	resolve(hostname, service, move(chain));
+}
+
+void connection::recv(recv_handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_receiving_);
+
+	is_receiving_ = true;
+#endif
+
+	auto chain = [this, handler] (auto code, auto xfer) {
+#if !defined(NDEBUG)
+		is_receiving_ = false;
+#endif
+		(void)this;
+
+		if (code == boost::asio::error::not_found)
+			return handler(make_error_code(errc::argument_list_too_long), message());
+		if (code == boost::asio::error::eof || xfer == 0)
+			return handler(make_error_code(errc::connection_reset), message());
+		else if (code)
+			return handler(move(code), message());
+
+		string data;
+
+		// 1. Convert the buffer safely.
+		try {
+			data = string(
+				buffers_begin(input_.data()),
+				buffers_begin(input_.data()) + xfer - 2
+			);
+
+			input_.consume(xfer);
+		} catch (...) {
+			return handler(make_error_code(errc::not_enough_memory), message());
+		}
+
+		handler(move(code), message::parse(data));
+	};
+
+#if defined(IRCCD_HAVE_SSL)
+	if (ssl_)
+		async_read_until(ssl_socket_, input_, "\r\n", move(chain));
+	else
+#endif
+		async_read_until(socket_, input_, "\r\n", move(chain));
+}
+
+void connection::send(string_view message, send_handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_sending_);
+
+	is_sending_ = true;
+#endif
+
+	auto chain = [this, handler] (auto code, auto xfer) {
+#if !defined(NDEBUG)
+		is_sending_ = false;
+#endif
+		(void)this;
+
+		if (code == boost::asio::error::eof || xfer == 0)
+			return handler(make_error_code(errc::connection_reset));
+
+		handler(move(code));
+	};
+
+	ostream out(&output_);
+
+	out << message;
+	out << "\r\n";
+	out << flush;
+
+#if defined(IRCCD_HAVE_SSL)
+	if (ssl_)
+		async_write(ssl_socket_, output_, move(chain));
+	else
+#endif
+		async_write(socket_, output_, move(chain));
+}
+
+} // !irccd::daemon::irc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/irc.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,1390 @@
+/*
+ * irc.hpp -- low level IRC functions
+ *
+ * Copyright (c) 2013-2018 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_IRC_HPP
+#define IRCCD_IRC_HPP
+
+/**
+ * \file irc.hpp
+ * \brief Low level IRC functions.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <functional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <boost/asio.hpp>
+
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
+
+namespace irccd::daemon::irc {
+
+/**
+ * \brief Describe errors.
+ *
+ * See [RFC1459 (6.1)](https://tools.ietf.org/html/rfc1459#section-6.1).
+ */
+enum class err {
+	/**
+	 * ERR_NOSUCHNICK
+	 *
+	 * "<nickname> :No such nick/channel"
+	 *
+	 * Used to indicate the nickname parameter supplied to a
+	 * command is currently unused.
+	 */
+	nosuchnick = 401,
+
+	/**
+	 * ERR_NOSUCHSERVER
+	 *
+	 * "<server name> :No such server"
+	 *
+	 * Used to indicate the server name given currently
+	 * doesn't exist.
+	 */
+	nosuchserver = 402,
+
+	/**
+	 * ERR_NOSUCHCHANNEL
+	 *
+	 * "<channel name> :No such channel"
+	 *
+	 * Used to indicate the given channel name is invalid.
+	 */
+	nosuchchannel = 403,
+
+	/**
+	 * ERR_CANNOTSENDTOCHAN
+	 *
+	 * "<channel name> :Cannot send to channel"
+	 *
+	 * Sent to a user who is either (a) not on a channel
+	 * which is mode +n or (b) not a chanop (or mode +v) on
+	 * a channel which has mode +m set and is trying to send
+	 * a PRIVMSG message to that channel.
+	 */
+	cannotsendtochan = 404,
+
+	/**
+	 * ERR_TOOMANYCHANNELS
+	 *
+	 * "<channel name> :You have joined too many channels"
+	 *
+	 * Sent to a user when they have joined the maximum
+	 * number of allowed channels and they try to join
+	 * another channel.
+	 */
+	toomanychannels = 405,
+
+	/**
+	 * ERR_WASNOSUCHNICK
+	 *
+	 * "<nickname> :There was no such nickname"
+	 *
+	 * Returned by WHOWAS to indicate there is no history
+	 * information for that nickname.
+	 */
+	wasnosuchnick = 406,
+
+	/**
+	 * ERR_TOOMANYTARGETS
+	 *
+	 * "<target> :Duplicate recipients. No message delivered"
+	 *
+	 * Returned to a client which is attempting to send a
+	 * PRIVMSG/NOTICE using the user@host destination format
+	 * and for a user@host which has several occurrences.
+	 */
+	toomanytargets = 407,
+
+	/**
+	 * ERR_NOORIGIN
+	 *
+	 * ":No origin specified"
+	 *
+	 * PING or PONG message missing the originator parameter
+	 * which is required since these commands must work
+	 * without valid prefixes.
+	 */
+	noorigin = 409,
+
+	/**
+	 * ERR_NORECIPIENT
+	 *
+	 * ":No recipient given (<command>)"
+	 */
+	norecipient = 411,
+
+	/**
+	 * ERR_NOTEXTTOSEND
+	 *
+	 * ":No text to send"
+	 */
+	notexttosend = 412,
+
+	/**
+	 * ERR_NOTOPLEVEL
+	 *
+	 * "<mask> :No toplevel domain specified"
+	 */
+	notoplevel = 413,
+
+	/**
+	 * ERR_WILDTOPLEVEL
+	 *
+	 * "<mask> :Wildcard in toplevel domain"
+	 *
+	 * are returned by PRIVMSG to indicate that
+	 * the message wasn't delivered for some reason.
+	 * ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that
+	 * are returned when an invalid use of
+	 * "PRIVMSG $<server>" or "PRIVMSG #<host>" is attempted.
+	 */
+	wildtoplevel = 414,
+
+	/**
+	 * ERR_UNKNOWNCOMMAND
+	 *
+	 * "<command> :Unknown command"
+	 *
+	 * Returned to a registered client to indicate that the
+	 * command sent is unknown by the server.
+	 */
+	unknowncommand = 421,
+
+	/**
+	 * ERR_NOMOTD
+	 *
+	 * ":MOTD File is missing"
+	 *
+	 * Server's MOTD file could not be opened by the server.
+	 */
+	nomotd = 422,
+
+	/**
+	 * ERR_NOADMININFO
+	 *
+	 * "<server> :No administrative info available"
+	 *
+	 * Returned by a server in response to an ADMIN message
+	 * when there is an error in finding the appropriate
+	 * information.
+	 */
+	noadmininfo = 423,
+
+	/**
+	 * ERR_FILEERROR
+	 *
+	 * ":File error doing <file op> on <file>"
+	 *
+	 * Generic error message used to report a failed file
+	 * operation during the processing of a message.
+	 */
+	fileerror = 424,
+
+	/**
+	 * ERR_NONICKNAMEGIVEN
+	 *
+	 * ":No nickname given"
+	 *
+	 * Returned when a nickname parameter expected for a
+	 * command and isn't found.
+	 */
+	nonicknamegiven = 431,
+
+	/**
+	 * ERR_ERRONEUSNICKNAME
+	 *
+	 * "<nick> :Erroneus nickname"
+	 *
+	 * Returned after receiving a NICK message which contains
+	 * characters which do not fall in the defined set.  See
+	 * section x.x.x for details on valid nicknames.
+	 */
+	erroneusnickname = 432,
+
+	/**
+	 * ERR_NICKNAMEINUSE
+	 *
+	 * "<nick> :Nickname is already in use"
+	 *
+	 * Returned when a NICK message is processed that results
+	 * in an attempt to change to a currently existing
+	 * nickname.
+	 */
+	nicknameinuse = 433,
+
+	/**
+	 * ERR_NICKCOLLISION
+	 *
+	 * "<nick> :Nickname collision KILL"
+	 *
+	 * Returned by a server to a client when it detects a
+	 * nickname collision (registered of a NICK that
+	 * already exists by another server).
+	 */
+	nickcollision = 436,
+
+	/**
+	 * ERR_USERNOTINCHANNEL
+	 *
+	 * "<nick> <channel> :They aren't on that channel"
+	 *
+	 * Returned by the server to indicate that the target
+	 * user of the command is not on the given channel.
+	 */
+	usernotinchannel = 441,
+
+	/**
+	 * ERR_NOTONCHANNEL
+	 *
+	 * "<channel> :You're not on that channel"
+	 *
+	 * Returned by the server whenever a client tries to
+	 * perform a channel effecting command for which the
+	 * client isn't a member.
+	 */
+	notonchannel = 442,
+
+	/**
+	 * ERR_USERONCHANNEL
+	 *
+	 * "<user> <channel> :is already on channel"
+	 *
+	 * Returned when a client tries to invite a user to a
+	 * channel they are already on.
+	 */
+	useronchannel = 443,
+
+	/**
+	 * ERR_NOLOGIN
+	 *
+	 * "<user> :User not logged in"
+	 *
+	 * Returned by the summon after a SUMMON command for a
+	 * user was unable to be performed since they were not
+	 * logged in.
+	 */
+	nologin = 444,
+
+	/**
+	 * ERR_SUMMONDISABLED
+	 *
+	 * ":SUMMON has been disabled"
+	 *
+	 * Returned as a response to the SUMMON command.  Must be
+	 * returned by any server which does not implement it.
+	 */
+	summondisabled = 445,
+
+	/**
+	 * ERR_USERSDISABLED
+	 *
+	 * ":USERS has been disabled"
+	 *
+	 * Returned as a response to the USERS command.  Must be
+	 * returned by any server which does not implement it.
+	 */
+	usersdisabled = 446,
+
+	/**
+	 * ERR_NOTREGISTERED
+	 *
+	 * ":You have not registered"
+	 *
+	 * Returned by the server to indicate that the client
+	 * must be registered before the server will allow it
+	 * to be parsed in detail.
+	 */
+	notregistered = 451,
+
+	/**
+	 * ERR_NEEDMOREPARAMS
+	 *
+	 * "<command> :Not enough parameters"
+	 *
+	 * Returned by the server by numerous commands to
+	 * indicate to the client that it didn't supply enough
+	 * parameters.
+	 */
+	needmoreparams = 461,
+
+	/**
+	 * ERR_ALREADYREGISTRED
+	 *
+	 * ":You may not reregister"
+	 *
+	 * Returned by the server to any link which tries to
+	 * change part of the registered details (such as
+	 * password or user details from second USER message).
+	 */
+	alreadyregistred = 462,
+
+	/**
+	 * ERR_NOPERMFORHOST
+	 *
+	 * ":Your host isn't among the privileged"
+	 *
+	 * Returned to a client which attempts to register with
+	 * a server which does not been setup to allow
+	 * connections from the host the attempted connection
+	 * is tried.
+	 */
+	nopermforhost = 463,
+
+	/**
+	 * ERR_PASSWDMISMATCH
+	 *
+	 * ":Password incorrect"
+	 *
+	 * Returned to indicate a failed attempt at registering
+	 * a connection for which a password was required and
+	 * was either not given or incorrect.
+	 */
+	passwdmismatch = 464,
+
+	/**
+	 * ERR_YOUREBANNEDCREEP
+	 *
+	 * ":You are banned from this server"
+	 *
+	 * Returned after an attempt to connect and register
+	 * yourself with a server which has been setup to
+	 * explicitly deny connections to you.
+	 */
+	yourebannedcreep = 465,
+
+	/**
+	 * ERR_KEYSET
+	 *
+	 * "<channel> :Channel key already set"
+	 */
+	keyset = 467,
+
+	/**
+	 * ERR_CHANNELISFULL
+	 *
+	 * "<channel> :Cannot join channel (+l)"
+	 */
+	channelisfull = 471,
+
+	/**
+	 * ERR_UNKNOWNMODE
+	 *
+	 * "<char> :is unknown mode char to me"
+	 */
+	unknownmode = 472,
+
+	/**
+	 * ERR_INVITEONLYCHAN
+	 *
+	 * "<channel> :Cannot join channel (+i)"
+	 */
+	inviteonlychan = 473,
+
+	/**
+	 * ERR_BANNEDFROMCHAN
+	 *
+	 * "<channel> :Cannot join channel (+b)"
+	 */
+	bannedfromchan = 474,
+
+	/**
+	 * ERR_BADCHANNELKEY
+	 *
+	 * "<channel> :Cannot join channel (+k)"
+	 */
+	badchannelkey = 475,
+
+	/**
+	 * ERR_NOPRIVILEGES
+	 *
+	 * ":Permission Denied- You're not an IRC operator"
+	 *
+	 * Any command requiring operator privileges to operate
+	 * must return this error to indicate the attempt was
+	 * unsuccessful.
+	 */
+	noprivileges = 481,
+
+	/**
+	 * ERR_CHANOPRIVSNEEDED
+	 *
+	 * "<channel> :You're not channel operator"
+	 *
+	 * Any command requiring 'chanop' privileges (such as
+	 * MODE messages) must return this error if the client
+	 * making the attempt is not a chanop on the specified
+	 * channel.
+	 */
+	chanoprivsneeded = 482,
+
+	/**
+	 * ERR_CANTKILLSERVER
+	 *
+	 * ":You cant kill a server!"
+	 *
+	 * Any attempts to use the KILL command on a server
+	 * are to be refused and this error returned directly
+	 * to the client.
+	 */
+	cantkillserver = 483,
+
+	/**
+	 * ERR_NOOPERHOST
+	 *
+	 * ":No O-lines for your host"
+	 *
+	 * If a client sends an OPER message and the server has
+	 * not been configured to allow connections from the
+	 * client's host as an operator, this error must be
+	 * returned.
+	 */
+	nooperhost = 491,
+
+	/**
+	 * ERR_UMODEUNKNOWNFLAG
+	 *
+	 * ":Unknown MODE flag"
+	 *
+	 * Returned by the server to indicate that a MODE
+	 * message was sent with a nickname parameter and that
+	 * the a mode flag sent was not recognized.
+	 */
+	umodeunknownflag = 501,
+
+	/**
+	 * ERR_USERSDONTMATCH
+	 *
+	 * ":Cant change mode for other users"
+	 *
+	 * Error sent to any user trying to view or change the
+	 * user mode for a user other than themselves.
+	 */
+	usersdontmatch = 502
+};
+
+/**
+ * \brief Describe numeric replies.
+ *
+ * See [RFC1459 (6.2)](https://tools.ietf.org/html/rfc1459#section-6.2).
+ */
+enum class rpl {
+	/**
+	 * RPL_NONE
+	 *
+	 * Dummy reply number. Not used.
+	 */
+	none = 300,
+
+	/**
+	 * RPL_USERHOST
+	 *
+	 * ":[<reply>{<space><reply>}]"
+	 *
+	 * Reply format used by USERHOST to list replies to
+	 * the query list.  The reply string is composed as
+	 * follows:
+	 *
+	 * <reply> ::= <nick>['*'] '=' <'+'|'-'><hostname>
+	 *
+	 * The '*' indicates whether the client has registered
+	 * as an Operator.  The '-' or '+' characters represent
+	 * whether the client has set an AWAY message or not
+	 * respectively.
+	 */
+	userhost = 302,
+
+	/**
+	 * RPL_ISON
+	 *
+	 * ":[<nick> {<space><nick>}]"
+	 *
+	 * Reply format used by ISON to list replies to the
+	 * query list.
+	 */
+	ison = 303,
+
+	/**
+	 * RPL_AWAY
+	 *
+	 * "<nick> :<away message>"
+	 */
+	away = 301,
+
+	/**
+	 * RPL_UNAWAY
+	 *
+	 * ":You are no longer marked as being away"
+	 */
+	unaway = 305,
+
+	/**
+	 * RPL_NOWAWAY
+	 *
+	 * ":You have been marked as being away"
+	 *
+	 * These replies are used with the AWAY command (if
+	 * allowed).  RPL_AWAY is sent to any client sending a
+	 * PRIVMSG to a client which is away.  RPL_AWAY is only
+	 * sent by the server to which the client is connected.
+	 * Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the
+	 * client removes and sets an AWAY message.
+	 */
+	nowaway = 306,
+
+	/**
+	 * RPL_WHOISUSER
+	 *
+	 * "<nick> <user> <host> * :<real name>"
+	 */
+	whoisuser = 311,
+
+	/**
+	 * RPL_WHOISSERVER
+	 *
+	 * "<nick> <server> :<server info>"
+	 */
+	whoisserver = 312,
+
+	/**
+	 * RPL_WHOISOPERATOR
+	 *
+	 * "<nick> :is an IRC operator"
+	 */
+	whoisoperator = 313,
+
+	/**
+	 * RPL_WHOISIDLE
+	 *
+	 * "<nick> <integer> :seconds idle"
+	 */
+	whoisidle = 317,
+
+	/**
+	 * RPL_ENDOFWHOIS
+	 *
+	 * "<nick> :End of /WHOIS list"
+	 */
+	endofwhois = 318,
+
+	/**
+	 * RPL_WHOISCHANNELS
+	 *
+	 * "<nick> :{[@|+]<channel><space>}"
+	 *
+	 * Replies 311 - 313, 317 - 319 are all replies
+	 * generated in response to a WHOIS message.  Given that
+	 * there are enough parameters present, the answering
+	 * server must either formulate a reply out of the above
+	 * numerics (if the query nick is found) or return an
+	 * error reply.  The '*' in RPL_WHOISUSER is there as
+	 * the literal character and not as a wild card.  For
+	 * each reply set, only RPL_WHOISCHANNELS may appear
+	 * more than once (for long lists of channel names).
+	 * The '@' and '+' characters next to the channel name
+	 * indicate whether a client is a channel operator or
+	 * has been granted permission to speak on a moderated
+	 * channel.  The RPL_ENDOFWHOIS reply is used to mark
+	 * the end of processing a WHOIS message.
+	 */
+	whoischannels = 319,
+
+	/**
+	 * RPL_WHOWASUSER
+	 *
+	 * "<nick> <user> <host> * :<real name>"
+	 */
+	whowasuser = 314,
+
+	/**
+	 * RPL_ENDOFWHOWAS
+	 *
+	 * "<nick> :End of WHOWAS"
+	 *
+	 * When replying to a WHOWAS message, a server must use
+	 * the replies RPL_WHOWASUSER, RPL_WHOISSERVER or
+	 * ERR_WASNOSUCHNICK for each nickname in the presented
+	 * list.  At the end of all reply batches, there must
+	 * be RPL_ENDOFWHOWAS (even if there was only one reply
+	 * and it was an error).
+	 */
+	endofwhowas = 369,
+
+	/**
+	 * RPL_LISTSTART
+	 *
+	 * "Channel :Users  Name"
+	 */
+	liststart = 321,
+
+	/**
+	 * RPL_LIST
+	 *
+	 * "<channel> <# visible> :<topic>"
+	 */
+	list = 322,
+
+	/**
+	 * RPL_LISTEND
+	 *
+	 * ":End of /LIST"
+	 *
+	 * Replies RPL_LISTSTART, RPL_LIST, RPL_LISTEND mark
+	 * the start, actual replies with data and end of the
+	 * server's response to a LIST command.  If there are
+	 * no channels available to return, only the start
+	 * and end reply must be sent.
+	 */
+	listend = 323,
+
+	/**
+	 * RPL_CHANNELMODEIS
+	 *
+	 * "<channel> <mode> <mode params>"
+	 */
+	channelmodeis = 324,
+
+	/**
+	 * RPL_NOTOPIC
+	 *
+	 * "<channel> :No topic is set"
+	 */
+	notopic = 331,
+
+	/**
+	 * RPL_TOPIC
+	 *
+	 * "<channel> :<topic>"
+	 *
+	 * When sending a TOPIC message to determine the
+	 * channel topic, one of two replies is sent.  If
+	 * the topic is set, RPL_TOPIC is sent back else
+	 * RPL_NOTOPIC.
+	 */
+	topic = 332,
+
+	/**
+	 * RPL_INVITING
+	 *
+	 * "<channel> <nick>"
+	 *
+	 * Returned by the server to indicate that the
+	 * attempted INVITE message was successful and is
+	 * being passed onto the end client.
+	 */
+	inviting = 341,
+
+	/**
+	 * RPL_SUMMONING
+	 *
+	 * "<user> :Summoning user to IRC"
+	 *
+	 * Returned by a server answering a SUMMON message to
+	 * indicate that it is summoning that user.
+	 */
+	summoning = 342,
+
+	/**
+	 * RPL_VERSION
+	 *
+	 * "<version>.<debuglevel> <server> :<comments>"
+	 *
+	 * Reply by the server showing its version details.
+	 * The <version> is the version of the software being
+	 * used (including any patchlevel revisions) and the
+	 * <debuglevel> is used to indicate if the server is
+	 * running in "debug mode".
+	 *
+	 * The "comments" field may contain any comments about
+	 * the version or further version details.
+	 */
+	version = 351,
+
+	/**
+	 * RPL_WHOREPLY
+	 *
+	 * "<channel> <user> <host> <server> <nick> \
+	 *  <H|G>[*][@|+] :<hopcount> <real name>"
+	 */
+	whoreply = 352,
+
+	/**
+	 * RPL_ENDOFWHO
+	 *
+	 * "<name> :End of /WHO list"
+	 *
+	 * The RPL_WHOREPLY and RPL_ENDOFWHO pair are used
+	 * to answer a WHO message.  The RPL_WHOREPLY is only
+	 * sent if there is an appropriate match to the WHO
+	 * query.  If there is a list of parameters supplied
+	 * with a WHO message, a RPL_ENDOFWHO must be sent
+	 * after processing each list item with <name> being
+	 * the item.
+	 */
+	endofwho = 315,
+
+	/**
+	 * RPL_NAMREPLY
+	 *
+	 * "<channel> :[[@|+]<nick> [[@|+]<nick> [...]]]"
+	 */
+	namreply = 353,
+
+	/**
+	 * RPL_ENDOFNAMES
+	 *
+	 * "<channel> :End of /NAMES list"
+	 *
+	 * To reply to a NAMES message, a reply pair consisting
+	 * of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the
+	 * server back to the client.  If there is no channel
+	 * found as in the query, then only RPL_ENDOFNAMES is
+	 * returned.  The exception to this is when a NAMES
+	 * message is sent with no parameters and all visible
+	 * channels and contents are sent back in a series of
+	 * RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark
+	 * the end.
+	 */
+	endofnames = 366,
+
+	/**
+	 * RPL_LINKS
+	 *
+	 * "<mask> <server> :<hopcount> <server info>"
+	 */
+	links = 364,
+
+	/**
+	 * RPL_ENDOFLINKS
+	 *
+	 * "<mask> :End of /LINKS list"
+	 *
+	 * In replying to the LINKS message, a server must send
+	 * replies back using the RPL_LINKS numeric and mark the
+	 * end of the list using an RPL_ENDOFLINKS reply.
+	 */
+	endoflinks = 365,
+
+	/**
+	 * RPL_BANLIST
+	 *
+	 * "<channel> <banid>"
+	 */
+	banlist = 367,
+
+	/**
+	 * RPL_ENDOFBANLIST
+	 *
+	 * "<channel> :End of channel ban list"
+	 *
+	 * When listing the active 'bans' for a given channel,
+	 * a server is required to send the list back using the
+	 * RPL_BANLIST and RPL_ENDOFBANLIST messages.  A separate
+	 * RPL_BANLIST is sent for each active banid.  After the
+	 * banids have been listed (or if none present) a
+	 * RPL_ENDOFBANLIST must be sent.
+	 */
+	endofbanlist = 368,
+
+	/**
+	 * RPL_INFO
+	 *
+	 * ":<string>"
+	 */
+	info = 371,
+
+	/**
+	 * RPL_ENDOFINFO
+	 *
+	 * ":End of /INFO list"
+	 *
+	 * A server responding to an INFO message is required to
+	 * send all its 'info' in a series of RPL_INFO messages
+	 * with a RPL_ENDOFINFO reply to indicate the end of the
+	 * replies.
+	 */
+	endofinfo = 374,
+
+	/**
+	 * RPL_MOTDSTART
+	 *
+	 * ":- <server> Message of the day - "
+	 */
+	motdstart = 375,
+
+	/**
+	 * RPL_MOTD
+	 *
+	 * ":- <text>"
+	 */
+	motd = 372,
+
+	/**
+	 * RPL_ENDOFMOTD
+	 *
+	 * ":End of /MOTD command"
+	 *
+	 * When responding to the MOTD message and the MOTD file
+	 * is found, the file is displayed line by line, with
+	 * each line no longer than 80 characters, using
+	 * RPL_MOTD format replies.  These should be surrounded
+	 * by a RPL_MOTDSTART (before the RPL_MOTDs) and an
+	 * RPL_ENDOFMOTD (after).
+	 */
+	endofmotd = 376,
+
+	/**
+	 * RPL_YOUREOPER
+	 *
+	 * ":You are now an IRC operator"
+	 *
+	 * RPL_YOUREOPER is sent back to a client which has
+	 * just successfully issued an OPER message and gained
+	 * operator status.
+	 */
+	youreoper = 381,
+
+	/**
+	 * RPL_REHASHING
+	 *
+	 * "<config file> :Rehashing"
+	 *
+	 * If the REHASH option is used and an operator sends
+	 * a REHASH message, an RPL_REHASHING is sent back to
+	 * the operator.
+	 */
+	rehashing = 382,
+
+	/**
+	 * RPL_TIME
+	 *
+	 * "<server> :<string showing server's local time>"
+	 *
+	 * When replying to the TIME message, a server must send
+	 * the reply using the RPL_TIME format above.  The string
+	 * showing the time need only contain the correct day and
+	 * time there.  There is no further requirement for the
+	 * time string.
+	 */
+	time = 391,
+
+	/**
+	 * RPL_USERSSTART
+	 *
+	 * ":UserID   Terminal  Host"
+	 */
+	userstart = 392,
+
+	/**
+	 * RPL_USERS
+	 *
+	 * ":%-8s %-9s %-8s"
+	 */
+	users = 393,
+
+	/**
+	 * RPL_ENDOFUSERS
+	 *
+	 * ":End of users"
+	 */
+	endofusers = 394,
+
+	/**
+	 * RPL_NOUSERS
+	 *
+	 * ":Nobody logged in"
+	 *
+	 * If the USERS message is handled by a server, the
+	 * replies RPL_USERSTART, RPL_USERS, RPL_ENDOFUSERS and
+	 * RPL_NOUSERS are used.  RPL_USERSSTART must be sent
+	 * first, following by either a sequence of RPL_USERS
+	 * or a single RPL_NOUSER.  Following this is
+	 * RPL_ENDOFUSERS.
+	 */
+	nousers = 395,
+
+	/**
+	 * RPL_TRACELINK
+	 *
+	 * "Link <version & debug level> <destination> <next server>"
+	 */
+	tracelink = 200,
+
+	/**
+	 * RPL_TRACECONNECTING
+	 *
+	 * "Try. <class> <server>"
+	 */
+	traceconnecting = 201,
+
+	/**
+	 * RPL_TRACEHANDSHAKE
+	 *
+	 * "H.S. <class> <server>"
+	 */
+	tracehandshake = 202,
+
+	/**
+	 * RPL_TRACEUNKNOWN
+	 *
+	 * "???? <class> [<client IP address in dot form>]"
+	 */
+	traceunknown = 203,
+
+	/**
+	 * RPL_TRACEOPERATOR
+	 *
+	 * "Oper <class> <nick>"
+	 */
+	traceoperator = 204,
+
+	/**
+	 * RPL_TRACEUSER
+	 *
+	 * "User <class> <nick>"
+	 */
+	traceuser = 205,
+
+	/**
+	 * RPL_TRACESERVER
+	 *
+	 * "Serv <class> <int>S <int>C <server> \
+	 *  <nick!user|*!*>@<host|server>
+	 */
+	traceserver = 206,
+
+	/**
+	 * RPL_TRACENEWTYPE
+	 *
+	 * "<newtype> 0 <client name>"
+	 */
+	tracenewtype = 208,
+
+	/**
+	 * RPL_TRACELOG
+	 *
+	 * "File <logfile> <debug level>"
+	 *
+	 * The RPL_TRACE* are all returned by the server in
+	 * response to the TRACE message.  How many are
+	 * returned is dependent on the the TRACE message and
+	 * whether it was sent by an operator or not.  There
+	 * is no predefined order for which occurs first.
+	 * Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and
+	 * RPL_TRACEHANDSHAKE are all used for connections
+	 * which have not been fully established and are either
+	 * unknown, still attempting to connect or in the
+	 * process of completing the 'server handshake'.
+	 * RPL_TRACELINK is sent by any server which handles
+	 * a TRACE message and has to pass it on to another
+	 * server.  The list of RPL_TRACELINKs sent in
+	 * response to a TRACE command traversing the IRC
+	 * network should reflect the actual connectivity of
+	 * the servers themselves along that path.
+	 * RPL_TRACENEWTYPE is to be used for any connection
+	 * which does not fit in the other categories but is
+	 * being displayed anyway.
+	 */
+	tracelog = 261,
+
+	/**
+	 * RPL_STATSLINKINFO
+	 *
+	 * "<linkname> <sendq> <sent messages> \
+	 *  <sent bytes> <received messages> \
+	 *  <received bytes> <time open>"
+	 */
+	statslinkinfo = 211,
+
+	/**
+	 * RPL_STATSCOMMANDS
+	 *
+	 * "<command> <count>"
+	 */
+	statscommands = 212,
+
+	/**
+	 * RPL_STATSCLINE
+	 *
+	 * "C <host> * <name> <port> <class>"
+	 */
+	statscline = 213,
+
+	/**
+	 * RPL_STATSNLINE
+	 *
+	 * "N <host> * <name> <port> <class>"
+	 */
+	statsnline = 214,
+
+	/**
+	 * RPL_STATSILINE
+	 *
+	 * "I <host> * <host> <port> <class>"
+	 */
+	statsiline = 215,
+
+	/**
+	 * RPL_STATSKLINE
+	 *
+	 * K <host> * <username> <port> <class>"
+	 */
+	statskline = 216,
+
+	/**
+	 * RPL_STATSYLINE
+	 *
+	 * "Y <class> <ping frequency> <connect frequency> <max sendq>"
+	 */
+	statsyline = 218,
+
+	/**
+	 * RPL_ENDOFSTATS
+	 *
+	 * "<stats letter> :End of /STATS report"
+	 */
+	endofstats = 219,
+
+	/**
+	 * RPL_STATSLLINE
+	 *
+	 * "L <hostmask> * <servername> <maxdepth>"
+	 */
+	statslline = 241,
+
+	/**
+	 * RPL_STATSUPTIME
+	 *
+	 * ":Server Up %d days %d:%02d:%02d"
+	 */
+	statsuptime = 242,
+
+	/**
+	 * RPL_STATSOLINE
+	 *
+	 * "O <hostmask> * <name>"
+	 */
+	statsoline = 243,
+
+	/**
+	 * RPL_STATSHLINE
+	 *
+	 * "H <hostmask> * <servername>"
+	 */
+	statshline = 244,
+
+	/**
+	 * RPL_UMODEIS
+	 *
+	 * "<user mode string>"
+	 *
+	 * To answer a query about a client's own mode,
+	 * RPL_UMODEIS is sent back.
+	 */
+	umodeis = 221,
+
+	/**
+	 * RPL_LUSERCLIENT
+	 *
+	 * ":There are <integer> users and <integer> \
+	 *  invisible on <integer> servers"
+	 */
+	luserclient = 251,
+
+	/**
+	 * RPL_LUSEROP
+	 *
+	 * "<integer> :operator(s) online"
+	 */
+	luserop = 252,
+
+	/**
+	 * RPL_LUSERUNKNOWN
+	 *
+	 * "<integer> :unknown connection(s)"
+	 */
+	luserunknown = 253,
+
+	/**
+	 * RPL_LUSERCHANNELS
+	 *
+	 * "<integer> :channels formed"
+	 */
+	luserchannels = 254,
+
+	/**
+	 * RPL_LUSERME
+	 *
+	 * ":I have <integer> clients and <integer> servers"
+	 *
+	 * In processing an LUSERS message, the server
+	 * sends a set of replies from RPL_LUSERCLIENT,
+	 * RPL_LUSEROP, RPL_USERUNKNOWN,
+	 * RPL_LUSERCHANNELS and RPL_LUSERME.  When
+	 * replying, a server must send back
+	 * RPL_LUSERCLIENT and RPL_LUSERME.  The other
+	 * replies are only sent back if a non-zero count
+	 * is found for them.
+	 */
+	luserme = 255,
+
+	/**
+	 * RPL_ADMINME
+	 *
+	 * "<server> :Administrative info"
+	 */
+	adminme = 256,
+
+	/**
+	 * RPL_ADMINLOC1
+	 *
+	 * ":<admin info>"
+	 */
+	adminloc1 = 257,
+
+	/**
+	 * RPL_ADMINLOC2
+	 *
+	 * ":<admin info>"
+	 */
+	adminloc2 = 258,
+
+	/**
+	 * RPL_ADMINEMAIL
+	 *
+	 * ":<admin info>"
+	 *
+	 * When replying to an ADMIN message, a server
+	 * is expected to use replies RLP_ADMINME
+	 * through to RPL_ADMINEMAIL and provide a text
+	 * message with each.  For RPL_ADMINLOC1 a
+	 * description of what city, state and country
+	 * the server is in is expected, followed by
+	 * details of the university and department
+	 * (RPL_ADMINLOC2) and finally the administrative
+	 * contact for the server (an email address here
+	 * is required) in RPL_ADMINEMAIL.
+	 */
+	adminemail = 259
+};
+
+/**
+ * \brief Describe a IRC message
+ */
+struct message {
+	std::string prefix;             //!< optional prefix
+	std::string command;            //!< command (maybe string or code)
+	std::vector<std::string> args;  //!< parameters
+
+	/**
+	 * Check if the command is of the given enum number.
+	 *
+	 * \param e the code
+	 * \return true if command is a number and equals to e
+	 */
+	template <typename Enum>
+	auto is(Enum e) const noexcept -> bool
+	{
+		try {
+			return std::stoi(command) == static_cast<int>(e);
+		} catch (...) {
+			return false;
+		}
+	}
+
+	/**
+	 * Convenient function that returns an empty string if the nth argument is
+	 * not defined.
+	 *
+	 * \param index the index
+	 * \return a string or empty if out of bounds
+	 */
+	auto get(unsigned short index) const noexcept -> const std::string&;
+
+	/**
+	 * Tells if the message is a CTCP.
+	 *
+	 * \param index the param index (maybe out of bounds)
+	 * \return true if CTCP
+	 */
+	auto is_ctcp(unsigned short index) const noexcept -> bool;
+
+	/**
+	 * Parse a CTCP message.
+	 *
+	 * \pre is_ctcp(index)
+	 * \param index the param index
+	 * \return the CTCP command
+	 */
+	auto ctcp(unsigned short index) const -> std::string;
+
+	/**
+	 * Parse a IRC message.
+	 *
+	 * \param line the buffer content (without `\r\n`)
+	 * \return the message (maybe empty if line is empty)
+	 */
+	static auto parse(const std::string& line) -> message;
+};
+
+/**
+ * \brief Describe a user.
+ */
+struct user {
+	std::string nick;       //!< The nickname
+	std::string host;       //!< The hostname
+
+	/**
+	 * Parse a nick/host combination.
+	 *
+	 * \param line the line to parse
+	 * \return a user
+	 */
+	static auto parse(std::string_view line) -> user;
+};
+
+/**
+ * \brief Abstract connection to a server.
+ */
+class connection {
+public:
+	/**
+	 * Handler for connecting.
+	 */
+	using connect_handler = std::function<void (std::error_code)>;
+
+	/**
+	 * Handler for receiving.
+	 */
+	using recv_handler = std::function<void (std::error_code, message)>;
+
+	/**
+	 * Handler for sending.
+	 */
+	using send_handler = std::function<void (std::error_code)>;
+
+private:
+	boost::asio::io_context& service_;
+	boost::asio::ip::tcp::socket socket_{service_};
+	boost::asio::ip::tcp::resolver resolver_{service_};
+	boost::asio::streambuf input_{1024};
+	boost::asio::streambuf output_;
+
+	bool ipv4_{true};
+	bool ipv6_{true};
+	bool ssl_{false};
+
+#if defined(IRCCD_HAVE_SSL)
+	boost::asio::ssl::context context_{boost::asio::ssl::context::tlsv12};
+	boost::asio::ssl::stream<boost::asio::ip::tcp::socket&> ssl_socket_{socket_, context_};
+#endif
+
+#if !defined(NDEBUG)
+	bool is_connecting_{false};
+	bool is_receiving_{false};
+	bool is_sending_{false};
+#endif
+
+	void handshake(const connect_handler&);
+	void connect(const boost::asio::ip::tcp::resolver::results_type&, const connect_handler&);
+	void resolve(std::string_view, std::string_view, const connect_handler&);
+
+public:
+	/**
+	 * Default constructor.
+	 *
+	 * \param service the I/O service
+	 */
+	connection(boost::asio::io_service& service);
+
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~connection() = default;
+
+	/**
+	 * Enable IPv4
+	 *
+	 * \param enable true to enable
+	 */
+	void use_ipv4(bool enable = true) noexcept;
+
+	/**
+	 * Enable IPv6
+	 *
+	 * \param enable true to enable
+	 */
+	void use_ipv6(bool enable = true) noexcept;
+
+	/**
+	 * Enable TLS.
+	 *
+	 * \pre IRCCD_HAVE_SSL must be defined
+	 * \param enable true to enable
+	 */
+	void use_ssl(bool enable = true) noexcept;
+
+	/**
+	 * Connect to the host.
+	 *
+	 * \pre handler the handler
+	 * \pre another connect operation must not be running
+	 * \pre ipv4 or ipv6 must be set
+	 * \param hostname the hostname
+	 * \param service the service or port number
+	 * \param handler the non-null handler
+	 */
+	void connect(std::string_view hostname, std::string_view service, connect_handler handler);
+
+	/**
+	 * Start receiving data.
+	 *
+	 * The handler must not throw exceptions and `this` must be valid in the
+	 * lifetime of the handler.
+	 *
+	 * \pre another recv operation must not be running
+	 * \pre handler != nullptr
+	 * \param handler the handler to call
+	 */
+	void recv(recv_handler handler);
+
+	/**
+	 * Start sending data.
+	 *
+	 * The handler must not throw exceptions and `this` must be valid in the
+	 * lifetime of the handler.
+	 *
+	 * \pre another send operation must not be running
+	 * \pre handler != nullptr
+	 * \param message the raw message
+	 * \param handler the handler to call
+	 */
+	void send(std::string_view message, send_handler handler);
+};
+
+} // !irccd::daemon::irc
+
+#endif // !IRCCD_IRC_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/logger.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,247 @@
+/*
+ * logger.cpp -- irccd logging
+ *
+ * Copyright (c) 2013-2018 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 <cassert>
+#include <fstream>
+#include <iostream>
+#include <streambuf>
+
+#include "logger.hpp"
+
+#if defined(IRCCD_HAVE_SYSLOG)
+#	include <syslog.h>
+#endif // !IRCCD_HAVE_SYSLOG
+
+namespace irccd::daemon::logger {
+
+void logger::debug(const std::string& line)
+{
+	// Print only in debug mode, the buffer is flushed anyway.
+#if !defined(NDEBUG)
+	parent_.write_debug(parent_.filter_->pre_debug(category_, component_, line));
+#else
+	(void)line;
+#endif
+}
+
+void logger::info(const std::string& line)
+{
+	// Print only if verbose, the buffer will be flushed anyway.
+	if (parent_.verbose_)
+		parent_.write_info(parent_.filter_->pre_info(category_, component_, line));
+}
+
+void logger::warning(const std::string& line)
+{
+	parent_.write_warning(parent_.filter_->pre_warning(category_, component_, line));
+}
+
+logger::logger(sink& parent, level level, std::string_view category, std::string_view component) noexcept
+	: std::ostream(this)
+	, level_(level)
+	, parent_(parent)
+	, category_(category)
+	, component_(component)
+{
+	assert(level >= level::debug && level <= level::warning);
+}
+
+int logger::sync()
+{
+	std::string buffer = str();
+	std::string::size_type pos;
+
+	while ((pos = buffer.find("\n")) != std::string::npos) {
+		auto line = buffer.substr(0, pos);
+
+		// Remove this line.
+		buffer.erase(buffer.begin(), buffer.begin() + pos + 1);
+
+		switch (level_) {
+		case level::debug:
+			debug(line);
+			break;
+		case level::info:
+			info(line);
+			break;
+		case level::warning:
+			warning(line);
+			break;
+		default:
+			break;
+		}
+	}
+
+	str(buffer);
+
+	return 0;
+}
+
+void console_sink::write_info(const std::string& line)
+{
+	std::cout << line << std::endl;
+}
+
+void console_sink::write_warning(const std::string& line)
+{
+	std::cerr << line << std::endl;
+}
+
+void console_sink::write_debug(const std::string& line)
+{
+	std::cout << line << std::endl;
+}
+
+file_sink::file_sink(std::string normal, std::string errors)
+	: output_normal_(std::move(normal))
+	, output_error_(std::move(errors))
+{
+}
+
+void file_sink::write_info(const std::string& line)
+{
+	std::ofstream(output_normal_, std::ofstream::out | std::ofstream::app) << line << std::endl;
+}
+
+void file_sink::write_warning(const std::string& line)
+{
+	std::ofstream(output_error_, std::ofstream::out | std::ofstream::app) << line << std::endl;
+}
+
+void file_sink::write_debug(const std::string& line)
+{
+	std::ofstream(output_normal_, std::ofstream::out | std::ofstream::app) << line << std::endl;
+}
+
+void silent_sink::write_info(const std::string&)
+{
+}
+
+void silent_sink::write_warning(const std::string&)
+{
+}
+
+void silent_sink::write_debug(const std::string&)
+{
+}
+
+#if defined(IRCCD_HAVE_SYSLOG)
+
+syslog_sink::syslog_sink()
+{
+	openlog("irccd", LOG_PID, LOG_DAEMON);
+}
+
+syslog_sink::~syslog_sink()
+{
+	closelog();
+}
+
+void syslog_sink::write_info(const std::string& line)
+{
+	syslog(LOG_INFO | LOG_USER, "%s", line.c_str());
+}
+
+void syslog_sink::write_warning(const std::string& line)
+{
+	syslog(LOG_WARNING | LOG_USER, "%s", line.c_str());
+}
+
+void syslog_sink::write_debug(const std::string& line)
+{
+	syslog(LOG_DEBUG | LOG_USER, "%s", line.c_str());
+}
+
+#endif // !IRCCD_HAVE_SYSLOG
+
+sink::sink()
+	: filter_(new filter)
+{
+}
+
+auto sink::is_verbose() const noexcept -> bool
+{
+	return verbose_;
+}
+
+void sink::set_verbose(bool mode) noexcept
+{
+	verbose_ = mode;
+}
+
+void sink::set_filter(std::unique_ptr<filter> filter) noexcept
+{
+	assert(filter);
+
+	filter_ = std::move(filter);
+}
+
+auto sink::info(std::string_view category, std::string_view component) -> logger
+{
+	return logger(*this, logger::level::info, category, component);;
+}
+
+auto sink::warning(std::string_view category, std::string_view component) -> logger
+{
+	return logger(*this, logger::level::warning, category, component);;
+}
+
+auto sink::debug(std::string_view category, std::string_view component) -> logger
+{
+	return logger(*this, logger::level::debug, category, component);;
+}
+
+auto filter::pre(std::string_view category,
+                 std::string_view component,
+                 std::string_view message) const -> std::string
+{
+	std::ostringstream oss;
+
+	oss << category;
+
+	if (!component.empty())
+		oss << " " << component;
+
+	oss << ": ";
+	oss << message;
+
+	return oss.str();
+}
+
+auto filter::pre_debug(std::string_view category,
+                       std::string_view component,
+                       std::string_view message) const -> std::string
+{
+	return pre(category, component, message);
+}
+
+auto filter::pre_info(std::string_view category,
+                      std::string_view component,
+                      std::string_view message) const -> std::string
+{
+	return pre(category, component, message);
+}
+
+auto filter::pre_warning(std::string_view category,
+                         std::string_view component,
+                         std::string_view message) const -> std::string
+{
+	return pre(category, component, message);
+}
+
+} // !irccd::daemon::logger
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/logger.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,457 @@
+/*
+ * logger.hpp -- irccd logging
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_LOGGER_HPP
+#define IRCCD_DAEMON_LOGGER_HPP
+
+/**
+ * \file logger.hpp
+ * \brief Logging facilities.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <utility>
+
+namespace irccd::daemon::logger {
+
+class filter;
+class sink;
+
+/**
+ * \brief Traits for loggable objects.
+ * \ingroup daemon-loggers-traits
+ *
+ * Specialize this structure and add the following static functions to be able
+ * to log object with convenience:
+ *
+ * ## get_category
+ *
+ * The get_category function should return a single word that describe the
+ * message entry category.
+ *
+ * Synopsis:
+ *
+ * ```cpp
+ * static auto get_category(const T&) noexcept -> std::string_view;
+ * ```
+ *
+ * ## get_component
+ *
+ * The get_component function should return the identifier or any valid
+ * information about the given object that is useful for the user.
+ *
+ * If no information could be provided, an empty string can be returned.
+ *
+ * Synopsis:
+ *
+ * ```cpp
+ * static auto get_component(const T&) noexcept -> std::string_view;
+ * ```
+ */
+template <typename T>
+struct loggable_traits;
+
+/**
+ * \brief Logger object.
+ * \ingroup daemon-loggers
+ */
+class logger : public std::ostream, public std::stringbuf {
+private:
+	/**
+	 * \brief Make sink friend.
+	 */
+	friend class sink;
+
+	enum class level {
+		debug,
+		info,
+		warning
+	} level_;
+
+	sink& parent_;
+
+	std::string_view category_;
+	std::string_view component_;
+
+	void debug(const std::string&);
+	void info(const std::string&);
+	void warning(const std::string&);
+	auto sync() -> int override;
+	logger(sink&, level, std::string_view, std::string_view) noexcept;
+};
+
+/**
+ * \brief Interface to implement new logger mechanisms.
+ * \ingroup daemon-loggers-sinks
+ *
+ * Derive from this class and implement write_info, write_warning and
+ * write_debug functions.
+ *
+ * \see file_sink
+ * \see console_sink
+ * \see syslog_sink
+ * \see silent_sink
+ */
+class sink {
+private:
+	/**
+	 * \brief Make logger friend.
+	 */
+	friend class logger;
+
+	// User options.
+	bool verbose_{false};
+	std::unique_ptr<filter> filter_;
+
+protected:
+	/**
+	 * Write a debug message.
+	 *
+	 * This function is called only if NDEBUG is not defined.
+	 *
+	 * \param line the data
+	 * \see log::debug
+	 */
+	virtual void write_debug(const std::string& line) = 0;
+
+	/**
+	 * Write a information message.
+	 *
+	 * The function is called only if verbose is true.
+	 *
+	 * \param line the data
+	 * \see log::info
+	 */
+	virtual void write_info(const std::string& line) = 0;
+
+	/**
+	 * Write an error message.
+	 *
+	 * This function is always called.
+	 *
+	 * \param line the data
+	 * \see log::warning
+	 */
+	virtual void write_warning(const std::string& line) = 0;
+
+public:
+	/**
+	 * Default constructor.
+	 */
+	sink();
+
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~sink() = default;
+
+	/**
+	 * Tells if logger is verbose.
+	 *
+	 * \return true if verbose
+	 */
+	auto is_verbose() const noexcept -> bool;
+
+	/**
+	 * Set the verbosity mode.
+	 *
+	 * \param mode the new mode
+	 */
+	void set_verbose(bool mode) noexcept;
+
+	/**
+	 * Set an optional filter.
+	 *
+	 * \pre filter must not be null
+	 * \param filter the filter
+	 */
+	void set_filter(std::unique_ptr<filter> filter) noexcept;
+
+	/**
+	 * Get the stream for informational messages.
+	 *
+	 * If message is specified, a new line character is appended.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \return the output stream
+	 * \note Has no effect if verbose is set to false.
+	 */
+	auto info(std::string_view category, std::string_view component) -> logger;
+
+	/**
+	 * Convenient function with loggable objects.
+	 *
+	 * \param loggable the loggable object
+	 * \return the output stream
+	 * \see loggable_traits
+	 */
+	template <typename Loggable>
+	auto info(const Loggable& loggable) -> logger
+	{
+		return info(
+			loggable_traits<Loggable>::get_category(loggable),
+			loggable_traits<Loggable>::get_component(loggable)
+		);
+	}
+
+	/**
+	 * Get the stream for warnings.
+	 *
+	 * If message is specified, a new line character is appended.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \return the output stream
+	 */
+	auto warning(std::string_view category, std::string_view component) -> logger;
+
+	/**
+	 * Convenient function with loggable objects.
+	 *
+	 * \param loggable the loggable object
+	 * \return the output stream
+	 * \see loggable_traits
+	 */
+	template <typename Loggable>
+	auto warning(const Loggable& loggable) -> logger
+	{
+		return warning(
+			loggable_traits<Loggable>::get_category(loggable),
+			loggable_traits<Loggable>::get_component(loggable)
+		);
+	}
+
+	/**
+	 * Get the stream for debug messages.
+	 *
+	 * If message is specified, a new line character is appended.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \return the output stream
+	 * \note Has no effect if compiled in release mode.
+	 */
+	auto debug(std::string_view category, std::string_view component) -> logger;
+
+	/**
+	 * Convenient function with loggable objects.
+	 *
+	 * \param loggable the loggable object
+	 * \return the output stream
+	 * \see loggable_traits
+	 */
+	template <typename Loggable>
+	auto debug(const Loggable& loggable) -> logger
+	{
+		return debug(
+			loggable_traits<Loggable>::get_category(loggable),
+			loggable_traits<Loggable>::get_component(loggable)
+		);
+	}
+};
+
+/**
+ * \brief Filter messages before printing them.
+ * \ingroup daemon-loggers
+ */
+class filter {
+private:
+public:
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~filter() = default;
+
+	/**
+	 * Default function called for each virtual ones.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \param message the message
+	 * \return default formatted message
+	 */
+	auto pre(std::string_view category,
+	         std::string_view component,
+	         std::string_view message) const -> std::string;
+
+
+	/**
+	 * Update the debug message.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \param message the message
+	 * \return the message
+	 */
+	virtual auto pre_debug(std::string_view category,
+	                       std::string_view component,
+	                       std::string_view message) const -> std::string;
+
+	/**
+	 * Update the information message.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \param message the message
+	 * \return the updated message
+	 */
+	virtual auto pre_info(std::string_view category,
+	                      std::string_view component,
+	                      std::string_view message) const -> std::string;
+
+	/**
+	 * Update the warning message.
+	 *
+	 * \param category the category subsystem
+	 * \param component the optional component
+	 * \param message the message
+	 * \return the updated message
+	 */
+	virtual auto pre_warning(std::string_view category,
+	                         std::string_view component,
+	                         std::string_view message) const -> std::string;
+};
+
+/**
+ * \brief Logger implementation for console output using std::cout and
+ *        std::cerr.
+ * \ingroup daemon-loggers-sinks
+ */
+class console_sink : public sink {
+protected:
+	/**
+	 * \copydoc sink::write_debug
+	 */
+	void write_debug(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_info
+	 */
+	void write_info(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_warning
+	 */
+	void write_warning(const std::string& line) override;
+};
+
+/**
+ * \brief Output to a files.
+ * \ingroup daemon-loggers-sinks
+ */
+class file_sink : public sink {
+private:
+	std::string output_normal_;
+	std::string output_error_;
+
+protected:
+	/**
+	 * \copydoc sink::write_debug
+	 */
+	void write_debug(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_info
+	 */
+	void write_info(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_warning
+	 */
+	void write_warning(const std::string& line) override;
+
+public:
+	/**
+	 * Outputs to files.
+	 *
+	 * \param normal the path to the normal logs
+	 * \param errors the path to the errors logs
+	 */
+	file_sink(std::string normal, std::string errors);
+};
+
+/**
+ * \brief Use to disable logs.
+ * \ingroup daemon-loggers-sinks
+ *
+ * Useful for unit tests when some classes may emits log.
+ */
+class silent_sink : public sink {
+protected:
+	/**
+	 * \copydoc sink::write_debug
+	 */
+	void write_debug(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_info
+	 */
+	void write_info(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_warning
+	 */
+	void write_warning(const std::string& line) override;
+};
+
+#if defined(IRCCD_HAVE_SYSLOG)
+
+/**
+ * \brief Implements logger into syslog.
+ * \ingroup daemon-loggers-sinks
+ */
+class syslog_sink : public sink {
+protected:
+	/**
+	 * \copydoc sink::write_debug
+	 */
+	void write_debug(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_info
+	 */
+	void write_info(const std::string& line) override;
+
+	/**
+	 * \copydoc sink::write_warning
+	 */
+	void write_warning(const std::string& line) override;
+
+public:
+	/**
+	 * Open the syslog.
+	 */
+	syslog_sink();
+
+	/**
+	 * Close the syslog.
+	 */
+	~syslog_sink();
+};
+
+#endif // !IRCCD_HAVE_SYSLOG
+
+} // !irccd::daemon::logger
+
+#endif // !IRCCD_DAEMON_LOGGER_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/plugin.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,277 @@
+/*
+ * plugin.cpp -- irccd JavaScript plugin interface
+ *
+ * Copyright (c) 2013-2018 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 <cassert>
+#include <sstream>
+
+#include <boost/filesystem.hpp>
+
+#include <irccd/system.hpp>
+#include <irccd/string_util.hpp>
+
+#include "plugin.hpp"
+
+namespace irccd::daemon {
+
+plugin::plugin(std::string id) noexcept
+	: id_(std::move(id))
+{
+	assert(string_util::is_identifier(id_));
+}
+
+auto plugin::get_id() const noexcept -> const std::string&
+{
+	return id_;
+}
+
+auto plugin::get_author() const noexcept -> std::string_view
+{
+	return "unknown";
+}
+
+auto plugin::get_license() const noexcept -> std::string_view
+{
+	return "unknown";
+}
+
+auto plugin::get_summary() const noexcept -> std::string_view
+{
+	return "unknown";
+}
+
+auto plugin::get_version() const noexcept -> std::string_view
+{
+	return "unknown";
+}
+
+auto plugin::get_options() const -> map
+{
+	return {};
+}
+
+void plugin::set_options(const map&)
+{
+}
+
+auto plugin::get_formats() const -> map
+{
+	return {};
+}
+
+void plugin::set_formats(const map&)
+{
+}
+
+auto plugin::get_paths() const -> map
+{
+	return {};
+}
+
+void plugin::set_paths(const map&)
+{
+}
+
+void plugin::handle_command(bot&, const message_event&)
+{
+}
+
+void plugin::handle_connect(bot&, const connect_event&)
+{
+}
+
+void plugin::handle_disconnect(bot&, const disconnect_event&)
+{
+}
+
+void plugin::handle_invite(bot&, const invite_event&)
+{
+}
+
+void plugin::handle_join(bot&, const join_event&)
+{
+}
+
+void plugin::handle_kick(bot&, const kick_event&)
+{
+}
+
+void plugin::handle_load(bot&)
+{
+}
+
+void plugin::handle_message(bot&, const message_event&)
+{
+}
+
+void plugin::handle_me(bot&, const me_event&)
+{
+}
+
+void plugin::handle_mode(bot&, const mode_event&)
+{
+}
+
+void plugin::handle_names(bot&, const names_event&)
+{
+}
+
+void plugin::handle_nick(bot&, const nick_event&)
+{
+}
+
+void plugin::handle_notice(bot&, const notice_event&)
+{
+}
+
+void plugin::handle_part(bot&, const part_event&)
+{
+}
+
+void plugin::handle_reload(bot&)
+{
+}
+
+void plugin::handle_topic(bot&, const topic_event&)
+{
+}
+
+void plugin::handle_unload(bot&)
+{
+}
+
+void plugin::handle_whois(bot&, const whois_event&)
+{
+}
+
+plugin_loader::plugin_loader(std::vector<std::string> directories,
+                             std::vector<std::string> extensions) noexcept
+	: directories_(std::move(directories))
+	, extensions_(std::move(extensions))
+{
+}
+
+auto plugin_loader::is_supported(std::string_view path) noexcept -> bool
+{
+	const std::string name(path);
+
+	for (const auto& ext : extensions_)
+		if (boost::filesystem::path(name).extension() == ext)
+			return true;
+
+	/*
+	 * If extensions are not specified, let plugin_loader::open a chance
+	 * to be called anyway.
+	 */
+	return extensions_.empty();
+}
+
+auto plugin_loader::find(std::string_view name) -> std::shared_ptr<plugin>
+{
+	std::vector<std::string> filenames;
+
+	if (directories_.empty())
+		filenames = sys::plugin_filenames(std::string(name), extensions_);
+	else {
+		for (const auto& dir : directories_)
+			for (const auto& ext : extensions_)
+				filenames.push_back(dir + std::string("/") + std::string(name) + ext);
+	}
+
+	for (const auto& candidate : filenames) {
+		boost::system::error_code ec;
+
+		if (!boost::filesystem::exists(candidate, ec) || ec)
+			continue;
+
+		auto plugin = open(name, candidate);
+
+		if (plugin)
+			return plugin;
+	}
+
+	return nullptr;
+}
+
+plugin_error::plugin_error(error errc, std::string_view name, std::string_view message)
+	: system_error(make_error_code(errc))
+	, name_(std::move(name))
+	, message_(std::move(message))
+{
+	std::ostringstream oss;
+
+	oss << code().message();
+
+	std::istringstream iss(message_);
+	std::string line;
+
+	while (getline(iss, line))
+		oss << "\n" << line;
+
+	what_ = oss.str();
+}
+
+auto plugin_error::get_name() const noexcept -> const std::string&
+{
+	return name_;
+}
+
+auto plugin_error::get_message() const noexcept -> const std::string&
+{
+	return message_;
+}
+
+auto plugin_error::what() const noexcept -> const char*
+{
+	return what_.c_str();
+}
+
+auto plugin_category() -> const std::error_category&
+{
+	static const class category : public std::error_category {
+	public:
+		auto name() const noexcept -> const char* override
+		{
+			return "plugin";
+		}
+
+		auto message(int e) const -> std::string override
+		{
+			switch (static_cast<plugin_error::error>(e)) {
+			case plugin_error::not_found:
+				return "plugin not found";
+			case plugin_error::invalid_identifier:
+				return "invalid plugin identifier";
+			case plugin_error::exec_error:
+				return "plugin exec error";
+			case plugin_error::already_exists:
+				return "plugin already exists";
+			default:
+				return "no error";
+			}
+		}
+	} category;
+
+	return category;
+}
+
+auto make_error_code(plugin_error::error e) -> std::error_code
+{
+	return { static_cast<int>(e), plugin_category() };
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/plugin.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,480 @@
+/*
+ * plugin.hpp -- irccd JavaScript plugin interface
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_PLUGIN_HPP
+#define IRCCD_DAEMON_PLUGIN_HPP
+
+/**
+ * \file plugin.hpp
+ * \brief irccd plugins
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <unordered_map>
+#include <vector>
+
+namespace irccd::daemon {
+
+class bot;
+
+struct connect_event;
+struct disconnect_event;
+struct invite_event;
+struct join_event;
+struct kick_event;
+struct me_event;
+struct message_event;
+struct mode_event;
+struct names_event;
+struct nick_event;
+struct notice_event;
+struct part_event;
+struct topic_event;
+struct whois_event;
+
+/**
+ * \ingroup plugins
+ * \brief Abstract plugin.
+ *
+ * A plugin is identified by name and can be loaded and unloaded at runtime.
+ */
+class plugin : public std::enable_shared_from_this<plugin> {
+public:
+	/**
+	 * Map for key/value pairs.
+	 *
+	 * Used in options, formats and paths.
+	 */
+	using map = std::unordered_map<std::string, std::string>;
+
+private:
+	std::string id_;
+
+public:
+	/**
+	 * Construct a plugin.
+	 *
+	 * \pre id must be a valid identifier
+	 * \param id the plugin id
+	 */
+	plugin(std::string id) noexcept;
+
+	/**
+	 * Temporary, close all timers.
+	 */
+	virtual ~plugin() = default;
+
+	/**
+	 * Get user unique id.
+	 *
+	 * \return the plugin id
+	 */
+	auto get_id() const noexcept -> const std::string&;
+
+	/**
+	 * Get the plugin name.
+	 *
+	 * \return the plugin name
+	 */
+	virtual auto get_name() const noexcept -> std::string_view = 0;
+
+	/**
+	 * Get the author.
+	 *
+	 * \return the author
+	 */
+	virtual auto get_author() const noexcept -> std::string_view;
+
+	/**
+	 * Get the license.
+	 *
+	 * \return the license
+	 */
+	virtual auto get_license() const noexcept -> std::string_view;
+
+	/**
+	 * Get the summary.
+	 *
+	 * \return the summary
+	 */
+	virtual auto get_summary() const noexcept -> std::string_view;
+
+	/**
+	 * Get the version.
+	 *
+	 * \return the version
+	 */
+	virtual auto get_version() const noexcept -> std::string_view;
+
+	/**
+	 * Get all options.
+	 *
+	 * \return options
+	 */
+	virtual auto get_options() const -> map;
+
+	/**
+	 * Set all options.
+	 *
+	 * \param map the options
+	 */
+	virtual void set_options(const map& map);
+
+	/**
+	 * Get all formats.
+	 *
+	 * \return formats
+	 */
+	virtual auto get_formats() const -> map;
+
+	/**
+	 * Set all formats.
+	 *
+	 * \param map the formats
+	 */
+	virtual void set_formats(const map& map);
+
+	/**
+	 * Get all paths.
+	 *
+	 * \return paths
+	 */
+	virtual auto get_paths() const -> map;
+
+	/**
+	 * Set all paths.
+	 *
+	 * \param map the paths
+	 */
+	virtual void set_paths(const map& map);
+
+	/**
+	 * On channel message. This event will call onMessage or
+	 * onCommand if the messages starts with the command character
+	 * plus the plugin name.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_command(bot& bot, const message_event& event);
+
+	/**
+	 * On successful connection.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_connect(bot& bot, const connect_event& event);
+
+	/**
+	 * On disconnection.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_disconnect(bot& bot, const disconnect_event& event);
+
+	/**
+	 * On invitation.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_invite(bot& bot, const invite_event& event);
+
+	/**
+	 * On join.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_join(bot& bot, const join_event& event);
+
+	/**
+	 * On kick.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_kick(bot& bot, const kick_event& event);
+
+	/**
+	 * On load.
+	 *
+	 * \param bot the irccd instance
+	 */
+	virtual void handle_load(bot& bot);
+
+	/**
+	 * On channel message.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_message(bot& bot, const message_event& event);
+
+	/**
+	 * On CTCP Action.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_me(bot& bot, const me_event& event);
+
+	/**
+	 * On user mode change.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_mode(bot& bot, const mode_event& event);
+
+	/**
+	 * On names listing.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_names(bot& bot, const names_event& event);
+
+	/**
+	 * On nick change.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_nick(bot& bot, const nick_event& event);
+
+	/**
+	 * On user notice.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_notice(bot& bot, const notice_event& event);
+
+	/**
+	 * On part.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_part(bot& bot, const part_event& event);
+
+	/**
+	 * On reload.
+	 *
+	 * \param bot the irccd instance
+	 */
+	virtual void handle_reload(bot& bot);
+
+	/**
+	 * On topic change.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_topic(bot& bot, const topic_event& event);
+
+	/**
+	 * On unload.
+	 *
+	 * \param bot the irccd instance
+	 */
+	virtual void handle_unload(bot& bot);
+
+	/**
+	 * On whois information.
+	 *
+	 * \param bot the irccd instance
+	 * \param event the event
+	 */
+	virtual void handle_whois(bot& bot, const whois_event& event);
+};
+
+/**
+ * \ingroup plugins
+ * \brief Abstract interface for searching plugins.
+ *
+ * This class is used to make loading of plugins extensible, the plugin_service
+ * knows some predefined plugins loaders and use them to search for available
+ * plugins.
+ *
+ * This makes easier to implement new plugins or new ways of loading them.
+ *
+ * \see dynlib_plugin_loader
+ * \see js_plugin_loader
+ */
+class plugin_loader {
+private:
+	std::vector<std::string> directories_;
+	std::vector<std::string> extensions_;
+
+public:
+	/**
+	 * Construct the loader with a predefined set of directories and
+	 * extensions.
+	 *
+	 * If directories is not specified, a sensible default list of system
+	 * and user paths are searched.
+	 *
+	 * \pre !extensions.empty()
+	 * \param directories optional list of directories to search
+	 * \param extensions optional list of extensions
+	 */
+	plugin_loader(std::vector<std::string> directories,
+	              std::vector<std::string> extensions) noexcept;
+
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~plugin_loader() = default;
+
+	/**
+	 * Tells if the plugin should be opened by checking file extension.
+	 *
+	 * \param path the path
+	 * \return true if the extension matches
+	 */
+	virtual auto is_supported(std::string_view path) noexcept -> bool;
+
+	/**
+	 * Try to open the plugin specified by path.
+	 *
+	 * The implementation must test if the plugin is suitable for opening, by
+	 * testing extension for example.
+	 *
+	 * \param id the plugin identifier
+	 * \param file the file path
+	 * \return the plugin
+	 * \throw plugin_error on errors
+	 */
+	virtual auto open(std::string_view id, std::string_view file) -> std::shared_ptr<plugin> = 0;
+
+	/**
+	 * Search for a plugin named by this id.
+	 *
+	 * \param id the plugin id
+	 * \return the plugin
+	 * \throw plugin_error on errors
+	 */
+	virtual auto find(std::string_view id) -> std::shared_ptr<plugin>;
+};
+
+/**
+ * \ingroup plugins
+ * \brief Plugin error.
+ */
+class plugin_error : public std::system_error {
+public:
+	/**
+	 * \brief Plugin related errors.
+	 */
+	enum error {
+		//!< No error.
+		no_error = 0,
+
+		//!< The specified identifier is invalid.
+		invalid_identifier,
+
+		//!< The specified plugin is not found.
+		not_found,
+
+		//!< The plugin was unable to run the function.
+		exec_error,
+
+		//!< The plugin is already loaded.
+		already_exists,
+	};
+
+private:
+	std::string name_;
+	std::string message_;
+	std::string what_;
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param code the error code
+	 * \param name the plugin name
+	 * \param message the optional message (e.g. error from plugin)
+	 */
+	plugin_error(error code, std::string_view name = "", std::string_view message = "");
+
+	/**
+	 * Get the plugin name.
+	 *
+	 * \return the name
+	 */
+	auto get_name() const noexcept -> const std::string&;
+
+	/**
+	 * Get the additional message.
+	 *
+	 * \return the message
+	 */
+	auto get_message() const noexcept -> const std::string&;
+
+	/**
+	 * Get message appropriate for use with logger.
+	 *
+	 * \return the error message
+	 */
+	auto what() const noexcept -> const char* override;
+};
+
+/**
+ * Get the plugin error category singleton.
+ *
+ * \return the singleton
+ */
+auto plugin_category() -> const std::error_category&;
+
+/**
+ * Create a std::error_code from plugin_error::error enum.
+ *
+ * \param e the error code
+ * \return the error code
+ */
+auto make_error_code(plugin_error::error e) -> std::error_code;
+
+} // !irccd::daemon
+
+/**
+ * \cond IRCCD_HIDDEN_SYMBOLS
+ */
+
+namespace std {
+
+template <>
+struct is_error_code_enum<irccd::daemon::plugin_error::error> : public std::true_type {
+};
+
+} // !std
+
+/**
+ * \endcond
+ */
+
+#endif // !IRCCD_DAEMON_PLUGIN_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/plugin_service.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,283 @@
+/*
+ * plugin_service.cpp -- plugin service
+ *
+ * Copyright (c) 2013-2018 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 <boost/format.hpp>
+
+#include <irccd/config.hpp>
+#include <irccd/string_util.hpp>
+#include <irccd/system.hpp>
+
+#include "bot.hpp"
+#include "logger.hpp"
+#include "plugin_service.hpp"
+
+using boost::format;
+using boost::str;
+
+namespace irccd::daemon {
+
+namespace {
+
+auto to_map(const config& conf, const std::string& section) -> plugin::map
+{
+	plugin::map ret;
+
+	for (const auto& opt : conf.get(section))
+		ret.emplace(opt.get_key(), opt.get_value());
+
+	return ret;
+}
+
+} // !namespace
+
+plugin_service::plugin_service(bot& bot) noexcept
+	: bot_(bot)
+{
+}
+
+plugin_service::~plugin_service()
+{
+	for (const auto& plg : plugins_) {
+		try {
+			plg->handle_unload(bot_);
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning(*plg) << ex.what() << std::endl;
+		}
+	}
+}
+
+auto plugin_service::list() const noexcept -> plugins
+{
+	return plugins_;
+}
+
+auto plugin_service::has(std::string_view id) const noexcept -> bool
+{
+	return get(id) != nullptr;
+}
+
+auto plugin_service::get(std::string_view id) const noexcept -> std::shared_ptr<plugin>
+{
+	const auto find = [id] (const auto& plg) {
+		return plg->get_id() == id;
+	};
+
+	if (const auto it = std::find_if(plugins_.begin(), plugins_.end(), find); it != plugins_.end())
+		return *it;
+
+	return nullptr;
+}
+
+auto plugin_service::require(std::string_view id) const -> std::shared_ptr<plugin>
+{
+	auto plugin = get(id);
+
+	if (!plugin)
+		throw plugin_error(plugin_error::not_found, id);
+
+	return plugin;
+}
+
+void plugin_service::add(std::shared_ptr<plugin> plugin)
+{
+	assert(plugin);
+
+	plugins_.push_back(std::move(plugin));
+}
+
+void plugin_service::add_loader(std::unique_ptr<plugin_loader> loader)
+{
+	assert(loader);
+
+	loaders_.push_back(std::move(loader));
+}
+
+auto plugin_service::get_options(std::string_view id) -> plugin::map
+{
+	return to_map(bot_.get_config(), str(format("plugin.%1%") % id));
+}
+
+auto plugin_service::get_formats(std::string_view id) -> plugin::map
+{
+	return to_map(bot_.get_config(), str(format("format.%1%") % id));
+}
+
+auto plugin_service::get_paths(std::string_view id) -> plugin::map
+{
+	auto defaults = to_map(bot_.get_config(), "paths");
+	auto paths = to_map(bot_.get_config(), str(format("paths.%1%") % id));
+
+	// Fill defaults paths.
+	if (!defaults.count("cache"))
+		defaults.emplace("cache", sys::cachedir().string());
+	if (!defaults.count("data"))
+		defaults.emplace("data", sys::datadir().string());
+	if (!defaults.count("config"))
+		defaults.emplace("config", sys::sysconfdir().string());
+
+	const auto join = [id] (auto path) {
+		return (boost::filesystem::path(path) / "plugin" / std::string(id)).string();
+	};
+
+	// Now fill missing fields.
+	if (!paths.count("cache"))
+		paths.emplace("cache", join(defaults["cache"]));
+	if (!paths.count("data"))
+		paths.emplace("data", join(defaults["data"]));
+	if (!paths.count("config"))
+		paths.emplace("config", join(defaults["config"]));
+
+	return paths;
+}
+
+auto plugin_service::open(std::string_view id, std::string_view path) -> std::shared_ptr<plugin>
+{
+	for (const auto& loader : loaders_) {
+		if (!loader->is_supported(path))
+			continue;
+
+		auto plugin = loader->open(id, path);
+
+		if (plugin)
+			return plugin;
+	}
+
+	return nullptr;
+}
+
+auto plugin_service::find(std::string_view id) -> std::shared_ptr<plugin>
+{
+	for (const auto& loader : loaders_) {
+		try {
+			auto plugin = loader->find(id);
+
+			if (plugin)
+				return plugin;
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning("plugin", id) << ex.what() << std::endl;
+		}
+	}
+
+	return nullptr;
+}
+
+void plugin_service::load(std::string_view id, std::string_view path)
+{
+	if (has(id))
+		throw plugin_error(plugin_error::already_exists, id);
+
+	std::shared_ptr<plugin> plugin;
+
+	if (path.empty())
+		plugin = find(id);
+	else
+		plugin = open(id, std::move(path));
+
+	if (!plugin)
+		throw plugin_error(plugin_error::not_found, id);
+
+	plugin->set_options(get_options(id));
+	plugin->set_formats(get_formats(id));
+	plugin->set_paths(get_paths(id));
+
+	exec(plugin, &plugin::handle_load, bot_);
+	add(plugin);
+
+	bot_.get_log().info(*plugin) << "loaded version " << plugin->get_version() << std::endl;
+}
+
+void plugin_service::reload(std::string_view id)
+{
+	auto plugin = get(id);
+
+	if (!plugin)
+		throw plugin_error(plugin_error::not_found, id);
+
+	exec(plugin, &plugin::handle_reload, bot_);
+}
+
+void plugin_service::unload(std::string_view id)
+{
+	const auto find = [id] (const auto& plg) {
+		return plg->get_id() == id;
+	};
+
+	const auto it = std::find_if(plugins_.begin(), plugins_.end(), find);
+
+	if (it == plugins_.end())
+		throw plugin_error(plugin_error::not_found, id);
+
+	// Erase first, in case of throwing.
+	const auto save = *it;
+
+	plugins_.erase(it);
+	exec(save, &plugin::handle_unload, bot_);
+}
+
+void plugin_service::clear() noexcept
+{
+	while (plugins_.size() > 0) {
+		const auto plugin = plugins_[0];
+
+		try {
+			unload(plugin->get_id());
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning(*plugin) << ex.what() << std::endl;
+		}
+	}
+}
+
+void plugin_service::load(const config& cfg) noexcept
+{
+	for (const auto& option : cfg.get("plugins")) {
+		if (!string_util::is_identifier(option.get_key()))
+			continue;
+
+		auto id = option.get_key();
+		auto p = get(id);
+
+		// Reload the plugin if already loaded.
+		if (p) {
+			p->set_options(get_options(id));
+			p->set_formats(get_formats(id));
+			p->set_paths(get_paths(id));
+		} else {
+			try {
+				load(id, option.get_value());
+			} catch (const std::exception& ex) {
+				bot_.get_log().warning("plugin", id) << ex.what() << std::endl;
+			}
+		}
+	}
+}
+
+namespace logger {
+
+auto loggable_traits<plugin>::get_category(const plugin&) -> std::string_view
+{
+	return "plugin";
+}
+
+auto loggable_traits<plugin>::get_component(const plugin& plugin) -> std::string_view
+{
+	return plugin.get_id();
+}
+
+} // !logger
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/plugin_service.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,290 @@
+/*
+ * plugin_service.hpp -- plugin service
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_PLUGIN_SERVICE_HPP
+#define IRCCD_DAEMON_PLUGIN_SERVICE_HPP
+
+/**
+ * \file plugin_service.hpp
+ * \brief Plugin service.
+ */
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "plugin.hpp"
+
+namespace irccd {
+
+class config;
+
+namespace daemon {
+
+class bot;
+
+/**
+ * \brief Manage plugins.
+ * \ingroup plugins
+ * \ingroup services
+ */
+class plugin_service {
+public:
+	/**
+	 * \brief Map of plugins.
+	 */
+	using plugins = std::vector<std::shared_ptr<plugin>>;
+
+	/**
+	 * \brief List of loaders.
+	 */
+	using plugin_loaders = std::vector<std::unique_ptr<plugin_loader>>;
+
+private:
+	bot& bot_;
+	plugins plugins_;
+	plugin_loaders loaders_;
+
+public:
+	/**
+	 * Create the plugin service.
+	 *
+	 * \param bot the irccd instance
+	 */
+	plugin_service(bot& bot) noexcept;
+
+	/**
+	 * Destroy plugins.
+	 */
+	virtual ~plugin_service();
+
+	/**
+	 * Get the list of plugins.
+	 *
+	 * \return the list of plugins
+	 */
+	auto list() const noexcept -> plugins;
+
+	/**
+	 * Check if a plugin is loaded.
+	 *
+	 * \param id the plugin id
+	 * \return true if has plugin
+	 */
+	auto has(std::string_view id) const noexcept -> bool;
+
+	/**
+	 * Get a loaded plugin or null if not found.
+	 *
+	 * \param id the plugin id
+	 * \return the plugin or empty one if not found
+	 */
+	auto get(std::string_view id) const noexcept -> std::shared_ptr<plugin>;
+
+	/**
+	 * Find a loaded plugin.
+	 *
+	 * \param id the plugin id
+	 * \return the plugin
+	 * \throw plugin_error on errors
+	 */
+	auto require(std::string_view id) const -> std::shared_ptr<plugin>;
+
+	/**
+	 * Add the specified plugin to the registry.
+	 *
+	 * \pre plg != nullptr
+	 * \param plg the plugin
+	 * \note the plugin is only added to the list, no action is performed on it
+	 */
+	void add(std::shared_ptr<plugin> plg);
+
+	/**
+	 * Add a loader.
+	 *
+	 * \pre loader != nullptr
+	 * \param loader the loader
+	 */
+	void add_loader(std::unique_ptr<plugin_loader> loader);
+
+	/**
+	 * Get the configuration for the specified plugin.
+	 *
+	 * \param id the plugin id
+	 * \return the configuration
+	 */
+	auto get_options(std::string_view id) -> plugin::map;
+
+	/**
+	 * Get the formats for the specified plugin.
+	 *
+	 * \param id the plugin id
+	 * \return the formats
+	 */
+	auto get_formats(std::string_view id) -> plugin::map;
+
+	/**
+	 * Get the paths for the specified plugin.
+	 *
+	 * If none is defined, return the default ones.
+	 *
+	 * \param id the plugin id
+	 * \return the paths
+	 */
+	auto get_paths(std::string_view id) -> plugin::map;
+
+	/**
+	 * Generic function for opening the plugin at the given path.
+	 *
+	 * This function will search for every pluginLoader and call open() on it,
+	 * the first one that success will be returned.
+	 *
+	 * \param id the plugin id
+	 * \param path the path to the file
+	 * \return the plugin or nullptr on failures
+	 */
+	auto open(std::string_view id, std::string_view path) -> std::shared_ptr<plugin>;
+
+	/**
+	 * Generic function for finding a plugin.
+	 *
+	 * \param id the plugin id
+	 * \return the plugin or nullptr on failures
+	 */
+	auto find(std::string_view id) -> std::shared_ptr<plugin>;
+
+	/**
+	 * Convenient wrapper that loads a plugin, call handle_load and add it
+	 * to the registry.
+	 *
+	 * Any errors are printed using logger.
+	 *
+	 * \param id the plugin id
+	 * \param path the optional path (searched if empty)
+	 */
+	void load(std::string_view id, std::string_view path = "");
+
+	/**
+	 * Unload a plugin and remove it.
+	 *
+	 * \param id the plugin id
+	 */
+	void unload(std::string_view id);
+
+	/**
+	 * Reload a plugin by calling onReload.
+	 *
+	 * \param id the plugin id
+	 * \throw std::exception on failures
+	 */
+	void reload(std::string_view id);
+
+	/**
+	 * Call a plugin function and throw an exception with the following errors:
+	 *
+	 *   - plugin_error::not_found if not loaded
+	 *   - plugin_error::exec_error if function failed
+	 *
+	 * \pre plugin != nullptr
+	 * \param plugin the plugin
+	 * \param fn the plugin member function (pointer to member)
+	 * \param args the arguments to pass
+	 */
+	template <typename Func, typename... Args>
+	void exec(std::shared_ptr<plugin> plugin, Func fn, Args&&... args)
+	{
+		assert(plugin);
+
+		// TODO: replace with C++17 std::invoke.
+		try {
+			((*plugin).*(fn))(std::forward<Args>(args)...);
+		} catch (const std::exception& ex) {
+			throw plugin_error(plugin_error::exec_error, plugin->get_name(), ex.what());
+		} catch (...) {
+			throw plugin_error(plugin_error::exec_error, plugin->get_name());
+		}
+	}
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \param name the plugin name
+	 * \param fn the plugin member function (pointer to member)
+	 * \param args the arguments to pass
+	 */
+	template <typename Func, typename... Args>
+	void exec(const std::string& name, Func fn, Args&&... args)
+	{
+		auto plugin = find(name);
+
+		if (!plugin)
+			throw plugin_error(plugin_error::not_found, plugin->get_name());
+
+		exec(plugin, fn, std::forward<Args>(args)...);
+	}
+
+	/**
+	 * Remove all plugins.
+	 */
+	void clear() noexcept;
+
+	/**
+	 * Load all plugins.
+	 *
+	 * \param cfg the config
+	 */
+	void load(const config& cfg) noexcept;
+};
+
+namespace logger {
+
+template <typename T>
+struct loggable_traits;
+
+/**
+ * \brief Implement Loggable traits for plugin.
+ * \ingroup logger-traits
+ */
+template <>
+struct loggable_traits<plugin> {
+	/**
+	 * Return "plugin"
+	 *
+	 * \param plugin the plugin
+	 * \return the category
+	 */
+	static auto get_category(const plugin& plugin) -> std::string_view;
+
+	/**
+	 * Return the plugin id.
+	 *
+	 * \param plugin the plugin
+	 * \return the plugin id
+	 */
+	static auto get_component(const plugin& plugin) -> std::string_view;
+};
+
+} // !logger
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_PLUGIN_SERVICE_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/rule.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,80 @@
+/*
+ * rule.cpp -- rule for server and channels
+ *
+ * Copyright (c) 2013-2018 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 <algorithm>
+#include <cctype>
+
+#include "rule.hpp"
+
+namespace irccd::daemon {
+
+auto rule::match_set(const set& set, const std::string& value) const noexcept -> bool
+{
+	return set.empty() || set.count(value) == 1;
+}
+
+auto rule::match(std::string_view server,
+                 std::string_view channel,
+                 std::string_view nick,
+                 std::string_view plugin,
+                 std::string_view event) const noexcept -> bool
+{
+	const auto tolower = [] (auto str) noexcept -> std::string {
+		std::string ret(str);
+		std::transform(ret.begin(), ret.end(), ret.begin(), ::tolower);
+		return ret;
+	};
+
+	return match_set(servers, tolower(server)) &&
+	       match_set(channels, tolower(channel)) &&
+	       match_set(origins, tolower(nick)) &&
+	       match_set(plugins, tolower(plugin)) &&
+	       match_set(events, std::string(event));
+}
+
+auto rule_category() -> const std::error_category&
+{
+	static const class category : public std::error_category {
+	public:
+		auto name() const noexcept -> const char* override
+		{
+			return "rule";
+		}
+
+		auto message(int e) const -> std::string override
+		{
+			switch (static_cast<rule_error::error>(e)) {
+			case rule_error::invalid_action:
+				return "invalid rule action";
+			case rule_error::invalid_index:
+				return "invalid rule index";
+			default:
+				return "no error";
+			}
+		}
+	} category;
+
+	return category;
+}
+
+auto make_error_code(rule_error::error e) -> std::error_code
+{
+	return { static_cast<int>(e), rule_category() };
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/rule.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,149 @@
+/*
+ * rule.hpp -- rule for server and channels
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_RULE_HPP
+#define IRCCD_DAEMON_RULE_HPP
+
+/**
+ * \file rule.hpp
+ * \brief Rule description
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <cassert>
+#include <set>
+#include <string>
+#include <system_error>
+
+namespace irccd::daemon {
+
+/**
+ * \brief Manage rule to activate or deactive events.
+ */
+struct rule {
+	/**
+	 * List of criterias.
+	 */
+	using set = std::set<std::string>;
+
+	/**
+	 * \brief Rule action type.
+	 */
+	enum class action_type {
+		accept,         //!< The event is accepted (default)
+		drop            //!< The event is dropped
+	};
+
+	set servers;            //!< The list of servers
+	set channels;           //!< The list of channels
+	set origins;            //!< The list of originators
+	set plugins;            //!< The list of plugins
+	set events;             //!< The list of events
+
+	/**
+	 * The action.
+	 */
+	action_type action{action_type::accept};
+
+	/**
+	 * Check if a set contains the value and return true if it is or return
+	 * true if value is empty (which means applicable).
+	 *
+	 * \param set the set to test
+	 * \param value the value
+	 * \return true if match
+	 */
+	auto match_set(const set& set, const std::string& value) const noexcept -> bool;
+
+	/**
+	 * Check if that rule apply for the given criterias.
+	 *
+	 * \param server the server
+	 * \param channel the channel
+	 * \param origin the origin
+	 * \param plugin the plugin
+	 * \param event the event
+	 * \return true if match
+	 */
+	auto match(std::string_view server,
+	           std::string_view channel,
+	           std::string_view origin,
+	           std::string_view plugin,
+	           std::string_view event) const noexcept -> bool;
+};
+
+/**
+ * \brief Rule error.
+ */
+class rule_error : public std::system_error {
+public:
+	/**
+	 * \brief Rule related errors.
+	 */
+	enum error {
+		//!< No error.
+		no_error = 0,
+
+		//!< Invalid action given.
+		invalid_action,
+
+		//!< Invalid rule index.
+		invalid_index,
+	};
+
+	/**
+	 * Inherited constructors.
+	 */
+	using system_error::system_error;
+};
+
+/**
+ * Get the rule error category singleton.
+ *
+ * \return the singleton
+ */
+auto rule_category() -> const std::error_category&;
+
+/**
+ * Create a std::error_code from rule_error::error enum.
+ *
+ * \param e the error code
+ * \return the error code
+ */
+auto make_error_code(rule_error::error e) -> std::error_code;
+
+} // !irccd::daemon
+
+/**
+ * \cond IRCCD_HIDDEN_SYMBOLS
+ */
+
+namespace std {
+
+template <>
+struct is_error_code_enum<irccd::daemon::rule_error::error> : public std::true_type {
+};
+
+} // !std
+
+/**
+ * \endcond
+ */
+
+#endif // !IRCCD_DAEMON_RULE_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/rule_service.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,121 @@
+/*
+ * rule_service.cpp -- rule service
+ *
+ * Copyright (c) 2013-2018 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 <stdexcept>
+
+#include <irccd/config.hpp>
+#include <irccd/string_util.hpp>
+
+#include "bot.hpp"
+#include "logger.hpp"
+#include "rule_service.hpp"
+#include "rule_util.hpp"
+
+namespace irccd::daemon {
+
+rule_service::rule_service(bot& bot)
+	: bot_(bot)
+{
+}
+
+auto rule_service::list() const noexcept -> const std::vector<rule>&
+{
+	return rules_;
+}
+
+void rule_service::add(rule rule)
+{
+	rules_.push_back(std::move(rule));
+}
+
+void rule_service::insert(rule rule, unsigned position)
+{
+	assert(position <= rules_.size());
+
+	rules_.insert(rules_.begin() + position, std::move(rule));
+}
+
+void rule_service::remove(unsigned position)
+{
+	assert(position < rules_.size());
+
+	rules_.erase(rules_.begin() + position);
+}
+
+auto rule_service::require(unsigned position) const -> const rule&
+{
+	if (position >= rules_.size())
+		throw rule_error(rule_error::invalid_index);
+
+	return rules_[position];
+}
+
+auto rule_service::require(unsigned position) -> rule&
+{
+	if (position >= rules_.size())
+		throw rule_error(rule_error::invalid_index);
+
+	return rules_[position];
+}
+
+auto rule_service::solve(std::string_view server,
+                         std::string_view channel,
+                         std::string_view origin,
+                         std::string_view plugin,
+                         std::string_view event) noexcept -> bool
+{
+	bool result = true;
+
+	for (const auto& rule : rules_)
+		if (rule.match(server, channel, origin, plugin, event))
+			result = rule.action == rule::action_type::accept;
+
+	return result;
+}
+
+void rule_service::load(const config& cfg) noexcept
+{
+	rules_.clear();
+
+	for (const auto& section : cfg) {
+		if (section.get_key() != "rule")
+			continue;
+
+		try {
+			rules_.push_back(rule_util::from_config(section));
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning("rule", "") << ex.what() << std::endl;
+		}
+	}
+}
+
+namespace logger {
+
+auto loggable_traits<rule>::get_category(const rule&) -> std::string_view
+{
+	return "rule";
+}
+
+auto loggable_traits<rule>::get_component(const rule&) -> std::string_view
+{
+	return "";
+}
+
+} // !logger
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/rule_service.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,162 @@
+/*
+ * rule_service.hpp -- rule service
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_RULE_SERVICE_HPP
+#define IRCCD_DAEMON_RULE_SERVICE_HPP
+
+/**
+ * \file rule_service.hpp
+ * \brief Rule service.
+ */
+
+#include <vector>
+
+#include <json.hpp>
+
+#include "rule.hpp"
+
+namespace irccd {
+
+class config;
+
+namespace daemon {
+
+class bot;
+
+/**
+ * \brief Store and solve rules.
+ * \ingroup services
+ */
+class rule_service {
+private:
+	bot& bot_;
+	std::vector<rule> rules_;
+
+public:
+	/**
+	 * Create the rule service.
+	 *
+	 * \param bot the irccd instance
+	 */
+	rule_service(bot& bot);
+
+	/**
+	 * Get the list of rules.
+	 *
+	 * \return the list of rules
+	 */
+	auto list() const noexcept -> const std::vector<rule>&;
+
+	/**
+	 * Append a rule.
+	 *
+	 * \param rule the rule to append
+	 */
+	void add(rule rule);
+
+	/**
+	 * Insert a new rule at the specified position.
+	 *
+	 * \param rule the rule
+	 * \param position the position
+	 */
+	void insert(rule rule, unsigned position);
+
+	/**
+	 * Remove a new rule from the specified position.
+	 *
+	 * \pre position must be valid
+	 * \param position the position
+	 */
+	void remove(unsigned position);
+
+	/**
+	 * Get a rule at the specified index or throw an exception if not found.
+	 *
+	 * \param position the position
+	 * \return the rule
+	 * \throw std::out_of_range if position is invalid
+	 */
+	auto require(unsigned position) const -> const rule&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \copydoc require
+	 */
+	auto require(unsigned position) -> rule&;
+
+	/**
+	 * Resolve the action to execute with the specified list of rules.
+	 *
+	 * \param server the server name
+	 * \param channel the channel name
+	 * \param origin the origin
+	 * \param plugin the plugin name
+	 * \param event the event name (e.g onKick)
+	 * \return true if the plugin must be called
+	 */
+	auto solve(std::string_view server,
+	           std::string_view channel,
+	           std::string_view origin,
+	           std::string_view plugin,
+	           std::string_view event) noexcept -> bool;
+
+	/**
+	 * Load rules from the configuration.
+	 *
+	 * \param cfg the config
+	 */
+	void load(const config& cfg) noexcept;
+};
+
+namespace logger {
+
+template <typename T>
+struct loggable_traits;
+
+/**
+ * \brief Specialization for rule.
+ * \ingroup logger-traits
+ */
+template <>
+struct loggable_traits<rule> {
+	/**
+	 * Get 'rule' category.
+	 *
+	 * \param rule the rule
+	 * \return rule
+	 */
+	static auto get_category(const rule& rule) -> std::string_view;
+
+	/**
+	 * Returns nothing for the moment.
+	 *
+	 * \param rule the rule
+	 * \return nothing
+	 */
+	static auto get_component(const rule& rule) -> std::string_view;
+};
+
+} // !logger
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_RULE_SERVICE_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/rule_util.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,148 @@
+/*
+ * rule_util.cpp -- rule utilities
+ *
+ * Copyright (c) 2013-2018 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 <irccd/ini.hpp>
+
+#include "rule.hpp"
+#include "rule_util.hpp"
+
+namespace irccd::daemon::rule_util {
+
+auto from_config(const ini::section& sc) -> rule
+{
+	// Simple converter from std::vector to std::unordered_set.
+	const auto toset = [] (const auto& v) {
+		return std::set<std::string>(v.begin(), v.end());
+	};
+
+	rule::set servers, channels, origins, plugins, events;
+	rule::action_type action = rule::action_type::accept;
+
+	// Get the sets.
+	ini::section::const_iterator it;
+
+	if ((it = sc.find("servers")) != sc.end())
+		servers = toset(*it);
+	if ((it = sc.find("channels")) != sc.end())
+		channels = toset(*it);
+	if ((it = sc.find("origins")) != sc.end())
+		origins = toset(*it);
+	if ((it = sc.find("plugins")) != sc.end())
+		plugins = toset(*it);
+	if ((it = sc.find("channels")) != sc.end())
+		channels = toset(*it);
+	if ((it = sc.find("events")) != sc.end())
+		events = toset(*it);
+
+	// Get the action.
+	auto actionstr = sc.get("action").get_value();
+
+	if (actionstr == "drop")
+		action = rule::action_type::drop;
+	else if (actionstr == "accept")
+		action = rule::action_type::accept;
+	else
+		throw rule_error(rule_error::invalid_action);
+
+	return {
+		std::move(servers),
+		std::move(channels),
+		std::move(origins),
+		std::move(plugins),
+		std::move(events),
+		action
+	};
+}
+
+auto from_json(const nlohmann::json& json) -> rule
+{
+	const auto toset = [] (auto object, auto name) {
+		rule::set result;
+
+		for (const auto& s : object[name])
+			if (s.is_string())
+				result.insert(s.template get<std::string>());
+
+		return result;
+	};
+	const auto toaction = [] (const auto& object, const auto& name) {
+		const auto v = object.find(name);
+
+		if (v == object.end() || !v->is_string())
+			throw rule_error(rule_error::invalid_action);
+
+		const auto s = v->template get<std::string>();
+
+		if (s == "accept")
+			return rule::action_type::accept;
+		if (s == "drop")
+			return rule::action_type::drop;
+
+		throw rule_error(rule_error::invalid_action);
+	};
+
+	return {
+		toset(json, "servers"),
+		toset(json, "channels"),
+		toset(json, "origins"),
+		toset(json, "plugins"),
+		toset(json, "events"),
+		toaction(json, "action")
+	};
+}
+
+auto get_index(const nlohmann::json& json, const std::string& key) -> unsigned
+{
+	const auto index = json.find(key);
+
+	if (index == json.end() || !index->is_number_unsigned())
+		throw rule_error(rule_error::invalid_index);
+
+	return index->get<unsigned>();
+}
+
+auto to_json(const rule& rule) -> nlohmann::json
+{
+	const auto join = [] (const auto& set) {
+		auto array = nlohmann::json::array();
+
+		for (const auto& entry : set)
+			array.push_back(entry);
+
+		return array;
+	};
+	const auto str = [] (auto action) {
+		switch (action) {
+		case rule::action_type::accept:
+			return "accept";
+		default:
+			return "drop";
+		}
+	};
+
+	return {
+		{ "servers",    join(rule.servers)      },
+		{ "channels",   join(rule.channels)     },
+		{ "origins",    join(rule.origins)      },
+		{ "plugins",    join(rule.plugins)      },
+		{ "events",     join(rule.events)       },
+		{ "action",     str(rule.action)        }
+	};
+}
+
+} // !irccd::daemon::rule_util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/rule_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,82 @@
+/*
+ * rule_util.hpp -- rule utilities
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_RULE_UTIL_HPP
+#define IRCCD_DAEMON_RULE_UTIL_HPP
+
+/**
+ * \file rule_util.hpp
+ * \brief Rule utilities.
+ */
+
+#include <json.hpp>
+
+namespace irccd {
+
+namespace ini {
+
+class section;
+
+} // !ini
+
+namespace daemon {
+
+struct rule;
+
+/**
+ * \brief Rule utilities.
+ */
+namespace rule_util {
+
+/**
+ * Load a rule from a JSON object.
+ *
+ * For possible use in transport commands or Javascript API.
+ *
+ * \pre json.is_object()
+ * \param json the JSON object
+ * \return the new rule
+ * \throw rule_error on errors
+ */
+auto from_json(const nlohmann::json& json) -> rule;
+
+/**
+ * Load a rule from a INI section.
+ *
+ * \param sc the ini section
+ * \return the rule
+ * \throw rule_error on errors
+ */
+auto from_config(const ini::section& sc) -> rule;
+
+/**
+ * Convert a rule into a JSON object.
+ *
+ * \param rule the rule
+ * \throw the JSON representation
+ * \return the JSON representation
+ */
+auto to_json(const rule& rule) -> nlohmann::json;
+
+} // !rule_util
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_RULE_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/server.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,896 @@
+/*
+ * server.cpp -- an IRC server
+ *
+ * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
+
+#include <boost/predef/os.h>
+#include <boost/format.hpp>
+
+#include <algorithm>
+#include <cerrno>
+#include <cstring>
+#include <stdexcept>
+
+#if !BOOST_OS_WINDOWS
+#	include <sys/types.h>
+#	include <netinet/in.h>
+#	include <arpa/nameser.h>
+#	include <resolv.h>
+#endif
+
+#include <irccd/json_util.hpp>
+#include <irccd/string_util.hpp>
+
+#include "server.hpp"
+
+using boost::format;
+using boost::str;
+
+namespace irccd::daemon {
+
+namespace {
+
+/*
+ * clean_prefix
+ * ------------------------------------------------------------------
+ *
+ * Remove the user prefix only if it is present in the mode table, for example
+ * removes @ from @irccd if and only if @ is a character mode (e.g. operator).
+ */
+auto clean_prefix(const std::map<channel_mode, char>& modes, std::string nickname) -> std::string
+{
+	if (nickname.length() == 0)
+		return nickname;
+
+	for (const auto& pair : modes)
+		if (nickname[0] == pair.second)
+			nickname.erase(0, 1);
+
+	return nickname;
+}
+
+/*
+ * isupport_extract_prefixes
+ * ------------------------------------------------------------------
+ *
+ * Read modes from the IRC event numeric.
+ */
+auto isupport_extract_prefixes(const std::string& line) -> std::map<channel_mode, char>
+{
+	// FIXME: what if line has different size?
+	std::pair<char, char> table[16];
+	std::string buf = line.substr(7);
+	std::map<channel_mode, char> modes;
+
+	for (int i = 0; i < 16; ++i)
+		table[i] = std::make_pair(-1, -1);
+
+	int j = 0;
+	bool read_modes = true;
+
+	for (size_t i = 0; i < buf.size(); ++i) {
+		if (buf[i] == '(')
+			continue;
+		if (buf[i] == ')') {
+			j = 0;
+			read_modes = false;
+			continue;
+		}
+
+		if (read_modes)
+			table[j++].first = buf[i];
+		else
+			table[j++].second = buf[i];
+	}
+
+	// Put these as a map of mode to prefix.
+	for (int i = 0; i < 16; ++i) {
+		auto key = static_cast<channel_mode>(table[i].first);
+		auto value = table[i].second;
+
+		modes.emplace(key, value);
+	}
+
+	return modes;
+}
+
+} // !namespace
+
+auto server::dispatch_connect(const irc::message&, const recv_handler& handler) -> bool
+{
+	state_ = state::connected;
+	handler({}, connect_event{shared_from_this()});
+
+	for (const auto& channel : rchannels_)
+		join(channel.name, channel.password);
+
+	return true;
+}
+
+auto server::dispatch_endofnames(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	/*
+	 * Called when end of name listing has finished on a channel.
+	 *
+	 * params[0] == originator
+	 * params[1] == channel
+	 * params[2] == End of NAMES list
+	 */
+	if (msg.args.size() < 3 || msg.get(1) == "")
+		return false;
+
+	const auto it = names_map_.find(msg.get(1));
+
+	if (it != names_map_.end()) {
+		handler({}, names_event{
+			shared_from_this(),
+			msg.get(1),
+			std::vector<std::string>(it->second.begin(), it->second.end())
+		});
+
+		names_map_.erase(it);
+	}
+
+	return true;
+}
+
+auto server::dispatch_endofwhois(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	/*
+	 * Called when whois is finished.
+	 *
+	 * params[0] == originator
+	 * params[1] == nickname
+	 * params[2] == End of WHOIS list
+	 */
+	const auto it = whois_map_.find(msg.get(1));
+
+	if (it != whois_map_.end()) {
+		handler({}, whois_event{shared_from_this(), it->second});
+		whois_map_.erase(it);
+	}
+
+	return true;
+}
+
+auto server::dispatch_invite(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	// If join-invite is set, join the channel.
+	if ((options_ & options::join_invite) == options::join_invite && is_self(msg.get(0)))
+		join(msg.get(1));
+
+	handler({}, invite_event{shared_from_this(), msg.prefix, msg.get(1), msg.get(0)});
+
+	return true;
+}
+
+auto server::dispatch_isupport(const irc::message& msg) -> bool
+{
+	for (unsigned int i = 0; i < msg.args.size(); ++i) {
+		if (msg.get(i).compare(0, 6, "PREFIX") == 0) {
+			modes_ = isupport_extract_prefixes(msg.get(i));
+			break;
+		}
+	}
+
+	return false;
+}
+
+auto server::dispatch_join(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	if (is_self(msg.prefix))
+		jchannels_.insert(msg.get(0));
+
+	handler({}, join_event{shared_from_this(), msg.prefix, msg.get(0)});
+
+	return true;
+}
+
+auto server::dispatch_kick(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	if (is_self(msg.get(1))) {
+		// Remove the channel from the joined list.
+		jchannels_.erase(msg.get(0));
+
+		// Rejoin the channel if the option has been set and I was kicked.
+		if ((options_ & options::auto_rejoin) == options::auto_rejoin)
+			join(msg.get(0));
+	}
+
+	handler({}, kick_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1), msg.get(2)});
+
+	return true;
+}
+
+auto server::dispatch_mode(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	handler({}, mode_event{
+		shared_from_this(),
+		msg.prefix,
+		msg.get(0),
+		msg.get(1),
+		msg.get(2),
+		msg.get(3),
+		msg.get(4)
+	});
+
+	return true;
+}
+
+auto server::dispatch_namreply(const irc::message& msg) -> bool
+{
+	/*
+	 * Called multiple times to list clients on a channel.
+	 *
+	 * params[0] == originator
+	 * params[1] == '='
+	 * params[2] == channel
+	 * params[3] == list of users with their prefixes
+	 *
+	 * IDEA for the future: maybe give the appropriate mode as a second
+	 * parameter in onNames.
+	 */
+	if (msg.args.size() < 4 || msg.get(2) == "" || msg.get(3) == "")
+		return false;
+
+	auto users = string_util::split(msg.get(3), " \t");
+
+	// The listing may add some prefixes, remove them if needed.
+	for (const auto& u : users)
+		names_map_[msg.get(2)].insert(clean_prefix(modes_, u));
+
+	return false;
+}
+
+auto server::dispatch_nick(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	// Update our nickname.
+	if (is_self(msg.prefix))
+		nickname_ = msg.get(0);
+
+	handler({}, nick_event{shared_from_this(), msg.prefix, msg.get(0)});
+
+	return true;
+}
+
+auto server::dispatch_notice(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	handler({}, notice_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
+
+	return true;
+}
+
+auto server::dispatch_part(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	// Remove the channel from the joined list if I left a channel.
+	if (is_self(msg.prefix))
+		jchannels_.erase(msg.get(1));
+
+	handler({}, part_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
+
+	return true;
+}
+
+auto server::dispatch_ping(const irc::message& msg) -> bool
+{
+	assert(msg.command == "PING");
+
+	send(str(format("PONG %1%") % msg.get(0)));
+
+	return false;
+}
+
+auto server::dispatch_privmsg(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	assert(msg.command == "PRIVMSG");
+
+	if (msg.is_ctcp(1)) {
+		auto cmd = msg.ctcp(1);
+
+		if (cmd.compare(0, 6, "ACTION") == 0)
+			handler({}, me_event{shared_from_this(), msg.prefix, msg.get(0), cmd.substr(7)});
+		else
+			return false;
+	} else
+		handler({}, message_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
+
+	return true;
+}
+
+auto server::dispatch_topic(const irc::message& msg, const recv_handler& handler) -> bool
+{
+	assert(msg.command == "TOPIC");
+
+	handler({}, topic_event{shared_from_this(), msg.get(0), msg.get(1), msg.get(2)});
+
+	return true;
+}
+
+auto server::dispatch_whoischannels(const irc::message& msg) -> bool
+{
+	/*
+	 * Called when we have received channels for one user.
+	 *
+	 * params[0] == originator
+	 * params[1] == nickname
+	 * params[2] == list of channels with their prefixes
+	 */
+	if (msg.args.size() < 3 || msg.get(1) == "" || msg.get(2) == "")
+		return false;
+
+	auto it = whois_map_.find(msg.get(1));
+
+	if (it != whois_map_.end()) {
+		auto channels = string_util::split(msg.get(2), " \t");
+
+		// Clean their prefixes.
+		for (auto& s : channels)
+			s = clean_prefix(modes_, s);
+
+		it->second.channels = std::move(channels);
+	}
+
+	return false;
+}
+
+auto server::dispatch_whoisuser(const irc::message& msg) -> bool
+{
+	/*
+	 * Called when whois information has been partially received.
+	 *
+	 * params[0] == originator
+	 * params[1] == nickname
+	 * params[2] == username
+	 * params[3] == hostname
+	 * params[4] == * (no idea what is that)
+	 * params[5] == realname
+	 */
+	if (msg.args.size() < 6 || msg.get(1) == "" || msg.get(2) == "" || msg.get(3) == "" || msg.get(5) == "")
+		return false;
+
+	whois_info info;
+
+	info.nick = msg.get(1);
+	info.user = msg.get(2);
+	info.hostname = msg.get(3);
+	info.realname = msg.get(5);
+
+	whois_map_.emplace(info.nick, info);
+
+	return false;
+}
+
+auto server::dispatch(const irc::message& message, const recv_handler& handler) -> bool
+{
+	bool handled = false;
+
+	if (message.is(5))
+		handled = dispatch_isupport(message);
+	else if (message.is(irc::err::nomotd) || message.is(irc::rpl::endofmotd))
+		handled = dispatch_connect(message, handler);
+	else if (message.command == "INVITE")
+		handled = dispatch_invite(message, handler);
+	else if (message.command == "JOIN")
+		handled = dispatch_join(message, handler);
+	else if (message.command == "KICK")
+		handled = dispatch_kick(message, handler);
+	else if (message.command == "MODE")
+		handled = dispatch_mode(message, handler);
+	else if (message.command == "NICK")
+		handled = dispatch_nick(message, handler);
+	else if (message.command == "NOTICE")
+		handled = dispatch_notice(message, handler);
+	else if (message.command == "TOPIC")
+		handled = dispatch_topic(message, handler);
+	else if (message.command == "PART")
+		handled = dispatch_part(message, handler);
+	else if (message.command == "PING")
+		handled = dispatch_ping(message);
+	else if (message.command == "PRIVMSG")
+		handled = dispatch_privmsg(message, handler);
+	else if (message.is(irc::rpl::namreply))
+		handled = dispatch_namreply(message);
+	else if (message.is(irc::rpl::endofnames))
+		handled = dispatch_endofnames(message, handler);
+	else if (message.is(irc::rpl::endofwhois))
+		handled = dispatch_endofwhois(message, handler);
+	else if (message.is(irc::rpl::whoischannels))
+		handled = dispatch_whoischannels(message);
+	else if (message.is(irc::rpl::whoisuser))
+		handled = dispatch_whoisuser(message);
+
+	return handled;
+}
+
+void server::handle_send(const std::error_code& code)
+{
+	/*
+	 * We don't notify server_service in case of error because in any case the
+	 * pending recv() will complete with an error.
+	 */
+	queue_.pop_front();
+
+	if (!code)
+		flush();
+}
+
+void server::handle_recv(const std::error_code& code,
+                         const irc::message& message,
+                         const recv_handler& handler)
+{
+	/*
+	 * Once a message is received, dispatch it to individual dispatch_*
+	 * functions. If the function calls handler by itself it returns true
+	 * otherwise we call handler with no event to tell the caller the message
+	 * has arrived and allowed to call recv() again.
+	 */
+	if (code) {
+		disconnect();
+		handler(std::move(code), event(std::monostate()));
+	} else if (!dispatch(message, handler))
+		handler({}, std::monostate{});
+}
+
+void server::recv(recv_handler handler) noexcept
+{
+	const auto self = shared_from_this();
+
+	conn_->recv([this, handler, self, c = conn_] (auto code, auto message) {
+		handle_recv(std::move(code), message, handler);
+	});
+}
+
+void server::flush()
+{
+	if (queue_.empty())
+		return;
+
+	const auto self = shared_from_this();
+
+	conn_->send(queue_.front(), [this, self, c = conn_] (auto code) {
+		handle_send(std::move(code));
+	});
+}
+
+void server::identify()
+{
+	state_ = state::identifying;
+
+	if (!password_.empty())
+		send(str(format("PASS %1%") % password_));
+
+	send(str(format("NICK %1%") % nickname_));
+	send(str(format("USER %1% unknown unknown :%2%") % username_ % realname_));
+}
+
+void server::handle_wait(const std::error_code& code, const connect_handler& handler)
+{
+	if (code && code != std::errc::operation_canceled)
+		handler(code);
+}
+
+void server::handle_connect(const std::error_code& code, const connect_handler& handler)
+{
+	timer_.cancel();
+
+	if (code)
+		disconnect();
+	else
+		identify();
+
+	handler(code);
+}
+
+server::server(boost::asio::io_service& service, std::string id, std::string hostname)
+	: id_(std::move(id))
+	, hostname_(std::move(hostname))
+	, options_(options::ipv4 | options::ipv6)
+	, service_(service)
+	, timer_(service)
+{
+	assert(!hostname_.empty());
+}
+
+server::~server()
+{
+	conn_ = nullptr;
+	state_ = state::disconnected;
+}
+
+auto server::get_state() const noexcept -> state
+{
+	return state_;
+}
+
+auto server::get_id() const noexcept -> const std::string&
+{
+	return id_;
+}
+
+auto server::get_hostname() const noexcept -> const std::string&
+{
+	return hostname_;
+}
+
+auto server::get_password() const noexcept -> const std::string&
+{
+	return password_;
+}
+
+void server::set_password(std::string password) noexcept
+{
+	password_ = std::move(password);
+}
+
+auto server::get_port() const noexcept -> std::uint16_t
+{
+	return port_;
+}
+
+void server::set_port(std::uint16_t port) noexcept
+{
+	port_ = port;
+}
+
+auto server::get_options() const noexcept -> options
+{
+	return options_;
+}
+
+void server::set_options(options flags) noexcept
+{
+#if !defined(IRCCD_HAVE_SSL)
+	assert((flags & options::ssl) != options::ssl);
+#endif
+
+	options_ = flags;
+}
+
+auto server::get_nickname() const noexcept -> const std::string&
+{
+	return nickname_;
+}
+
+void server::set_nickname(std::string nickname)
+{
+	if (state_ == state::connected)
+		send(str(format("NICK %1%") % nickname));
+	else
+		nickname_ = std::move(nickname);
+}
+
+auto server::get_username() const noexcept -> const std::string&
+{
+	return username_;
+}
+
+void server::set_username(std::string name) noexcept
+{
+	username_ = std::move(name);
+}
+
+auto server::get_realname() const noexcept -> const std::string&
+{
+	return realname_;
+}
+
+void server::set_realname(std::string realname) noexcept
+{
+	realname_ = std::move(realname);
+}
+
+auto server::get_ctcp_version() const noexcept -> const std::string&
+{
+	return ctcpversion_;
+}
+
+void server::set_ctcp_version(std::string ctcpversion)
+{
+	ctcpversion_ = std::move(ctcpversion);
+}
+
+auto server::get_command_char() const noexcept -> const std::string&
+{
+	return command_char_;
+}
+
+void server::set_command_char(std::string command_char) noexcept
+{
+	assert(!command_char.empty());
+
+	command_char_ = std::move(command_char);
+}
+
+auto server::get_reconnect_delay() const noexcept -> std::uint16_t
+{
+	return recodelay_;
+}
+
+void server::set_reconnect_delay(std::uint16_t reconnect_delay) noexcept
+{
+	recodelay_ = reconnect_delay;
+}
+
+auto server::get_ping_timeout() const noexcept -> std::uint16_t
+{
+	return timeout_;
+}
+
+void server::set_ping_timeout(std::uint16_t ping_timeout) noexcept
+{
+	timeout_ = ping_timeout;
+}
+
+auto server::get_channels() const noexcept -> const std::set<std::string>&
+{
+	return jchannels_;
+}
+
+auto server::is_self(std::string_view target) const noexcept -> bool
+{
+	return nickname_ == irc::user::parse(target).nick;
+}
+
+void server::connect(connect_handler handler) noexcept
+{
+	assert(state_ == state::disconnected);
+	assert((options_ & options::ipv4) == options::ipv4 || (options_ & options::ipv6) == options::ipv6);
+
+	/*
+	 * This is needed if irccd is started before DHCP or if DNS cache is
+	 * outdated.
+	 */
+#if !BOOST_OS_WINDOWS
+	(void)res_init();
+#endif
+
+	conn_ = std::make_unique<irc::connection>(service_);
+	conn_->use_ssl((options_ & options::ssl) == options::ssl);
+	conn_->use_ipv4((options_ & options::ipv4) == options::ipv4);
+	conn_->use_ipv6((options_ & options::ipv6) == options::ipv6);
+
+	jchannels_.clear();
+	state_ = state::connecting;
+
+	timer_.expires_from_now(boost::posix_time::seconds(timeout_));
+	timer_.async_wait([this, handler] (auto code) {
+		handle_wait(code, handler);
+	});
+
+	const auto self = shared_from_this();
+
+	conn_->connect(hostname_, std::to_string(port_), [this, handler, c = conn_] (auto code) {
+		handle_connect(code, handler);
+	});
+}
+
+void server::disconnect() noexcept
+{
+	conn_ = nullptr;
+	state_ = state::disconnected;
+	queue_.clear();
+}
+
+void server::invite(std::string_view target, std::string_view channel)
+{
+	assert(!target.empty());
+	assert(!channel.empty());
+
+	send(str(format("INVITE %1% %2%") % target % channel));
+}
+
+void server::join(std::string_view channel, std::string_view password)
+{
+	assert(!channel.empty());
+
+	auto it = std::find_if(rchannels_.begin(), rchannels_.end(), [&] (const auto& c) {
+		return c.name == channel;
+	});
+
+	if (it == rchannels_.end())
+		rchannels_.push_back({ std::string(channel), std::string(password) });
+	else
+		*it = { std::string(channel), std::string(password) };
+
+	if (state_ == state::connected) {
+		if (password.empty())
+			send(str(format("JOIN %1%") % channel));
+		else
+			send(str(format("JOIN %1% :%2%") % channel % password));
+	}
+}
+
+void server::kick(std::string_view target, std::string_view channel, std::string_view reason)
+{
+	assert(!target.empty());
+	assert(!channel.empty());
+
+	if (!reason.empty())
+		send(str(format("KICK %1% %2% :%3%") % channel % target % reason));
+	else
+		send(str(format("KICK %1% %2%") % channel % target));
+}
+
+void server::me(std::string_view target, std::string_view message)
+{
+	assert(!target.empty());
+	assert(!message.empty());
+
+	send(str(format("PRIVMSG %1% :\x01" "ACTION %2%\x01") % target % message));
+}
+
+void server::message(std::string_view target, std::string_view message)
+{
+	assert(!target.empty());
+	assert(!message.empty());
+
+	send(str(format("PRIVMSG %1% :%2%") % target % message));
+}
+
+void server::mode(std::string_view channel,
+                  std::string_view mode,
+                  std::string_view limit,
+                  std::string_view user,
+                  std::string_view mask)
+{
+	assert(!channel.empty());
+	assert(!mode.empty());
+
+	std::ostringstream oss;
+
+	oss << "MODE " << channel << " " << mode;
+
+	if (!limit.empty())
+		oss << " " << limit;
+	if (!user.empty())
+		oss << " " << user;
+	if (!mask.empty())
+		oss << " " << mask;
+
+	send(oss.str());
+}
+
+void server::names(std::string_view channel)
+{
+	assert(!channel.empty());
+
+	send(str(format("NAMES %1%") % channel));
+}
+
+void server::notice(std::string_view target, std::string_view message)
+{
+	assert(!target.empty());
+	assert(!message.empty());
+
+	send(str(format("NOTICE %1% :%2%") % target % message));
+}
+
+void server::part(std::string_view channel, std::string_view reason)
+{
+	assert(!channel.empty());
+
+	if (!reason.empty())
+		send(str(format("PART %1% :%2%") % channel % reason));
+	else
+		send(str(format("PART %1%") % channel));
+}
+
+void server::send(std::string_view raw)
+{
+	assert(!raw.empty());
+
+	if (state_ == state::identifying || state_ == state::connected) {
+		const auto in_progress = queue_.size() > 0;
+
+		queue_.push_back(std::string(raw));
+
+		if (!in_progress)
+			flush();
+	} else
+		queue_.push_back(std::string(raw));
+}
+
+void server::topic(std::string_view channel, std::string_view topic)
+{
+	assert(!channel.empty());
+
+	if (!topic.empty())
+		send(str(format("TOPIC %1% :%2%") % channel % topic));
+	else
+		send(str(format("TOPIC %1%") % channel));
+}
+
+void server::whois(std::string_view target)
+{
+	assert(!target.empty());
+
+	send(str(format("WHOIS %1% %2%") % target % target));
+}
+
+server_error::server_error(error code) noexcept
+	: system_error(make_error_code(code))
+{
+}
+
+auto server_category() -> const std::error_category&
+{
+	static const class category : public std::error_category {
+	public:
+		auto name() const noexcept -> const char* override
+		{
+			return "server";
+		}
+
+		auto message(int e) const -> std::string override
+		{
+			switch (static_cast<server_error::error>(e)) {
+			case server_error::not_found:
+				return "server not found";
+			case server_error::invalid_identifier:
+				return "invalid server identifier";
+			case server_error::not_connected:
+				return "server is not connected";
+			case server_error::already_connected:
+				return "server is already connected";
+			case server_error::already_exists:
+				return "server already exists";
+			case server_error::invalid_port:
+				return "invalid port number specified";
+			case server_error::invalid_reconnect_delay:
+				return "invalid reconnect delay number";
+			case server_error::invalid_hostname:
+				return "invalid hostname";
+			case server_error::invalid_channel:
+				return "invalid or empty channel";
+			case server_error::invalid_mode:
+				return "invalid or empty mode";
+			case server_error::invalid_nickname:
+				return "invalid nickname";
+			case server_error::invalid_username:
+				return "invalid username";
+			case server_error::invalid_realname:
+				return "invalid realname";
+			case server_error::invalid_password:
+				return "invalid password";
+			case server_error::invalid_ping_timeout:
+				return "invalid ping timeout";
+			case server_error::invalid_ctcp_version:
+				return "invalid CTCP VERSION";
+			case server_error::invalid_command_char:
+				return "invalid character command";
+			case server_error::invalid_message:
+				return "invalid message";
+			case server_error::ssl_disabled:
+				return "ssl is not enabled";
+			case server_error::invalid_family:
+				return "invalid family";
+			default:
+				return "no error";
+			}
+		}
+	} category;
+
+	return category;
+}
+
+auto make_error_code(server_error::error e) -> std::error_code
+{
+	return { static_cast<int>(e), server_category() };
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/server.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,906 @@
+/*
+ * server.hpp -- an IRC server
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_SERVER_HPP
+#define IRCCD_DAEMON_SERVER_HPP
+
+/**
+ * \file server.hpp
+ * \brief IRC Server.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <cstdint>
+#include <deque>
+#include <functional>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include <json.hpp>
+
+#include "irc.hpp"
+
+namespace irccd::daemon {
+
+class server;
+
+/**
+ * \brief Prefixes for nicknames.
+ * \ingroup daemon-servers
+ */
+enum class channel_mode {
+	creator         = 'O',                  //!< Channel creator
+	half_op         = 'h',                  //!< Half operator
+	op              = 'o',                  //!< Channel operator
+	protection      = 'a',                  //!< Unkillable
+	voiced          = 'v'                   //!< Voice power
+};
+
+/**
+ * \brief A channel to join with an optional password.
+ * \ingroup daemon-servers
+ */
+struct channel {
+	std::string name;                       //!< the channel to join
+	std::string password;                   //!< the optional password
+};
+
+/**
+ * \brief Describe a whois information.
+ * \ingroup daemon-events
+ */
+struct whois_info {
+	std::string nick;                       //!< user's nickname
+	std::string user;                       //!< user's user
+	std::string hostname;                   //!< hostname
+	std::string realname;                   //!< realname
+	std::vector<std::string> channels;      //!< the channels where the user is
+};
+
+/**
+ * \brief Connection success event.
+ * \ingroup daemon-events
+ */
+struct connect_event {
+	std::shared_ptr<class server> server;   //!< The server.
+};
+
+/**
+ * \brief Connection success event.
+ * \ingroup daemon-events
+ */
+struct disconnect_event {
+	std::shared_ptr<class server> server;   //!< The server.
+};
+
+/**
+ * \brief Invite event.
+ * \ingroup daemon-events
+ */
+struct invite_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+	std::string nickname;                   //!< The nickname (you).
+};
+
+/**
+ * \brief Join event.
+ * \ingroup daemon-events
+ */
+struct join_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+};
+
+/**
+ * \brief Kick event.
+ * \ingroup daemon-events
+ */
+struct kick_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+	std::string target;                     //!< The target.
+	std::string reason;                     //!< The reason (Optional).
+};
+
+/**
+ * \brief Message event.
+ * \ingroup daemon-events
+ */
+struct message_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+	std::string message;                    //!< The message.
+};
+
+/**
+ * \brief CTCP action event.
+ * \ingroup daemon-events
+ */
+struct me_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+	std::string message;                    //!< The message.
+};
+
+/**
+ * \brief Mode event.
+ * \ingroup daemon-events
+ */
+struct mode_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel or target.
+	std::string mode;                       //!< The mode.
+	std::string limit;                      //!< The optional limit.
+	std::string user;                       //!< The optional user.
+	std::string mask;                       //!< The optional ban mask.
+};
+
+/**
+ * \brief Names listing event.
+ * \ingroup daemon-events
+ */
+struct names_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string channel;                    //!< The channel.
+	std::vector<std::string> names;         //!< The names.
+};
+
+/**
+ * \brief Nick change event.
+ * \ingroup daemon-events
+ */
+struct nick_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string nickname;                   //!< The new nickname.
+};
+
+/**
+ * \brief Notice event.
+ * \ingroup daemon-events
+ */
+struct notice_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel or target.
+	std::string message;                    //!< The message.
+};
+
+/**
+ * \brief Part event.
+ * \ingroup daemon-events
+ */
+struct part_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+	std::string reason;                     //!< The reason.
+};
+
+/**
+ * \brief Topic event.
+ * \ingroup daemon-events
+ */
+struct topic_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	std::string origin;                     //!< The originator.
+	std::string channel;                    //!< The channel.
+	std::string topic;                      //!< The topic message.
+};
+
+/**
+ * \brief Whois event.
+ * \ingroup daemon-events
+ */
+struct whois_event {
+	std::shared_ptr<class server> server;   //!< The server.
+	whois_info whois;                       //!< The whois information.
+};
+
+/**
+ * \brief Store all possible events.
+ * \ingroup daemon-events
+ */
+using event = std::variant<
+	std::monostate,
+	connect_event,
+	disconnect_event,
+	invite_event,
+	join_event,
+	kick_event,
+	me_event,
+	message_event,
+	mode_event,
+	names_event,
+	nick_event,
+	notice_event,
+	part_event,
+	topic_event,
+	whois_event
+>;
+
+/**
+ * \brief The class that connect to a IRC server.
+ * \ingroup server
+ *
+ * This class is higher level than irc connection, it does identify process,
+ * parsing message, translating messages and queue'ing user requests.
+ */
+class server : public std::enable_shared_from_this<server> {
+public:
+	/**
+	 * Completion handler once network connection is complete.
+	 */
+	using connect_handler = std::function<void (std::error_code)>;
+
+	/**
+	 * Completion handler once a network message has arrived.
+	 */
+	using recv_handler = std::function<void (std::error_code, event)>;
+
+	/**
+	 * \brief Various options for server.
+	 */
+	enum class options : std::uint8_t {
+		none            = 0,            //!< No options
+		ipv4            = (1 << 0),     //!< Connect using IPv4
+		ipv6            = (1 << 1),     //!< Connect using IPv6
+		ssl             = (1 << 2),     //!< Use SSL
+		auto_rejoin     = (1 << 3),     //!< Auto rejoin a kick
+		auto_reconnect  = (1 << 4),     //!< Auto reconnect on disconnection
+		join_invite     = (1 << 5)      //!< Join a channel on invitation
+	};
+
+	/**
+	 * \brief Describe current server state.
+	 */
+	enum class state : std::uint8_t {
+		disconnected,                   //!< not connected at all,
+		connecting,                     //!< network connection in progress,
+		identifying,                    //!< sending nick, user and password commands,
+		connected                       //!< ready for use.
+	};
+
+protected:
+	/**
+	 * \brief Server state.
+	 */
+	state state_{state::disconnected};
+
+private:
+	// Requested and joined channels.
+	std::vector<channel> rchannels_;
+	std::set<std::string> jchannels_;
+
+	// Identifier.
+	std::string id_;
+
+	// Connection information.
+	std::string hostname_;
+	std::string password_;
+	std::uint16_t port_{6667};
+	options options_;
+
+	// Identity.
+	std::string nickname_{"irccd"};
+	std::string username_{"irccd"};
+	std::string realname_{"IRC Client Daemon"};
+	std::string ctcpversion_{"IRC Client Daemon"};
+
+	// Settings.
+	std::string command_char_{"!"};
+	std::uint16_t recodelay_{30};
+	std::uint16_t timeout_{1000};
+
+	// Server information.
+	std::map<channel_mode, char> modes_;
+
+	// Misc.
+	boost::asio::io_service& service_;
+	boost::asio::deadline_timer timer_;
+	std::shared_ptr<irc::connection> conn_;
+	std::deque<std::string> queue_;
+	std::map<std::string, std::set<std::string>> names_map_;
+	std::map<std::string, whois_info> whois_map_;
+
+	auto dispatch_connect(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_endofnames(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_endofwhois(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_invite(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_isupport(const irc::message&) -> bool;
+	auto dispatch_join(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_kick(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_mode(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_namreply(const irc::message&) -> bool;
+	auto dispatch_nick(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_notice(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_part(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_ping(const irc::message&) -> bool;
+	auto dispatch_privmsg(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_topic(const irc::message&, const recv_handler&) -> bool;
+	auto dispatch_whoischannels(const irc::message&) -> bool;
+	auto dispatch_whoisuser(const irc::message&) -> bool;
+	auto dispatch(const irc::message&, const recv_handler&) -> bool;
+
+	// I/O and connection.
+	void flush();
+	void identify();
+	void handle_send(const std::error_code&);
+	void handle_recv(const std::error_code&, const irc::message&, const recv_handler&);
+	void handle_wait(const std::error_code&, const connect_handler&);
+	void handle_connect(const std::error_code&, const connect_handler&);
+
+public:
+	/**
+	 * Construct a server.
+	 *
+	 * \pre !host.empty()
+	 * \param service the service
+	 * \param id the identifier
+	 * \param hostname the hostname
+	 */
+	server(boost::asio::io_service& service, std::string id, std::string hostname = "localhost");
+
+	/**
+	 * Destructor. Close the connection if needed.
+	 */
+	virtual ~server();
+
+	/**
+	 * Get the current server state.
+	 *
+	 * \return the state
+	 */
+	auto get_state() const noexcept -> state;
+
+	/**
+	 * Get the server identifier.
+	 *
+	 * \return the id
+	 */
+	auto get_id() const noexcept -> const std::string&;
+
+	/**
+	 * Get the hostname.
+	 *
+	 * \return the hostname
+	 */
+	auto get_hostname() const noexcept -> const std::string&;
+
+	/**
+	 * Get the password.
+	 *
+	 * \return the password
+	 */
+	auto get_password() const noexcept -> const std::string&;
+
+	/**
+	 * Set the password.
+	 *
+	 * An empty password means no password.
+	 *
+	 * \param password the password
+	 */
+	void set_password(std::string password) noexcept;
+
+	/**
+	 * Get the port.
+	 *
+	 * \return the port
+	 */
+	auto get_port() const noexcept -> std::uint16_t;
+
+	/**
+	 * Set the port.
+	 *
+	 * \param port the port
+	 */
+	void set_port(std::uint16_t port) noexcept;
+
+	/**
+	 * Get the options flags.
+	 *
+	 * \return the flags
+	 */
+	auto get_options() const noexcept -> options;
+
+	/**
+	 * Set the options flags.
+	 *
+	 * \param flags the flags
+	 */
+	void set_options(options flags) noexcept;
+
+	/**
+	 * Get the nickname.
+	 *
+	 * \return the nickname
+	 */
+	auto get_nickname() const noexcept -> const std::string&;
+
+	/**
+	 * Set the nickname.
+	 *
+	 * If the server is connected, send a nickname command to the IRC server,
+	 * otherwise change it instantly.
+	 *
+	 * \param nickname the nickname
+	 */
+	void set_nickname(std::string nickname);
+
+	/**
+	 * Get the username.
+	 *
+	 * \return the username
+	 */
+	auto get_username() const noexcept -> const std::string&;
+
+	/**
+	 * Set the username.
+	 *
+	 * \param name the username
+	 * \note the username will be changed on the next connection
+	 */
+	void set_username(std::string name) noexcept;
+
+	/**
+	 * Get the realname.
+	 *
+	 * \return the realname
+	 */
+	auto get_realname() const noexcept -> const std::string&;
+
+	/**
+	 * Set the realname.
+	 *
+	 * \param realname the username
+	 * \note the username will be changed on the next connection
+	 */
+	void set_realname(std::string realname) noexcept;
+
+	/**
+	 * Get the CTCP version.
+	 *
+	 * \return the CTCP version
+	 */
+	auto get_ctcp_version() const noexcept -> const std::string&;
+
+	/**
+	 * Set the CTCP version.
+	 *
+	 * \param ctcpversion the version
+	 */
+	void set_ctcp_version(std::string ctcpversion);
+
+	/**
+	 * Get the command character.
+	 *
+	 * \return the character
+	 */
+	auto get_command_char() const noexcept -> const std::string&;
+
+	/**
+	 * Set the command character.
+	 *
+	 * \pre !command_char_.empty()
+	 * \param command_char the command character
+	 */
+	void set_command_char(std::string command_char) noexcept;
+
+	/**
+	 * Get the reconnection delay before retrying.
+	 *
+	 * \return the number of seconds
+	 */
+	auto get_reconnect_delay() const noexcept -> std::uint16_t;
+
+	/**
+	 * Set the number of seconds before retrying.
+	 *
+	 * \param reconnect_delay the number of seconds
+	 */
+	void set_reconnect_delay(std::uint16_t reconnect_delay) noexcept;
+
+	/**
+	 * Get the ping timeout.
+	 *
+	 * \return the ping timeout
+	 */
+	auto get_ping_timeout() const noexcept -> std::uint16_t;
+
+	/**
+	 * Set the ping timeout before considering a server as dead.
+	 *
+	 * \param ping_timeout the delay in seconds
+	 */
+	void set_ping_timeout(std::uint16_t ping_timeout) noexcept;
+
+	/**
+	 * Get the list of channels joined.
+	 *
+	 * \return the channels
+	 */
+	auto get_channels() const noexcept -> const std::set<std::string>&;
+
+	/**
+	 * Determine if the nickname is the bot itself.
+	 *
+	 * \param nick the nickname to check
+	 * \return true if it is the bot
+	 */
+	auto is_self(std::string_view nick) const noexcept -> bool;
+
+	/**
+	 * Start connecting.
+	 *
+	 * This only initiate TCP connection and/or SSL handshaking, the identifying
+	 * process may take some time and you must repeatedly call recv() to wait
+	 * for connect_event.
+	 *
+	 * \pre handler != nullptr
+	 * \param handler the completion handler
+	 * \note the server must be kept alive until completion
+	 */
+	virtual void connect(connect_handler handler) noexcept;
+
+	/**
+	 * Force disconnection.
+	 */
+	virtual void disconnect() noexcept;
+
+	/**
+	 * Receive next event.
+	 *
+	 * \pre handler != nullptr
+	 * \param handler the handler
+	 * \note the server must be kept alive until completion
+	 */
+	virtual void recv(recv_handler handler) noexcept;
+
+	/**
+	 * Invite a user to a channel.
+	 *
+	 * \param target the target nickname
+	 * \param channel the channel
+	 */
+	virtual void invite(std::string_view target, std::string_view channel);
+
+	/**
+	 * Join a channel, the password is optional and can be kept empty.
+	 *
+	 * \param channel the channel to join
+	 * \param password the optional password
+	 */
+	virtual void join(std::string_view channel, std::string_view password = "");
+
+	/**
+	 * Kick someone from the channel. Please be sure to have the rights
+	 * on that channel because errors won't be reported.
+	 *
+	 * \param target the target to kick
+	 * \param channel from which channel
+	 * \param reason the optional reason
+	 */
+	virtual void kick(std::string_view target,
+	                  std::string_view channel,
+	                  std::string_view reason = "");
+
+	/**
+	 * Send a CTCP Action as known as /me. The target may be either a
+	 * channel or a nickname.
+	 *
+	 * \param target the nickname or the channel
+	 * \param message the message
+	 */
+	virtual void me(std::string_view target, std::string_view message);
+
+	/**
+	 * Send a message to the specified target or channel.
+	 *
+	 * \param target the target
+	 * \param message the message
+	 */
+	virtual void message(std::string_view target, std::string_view message);
+
+	/**
+	 * Change channel/user mode.
+	 *
+	 * \param channel the channel or nickname
+	 * \param mode the mode
+	 * \param limit the optional limit
+	 * \param user the optional user
+	 * \param mask the optional ban mask
+	 */
+	virtual void mode(std::string_view channel,
+	                  std::string_view mode,
+	                  std::string_view limit = "",
+	                  std::string_view user = "",
+	                  std::string_view mask = "");
+
+	/**
+	 * Request the list of names.
+	 *
+	 * \param channel the channel
+	 */
+	virtual void names(std::string_view channel);
+
+	/**
+	 * Send a private notice.
+	 *
+	 * \param target the target
+	 * \param message the notice message
+	 */
+	virtual void notice(std::string_view target, std::string_view message);
+
+	/**
+	 * Part from a channel.
+	 *
+	 * Please note that the reason is not supported on all servers so if you
+	 * want portability, don't provide it.
+	 *
+	 * \param channel the channel to leave
+	 * \param reason the optional reason
+	 */
+	virtual void part(std::string_view channel, std::string_view reason = "");
+
+	/**
+	 * Send a raw message to the IRC server. You don't need to add
+	 * message terminators.
+	 *
+	 * If the server is not yet connected, the command is postponed and will be
+	 * ran when ready.
+	 *
+	 * \param raw the raw message (without `\r\n\r\n`)
+	 */
+	virtual void send(std::string_view raw);
+
+	/**
+	 * Change the channel topic.
+	 *
+	 * \param channel the channel
+	 * \param topic the desired topic
+	 */
+	virtual void topic(std::string_view channel, std::string_view topic);
+
+	/**
+	 * Request for whois information.
+	 *
+	 * \param target the target nickname
+	 */
+	virtual void whois(std::string_view target);
+};
+
+/**
+ * \cond IRCCD_HIDDEN_SYMBOLS
+ */
+
+/**
+ * Apply bitwise XOR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator^(server::options v1, server::options v2) noexcept -> server::options
+{
+	return static_cast<server::options>(static_cast<unsigned>(v1) ^ static_cast<unsigned>(v2));
+}
+
+/**
+ * Apply bitwise AND.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator&(server::options v1, server::options v2) noexcept -> server::options
+{
+	return static_cast<server::options>(static_cast<unsigned>(v1) & static_cast<unsigned>(v2));
+}
+
+/**
+ * Apply bitwise OR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator|(server::options v1, server::options v2) noexcept -> server::options
+{
+	return static_cast<server::options>(static_cast<unsigned>(v1) | static_cast<unsigned>(v2));
+}
+
+/**
+ * Apply bitwise NOT.
+ *
+ * \param v the value
+ * \return the complement
+ */
+inline auto operator~(server::options v) noexcept -> server::options
+{
+	return static_cast<server::options>(~static_cast<unsigned>(v));
+}
+
+/**
+ * Assign bitwise OR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator|=(server::options& v1, server::options v2) noexcept -> server::options&
+{
+	return v1 = v1 | v2;
+}
+
+/**
+ * Assign bitwise AND.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator&=(server::options& v1, server::options v2) noexcept -> server::options&
+{
+	return v1 = v1 & v2;
+}
+
+/**
+ * Assign bitwise XOR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator^=(server::options& v1, server::options v2) noexcept -> server::options&
+{
+	return v1 = v1 ^ v2;
+}
+
+/**
+ * \endcond
+ */
+
+/**
+ * \brief Server error.
+ */
+class server_error : public std::system_error {
+public:
+	/**
+	 * \brief Server related errors.
+	 */
+	enum error {
+		//!< No error.
+		no_error = 0,
+
+		//!< The specified server was not found.
+		not_found,
+
+		//!< The specified identifier is invalid.
+		invalid_identifier,
+
+		//!< The server is not connected.
+		not_connected,
+
+		//!< The server is already connected.
+		already_connected,
+
+		//!< Server with same name already exists.
+		already_exists,
+
+		//!< The specified port number is invalid.
+		invalid_port,
+
+		//!< The specified reconnect delay number is invalid.
+		invalid_reconnect_delay,
+
+		//!< The specified host was invalid.
+		invalid_hostname,
+
+		//!< The channel was empty or invalid.
+		invalid_channel,
+
+		//!< The mode given was empty.
+		invalid_mode,
+
+		//!< The nickname was empty or invalid.
+		invalid_nickname,
+
+		//!< The username was empty or invalid.
+		invalid_username,
+
+		//!< The realname was empty or invalid.
+		invalid_realname,
+
+		//!< Invalid password property.
+		invalid_password,
+
+		//!< Invalid ping timeout.
+		invalid_ping_timeout,
+
+		//!< Invalid ctcp version.
+		invalid_ctcp_version,
+
+		//!< Invalid command character.
+		invalid_command_char,
+
+		//!< Message (PRIVMSG) was invalid
+		invalid_message,
+
+		//!< SSL was requested but is disabled.
+		ssl_disabled,
+
+		//!< IPv4 or IPv6 must be defined.
+		invalid_family
+	};
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param code the error code
+	 */
+	server_error(error code) noexcept;
+};
+
+/**
+ * Get the server error category singleton.
+ *
+ * \return the singleton
+ */
+auto server_category() -> const std::error_category&;
+
+/**
+ * Create a std::error_code from server_error::error enum.
+ *
+ * \param e the error code
+ * \return the error code
+ */
+auto make_error_code(server_error::error e) -> std::error_code;
+
+} // !irccd::daemon
+
+/**
+ * \cond IRCCD_HIDDEN_SYMBOLS
+ */
+
+namespace std {
+
+template <>
+struct is_error_code_enum<irccd::daemon::server_error::error> : public std::true_type {
+};
+
+} // !std
+
+/**
+ * \endcond
+ */
+
+#endif // !IRCCD_DAEMON_SERVER_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/server_service.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,689 @@
+/*
+ * server_service.cpp -- server service
+ *
+ * Copyright (c) 2013-2018 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 <irccd/json_util.hpp>
+#include <irccd/string_util.hpp>
+
+#include "bot.hpp"
+#include "logger.hpp"
+#include "plugin_service.hpp"
+#include "rule_service.hpp"
+#include "server.hpp"
+#include "server_service.hpp"
+#include "server_util.hpp"
+#include "transport_service.hpp"
+
+namespace irccd::daemon {
+
+namespace {
+
+class dispatcher {
+private:
+	bot& bot_;
+
+	template <typename EventNameFunc, typename ExecFunc>
+	void dispatch(std::string_view, std::string_view, std::string_view, EventNameFunc&&, ExecFunc);
+
+public:
+	dispatcher(bot& bot);
+	void operator()(const std::monostate&);
+	void operator()(const connect_event&);
+	void operator()(const disconnect_event&);
+	void operator()(const invite_event&);
+	void operator()(const join_event&);
+	void operator()(const kick_event&);
+	void operator()(const message_event&);
+	void operator()(const me_event&);
+	void operator()(const mode_event&);
+	void operator()(const names_event&);
+	void operator()(const nick_event&);
+	void operator()(const notice_event&);
+	void operator()(const part_event&);
+	void operator()(const topic_event&);
+	void operator()(const whois_event&);
+};
+
+template <typename EventNameFunc, typename ExecFunc>
+void dispatcher::dispatch(std::string_view server,
+                          std::string_view origin,
+                          std::string_view target,
+                          EventNameFunc&& name_func,
+                          ExecFunc exec_func)
+{
+	for (const auto& plugin : bot_.plugins().list()) {
+		const auto eventname = name_func(*plugin);
+		const auto allowed = bot_.rules().solve(server, target, origin, plugin->get_name(), eventname);
+
+		if (!allowed) {
+			bot_.get_log().debug("rule", "") << "event skipped on match" << std::endl;
+			continue;
+		}
+
+		bot_.get_log().debug("rule", "") << "event allowed" << std::endl;
+
+		try {
+			exec_func(*plugin);
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning(*plugin) << ex.what() << std::endl;
+		}
+	}
+}
+
+dispatcher::dispatcher(bot& bot)
+	: bot_(bot)
+{
+}
+
+void dispatcher::operator()(const std::monostate&)
+{
+}
+
+void dispatcher::operator()(const connect_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onConnect" << std::endl;
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onConnect"             },
+		{ "server",     ev.server->get_id()     }
+	}));
+
+	dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "",
+		[=] (plugin&) -> std::string {
+			return "onConnect";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_connect(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const disconnect_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onDisconnect" << std::endl;
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onDisconnect"          },
+		{ "server",     ev.server->get_id()     }
+	}));
+
+	dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "",
+		[=] (plugin&) -> std::string {
+			return "onDisconnect";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_disconnect(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const invite_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onInvite:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  target: " << ev.nickname << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onInvite"              },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onInvite";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_invite(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const join_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onJoin:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onJoin"                },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onJoin";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_join(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const kick_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onKick:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  target: " << ev.target << std::endl;
+	bot_.get_log().debug(*ev.server) << "  reason: " << ev.reason << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onKick"                },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              },
+		{ "target",     ev.target               },
+		{ "reason",     ev.reason               }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onKick";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_kick(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const message_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onMessage:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  message: " << ev.message << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onMessage"             },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              },
+		{ "message",    ev.message              }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin& plugin) -> std::string {
+			return server_util::message_type::parse(
+				ev.message,
+				ev.server->get_command_char(),
+				plugin.get_id()
+			).type == server_util::message_type::is_command ? "onCommand" : "onMessage";
+		},
+		[=] (plugin& plugin) mutable {
+			auto copy = ev;
+			auto pack = server_util::message_type::parse(
+				copy.message,
+				copy.server->get_command_char(),
+				plugin.get_id()
+			);
+
+			copy.message = pack.message;
+
+			if (pack.type == server_util::message_type::is_command)
+				plugin.handle_command(bot_, copy);
+			else
+				plugin.handle_message(bot_, copy);
+		}
+	);
+}
+
+void dispatcher::operator()(const me_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onMe:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  target: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  message: " << ev.message << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onMe"                  },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "target",     ev.channel              },
+		{ "message",    ev.message              }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onMe";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_me(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const mode_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onMode" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  mode: " << ev.mode << std::endl;
+	bot_.get_log().debug(*ev.server) << "  limit: " << ev.limit << std::endl;
+	bot_.get_log().debug(*ev.server) << "  user: " << ev.user << std::endl;
+	bot_.get_log().debug(*ev.server) << "  mask: " << ev.mask << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onMode"                },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              },
+		{ "mode",       ev.mode                 },
+		{ "limit",      ev.limit                },
+		{ "user",       ev.user                 },
+		{ "mask",       ev.mask                 }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, /* channel */ "",
+		[=] (plugin &) -> std::string {
+			return "onMode";
+		},
+		[=] (plugin &plugin) {
+			plugin.handle_mode(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const names_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onNames:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  names: " << string_util::join(ev.names.begin(), ev.names.end(), ", ") << std::endl;
+
+	auto names = nlohmann::json::array();
+
+	for (const auto& v : ev.names)
+		names.push_back(v);
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onNames"               },
+		{ "server",     ev.server->get_id()     },
+		{ "channel",    ev.channel              },
+		{ "names",      std::move(names)        }
+	}));
+
+	dispatch(ev.server->get_id(), /* origin */ "", ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onNames";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_names(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const nick_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onNick:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  nickname: " << ev.nickname << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onNick"                },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "nickname",   ev.nickname             }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, /* channel */ "",
+		[=] (plugin&) -> std::string {
+			return "onNick";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_nick(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const notice_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onNotice:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  message: " << ev.message << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onNotice"              },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              },
+		{ "message",    ev.message              }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, /* channel */ "",
+		[=] (plugin&) -> std::string {
+			return "onNotice";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_notice(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const part_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onPart:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  reason: " << ev.reason << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onPart"                },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              },
+		{ "reason",     ev.reason               }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onPart";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_part(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const topic_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onTopic:" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
+	bot_.get_log().debug(*ev.server) << "  topic: " << ev.topic << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onTopic"               },
+		{ "server",     ev.server->get_id()     },
+		{ "origin",     ev.origin               },
+		{ "channel",    ev.channel              },
+		{ "topic",      ev.topic                }
+	}));
+
+	dispatch(ev.server->get_id(), ev.origin, ev.channel,
+		[=] (plugin&) -> std::string {
+			return "onTopic";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_topic(bot_, ev);
+		}
+	);
+}
+
+void dispatcher::operator()(const whois_event& ev)
+{
+	bot_.get_log().debug(*ev.server) << "event onWhois" << std::endl;
+	bot_.get_log().debug(*ev.server) << "  nickname: " << ev.whois.nick << std::endl;
+	bot_.get_log().debug(*ev.server) << "  username: " << ev.whois.user << std::endl;
+	bot_.get_log().debug(*ev.server) << "  hostname: " << ev.whois.hostname << std::endl;
+	bot_.get_log().debug(*ev.server) << "  realname: " << ev.whois.realname << std::endl;
+	bot_.get_log().debug(*ev.server) << "  channels: " << string_util::join(ev.whois.channels, ", ") << std::endl;
+
+	bot_.transports().broadcast(nlohmann::json::object({
+		{ "event",      "onWhois"               },
+		{ "server",     ev.server->get_id()     },
+		{ "nickname",   ev.whois.nick           },
+		{ "username",   ev.whois.user           },
+		{ "hostname",   ev.whois.hostname       },
+		{ "realname",   ev.whois.realname       }
+	}));
+
+	dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "",
+		[=] (plugin&) -> std::string {
+			return "onWhois";
+		},
+		[=] (plugin& plugin) {
+			plugin.handle_whois(bot_, ev);
+		}
+	);
+}
+
+} // !namespace
+
+void server_service::handle_error(const std::shared_ptr<server>& server,
+                                  const std::error_code& code)
+{
+	assert(server);
+
+	bot_.get_log().warning(*server) << code.message() << std::endl;
+	bot_.get_log().warning(*server) << int(server->get_options()) << std::endl;
+
+	if ((server->get_options() & server::options::auto_reconnect) != server::options::auto_reconnect)
+		remove(server->get_id());
+	else {
+		bot_.get_log().info(*server) << "reconnecting in "
+			<< server->get_reconnect_delay() << " second(s)" << std::endl;
+		wait(server);
+	}
+}
+
+void server_service::handle_wait(const std::shared_ptr<server>& server, const std::error_code& code)
+{
+	/*
+	 * The timer runs on his own control, it will complete either if the delay
+	 * was reached, there was an error or if the io_context was called to cancel
+	 * all pending operations.
+	 *
+	 * This means while the timer is running someone may already have ask a
+	 * server for explicit reconnection (e.g. remote command, plugin). Thus we
+	 * check for server state and if it is still present in service.
+	 */
+	if (code && code != std::errc::operation_canceled) {
+		bot_.get_log().warning(*server) << code.message() << std::endl;
+		return;
+	}
+
+	if (server->get_state() == server::state::connected || !has(server->get_id()))
+		return;
+
+	connect(server);
+}
+
+void server_service::handle_recv(const std::shared_ptr<server>& server,
+                                 const std::error_code& code,
+                                 const event& event)
+{
+	assert(server);
+
+	if (code)
+		handle_error(server, code);
+	else {
+		recv(server);
+		std::visit(dispatcher(bot_), event);
+	}
+}
+
+void server_service::handle_connect(const std::shared_ptr<server>& server, const std::error_code& code)
+{
+	if (code)
+		handle_error(server, code);
+	else
+		recv(server);
+}
+
+void server_service::wait(const std::shared_ptr<server>& server)
+{
+	assert(server);
+
+	auto timer = std::make_shared<boost::asio::deadline_timer>(bot_.get_service());
+
+	timer->expires_from_now(boost::posix_time::seconds(server->get_reconnect_delay()));
+	timer->async_wait([this, server, timer] (auto code) {
+		handle_wait(server, code);
+	});
+}
+
+void server_service::recv(const std::shared_ptr<server>& server)
+{
+	assert(server);
+
+	server->recv([this, server] (auto code, auto event) {
+		handle_recv(server, code, event);
+	});
+}
+
+void server_service::connect(const std::shared_ptr<server>& server)
+{
+	assert(server);
+
+	server->connect([this, server] (auto code) {
+		handle_connect(server, code);
+	});
+}
+
+server_service::server_service(bot& bot)
+	: bot_(bot)
+{
+}
+
+auto server_service::list() const noexcept -> const std::vector<std::shared_ptr<server>>&
+{
+	return servers_;
+}
+
+auto server_service::has(const std::string& name) const noexcept -> bool
+{
+	return std::count_if(servers_.begin(), servers_.end(), [&] (const auto& server) {
+		return server->get_id() == name;
+	}) > 0;
+}
+
+void server_service::add(std::shared_ptr<server> server)
+{
+	assert(server);
+	assert(!has(server->get_id()));
+
+	servers_.push_back(server);
+	connect(server);
+}
+
+auto server_service::get(std::string_view name) const noexcept -> std::shared_ptr<server>
+{
+	const auto it = std::find_if(servers_.begin(), servers_.end(), [&] (const auto& server) {
+		return server->get_id() == name;
+	});
+
+	if (it == servers_.end())
+		return nullptr;
+
+	return *it;
+}
+
+auto server_service::require(std::string_view name) const -> std::shared_ptr<server>
+{
+	if (!string_util::is_identifier(name))
+		throw server_error(server_error::invalid_identifier);
+
+	const auto s = get(name);
+
+	if (!s)
+		throw server_error(server_error::not_found);
+
+	return s;
+}
+
+void server_service::disconnect(std::string_view id)
+{
+	const auto s = require(id);
+
+	s->disconnect();
+	dispatcher{bot_}(disconnect_event{s});
+}
+
+void server_service::reconnect(std::string_view id)
+{
+	disconnect(id);
+	connect(require(id));
+}
+
+void server_service::reconnect()
+{
+	for (const auto& s : servers_) {
+		try {
+			s->disconnect();
+			dispatcher{bot_}(disconnect_event{s});
+			connect(s);
+		} catch (const server_error& ex) {
+			bot_.get_log().warning(*s) << ex.what() << std::endl;
+		}
+	}
+}
+
+void server_service::remove(std::string_view name)
+{
+	const auto it = std::find_if(servers_.begin(), servers_.end(), [&] (const auto& server) {
+		return server->get_id() == name;
+	});
+
+	if (it != servers_.end()) {
+		(*it)->disconnect();
+		servers_.erase(it);
+	}
+}
+
+void server_service::clear() noexcept
+{
+	/*
+	 * Copy the array, because disconnect() may trigger on_die signal which
+	 * erase the server from itself.
+	 */
+	const auto save = servers_;
+
+	for (const auto& server : save)
+		server->disconnect();
+
+	servers_.clear();
+}
+
+void server_service::load(const config& cfg) noexcept
+{
+	for (const auto& section : cfg) {
+		if (section.get_key() != "server")
+			continue;
+
+		const auto id = section.get("name").get_value();
+
+		try {
+			auto server = server_util::from_config(bot_.get_service(), section);
+
+			if (has(server->get_id()))
+				throw server_error(server_error::already_exists);
+
+			add(std::move(server));
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning("server", id) << ex.what() << std::endl;
+		}
+	}
+}
+
+namespace logger {
+
+auto loggable_traits<server>::get_category(const server&) -> std::string_view
+{
+	return "server";
+}
+
+auto loggable_traits<server>::get_component(const server& sv) -> std::string_view
+{
+	return sv.get_id();
+}
+
+} // !logger
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/server_service.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,187 @@
+/*
+ * server_service.hpp -- server service
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_SERVER_SERVICE_HPP
+#define IRCCD_DAEMON_SERVER_SERVICE_HPP
+
+/**
+ * \file server_service.hpp
+ * \brief Server service.
+ */
+
+#include <memory>
+#include <system_error>
+#include <vector>
+
+#include "server.hpp"
+
+namespace irccd {
+
+class config;
+
+namespace daemon {
+
+class bot;
+
+/**
+ * \brief Manage IRC servers.
+ * \ingroup services
+ */
+class server_service {
+private:
+	bot& bot_;
+	std::vector<std::shared_ptr<server>> servers_;
+
+	void handle_error(const std::shared_ptr<server>&, const std::error_code&);
+	void handle_wait(const std::shared_ptr<server>&, const std::error_code&);
+	void handle_recv(const std::shared_ptr<server>&, const std::error_code&, const event&);
+	void handle_connect(const std::shared_ptr<server>&, const std::error_code&);
+
+	void wait(const std::shared_ptr<server>&);
+	void recv(const std::shared_ptr<server>&);
+	void connect(const std::shared_ptr<server>&);
+
+public:
+	/**
+	 * Create the server service.
+	 *
+	 * \param bot the irccd instance
+	 */
+	server_service(bot& bot);
+
+	/**
+	 * Get the list of servers
+	 *
+	 * \return the servers
+	 */
+	auto list() const noexcept -> const std::vector<std::shared_ptr<server>>&;
+
+	/**
+	 * Check if a server exists.
+	 *
+	 * \param name the name
+	 * \return true if exists
+	 */
+	auto has(const std::string& name) const noexcept -> bool;
+
+	/**
+	 * Add a new server to the application.
+	 *
+	 * \pre hasServer must return false
+	 * \param sv the server
+	 */
+	void add(std::shared_ptr<server> sv);
+
+	/**
+	 * Get a server or empty one if not found
+	 *
+	 * \param name the server name
+	 * \return the server or empty one if not found
+	 */
+	auto get(std::string_view name) const noexcept -> std::shared_ptr<server>;
+
+	/**
+	 * Find a server from a JSON object.
+	 *
+	 * \param name the server name
+	 * \return the server
+	 * \throw server_error on errors
+	 */
+	auto require(std::string_view name) const -> std::shared_ptr<server>;
+
+	/**
+	 * Force disconnection, this also call plugin::handle_disconnect handler.
+	 *
+	 * \param id the server id
+	 * \throw server_error on errors
+	 */
+	void disconnect(std::string_view id);
+
+	/**
+	 * Force reconnection, this also call plugin::handle_disconnect handler.
+	 *
+	 * \param id the server id
+	 * \return the server
+	 * \throw server_error on errors
+	 */
+	void reconnect(std::string_view id);
+
+	/**
+	 * Force reconnection of all servers.
+	 */
+	void reconnect();
+
+	/**
+	 * Remove a server from the irccd instance.
+	 *
+	 * The server if any, will be disconnected.
+	 *
+	 * \param name the server name
+	 */
+	void remove(std::string_view name);
+
+	/**
+	 * Remove all servers.
+	 *
+	 * All servers will be disconnected.
+	 */
+	void clear() noexcept;
+
+	/**
+	 * Load servers from the configuration.
+	 *
+	 * \param cfg the config
+	 */
+	void load(const config& cfg) noexcept;
+};
+
+namespace logger {
+
+template <typename T>
+struct loggable_traits;
+
+/**
+ * \brief Specialization for server.
+ * \ingroup logger-traits
+ */
+template <>
+struct loggable_traits<server> {
+	/**
+	 * Get 'server' category.
+	 *
+	 * \param server the server
+	 * \return server
+	 */
+	static auto get_category(const server& server) -> std::string_view;
+
+	/**
+	 * Get the server name.
+	 *
+	 * \param server the server
+	 * \return the server
+	 */
+	static auto get_component(const server& server) -> std::string_view;
+};
+
+} // !logger
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_SERVER_SERVICE_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/server_util.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,281 @@
+/*
+ * server_util.cpp -- server utilities
+ *
+ * Copyright (c) 2013-2018 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 <algorithm>
+
+#include <irccd/config.hpp>
+#include <irccd/ini_util.hpp>
+#include <irccd/json_util.hpp>
+#include <irccd/string_util.hpp>
+
+#include "server.hpp"
+#include "server_util.hpp"
+
+using irccd::json_util::deserializer;
+
+namespace irccd::daemon::server_util {
+
+namespace {
+
+void toggle(server& s, server::options opt, bool value) noexcept
+{
+	if (value)
+		s.set_options(s.get_options() | opt);
+	else
+		s.set_options(s.get_options() & ~(opt));
+}
+
+void from_config_load_identity(server& sv, const ini::section& sc)
+{
+	const auto username = ini_util::optional_string(sc, "username", sv.get_username());
+	const auto realname = ini_util::optional_string(sc, "realname", sv.get_realname());
+	const auto nickname = ini_util::optional_string(sc, "nickname", sv.get_nickname());
+	const auto ctcp_version = ini_util::optional_string(sc, "ctcp-version", sv.get_ctcp_version());
+
+	if (username.empty())
+		throw server_error(server_error::invalid_username);
+	if (realname.empty())
+		throw server_error(server_error::invalid_realname);
+	if (nickname.empty())
+		throw server_error(server_error::invalid_nickname);
+	if (ctcp_version.empty())
+		throw server_error(server_error::invalid_ctcp_version);
+
+	sv.set_username(username);
+	sv.set_realname(realname);
+	sv.set_nickname(nickname);
+	sv.set_ctcp_version(ctcp_version);
+}
+
+void from_config_load_channels(server& sv, const ini::section& sc)
+{
+	for (const auto& s : sc.get("channels")) {
+		channel channel;
+
+		if (auto pos = s.find(":") != std::string::npos) {
+			channel.name = s.substr(0, pos);
+			channel.password = s.substr(pos + 1);
+		} else
+			channel.name = s;
+
+		sv.join(channel.name, channel.password);
+	}
+}
+
+void from_config_load_flags(server& sv, const ini::section& sc)
+{
+	const auto ssl = sc.find("ssl");
+	const auto auto_rejoin = sc.find("auto-rejoin");
+	const auto auto_reconnect = sc.find("auto-reconnect");
+	const auto join_invite = sc.find("join-invite");
+	const auto ipv4 = sc.find("ipv4");
+	const auto ipv6 = sc.find("ipv6");
+
+	if (ssl != sc.end())
+		toggle(sv, server::options::ssl, string_util::is_boolean(ssl->get_value()));
+	if (auto_rejoin != sc.end())
+		toggle(sv, server::options::auto_rejoin, string_util::is_boolean(auto_rejoin->get_value()));
+	if (auto_reconnect != sc.end())
+		toggle(sv, server::options::auto_reconnect, string_util::is_boolean(auto_reconnect->get_value()));
+	if (join_invite != sc.end())
+		toggle(sv, server::options::join_invite, string_util::is_boolean(join_invite->get_value()));
+	if (ipv4 != sc.end())
+		toggle(sv, server::options::ipv4, string_util::is_boolean(ipv4->get_value()));
+	if (ipv6 != sc.end())
+		toggle(sv, server::options::ipv6, string_util::is_boolean(ipv6->get_value()));
+
+	if ((sv.get_options() & server::options::ipv4) != server::options::ipv4 &&
+	    (sv.get_options() & server::options::ipv6) != server::options::ipv6)
+		throw server_error(server_error::invalid_family);
+}
+
+void from_config_load_numeric_parameters(server& sv, const ini::section& sc)
+{
+	const auto port = ini_util::optional_uint<std::uint16_t>(sc, "port", sv.get_port());
+	const auto ping_timeout = ini_util::optional_uint<uint16_t>(sc, "ping-timeout", sv.get_ping_timeout());
+	const auto reco_timeout = ini_util::optional_uint<uint16_t>(sc, "auto-reconnect-delay", sv.get_reconnect_delay());
+
+	if (!port)
+		throw server_error(server_error::invalid_port);
+	if (!ping_timeout)
+		throw server_error(server_error::invalid_ping_timeout);
+	if (!reco_timeout)
+		throw server_error(server_error::invalid_reconnect_delay);
+
+	sv.set_port(*port);
+	sv.set_ping_timeout(*ping_timeout);
+	sv.set_reconnect_delay(*reco_timeout);
+}
+
+void from_config_load_options(server& sv, const ini::section& sc)
+{
+	const auto password = ini_util::optional_string(sc, "password", "");
+	const auto command_char = ini_util::optional_string(sc, "command-char", sv.get_command_char());
+
+	sv.set_password(password);
+	sv.set_command_char(command_char);
+}
+
+void from_json_load_general(server& sv, const deserializer& parser)
+{
+	const auto port = parser.optional<std::uint16_t>("port", sv.get_port());
+	const auto nickname = parser.optional<std::string>("nickname", sv.get_nickname());
+	const auto realname = parser.optional<std::string>("realname", sv.get_realname());
+	const auto username = parser.optional<std::string>("username", sv.get_username());
+	const auto ctcp_version = parser.optional<std::string>("ctcpVersion", sv.get_ctcp_version());
+	const auto command = parser.optional<std::string>("commandChar", sv.get_command_char());
+	const auto password = parser.optional<std::string>("password", sv.get_password());
+
+	if (!port || *port > std::numeric_limits<std::uint16_t>::max())
+		throw server_error(server_error::invalid_port);
+	if (!nickname)
+		throw server_error(server_error::invalid_nickname);
+	if (!realname)
+		throw server_error(server_error::invalid_realname);
+	if (!username)
+		throw server_error(server_error::invalid_username);
+	if (!ctcp_version)
+		throw server_error(server_error::invalid_ctcp_version);
+	if (!command)
+		throw server_error(server_error::invalid_command_char);
+	if (!password)
+		throw server_error(server_error::invalid_password);
+
+	sv.set_port(*port);
+	sv.set_nickname(*nickname);
+	sv.set_realname(*realname);
+	sv.set_username(*username);
+	sv.set_ctcp_version(*ctcp_version);
+	sv.set_command_char(*command);
+	sv.set_password(*password);
+}
+
+void from_json_load_options(server& sv, const deserializer& parser)
+{
+	const auto auto_rejoin = parser.get<bool>("autoRejoin");
+	const auto join_invite = parser.get<bool>("joinInvite");
+	const auto ssl = parser.get<bool>("ssl");
+	const auto ipv4 = parser.optional<bool>("ipv4", true);
+	const auto ipv6 = parser.optional<bool>("ipv6", true);
+
+	if (!ipv4 || !ipv6)
+		throw server_error(server_error::invalid_family);
+
+	toggle(sv, server::options::ipv4, *ipv4);
+	toggle(sv, server::options::ipv6, *ipv6);
+	toggle(sv, server::options::auto_rejoin, *auto_rejoin);
+	toggle(sv, server::options::join_invite, *join_invite);
+	toggle(sv, server::options::ssl, *ssl);
+
+#if !defined(IRCCD_HAVE_SSL)
+	if ((server::get_options() & server::options::ssl) == server::options::ssl)
+		throw server_error(server_error::ssl_disabled);
+#endif
+
+	// Verify that at least IPv4 or IPv6 is set.
+	if ((sv.get_options() & server::options::ipv4) != server::options::ipv4 &&
+	    (sv.get_options() & server::options::ipv6) != server::options::ipv6)
+		throw server_error(server_error::invalid_family);
+}
+
+} // !namespace
+
+auto message_type::parse(std::string_view message,
+                         std::string_view cchar,
+                         std::string_view plugin) -> message_type
+{
+	auto result = std::string(message);
+	auto cc = std::string(cchar);
+	auto name = std::string(plugin);
+	auto type = is_message;
+
+	// handle special commands "!<plugin> command"
+	if (cc.length() > 0) {
+		auto pos = result.find_first_of(" \t");
+		auto fullcommand = cc + name;
+
+		/*
+		 * If the message that comes is "!foo" without spaces we
+		 * compare the command char + the plugin name. If there
+		 * is a space, we check until we find a space, if not
+		 * typing "!foo123123" will trigger foo plugin.
+		 */
+		if (pos == std::string::npos)
+			type = result == fullcommand ? is_command : is_message;
+		else if (result.length() >= fullcommand.length() && result.compare(0, pos, fullcommand) == 0)
+			type = is_command;
+
+		if (type == is_command) {
+			/*
+			 * If no space is found we just set the message to "" otherwise
+			 * the plugin name will be passed through onCommand
+			 */
+			if (pos == std::string::npos)
+				result = "";
+			else
+				result = message.substr(pos + 1);
+		}
+	}
+
+	return {type, result};
+}
+
+auto from_json(boost::asio::io_service& service, const nlohmann::json& object) -> std::shared_ptr<server>
+{
+	// Mandatory parameters.
+	const deserializer parser(object);
+	const auto id = parser.get<std::string>("name");
+	const auto hostname = parser.get<std::string>("hostname");
+
+	if (!id || !string_util::is_identifier(*id))
+		throw server_error(server_error::invalid_identifier);
+	if (!hostname || hostname->empty())
+		throw server_error(server_error::invalid_hostname);
+
+	const auto sv = std::make_shared<server>(service, *id, *hostname);
+
+	from_json_load_general(*sv, parser);
+	from_json_load_options(*sv, parser);
+
+	return sv;
+}
+
+auto from_config(boost::asio::io_service& service,
+                 const ini::section& sc) -> std::shared_ptr<server>
+{
+	// Mandatory parameters.
+	const auto id = sc.get("name");
+	const auto hostname = sc.get("hostname");
+
+	if (!string_util::is_identifier(id.get_value()))
+		throw server_error(server_error::invalid_identifier);
+	if (hostname.get_value().empty())
+		throw server_error(server_error::invalid_hostname);
+
+	const auto sv = std::make_shared<server>(service, id.get_value(), hostname.get_value());
+
+	from_config_load_channels(*sv, sc);
+	from_config_load_flags(*sv, sc);
+	from_config_load_numeric_parameters(*sv, sc);
+	from_config_load_options(*sv, sc);
+	from_config_load_identity(*sv, sc);
+
+	return sv;
+}
+
+} // !irccd::daemon::server_util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/server_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,125 @@
+/*
+ * server_util.hpp -- server utilities
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_SERVER_UTIL_HPP
+#define IRCCD_DAEMON_SERVER_UTIL_HPP
+
+/**
+ * \file server_util.hpp
+ * \brief Server utilities.
+ */
+
+#include <memory>
+#include <string_view>
+
+#include <boost/asio/io_service.hpp>
+
+#include <json.hpp>
+
+namespace irccd {
+
+namespace ini {
+
+class section;
+
+} // !ini
+
+class config;
+
+namespace daemon {
+
+class server;
+
+/**
+ * \brief Server utilities.
+ */
+namespace server_util {
+
+/**
+ * \brief Pack a message and its type
+ *
+ * On channels and queries, you may have a special command or a standard message
+ * depending on the beginning of the message.
+ *
+ * Example: `!reminder help' may invoke the command event if a plugin reminder
+ * exists.
+ */
+struct message_type {
+	/**
+	 * \brief Describe which type of message has been received.
+	 */
+	enum kind {
+		is_command,     //!< special command
+		is_message      //!< standard message
+	};
+
+	/**
+	 * Message kind.
+	 */
+	kind type;
+
+	/**
+	 * Message content.
+	 */
+	std::string message;
+
+	/**
+	 * Parse IRC message and determine if it's a command or a simple message.
+	 *
+	 * If it's a command, the plugin invocation command is removed from the
+	 * original message, otherwise it is copied verbatime.
+	 *
+	 * \param message the message line
+	 * \param cchar the command char (e.g '!')
+	 * \param plugin the plugin name
+	 * \return the pair
+	 */
+	static auto parse(std::string_view message,
+	                  std::string_view cchar,
+	                  std::string_view plugin) -> message_type;
+};
+
+/**
+ * Convert a JSON object to a server.
+ *
+ * \param service the io service
+ * \param object the object
+ * \return the server
+ * \throw server_error on errors
+ */
+auto from_json(boost::asio::io_service& service,
+               const nlohmann::json& object) -> std::shared_ptr<server>;
+
+/**
+ * Convert a INI section to a server.
+ *
+ * \param service the io service
+ * \param sc the server section
+ * \return the server
+ * \throw server_error on errors
+ */
+auto from_config(boost::asio::io_service& service,
+                 const ini::section& sc) -> std::shared_ptr<server>;
+
+} // !server_util
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_SERVER_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_client.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,136 @@
+/*
+ * transport_client.cpp -- server side transport clients
+ *
+ * Copyright (c) 2013-2018 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 <cassert>
+
+#include "transport_client.hpp"
+#include "transport_server.hpp"
+
+namespace irccd::daemon {
+
+void transport_client::flush()
+{
+	if (queue_.empty())
+		return;
+
+	const auto self = shared_from_this();
+
+	stream_->send(queue_.front().first, [this, self] (auto code) {
+		if (queue_.front().second)
+			queue_.front().second(code);
+
+		queue_.pop_front();
+
+		if (code)
+			erase();
+		else
+			flush();
+	});
+}
+
+void transport_client::erase()
+{
+	state_ = state::closing;
+
+	if (auto parent = parent_.lock())
+		parent->get_clients().erase(shared_from_this());
+}
+
+transport_client::transport_client(std::weak_ptr<transport_server> server,
+                                   std::shared_ptr<stream> stream) noexcept
+	: parent_(server)
+	, stream_(std::move(stream))
+{
+	assert(stream_);
+}
+
+auto transport_client::get_state() const noexcept -> state
+{
+	return state_;
+}
+
+void transport_client::set_state(state state) noexcept
+{
+	state_ = state;
+}
+
+void transport_client::read(stream::recv_handler handler)
+{
+	assert(handler);
+
+	if (state_ != state::closing) {
+		const auto self = shared_from_this();
+
+		stream_->recv([this, self, handler] (auto code, auto msg) {
+			handler(code, msg);
+
+			if (code)
+				erase();
+		});
+	}
+}
+
+void transport_client::write(nlohmann::json json, stream::send_handler handler)
+{
+	const auto in_progress = queue_.size() > 0;
+
+	queue_.emplace_back(std::move(json), std::move(handler));
+
+	if (!in_progress)
+		flush();
+}
+
+void transport_client::success(const std::string& cname, stream::send_handler handler)
+{
+	assert(!cname.empty());
+
+	write({{ "command", cname }}, std::move(handler));
+}
+
+void transport_client::error(std::error_code code, stream::send_handler handler)
+{
+	error(std::move(code), "", std::move(handler));
+}
+
+void transport_client::error(std::error_code code, std::string_view cname, stream::send_handler handler)
+{
+	assert(code);
+
+	auto json = nlohmann::json::object({
+		{ "error",              code.value()            },
+		{ "errorCategory",      code.category().name()  },
+		{ "errorMessage",       code.message()          }
+	});
+
+	// TODO: check newer version of JSON for string_view support.
+	if (!cname.empty())
+		json["command"] = std::string(cname);
+
+	const auto self = shared_from_this();
+
+	write(std::move(json), [this, handler, self] (auto code) {
+		erase();
+
+		if (handler)
+			handler(code);
+	});
+
+	state_ = state::closing;
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_client.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,150 @@
+/*
+ * transport_client.hpp -- server side transport clients
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_TRANSPORT_CLIENT_HPP
+#define IRCCD_DAEMON_TRANSPORT_CLIENT_HPP
+
+/**
+ * \file transport_client.hpp
+ * \brief Server side transport clients.
+ */
+
+#include <deque>
+#include <memory>
+#include <string_view>
+
+#include <irccd/stream.hpp>
+
+namespace irccd::daemon {
+
+class transport_server;
+
+/**
+ * \brief Abstract transport client class.
+ * \ingroup transports
+ *
+ * This class is responsible of receiving/sending data.
+ */
+class transport_client : public std::enable_shared_from_this<transport_client> {
+public:
+	/**
+	 * Client state.
+	 */
+	enum class state {
+		authenticating,         //!< client is authenticating
+		ready,                  //!< client is ready
+		closing                 //!< client is closing
+	};
+
+private:
+	state state_{state::authenticating};
+	std::weak_ptr<transport_server> parent_;
+	std::shared_ptr<stream> stream_;
+	std::deque<std::pair<nlohmann::json, stream::send_handler>> queue_;
+
+	void flush();
+	void erase();
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \pre stream != nullptr
+	 * \param server the parent
+	 * \param stream the I/O stream
+	 */
+	transport_client(std::weak_ptr<transport_server> server,
+	                 std::shared_ptr<stream> stream) noexcept;
+
+	/**
+	 * Get the current client state.
+	 *
+	 * \return the state
+	 */
+	auto get_state() const noexcept -> state;
+
+	/**
+	 * Set the client state.
+	 *
+	 * \param state the new state
+	 */
+	void set_state(state state) noexcept;
+
+	/**
+	 * Start receiving if not closed.
+	 *
+	 * Possible error codes:
+	 *
+	 *   - std::errc::network_down in case of errors,
+	 *   - std::errc::invalid_argument if the JSON message is invalid,
+	 *   - std::errc::not_enough_memory in case of memory failure.
+	 *
+	 * \pre handler != nullptr
+	 * \param handler the handler
+	 * \warning Another read operation MUST NOT be running.
+	 */
+	void read(stream::recv_handler handler);
+
+	/**
+	 * Start sending if not closed.
+	 *
+	 * Possible error codes:
+	 *
+	 *   - boost::system::errc::network_down in case of errors,
+	 *
+	 * \pre json.is_object()
+	 * \param json the json message
+	 * \param handler the optional handler
+	 * \note If a write operation is running, it is postponed once ready.
+	 */
+	void write(nlohmann::json json, stream::send_handler handler = nullptr);
+
+	/**
+	 * Convenient success message.
+	 *
+	 * \param command the command name
+	 * \param handler the optional handler
+	 * \note If a write operation is running, it is postponed once ready.
+	 */
+	void success(const std::string& command, stream::send_handler handler = nullptr);
+
+	/**
+	 * Send an error code to the client.
+	 *
+	 * \pre code is not 0
+	 * \param code the error code
+	 * \param handler the optional handler
+	 * \note If a write operation is running, it is postponed once ready.
+	 */
+	void error(std::error_code code, stream::send_handler handler = nullptr);
+
+	/**
+	 * Send an error code to the client.
+	 *
+	 * \pre code is not 0
+	 * \param code the error code
+	 * \param command the command name
+	 * \param handler the optional handler
+	 * \note If a write operation is running, it is postponed once ready.
+	 */
+	void error(std::error_code code, std::string_view command, stream::send_handler handler = nullptr);
+};
+
+} // !irccd::daemon
+
+#endif // !IRCCD_DAEMON_TRANSPORT_CLIENT_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_server.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,192 @@
+/*
+ * transport_server.cpp -- server side transports
+ *
+ * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
+
+#include <cassert>
+#include <system_error>
+
+#include <irccd/json_util.hpp>
+
+#include "bot.hpp"
+#include "transport_client.hpp"
+#include "transport_server.hpp"
+
+namespace irccd::daemon {
+
+void transport_server::do_auth(std::shared_ptr<transport_client> client, accept_handler handler)
+{
+	assert(client);
+	assert(handler);
+
+	client->read([this, client, handler] (auto code, auto message) {
+		if (code) {
+			handler(std::move(code), std::move(client));
+			return;
+		}
+
+		const json_util::deserializer doc(message);
+		const auto command = doc.get<std::string>("command");
+		const auto password = doc.get<std::string>("password");
+
+		if (!command || *command != "auth") {
+			client->error(bot_error::auth_required);
+			code = bot_error::auth_required;
+		} else if (!password || *password != password_) {
+			client->error(bot_error::invalid_auth);
+			code = bot_error::invalid_auth;
+		} else {
+			clients_.insert(client);
+			client->set_state(transport_client::state::ready);
+			client->success("auth");
+			code = bot_error::no_error;
+		}
+
+		handler(std::move(code), std::move(client));
+	});
+}
+
+void transport_server::do_greetings(std::shared_ptr<transport_client> client, accept_handler handler)
+{
+	assert(client);
+	assert(handler);
+
+	const auto greetings = nlohmann::json({
+		{ "program",    "irccd"                 },
+		{ "major",      IRCCD_VERSION_MAJOR     },
+		{ "minor",      IRCCD_VERSION_MINOR     },
+		{ "patch",      IRCCD_VERSION_PATCH     },
+#if defined(IRCCD_HAVE_JS)
+		{ "javascript", true                    },
+#endif
+#if defined(IRCCD_HAVE_SSL)
+		{ "ssl",        true                    },
+#endif
+	});
+
+	client->write(greetings, [this, client, handler] (auto code) {
+		if (code) {
+			handler(std::move(code), std::move(client));
+			return;
+		}
+
+		if (!password_.empty())
+			do_auth(std::move(client), std::move(handler));
+		else {
+			clients_.insert(client);
+			client->set_state(transport_client::state::ready);
+			handler(std::move(code), std::move(client));
+		}
+	});
+}
+
+transport_server::transport_server(std::unique_ptr<acceptor> acceptor) noexcept
+	: acceptor_(std::move(acceptor))
+{
+	assert(acceptor_);
+}
+
+auto transport_server::get_clients() const noexcept -> const client_set&
+{
+	return clients_;
+}
+
+auto transport_server::get_clients() noexcept -> client_set&
+{
+	return clients_;
+}
+
+auto transport_server::get_password() const noexcept -> const std::string&
+{
+	return password_;
+}
+
+void transport_server::set_password(std::string password) noexcept
+{
+	password_ = std::move(password);
+}
+
+void transport_server::accept(accept_handler handler)
+{
+	acceptor_->accept([this, handler] (auto code, auto stream) {
+		if (code) {
+			handler(code, nullptr);
+			return;
+		}
+
+		do_greetings(
+			std::make_shared<transport_client>(shared_from_this(), std::move(stream)),
+			std::move(handler)
+		);
+	});
+}
+
+transport_error::transport_error(error code) noexcept
+	: system_error(make_error_code(code))
+{
+}
+
+auto transport_category() noexcept -> const std::error_category&
+{
+	static const class category : public std::error_category {
+	public:
+		auto name() const noexcept -> const char* override
+		{
+			return "transport";
+		}
+
+		auto message(int e) const -> std::string override
+		{
+			switch (static_cast<transport_error::error>(e)) {
+			case transport_error::auth_required:
+				return "authentication required";
+			case transport_error::invalid_auth:
+				return "invalid authentication";
+			case transport_error::invalid_port:
+				return "invalid port";
+			case transport_error::invalid_address:
+				return "invalid address";
+			case transport_error::invalid_hostname:
+				return "invalid hostname";
+			case transport_error::invalid_path:
+				return "invalid socket path";
+			case transport_error::invalid_family:
+				return "invalid family";
+			case transport_error::invalid_certificate:
+				return "invalid certificate";
+			case transport_error::invalid_private_key:
+				return "invalid private key";
+			case transport_error::ssl_disabled:
+				return "ssl is not enabled";
+			case transport_error::not_supported:
+				return "transport not supported";
+			default:
+				return "no error";
+			}
+		}
+	} category;
+
+	return category;
+};
+
+auto make_error_code(transport_error::error e) noexcept -> std::error_code
+{
+	return { static_cast<int>(e), transport_category() };
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_server.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,276 @@
+/*
+ * transport_server.hpp -- server side transports
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_TRANSPORT_SERVER_HPP
+#define IRCCD_DAEMON_TRANSPORT_SERVER_HPP
+
+/**
+ * \file transport_server.hpp
+ * \brief Server side transports.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <functional>
+#include <memory>
+#include <unordered_set>
+#include <type_traits>
+
+#include <irccd/acceptor.hpp>
+
+namespace irccd::daemon {
+
+class transport_client;
+
+/**
+ * \brief Abstract transport server class.
+ * \ingroup transports
+ *
+ * The transport_server class is an abstract interface that waits for clients to
+ * connect and store them locally. It does not know the underlying
+ * implementation so derived classes may be implemented in any shape of form.
+ *
+ * As only constraint the implementation must provide an asynchronous operation
+ * to avoid blocking the daemon.
+ *
+ * The derived class only have to implement accept function which is only
+ * responsible of getting a client ready for I/O (receiving and sending), the
+ * transport_server does authentication and greeting by itself.
+ *
+ * # Accept procedure
+ *
+ * The connection procedure looks like this:
+ *
+ * ~~~
+ *            o (transport_server::accept is called)
+ *            |
+ *            v                          [error]
+ *   +-----------------------------+                  +---------------------+
+ *   | asynchronous accept process |----------------->| client is discarded |
+ *   +-----------------------------+                  +---------------------+
+ *            |                                                          ^
+ *            | [success]                                                |
+ *            v                                                          |
+ *   +-----------------------------------------+  [error while sending]  |
+ *   | sending irccd information to the client |------------------------>+
+ *   +-----------------------------------------+                         |
+ *     |              |                                                  |
+ *     |              | [authentication required]                        |
+ *     |              |                                                  |
+ *     |              v                    [error or invalid password]   |
+ *     |      +-------------------------+         +------------+         |
+ *     |      | wait for authentication |-------->| send error |-------->+
+ *     |      +-------------------------+         +------------+         ^
+ *     |              |                                                  |
+ *     |              | [correct password]                               |
+ *     v              v                                                  |
+ *   +---------------------------------------+  [incorrect]              |
+ *   | client is added to the list and ready ]-------------------------- +
+ *   +---------------------------------------+
+ * ~~~
+ *
+ * # I/O procedures
+ *
+ * Each client has a reference to its parent, since operations are asynchronous,
+ * they maintain their lifetime by themselve to update the parent list on
+ * errors.
+ *
+ * See the following diagram:
+ *
+ * ```
+ *       o (transport_client::recv or send is called) o
+ *       |                                            |
+ *       | [no operations in queue]                   | [operation in progress]
+ *       |                                            v
+ *       |                                    +---------------+
+ *       |                                    | push in queue |
+ *       |                                    +---------------+
+ *       |
+ *       |
+ *       |                                [pending operations in queue]
+ *       |<-----------------------------------------------+
+ *       |                                                ^
+ *       |                                                |
+ *       v                             [success]          |
+ *   +-------------------------------+           +-------------------+
+ *   | asynchronous operation starts |---------->| handler is called |
+ *   +-------------------------------+           +-------------------+
+ *       |
+ *       v [error]
+ *   +--------------------------------------+
+ *   | handler is called with an error code |
+ *   +--------------------------------------+
+ *       |
+ *       v
+ *   +----------------------------------+
+ *   | client delete itself from parent |
+ *   +----------------------------------+
+ * ```
+ *
+ * \see transport_client
+ * \see transport_service
+ */
+class transport_server : public std::enable_shared_from_this<transport_server> {
+public:
+	/**
+	 * Set of clients.
+	 */
+	using client_set = std::unordered_set<std::shared_ptr<transport_client>>;
+
+	/**
+	 * Accept completion handler.
+	 */
+	using accept_handler = std::function<void (std::error_code, std::shared_ptr<transport_client>)>;
+
+private:
+	client_set clients_;
+	std::unique_ptr<acceptor> acceptor_;
+	std::string password_;
+
+	void do_auth(std::shared_ptr<transport_client>, accept_handler);
+	void do_greetings(std::shared_ptr<transport_client>, accept_handler);
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \pre acceptor != nullptr
+	 * \param acceptor the stream acceptor
+	 */
+	transport_server(std::unique_ptr<acceptor> acceptor) noexcept;
+
+	/**
+	 * Get the clients.
+	 *
+	 * \return the clients
+	 */
+	auto get_clients() const noexcept -> const client_set&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the clients
+	 */
+	auto get_clients() noexcept -> client_set&;
+
+	/**
+	 * Get the current password, empty string means no password.
+	 *
+	 * \return the password
+	 */
+	auto get_password() const noexcept -> const std::string&;
+
+	/**
+	 * Set an optional password, empty string means no password.
+	 *
+	 * \param password the password
+	 */
+	void set_password(std::string password) noexcept;
+
+	/**
+	 * Accept a client.
+	 *
+	 * Also perform greetings and authentication under the hood. On success, the
+	 * client is added into the server and is ready to use.
+	 *
+	 * \pre handler != nullptr
+	 * \param handler the completion handler
+	 */
+	void accept(accept_handler handler);
+};
+
+/**
+ * \brief Transport error.
+ */
+class transport_error : public std::system_error {
+public:
+	/**
+	 * \brief Transport related errors.
+	 */
+	enum error {
+		//!< No error.
+		no_error = 0,
+
+		//!< Authentication is required.
+		auth_required,
+
+		//!< Authentication was invalid.
+		invalid_auth,
+
+		//!< Invalid TCP/IP port.
+		invalid_port,
+
+		//!< Invalid TCP/IP address.
+		invalid_address,
+
+		//!< The specified host was invalid.
+		invalid_hostname,
+
+		//!< Invalid unix local path.
+		invalid_path,
+
+		//!< Invalid IPv4/IPv6 family.
+		invalid_family,
+
+		//!< Invalid certificate given.
+		invalid_certificate,
+
+		//!< Invalid private key given.
+		invalid_private_key,
+
+		//!< SSL was requested but is disabled.
+		ssl_disabled,
+
+		//!< Kind of transport not supported on this platform.
+		not_supported
+	};
+
+	/**
+	 * Constructor.
+	 *
+	 * \param code the error code
+	 */
+	transport_error(error code) noexcept;
+};
+
+/**
+ * Get the transport error category singleton.
+ *
+ * \return the singleton
+ */
+auto transport_category() noexcept -> const std::error_category&;
+
+/**
+ * Create a std::error_code from server_error::error enum.
+ *
+ * \param e the error code
+ * \return the error code
+ */
+auto make_error_code(transport_error::error e) noexcept -> std::error_code;
+
+} // !irccd::daemon
+
+namespace std {
+
+template <>
+struct is_error_code_enum<irccd::daemon::transport_error::error> : public std::true_type {
+};
+
+} // !std
+
+#endif // !IRCCD_DAEMON_TRANSPORT_SERVER_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_service.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,150 @@
+/*
+ * transport_service.cpp -- transport service
+ *
+ * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
+
+#include <cassert>
+
+#include <irccd/json_util.hpp>
+
+#include "bot.hpp"
+#include "command.hpp"
+#include "logger.hpp"
+#include "transport_client.hpp"
+#include "transport_server.hpp"
+#include "transport_service.hpp"
+#include "transport_util.hpp"
+
+namespace irccd::daemon {
+
+void transport_service::handle_command(std::shared_ptr<transport_client> tc, const nlohmann::json& object)
+{
+	assert(object.is_object());
+
+	const json_util::deserializer doc(object);
+	const auto name = doc.get<std::string>("command");
+
+	if (!name) {
+		tc->error(bot_error::invalid_message);
+		return;
+	}
+
+	const auto cmd = std::find_if(commands_.begin(), commands_.end(), [&] (const auto& cptr) {
+		return cptr->get_name() == *name;
+	});
+
+	if (cmd == commands_.end())
+		tc->error(bot_error::invalid_command, *name);
+	else {
+		try {
+			(*cmd)->exec(bot_, *tc, doc);
+		} catch (const std::system_error& ex) {
+			tc->error(ex.code(), (*cmd)->get_name());
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning("transport", "")
+				<< "unknown error not reported: "
+				<< ex.what() << std::endl;
+		}
+	}
+}
+
+void transport_service::do_recv(std::shared_ptr<transport_client> tc)
+{
+	tc->read([this, tc] (auto code, auto json) {
+		switch (static_cast<std::errc>(code.value())) {
+		case std::errc::not_connected:
+			bot_.get_log().info("transport", "") << "client disconnected" << std::endl;
+			break;
+		case std::errc::invalid_argument:
+			tc->error(bot_error::invalid_message);
+			break;
+		default:
+			// Other errors.
+			if (!code) {
+				handle_command(tc, json);
+
+				if (tc->get_state() == transport_client::state::ready)
+					do_recv(std::move(tc));
+			}
+
+			break;
+		}
+	});
+}
+
+void transport_service::do_accept(transport_server& ts)
+{
+	ts.accept([this, &ts] (auto code, auto client) {
+		if (!code) {
+			do_accept(ts);
+			do_recv(std::move(client));
+
+			bot_.get_log().info("transport", "") << "new client connected" << std::endl;
+		}
+	});
+}
+
+transport_service::transport_service(bot& bot) noexcept
+	: bot_(bot)
+{
+}
+
+transport_service::~transport_service() noexcept = default;
+
+auto transport_service::get_commands() const noexcept -> const commands&
+{
+	return commands_;
+}
+
+auto transport_service::get_commands() noexcept -> commands&
+{
+	return commands_;
+}
+
+void transport_service::add(std::shared_ptr<transport_server> ts)
+{
+	assert(ts);
+
+	do_accept(*ts);
+	servers_.push_back(std::move(ts));
+}
+
+void transport_service::broadcast(const nlohmann::json& json)
+{
+	assert(json.is_object());
+
+	for (const auto& servers : servers_)
+		for (const auto& client : servers->get_clients())
+			client->write(json);
+}
+
+void transport_service::load(const config& cfg) noexcept
+{
+	for (const auto& section : cfg) {
+		if (section.get_key() != "transport")
+			continue;
+
+		try {
+			add(transport_util::from_config(bot_.get_service(), section));
+		} catch (const std::exception& ex) {
+			bot_.get_log().warning("transport", "") << ex.what() << std::endl;
+		}
+	}
+}
+
+} // !irccd::daemon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_service.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,123 @@
+/*
+ * transport_service.hpp -- transport service
+ *
+ * Copyright (c) 2013-2018 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_DAEMON_TRANSPORT_SERVICE_HPP
+#define IRCCD_DAEMON_TRANSPORT_SERVICE_HPP
+
+/**
+ * \file transport_service.hpp
+ * \brief Transport service.
+ */
+
+#include <memory>
+#include <vector>
+
+#include <json.hpp>
+
+namespace irccd {
+
+class config;
+
+namespace daemon {
+
+class bot;
+class command;
+class transport_client;
+class transport_server;
+
+/**
+ * \brief Manage transport servers and clients.
+ * \ingroup services
+ * \ingroup transports
+ */
+class transport_service {
+public:
+	/**
+	 * \brief the list of transport commands.
+	 */
+	using commands = std::vector<std::unique_ptr<command>>;
+
+	/**
+	 * \brief The list of transport acceptors.
+	 */
+	using servers = std::vector<std::shared_ptr<transport_server>>;
+
+private:
+	bot& bot_;
+	commands commands_;
+	servers servers_;
+
+	void handle_command(std::shared_ptr<transport_client>, const nlohmann::json&);
+	void do_recv(std::shared_ptr<transport_client>);
+	void do_accept(transport_server&);
+
+public:
+	/**
+	 * Create the transport service.
+	 *
+	 * \param bot the irccd instance
+	 */
+	transport_service(bot& bot) noexcept;
+
+	/**
+	 * Default destructor.
+	 */
+	~transport_service() noexcept;
+
+	/**
+	 * Get underlying commands.
+	 *
+	 * \return the commands
+	 */
+	auto get_commands() const noexcept -> const commands&;
+
+	/**
+	 * Get underlying commands.
+	 *
+	 * \return the commands
+	 */
+	auto get_commands() noexcept -> commands&;
+
+	/**
+	 * Add a transport server.
+	 *
+	 * \param ts the transport server
+	 */
+	void add(std::shared_ptr<transport_server> ts);
+
+	/**
+	 * Send data to all clients.
+	 *
+	 * \pre object.is_object()
+	 * \param object the json object
+	 */
+	void broadcast(const nlohmann::json& object);
+
+	/**
+	 * Load transports from the configuration.
+	 *
+	 * \param cfg the config
+	 */
+	void load(const config& cfg) noexcept;
+};
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_TRANSPORT_SERVICE_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_util.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,151 @@
+/*
+ * transport_util.cpp -- transport utilities
+ *
+ * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
+
+#include <cassert>
+
+#include <irccd/acceptor.hpp>
+#include <irccd/ini_util.hpp>
+#include <irccd/string_util.hpp>
+
+#include "transport_util.hpp"
+#include "transport_server.hpp"
+
+namespace asio = boost::asio;
+
+namespace irccd::daemon::transport_util {
+
+namespace {
+
+auto from_config_load_ip_protocols(const ini::section& sc) -> std::pair<bool, bool>
+{
+	bool ipv4 = true, ipv6 = true;
+
+	if (const auto it = sc.find("ipv4"); it != sc.end())
+		ipv4 = string_util::is_boolean(it->get_value());
+	if (const auto it = sc.find("ipv6"); it != sc.end())
+		ipv6 = string_util::is_boolean(it->get_value());
+
+	if (!ipv4 && !ipv6)
+		throw transport_error(transport_error::invalid_family);
+
+	return { ipv4, ipv6 };
+}
+
+#if defined(IRCCD_HAVE_SSL)
+
+auto from_config_load_ssl(const ini::section& sc) -> boost::asio::ssl::context
+{
+	const auto key = sc.get("key").get_value();
+	const auto cert = sc.get("certificate").get_value();
+
+	if (key.empty())
+		throw transport_error(transport_error::invalid_private_key);
+	if (cert.empty())
+		throw transport_error(transport_error::invalid_certificate);
+
+	boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv12);
+
+	ctx.use_private_key_file(key, boost::asio::ssl::context::pem);
+	ctx.use_certificate_file(cert, boost::asio::ssl::context::pem);
+
+	return ctx;
+}
+
+#endif // !IRCCD_HAVE_SSL
+
+auto from_config_load_ip(asio::io_context& service, const ini::section& sc) -> std::unique_ptr<acceptor>
+{
+	assert(sc.get_key() == "transport");
+
+	const auto port = ini_util::get_uint<std::uint16_t>(sc, "port");
+	const auto address = ini_util::optional_string(sc, "address", "*");
+	const auto [ ipv4, ipv6 ] = from_config_load_ip_protocols(sc);
+
+	if (!port)
+		throw transport_error(transport_error::invalid_port);
+	if (address.empty())
+		throw transport_error(transport_error::invalid_address);
+
+	if (string_util::is_boolean(sc.get("ssl").get_value()))
+#if !defined(IRCCD_HAVE_SSL)
+	throw transport_error(transport_error::ssl_disabled);
+#else
+		return std::make_unique<tls_ip_acceptor>(from_config_load_ssl(sc),
+			service, address, *port, ipv4, ipv6);
+#endif // !IRCCD_HAVE_SSL
+
+	return std::make_unique<ip_acceptor>(service, address, *port, ipv4, ipv6);
+}
+
+auto from_config_load_local(asio::io_context& service, const ini::section& sc) -> std::unique_ptr<acceptor>
+{
+	assert(sc.get_key() == "transport");
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+	const auto path = sc.get("path").get_value();
+
+	if (path.empty())
+		throw transport_error(transport_error::invalid_path);
+
+	if (string_util::is_boolean(sc.get("ssl").get_value()))
+#if !defined(IRCCD_HAVE_SSL)
+	throw transport_error(transport_error::ssl_disabled);
+#else
+		return std::make_unique<tls_local_acceptor>(from_config_load_ssl(sc), service, path);
+#endif // !IRCCD_HAVE_SSL
+
+	return std::make_unique<local_acceptor>(service, path);
+#else
+	(void)service;
+	(void)sc;
+
+	throw transport_error(transport_error::not_supported);
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+}
+
+} // !namespace
+
+auto from_config(asio::io_context& service, const ini::section& sc) -> std::unique_ptr<transport_server>
+{
+	assert(sc.get_key() == "transport");
+
+	const auto type = sc.get("type").get_value();
+	const auto password = sc.get("password").get_value();
+
+	if (type.empty())
+		throw transport_error(transport_error::not_supported);
+
+	std::unique_ptr<acceptor> acceptor;
+
+	if (type == "ip")
+		acceptor = from_config_load_ip(service, sc);
+	else if (type == "unix")
+		acceptor = from_config_load_local(service, sc);
+	else
+		throw transport_error(transport_error::not_supported);
+
+	auto transport = std::make_unique<transport_server>(std::move(acceptor));
+
+	transport->set_password(password);
+
+	return transport;
+}
+
+} // !irccd::daemon::transport_util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd-daemon/irccd/daemon/transport_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,70 @@
+/*
+ * transport_util.hpp -- transport utilities
+ *
+ * Copyright (c) 2013-2018 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.
+ */
+
+/**
+ * \file transport_util.hpp
+ * \brief Transport utilities.
+ */
+
+#ifndef IRCCD_DAEMON_TRANSPORT_UTIL_HPP
+#define IRCCD_DAEMON_TRANSPORT_UTIL_HPP
+
+/*
+ * \file transport_util.hpp
+ * \brief Transport utilities.
+ */
+
+#include <memory>
+
+#include <boost/asio/io_service.hpp>
+
+namespace irccd {
+
+namespace ini {
+
+class section;
+
+} // !ini
+
+namespace daemon {
+
+class transport_server;
+
+/*
+ * \brief Transport utilities.
+ */
+namespace transport_util {
+
+/**
+ * Load a transport from a [transport] configuration section.
+ *
+ * \param service the IO service
+ * \param sc the configuration
+ * \throw transport_error on errors
+ * \return the transport
+ */
+auto from_config(boost::asio::io_context& service,
+                 const ini::section& sc) -> std::unique_ptr<transport_server>;
+
+} // !transport_util
+
+} // !daemon
+
+} // !irccd
+
+#endif // !IRCCD_DAEMON_TRANSPORT_UTIL_HPP
--- a/libirccd-js/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -63,7 +63,7 @@
 	LIBRARIES
 		Boost::timer
 		libduktape
-		libirccd
+		libirccd-daemon
 	PUBLIC_INCLUDES
 		$<BUILD_INTERFACE:${libirccd-js_SOURCE_DIR}>
 
--- a/libirccd-js/irccd/js/directory_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/directory_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -377,7 +377,7 @@
 	return "Irccd.Directory";
 }
 
-void directory_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void directory_js_api::load(daemon::bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/directory_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/directory_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -30,7 +30,7 @@
 
 /**
  * \brief Irccd.Directory Javascript API.
- * \ingroup js_api
+ * \ingroup js-api
  */
 class directory_js_api : public js_api {
 public:
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-js/irccd/js/elapsed_timer_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/elapsed_timer_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,6 +21,8 @@
 #include "elapsed_timer_js_api.hpp"
 #include "js_plugin.hpp"
 
+using irccd::daemon::bot;
+
 namespace irccd::js {
 
 namespace {
@@ -160,7 +162,7 @@
 	return "Irccd.ElapsedTimer";
 }
 
-void elapsed_timer_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void elapsed_timer_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/elapsed_timer_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/elapsed_timer_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.ElapsedTimer Javascript API.
  */
 class elapsed_timer_js_api : public js_api {
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-js/irccd/js/file_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/file_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -30,6 +30,8 @@
 #include "irccd_js_api.hpp"
 #include "js_plugin.hpp"
 
+using irccd::daemon::bot;
+
 namespace irccd::js {
 
 namespace {
@@ -681,7 +683,7 @@
 	return "Irccd.File";
 }
 
-void file_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void file_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/file_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/file_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -140,7 +140,7 @@
 };
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.File Javascript API.
  */
 class file_js_api : public js_api {
@@ -153,7 +153,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 namespace duk {
--- a/libirccd-js/irccd/js/irccd_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/irccd_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -25,6 +25,8 @@
 #include "irccd_js_api.hpp"
 #include "js_plugin.hpp"
 
+using irccd::daemon::bot;
+
 namespace irccd::js {
 
 namespace {
@@ -188,7 +190,7 @@
 	return "Irccd";
 }
 
-void irccd_js_api::load(irccd& irccd, std::shared_ptr<js_plugin> plugin)
+void irccd_js_api::load(bot& bot, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
@@ -226,16 +228,16 @@
 	duk_put_global_string(plugin->get_context(), "Irccd");
 
 	// Store global instance.
-	duk_push_pointer(plugin->get_context(), &irccd);
+	duk_push_pointer(plugin->get_context(), &bot);
 	duk_put_global_string(plugin->get_context(), "\xff""\xff""irccd-ref");
 }
 
-auto duk::type_traits<irccd>::self(duk_context *ctx) -> irccd&
+auto duk::type_traits<bot>::self(duk_context *ctx) -> bot&
 {
 	duk::stack_guard sa(ctx);
 
 	duk_get_global_string(ctx, "\xff""\xff""irccd-ref");
-	const auto ptr = static_cast<irccd*>(duk_to_pointer(ctx, -1));
+	const auto ptr = static_cast<bot*>(duk_to_pointer(ctx, -1));
 	duk_pop(ctx);
 
 	return *ptr;
--- a/libirccd-js/irccd/js/irccd_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/irccd_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -33,10 +33,16 @@
 
 #include "js_api.hpp"
 
+namespace irccd::daemon {
+
+class bot;
+
+} // !irccd
+
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd Javascript API.
  */
 class irccd_js_api : public js_api {
@@ -49,23 +55,23 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 namespace duk {
 
 /**
- * \brief Specialize dukx_type_traits for irccd.
+ * \brief Specialize dukx_type_traits for bot.
  */
 template <>
-struct type_traits<irccd> {
+struct type_traits<daemon::bot> {
 	/**
 	 * Get irccd instance stored in this context.
 	 *
 	 * \param ctx the context
 	 * \return the irccd reference
 	 */
-	static auto self(duk_context* ctx) -> irccd&;
+	static auto self(duk_context* ctx) -> daemon::bot&;
 };
 
 /**
--- a/libirccd-js/irccd/js/js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -24,11 +24,6 @@
  * \brief Javascript API module.
  */
 
-/**
- * \defgroup jsapi Javascript APIs
- * \brief Modules for the Javascript API.
- */
-
 #include <functional>
 #include <memory>
 #include <string_view>
@@ -38,13 +33,18 @@
 
 namespace irccd {
 
-class irccd;
+namespace daemon {
+
+class bot;
+
+} // !daemon
 
 namespace js {
 
 class js_plugin;
 
 /**
+ * \ingroup js-api
  * \brief Javascript API module.
  */
 class js_api {
@@ -79,10 +79,10 @@
 	/**
 	 * Load the module into the Javascript plugin.
 	 *
-	 * \param irccd the irccd instance
+	 * \param bot the irccd instance
 	 * \param plugin the plugin
 	 */
-	virtual void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) = 0;
+	virtual void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) = 0;
 };
 
 } // !js
--- a/libirccd-js/irccd/js/js_plugin.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/js_plugin.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,12 +22,31 @@
 #include <iterator>
 #include <stdexcept>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 
 #include "js_api.hpp"
 #include "js_plugin.hpp"
 #include "server_js_api.hpp"
 
+using irccd::daemon::bot;
+using irccd::daemon::connect_event;
+using irccd::daemon::disconnect_event;
+using irccd::daemon::invite_event;
+using irccd::daemon::join_event;
+using irccd::daemon::kick_event;
+using irccd::daemon::me_event;
+using irccd::daemon::message_event;
+using irccd::daemon::mode_event;
+using irccd::daemon::names_event;
+using irccd::daemon::nick_event;
+using irccd::daemon::notice_event;
+using irccd::daemon::part_event;
+using irccd::daemon::plugin;
+using irccd::daemon::plugin_error;
+using irccd::daemon::topic_event;
+using irccd::daemon::whois_event;
+using irccd::daemon::whois_info;
+
 namespace irccd::js {
 
 namespace {
@@ -222,102 +241,102 @@
 		throw plugin_error(plugin_error::exec_error, get_name(), duk::get_stack(context_, -1).get_stack());
 }
 
-void js_plugin::handle_command(irccd&, const message_event& event)
+void js_plugin::handle_command(bot&, const message_event& event)
 {
 	call("onCommand", event.server, event.origin, event.channel, event.message);
 }
 
-void js_plugin::handle_connect(irccd&, const connect_event& event)
+void js_plugin::handle_connect(bot&, const connect_event& event)
 {
 	call("onConnect", event.server);
 }
 
-void js_plugin::handle_disconnect(irccd&, const disconnect_event& event)
+void js_plugin::handle_disconnect(bot&, const disconnect_event& event)
 {
 	call("onDisconnect", event.server);
 }
 
-void js_plugin::handle_invite(irccd&, const invite_event& event)
+void js_plugin::handle_invite(bot&, const invite_event& event)
 {
 	call("onInvite", event.server, event.origin, event.channel);
 }
 
-void js_plugin::handle_join(irccd&, const join_event& event)
+void js_plugin::handle_join(bot&, const join_event& event)
 {
 	call("onJoin", event.server, event.origin, event.channel);
 }
 
-void js_plugin::handle_kick(irccd&, const kick_event& event)
+void js_plugin::handle_kick(bot&, const kick_event& event)
 {
 	call("onKick", event.server, event.origin, event.channel, event.target, event.reason);
 }
 
-void js_plugin::handle_load(irccd&)
+void js_plugin::handle_load(bot&)
 {
 	call("onLoad");
 }
 
-void js_plugin::handle_message(irccd&, const message_event& event)
+void js_plugin::handle_message(bot&, const message_event& event)
 {
 	call("onMessage", event.server, event.origin, event.channel, event.message);
 }
 
-void js_plugin::handle_me(irccd&, const me_event& event)
+void js_plugin::handle_me(bot&, const me_event& event)
 {
 	call("onMe", event.server, event.origin, event.channel, event.message);
 }
 
-void js_plugin::handle_mode(irccd&, const mode_event& event)
+void js_plugin::handle_mode(bot&, const mode_event& event)
 {
 	call("onMode", event.server, event.origin, event.channel, event.mode,
 		event.limit, event.user, event.mask);
 }
 
-void js_plugin::handle_names(irccd&, const names_event& event)
+void js_plugin::handle_names(bot&, const names_event& event)
 {
 	call("onNames", event.server, event.channel, event.names);
 }
 
-void js_plugin::handle_nick(irccd&, const nick_event& event)
+void js_plugin::handle_nick(bot&, const nick_event& event)
 {
 	call("onNick", event.server, event.origin, event.nickname);
 }
 
-void js_plugin::handle_notice(irccd&, const notice_event& event)
+void js_plugin::handle_notice(bot&, const notice_event& event)
 {
 	call("onNotice", event.server, event.origin, event.channel, event.message);
 }
 
-void js_plugin::handle_part(irccd&, const part_event& event)
+void js_plugin::handle_part(bot&, const part_event& event)
 {
 	call("onPart", event.server, event.origin, event.channel, event.reason);
 }
 
-void js_plugin::handle_reload(irccd&)
+void js_plugin::handle_reload(bot&)
 {
 	call("onReload");
 }
 
-void js_plugin::handle_topic(irccd&, const topic_event& event)
+void js_plugin::handle_topic(bot&, const topic_event& event)
 {
 	call("onTopic", event.server, event.origin, event.channel, event.topic);
 }
 
-void js_plugin::handle_unload(irccd&)
+void js_plugin::handle_unload(bot&)
 {
 	call("onUnload");
 }
 
-void js_plugin::handle_whois(irccd&, const whois_event& event)
+void js_plugin::handle_whois(bot&, const whois_event& event)
 {
 	call("onWhois", event.server, event.whois);
 }
 
-js_plugin_loader::js_plugin_loader(irccd& irccd,
+js_plugin_loader::js_plugin_loader(bot& bot,
                                    std::vector<std::string> directories,
                                    std::vector<std::string> extensions) noexcept
 	: plugin_loader(std::move(directories), std::move(extensions))
-	, irccd_(irccd)
+	, bot_(bot)
 {
 }
 
@@ -338,7 +357,7 @@
 	auto plugin = std::make_shared<js_plugin>(std::string(id), std::string(path));
 
 	for (const auto& mod : modules_)
-		mod->load(irccd_, plugin);
+		mod->load(bot_, plugin);
 
 	plugin->open();
 
--- a/libirccd-js/irccd/js/js_plugin.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/js_plugin.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -37,10 +37,11 @@
 class js_api;
 
 /**
- * \ingroup plugins
+ * \ingroup js
+ * \ingroup daemon-plugins
  * \brief JavaScript plugins for irccd.
  */
-class js_plugin : public plugin {
+class js_plugin : public daemon::plugin {
 public:
 	/**
 	 * Global property where to read/write plugin configuration (object).
@@ -94,156 +95,156 @@
 	void open();
 
 	/**
-	 * \copydoc plugin::get_name
+	 * \copydoc daemon::plugin::get_name
 	 */
 	auto get_name() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_author
+	 * \copydoc daemon::plugin::get_author
 	 */
 	auto get_author() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_license
+	 * \copydoc daemon::plugin::get_license
 	 */
 	auto get_license() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_summary
+	 * \copydoc daemon::plugin::get_summary
 	 */
 	auto get_summary() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_version
+	 * \copydoc daemon::plugin::get_version
 	 */
 	auto get_version() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_options
+	 * \copydoc daemon::plugin::get_options
 	 */
 	auto get_options() const -> map override;
 
 	/**
-	 * \copydoc plugin::set_options
+	 * \copydoc daemon::plugin::set_options
 	 */
 	void set_options(const map& map) override;
 
 	/**
-	 * \copydoc plugin::get_formats
+	 * \copydoc daemon::plugin::get_formats
 	 */
 	auto get_formats() const -> map override;
 
 	/**
-	 * \copydoc plugin::set_formats
+	 * \copydoc daemon::plugin::set_formats
 	 */
 	void set_formats(const map& map) override;
 
 	/**
-	 * \copydoc plugin::get_paths
+	 * \copydoc daemon::plugin::get_paths
 	 */
 	auto get_paths() const -> map override;
 
 	/**
-	 * \copydoc plugin::set_paths
+	 * \copydoc daemon::plugin::set_paths
 	 */
 	void set_paths(const map& map) override;
 
 	/**
-	 * \copydoc plugin::handle_command
+	 * \copydoc daemon::plugin::handle_command
 	 */
-	void handle_command(irccd& irccd, const message_event& event) override;
+	void handle_command(daemon::bot& bot, const daemon::message_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_connect
+	 * \copydoc daemon::plugin::handle_connect
 	 */
-	void handle_connect(irccd& irccd, const connect_event& event) override;
+	void handle_connect(daemon::bot& bot, const daemon::connect_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_disconnect
+	 * \copydoc daemon::plugin::handle_disconnect
 	 */
-	void handle_disconnect(irccd& irccd, const disconnect_event& event) override;
+	void handle_disconnect(daemon::bot& bot, const daemon::disconnect_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_invite
+	 * \copydoc daemon::plugin::handle_invite
 	 */
-	void handle_invite(irccd& irccd, const invite_event& event) override;
+	void handle_invite(daemon::bot& bot, const daemon::invite_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_join
+	 * \copydoc daemon::plugin::handle_join
 	 */
-	void handle_join(irccd& irccd, const join_event& event) override;
+	void handle_join(daemon::bot& bot, const daemon::join_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_kick
+	 * \copydoc daemon::plugin::handle_kick
 	 */
-	void handle_kick(irccd& irccd, const kick_event& event) override;
+	void handle_kick(daemon::bot& bot, const daemon::kick_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_load
+	 * \copydoc daemon::plugin::handle_load
 	 */
-	void handle_load(irccd& irccd) override;
+	void handle_load(daemon::bot& bot) override;
 
 	/**
-	 * \copydoc plugin::handle_message
+	 * \copydoc daemon::plugin::handle_message
 	 */
-	void handle_message(irccd& irccd, const message_event& event) override;
+	void handle_message(daemon::bot& bot, const daemon::message_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_me
+	 * \copydoc daemon::plugin::handle_me
 	 */
-	void handle_me(irccd& irccd, const me_event& event) override;
+	void handle_me(daemon::bot& bot, const daemon::me_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_mode
+	 * \copydoc daemon::plugin::handle_mode
 	 */
-	void handle_mode(irccd& irccd, const mode_event& event) override;
+	void handle_mode(daemon::bot& bot, const daemon::mode_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_names
+	 * \copydoc daemon::plugin::handle_names
 	 */
-	void handle_names(irccd& irccd, const names_event& event) override;
+	void handle_names(daemon::bot& bot, const daemon::names_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_nick
+	 * \copydoc daemon::plugin::handle_nick
 	 */
-	void handle_nick(irccd& irccd, const nick_event& event) override;
+	void handle_nick(daemon::bot& bot, const daemon::nick_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_notice
+	 * \copydoc daemon::plugin::handle_notice
 	 */
-	void handle_notice(irccd& irccd, const notice_event& event) override;
+	void handle_notice(daemon::bot& bot, const daemon::notice_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_part
+	 * \copydoc daemon::plugin::handle_part
 	 */
-	void handle_part(irccd& irccd, const part_event& event) override;
+	void handle_part(daemon::bot& bot, const daemon::part_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_reload
+	 * \copydoc daemon::plugin::handle_reload
 	 */
-	void handle_reload(irccd& irccd) override;
+	void handle_reload(daemon::bot& bot) override;
 
 	/**
-	 * \copydoc plugin::handle_topic
+	 * \copydoc daemon::plugin::handle_topic
 	 */
-	void handle_topic(irccd& irccd, const topic_event& event) override;
+	void handle_topic(daemon::bot& bot, const daemon::topic_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_unload
+	 * \copydoc daemon::plugin::handle_unload
 	 */
-	void handle_unload(irccd& irccd) override;
+	void handle_unload(daemon::bot& bot) override;
 
 	/**
-	 * \copydoc plugin::handle_whois
+	 * \copydoc daemon::plugin::handle_whois
 	 */
-	void handle_whois(irccd& irccd, const whois_event& event) override;
+	void handle_whois(daemon::bot& bot, const daemon::whois_event& event) override;
 };
 
 /**
  * \ingroup plugins
  * \brief Implementation for searching Javascript plugins.
  */
-class js_plugin_loader : public plugin_loader {
+class js_plugin_loader : public daemon::plugin_loader {
 public:
 	/**
 	 * \brief The list of Javascript API modules.
@@ -251,18 +252,18 @@
 	using modules = std::vector<std::unique_ptr<js_api>>;
 
 private:
-	irccd& irccd_;
+	daemon::bot& bot_;
 	modules modules_;
 
 public:
 	/**
 	 * Constructor.
 	 *
-	 * \param irccd the irccd instance
+	 * \param bot the irccd instance
 	 * \param directories directories to search
 	 * \param extensions extensions to search
 	 */
-	js_plugin_loader(irccd& irccd,
+	js_plugin_loader(daemon::bot& bot,
 	                 std::vector<std::string> directories = {},
 	                 std::vector<std::string> extensions = {".js"}) noexcept;
 
@@ -286,9 +287,9 @@
 	auto get_modules() noexcept -> modules&;
 
 	/**
-	 * \copydoc plugin_loader::open
+	 * \copydoc daemon::plugin_loader::open
 	 */
-	auto open(std::string_view id, std::string_view file) -> std::shared_ptr<plugin>;
+	auto open(std::string_view id, std::string_view file) -> std::shared_ptr<daemon::plugin>;
 };
 
 namespace duk {
@@ -297,14 +298,14 @@
  * \brief Specialization for type_traits<whois_info>
  */
 template <>
-struct type_traits<whois_info> : public std::true_type {
+struct type_traits<daemon::whois_info> : public std::true_type {
 	/**
 	 * Push a whois_info.
 	 *
 	 * \param ctx the Duktape context
 	 * \param who the information
 	 */
-	static void push(duk_context* ctx, const whois_info& who);
+	static void push(duk_context* ctx, const daemon::whois_info& who);
 };
 
 } // !duk
--- a/libirccd-js/irccd/js/logger_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/logger_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -16,7 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/logger.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 
@@ -25,6 +25,9 @@
 #include "logger_js_api.hpp"
 #include "plugin_js_api.hpp"
 
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
+
 namespace irccd::js {
 
 namespace {
@@ -36,7 +39,7 @@
 	assert(level <= 2);
 
 	try {
-		auto& sink = duk::type_traits<irccd>::self(ctx).get_log();
+		auto& sink = duk::type_traits<bot>::self(ctx).get_log();
 		auto& self = duk::type_traits<js_plugin>::self(ctx);
 
 		switch (level) {
@@ -133,7 +136,7 @@
 	return "Irccd.Logger";
 }
 
-void logger_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void logger_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/logger_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/logger_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief irccd.Logger Javascript API.
  */
 class logger_js_api : public js_api {
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-js/irccd/js/plugin_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/plugin_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -16,13 +16,17 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 
 #include "irccd_js_api.hpp"
 #include "js_plugin.hpp"
 #include "plugin_js_api.hpp"
 
+using irccd::daemon::bot;
+using irccd::daemon::plugin_error;
+using irccd::daemon::plugin;
+
 namespace irccd::js {
 
 namespace {
@@ -209,7 +213,7 @@
 		plugin* plugin;
 
 		if (duk_get_top(ctx) >= 1)
-			plugin = duk::type_traits<irccd>::self(ctx).plugins().get(duk_require_string(ctx, 0)).get();
+			plugin = duk::type_traits<bot>::self(ctx).plugins().get(duk_require_string(ctx, 0)).get();
 		else
 			plugin = std::addressof(duk::type_traits<js_plugin>::self(ctx));
 
@@ -251,7 +255,7 @@
 
 	duk_push_array(ctx);
 
-	for (const auto& plg : duk::type_traits<irccd>::self(ctx).plugins().list()) {
+	for (const auto& plg : duk::type_traits<bot>::self(ctx).plugins().list()) {
 		duk::push(ctx, plg->get_id());
 		duk_put_prop_index(ctx, -2, i++);
 	}
@@ -279,7 +283,7 @@
 auto Plugin_load(duk_context* ctx) -> duk_idx_t
 {
 	return wrap(ctx, [&] {
-		duk::type_traits<irccd>::self(ctx).plugins().load(
+		duk::type_traits<bot>::self(ctx).plugins().load(
 			duk::require<std::string_view>(ctx, 0), "");
 
 		return 0;
@@ -305,7 +309,7 @@
 auto Plugin_reload(duk_context* ctx) -> duk_idx_t
 {
 	return wrap(ctx, [&] {
-		duk::type_traits<irccd>::self(ctx).plugins().reload(duk::require<std::string>(ctx, 0));
+		duk::type_traits<bot>::self(ctx).plugins().reload(duk::require<std::string>(ctx, 0));
 
 		return 0;
 	});
@@ -330,7 +334,7 @@
 auto Plugin_unload(duk_context* ctx) -> duk_idx_t
 {
 	return wrap(ctx, [&] {
-		duk::type_traits<irccd>::self(ctx).plugins().unload(duk::require<std::string>(ctx, 0));
+		duk::type_traits<bot>::self(ctx).plugins().unload(duk::require<std::string>(ctx, 0));
 
 		return 0;
 	});
@@ -382,7 +386,7 @@
 	return "Irccd.Plugin";
 }
 
-void plugin_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void plugin_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/plugin_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/plugin_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -30,7 +30,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.Plugin Javascript API.
  */
 class plugin_js_api : public js_api {
@@ -43,7 +43,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 namespace duk {
@@ -66,14 +66,14 @@
  * \brief Specialization for plugin_error.
  */
 template <>
-struct type_traits<plugin_error> {
+struct type_traits<daemon::plugin_error> {
 	/**
 	 * Raise a plugin_error.
 	 *
 	 * \param ctx the context
 	 * \param error the error
 	 */
-	static void raise(duk_context* ctx, const plugin_error& error);
+	static void raise(duk_context* ctx, const daemon::plugin_error& error);
 };
 
 } // !duk
--- a/libirccd-js/irccd/js/server_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/server_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -20,7 +20,7 @@
 #include <sstream>
 #include <unordered_map>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server_service.hpp>
 #include <irccd/daemon/server_util.hpp>
 
@@ -28,6 +28,11 @@
 #include "js_plugin.hpp"
 #include "server_js_api.hpp"
 
+using irccd::daemon::bot;
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+using irccd::daemon::server_util::from_json;
+
 namespace irccd::js {
 
 namespace {
@@ -617,7 +622,7 @@
 		duk_check_type(ctx, 0, DUK_TYPE_OBJECT);
 
 		auto json = nlohmann::json::parse(duk_json_encode(ctx, 0));
-		auto s = server_util::from_json(duk::type_traits<irccd>::self(ctx).get_service(), json);
+		auto s = from_json(duk::type_traits<bot>::self(ctx).get_service(), json);
 
 		duk_push_this(ctx);
 		duk_push_pointer(ctx, new std::shared_ptr<server>(std::move(s)));
@@ -666,7 +671,7 @@
 auto Server_add(duk_context* ctx) -> duk_ret_t
 {
 	return wrap(ctx, [] (auto ctx) {
-		duk::type_traits<irccd>::self(ctx).servers().add(
+		duk::type_traits<bot>::self(ctx).servers().add(
 			duk::require<std::shared_ptr<server>>(ctx, 0));
 
 		return 0;
@@ -694,7 +699,7 @@
 {
 	return wrap(ctx, [] (auto ctx) {
 		auto id = duk::require<std::string>(ctx, 0);
-		auto server = duk::type_traits<irccd>::self(ctx).servers().get(id);
+		auto server = duk::type_traits<bot>::self(ctx).servers().get(id);
 
 		if (!server)
 			return 0;
@@ -722,7 +727,7 @@
 {
 	duk_push_object(ctx);
 
-	for (const auto& server : duk::type_traits<irccd>::self(ctx).servers().list()) {
+	for (const auto& server : duk::type_traits<bot>::self(ctx).servers().list()) {
 		duk::push(ctx, server);
 		duk_put_prop_string(ctx, -2, server->get_id().c_str());
 	}
@@ -746,7 +751,7 @@
  */
 auto Server_remove(duk_context* ctx) -> duk_ret_t
 {
-	duk::type_traits<irccd>::self(ctx).servers().remove(duk_require_string(ctx, 0));
+	duk::type_traits<bot>::self(ctx).servers().remove(duk_require_string(ctx, 0));
 
 	return 0;
 }
@@ -816,7 +821,7 @@
 	return "Irccd.Server";
 }
 
-void server_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void server_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/server_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/server_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -31,7 +31,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.Server Javascript API.
  */
 class server_js_api : public js_api {
@@ -44,7 +44,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 namespace duk {
@@ -55,7 +55,7 @@
  * Supports push, require.
  */
 template <>
-struct type_traits<std::shared_ptr<server>> {
+struct type_traits<std::shared_ptr<daemon::server>> {
 	/**
 	 * Push a server.
 	 *
@@ -63,7 +63,7 @@
 	 * \param ctx the context
 	 * \param server the server
 	 */
-	static void push(duk_context* ctx, std::shared_ptr<server> server);
+	static void push(duk_context* ctx, std::shared_ptr<daemon::server> server);
 
 	/**
 	 * Require a server. Raise a Javascript error if not a Server.
@@ -72,21 +72,21 @@
 	 * \param index the index
 	 * \return the server
 	 */
-	static auto require(duk_context* ctx, duk_idx_t index) -> std::shared_ptr<server>;
+	static auto require(duk_context* ctx, duk_idx_t index) -> std::shared_ptr<daemon::server>;
 };
 
 /**
  * \brief Specialization for server_error.
  */
 template <>
-struct type_traits<server_error> {
+struct type_traits<daemon::server_error> {
 	/**
 	 * Raise a server_error.
 	 *
 	 * \param ctx the context
 	 * \param error the error
 	 */
-	static void raise(duk_context* ctx, const server_error& error);
+	static void raise(duk_context* ctx, const daemon::server_error& error);
 };
 
 } // !duk
--- a/libirccd-js/irccd/js/system_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/system_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -33,6 +33,8 @@
 #include "js_plugin.hpp"
 #include "system_js_api.hpp"
 
+using irccd::daemon::bot;
+
 namespace irccd::js {
 
 namespace {
@@ -316,7 +318,7 @@
 	return "Irccd.System";
 }
 
-void system_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void system_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/system_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/system_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.System Javascript API.
  */
 class system_js_api : public js_api {
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-js/irccd/js/timer_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/timer_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -18,7 +18,7 @@
 
 #include <boost/asio.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/logger.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 
@@ -29,6 +29,9 @@
 
 namespace asio = boost::asio;
 
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
+
 namespace irccd::js {
 
 namespace {
@@ -79,7 +82,7 @@
 	duk_remove(plugin_.get_context(), -2);
 
 	if (duk_pcall(plugin_.get_context(), 0)) {
-		auto& log = duk::type_traits<irccd>::self(plugin_.get_context()).get_log();
+		auto& log = duk::type_traits<bot>::self(plugin_.get_context()).get_log();
 
 		log.warning(static_cast<const plugin&>(plugin_)) << "timer error:" << std::endl;
 		log.warning(static_cast<const plugin&>(plugin_)) << "  " << duk::get_stack(plugin_.get_context(), -1).what() << std::endl;
@@ -248,7 +251,7 @@
 			duk_error(ctx, DUK_ERR_TYPE_ERROR, "missing callback function");
 
 		auto& plugin = duk::type_traits<js_plugin>::self(ctx);
-		auto& daemon = duk::type_traits<irccd>::self(ctx);
+		auto& daemon = duk::type_traits<bot>::self(ctx);
 		auto object = new timer(daemon.get_service(), plugin, static_cast<timer::type>(type), delay);
 
 		duk_push_this(ctx);
@@ -293,7 +296,7 @@
 	return "Irccd.Timer";
 }
 
-void timer_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void timer_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/timer_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/timer_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.Timer Javascript API.
  */
 class timer_js_api : public js_api {
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-js/irccd/js/unicode_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/unicode_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -20,6 +20,8 @@
 #include "unicode.hpp"
 #include "unicode_js_api.hpp"
 
+using irccd::daemon::bot;
+
 namespace irccd::js {
 
 namespace {
@@ -149,7 +151,7 @@
 	return "Irccd.Unicode";
 }
 
-void unicode_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void unicode_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/unicode_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/unicode_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.Unicode Javascript API.
  */
 class unicode_js_api : public js_api {
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-js/irccd/js/util_js_api.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/util_js_api.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -24,6 +24,9 @@
 #include "js_plugin.hpp"
 #include "util_js_api.hpp"
 
+using irccd::daemon::bot;
+using irccd::daemon::irc::user;
+
 namespace irccd::js {
 
 namespace {
@@ -292,7 +295,7 @@
 auto Util_splituser(duk_context* ctx) -> duk_ret_t
 {
 	return wrap(ctx, [&] {
-		return duk::push(ctx, irc::user::parse(duk::require<std::string>(ctx, 0)).nick);
+		return duk::push(ctx, user::parse(duk::require<std::string>(ctx, 0)).nick);
 	});
 }
 
@@ -316,7 +319,7 @@
 auto Util_splithost(duk_context* ctx) -> duk_ret_t
 {
 	return wrap(ctx, [&] {
-		return duk::push(ctx, irc::user::parse(duk::require<std::string>(ctx, 0)).host);
+		return duk::push(ctx, user::parse(duk::require<std::string>(ctx, 0)).host);
 	});
 }
 
@@ -337,7 +340,7 @@
 	return "Irccd.Util";
 }
 
-void util_js_api::load(irccd&, std::shared_ptr<js_plugin> plugin)
+void util_js_api::load(bot&, std::shared_ptr<js_plugin> plugin)
 {
 	duk::stack_guard sa(plugin->get_context());
 
--- a/libirccd-js/irccd/js/util_js_api.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-js/irccd/js/util_js_api.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -29,7 +29,7 @@
 namespace irccd::js {
 
 /**
- * \ingroup jsapi
+ * \ingroup js-api
  * \brief Irccd.Util Javascript API.
  */
 class util_js_api : public js_api {
@@ -42,7 +42,7 @@
 	/**
 	 * \copydoc js_api::load
 	 */
-	void load(irccd& irccd, std::shared_ptr<js_plugin> plugin) override;
+	void load(daemon::bot& bot, std::shared_ptr<js_plugin> plugin) override;
 };
 
 } // !irccd::js
--- a/libirccd-test/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -59,7 +59,7 @@
 	SOURCES ${SOURCES}
 	LIBRARIES
 		${LIBRARIES}
-		libirccd
+		libirccd-daemon
 		libirccd-ctl
 	PUBLIC_INCLUDES
 		$<BUILD_INTERFACE:${libirccd-test_SOURCE_DIR}>
--- a/libirccd-test/irccd/test/cli_fixture.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/cli_fixture.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -32,6 +32,10 @@
 
 namespace proc = boost::process;
 
+using irccd::daemon::bot;
+using irccd::daemon::command;
+using irccd::daemon::transport_server;
+
 namespace irccd::test {
 
 namespace {
@@ -48,22 +52,22 @@
 
 cli_fixture::cli_fixture(std::string irccdctl)
 	: irccdctl_(std::move(irccdctl))
-	, server_(new mock_server(irccd_.get_service(), "test", "localhost"))
+	, server_(new mock_server(bot_.get_service(), "test", "localhost"))
 {
 	using boost::asio::ip::tcp;
 
 	tcp::endpoint ep(tcp::v4(), 0U);
-	tcp::acceptor raw_acceptor(irccd_.get_service(), std::move(ep));
+	tcp::acceptor raw_acceptor(bot_.get_service(), std::move(ep));
 
 	port_ = raw_acceptor.local_endpoint().port();
 
-	auto acceptor = std::make_unique<ip_acceptor>(irccd_.get_service(), std::move(raw_acceptor));
+	auto acceptor = std::make_unique<ip_acceptor>(bot_.get_service(), std::move(raw_acceptor));
 
 	for (const auto& f : command::registry)
-		irccd_.transports().get_commands().push_back(f());
+		bot_.transports().get_commands().push_back(f());
 
-	irccd_.servers().add(server_);
-	irccd_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
+	bot_.servers().add(server_);
+	bot_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
 	server_->disconnect();
 	server_->clear();
 }
--- a/libirccd-test/irccd/test/cli_fixture.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/cli_fixture.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -30,7 +30,7 @@
 
 #include <boost/asio.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 #include <irccd/daemon/rule_service.hpp>
 #include <irccd/daemon/server_service.hpp>
@@ -46,17 +46,16 @@
  * called.
  *
  * Before starting the daemon, the test can manually modify irccd instance
- * through `irccd_` member variable. Once started, call `exec` with arguments
+ * through `bot_` member variable. Once started, call `exec` with arguments
  * you want to pass through irccdctl utility.
  */
 class cli_fixture {
 private:
-	using io_service = boost::asio::io_service;
-
 	std::string irccdctl_;
 	std::thread thread_;
 	std::uint16_t port_{0U};
-	io_service service_;
+
+	boost::asio::io_context service_;
 
 protected:
 	/**
@@ -64,7 +63,7 @@
 	 *
 	 * \warning Do not modify once `start()` has been called.
 	 */
-	irccd irccd_{service_};
+	daemon::bot bot_{service_};
 
 	/**
 	 * Server automatically added as "test".
--- a/libirccd-test/irccd/test/command_fixture.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/command_fixture.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -27,6 +27,9 @@
 
 using boost::asio::ip::tcp;
 
+using irccd::daemon::command;
+using irccd::daemon::transport_server;
+
 namespace irccd::test {
 
 template <typename Condition>
@@ -62,19 +65,19 @@
 	, plugin_(new mock_plugin("test"))
 {
 	tcp::endpoint ep(tcp::v4(), 0U);
-	tcp::acceptor raw_acceptor(irccd_.get_service(), std::move(ep));
+	tcp::acceptor raw_acceptor(bot_.get_service(), std::move(ep));
 
 	auto service = std::to_string(raw_acceptor.local_endpoint().port());
-	auto acceptor = std::make_unique<ip_acceptor>(irccd_.get_service(), std::move(raw_acceptor));
-	auto connector = std::make_unique<ip_connector>(irccd_.get_service(), "127.0.0.1", service, true, false);
+	auto acceptor = std::make_unique<ip_acceptor>(bot_.get_service(), std::move(raw_acceptor));
+	auto connector = std::make_unique<ip_connector>(bot_.get_service(), "127.0.0.1", service, true, false);
 
 	// 1. Add all commands.
 	for (const auto& f : command::registry)
-		irccd_.transports().get_commands().push_back(f());
+		bot_.transports().get_commands().push_back(f());
 
 	// 2. Create controller and transport server.
 	ctl_ = std::make_unique<ctl::controller>(std::move(connector));
-	irccd_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
+	bot_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
 
 	// 3. Wait for controller to connect.
 	boost::asio::deadline_timer timer(ctx_);
@@ -103,8 +106,8 @@
 	while (!connected)
 		ctx_.poll();
 
-	irccd_.servers().add(server_);
-	irccd_.plugins().add(plugin_);
+	bot_.servers().add(server_);
+	bot_.plugins().add(plugin_);
 	server_->disconnect();
 	server_->clear();
 	plugin_->clear();
--- a/libirccd-test/irccd/test/debug_server.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/debug_server.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -31,7 +31,7 @@
 /**
  * \brief Server which prints everything in the console.
  */
-class debug_server : public server {
+class debug_server : public daemon::server {
 public:
 	/**
 	 * Inherited constructors.
@@ -39,42 +39,42 @@
 	using server::server;
 
 	/**
-	 * \copydoc server::connect
+	 * \copydoc daemon::server::connect
 	 */
 	void connect(connect_handler handler) noexcept override;
 
 	/**
-	 * \copydoc server::disconnect
+	 * \copydoc daemon::server::disconnect
 	 */
 	void disconnect() noexcept override;
 
 	/**
-	 * \copydoc server::invite
+	 * \copydoc daemon::server::invite
 	 */
 	void invite(std::string_view target, std::string_view channel) override;
 
 	/**
-	 * \copydoc server::join
+	 * \copydoc daemon::server::join
 	 */
 	void join(std::string_view channel, std::string_view password = "") override;
 
 	/**
-	 * \copydoc server::kick
+	 * \copydoc daemon::server::kick
 	 */
 	void kick(std::string_view target, std::string_view channel, std::string_view reason = "") override;
 
 	/**
-	 * \copydoc server::me
+	 * \copydoc daemon::server::me
 	 */
 	void me(std::string_view target, std::string_view message) override;
 
 	/**
-	 * \copydoc server::message
+	 * \copydoc daemon::server::message
 	 */
 	void message(std::string_view target, std::string_view message) override;
 
 	/**
-	 * \copydoc server::mode
+	 * \copydoc daemon::server::mode
 	 */
 	void mode(std::string_view channel,
 	          std::string_view mode,
@@ -83,32 +83,32 @@
 	          std::string_view mask = "") override;
 
 	/**
-	 * \copydoc server::names
+	 * \copydoc daemon::server::names
 	 */
 	void names(std::string_view channel) override;
 
 	/**
-	 * \copydoc server::notice
+	 * \copydoc daemon::server::notice
 	 */
 	void notice(std::string_view target, std::string_view message) override;
 
 	/**
-	 * \copydoc server::part
+	 * \copydoc daemon::server::part
 	 */
 	void part(std::string_view channel, std::string_view reason = "") override;
 
 	/**
-	 * \copydoc server::send
+	 * \copydoc daemon::server::send
 	 */
 	void send(std::string_view raw) override;
 
 	/**
-	 * \copydoc server::topic
+	 * \copydoc daemon::server::topic
 	 */
 	void topic(std::string_view channel, std::string_view topic) override;
 
 	/**
-	 * \copydoc server::whois
+	 * \copydoc daemon::server::whois
 	 */
 	void whois(std::string_view target) override;
 };
--- a/libirccd-test/irccd/test/irccd_fixture.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/irccd_fixture.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -24,7 +24,7 @@
 
 irccd_fixture::irccd_fixture()
 {
-	irccd_.set_log(std::make_unique<logger::silent_sink>());
+	bot_.set_log(std::make_unique<daemon::logger::silent_sink>());
 }
 
 } // !irccd
--- a/libirccd-test/irccd/test/irccd_fixture.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/irccd_fixture.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -24,7 +24,7 @@
  * \brief Test fixture for irccd.
  */
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 
 namespace irccd::test {
 
@@ -41,7 +41,7 @@
 	/**
 	 * \brief Main irccd daemon.
 	 */
-	irccd irccd_{ctx_};
+	daemon::bot bot_{ctx_};
 
 	/**
 	 * Default constructor.
--- a/libirccd-test/irccd/test/js_fixture.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/js_fixture.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -24,7 +24,7 @@
 	: plugin_(new js::js_plugin("test", path))
 {
 	for (const auto& f : js::js_api::registry)
-		f()->load(irccd_, plugin_);
+		f()->load(bot_, plugin_);
 
 	if (!path.empty())
 		plugin_->open();
--- a/libirccd-test/irccd/test/js_plugin_fixture.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/js_plugin_fixture.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -37,24 +37,28 @@
 
 #include "js_plugin_fixture.hpp"
 
+using irccd::js::js_plugin;
+
+using irccd::daemon::logger::silent_sink;
+
 namespace irccd::test {
 
 js_plugin_fixture::js_plugin_fixture(std::string path)
 	: server_(std::make_shared<mock_server>(service_, "test", "local"))
 {
-	plugin_ = std::make_unique<js::js_plugin>("test", std::move(path));
+	plugin_ = std::make_unique<js_plugin>("test", std::move(path));
 
-	irccd_.set_log(std::make_unique<logger::silent_sink>());
-	irccd_.get_log().set_verbose(false);
-	irccd_.plugins().add(plugin_);
-	irccd_.servers().add(server_);
+	bot_.set_log(std::make_unique<silent_sink>());
+	bot_.get_log().set_verbose(false);
+	bot_.plugins().add(plugin_);
+	bot_.servers().add(server_);
 
 	server_->disconnect();
 	server_->set_nickname("irccd");
 	server_->clear();
 
 	for (const auto& f : js::js_api::registry)
-		f()->load(irccd_, plugin_);
+		f()->load(bot_, plugin_);
 
 	plugin_->open();
 }
--- a/libirccd-test/irccd/test/js_plugin_fixture.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/js_plugin_fixture.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -24,7 +24,7 @@
  * \brief test fixture helper for Javascript plugins.
  */
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 
 #include <irccd/js/js_plugin.hpp>
 
@@ -40,7 +40,7 @@
 class js_plugin_fixture {
 protected:
 	boost::asio::io_service service_;               //!< The I/O service.
-	irccd irccd_{service_};                         //!< The irccd instance.
+	daemon::bot bot_{service_};                     //!< The irccd instance.
 	std::shared_ptr<js::js_plugin> plugin_;         //!< The plugin to test.
 	std::shared_ptr<mock_server> server_;           //!< A mock server.
 
--- a/libirccd-test/irccd/test/mock_plugin.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/mock_plugin.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -20,6 +20,23 @@
 
 #include "mock_plugin.hpp"
 
+using irccd::daemon::connect_event;
+using irccd::daemon::disconnect_event;
+using irccd::daemon::invite_event;
+using irccd::daemon::bot;
+using irccd::daemon::join_event;
+using irccd::daemon::kick_event;
+using irccd::daemon::me_event;
+using irccd::daemon::message_event;
+using irccd::daemon::mode_event;
+using irccd::daemon::names_event;
+using irccd::daemon::nick_event;
+using irccd::daemon::notice_event;
+using irccd::daemon::part_event;
+using irccd::daemon::plugin;
+using irccd::daemon::topic_event;
+using irccd::daemon::whois_event;
+
 namespace irccd::test {
 
 auto mock_plugin::get_name() const noexcept -> std::string_view
@@ -99,92 +116,92 @@
 	paths_ = map;
 }
 
-void mock_plugin::handle_command(irccd&, const message_event& event)
+void mock_plugin::handle_command(bot&, const message_event& event)
 {
 	push("handle_command", { event });
 }
 
-void mock_plugin::handle_connect(irccd&, const connect_event& event)
+void mock_plugin::handle_connect(bot&, const connect_event& event)
 {
 	push("handle_connect", { event });
 }
 
-void mock_plugin::handle_disconnect(irccd&, const disconnect_event& event)
+void mock_plugin::handle_disconnect(bot&, const disconnect_event& event)
 {
 	push("handle_disconnect", { event });
 }
 
-void mock_plugin::handle_invite(irccd&, const invite_event& event)
+void mock_plugin::handle_invite(bot&, const invite_event& event)
 {
 	push("handle_invite", { event });
 }
 
-void mock_plugin::handle_join(irccd&, const join_event& event)
+void mock_plugin::handle_join(bot&, const join_event& event)
 {
 	push("handle_join", { event });
 }
 
-void mock_plugin::handle_kick(irccd&, const kick_event& event)
+void mock_plugin::handle_kick(bot&, const kick_event& event)
 {
 	push("handle_kick", { event });
 }
 
-void mock_plugin::handle_load(irccd&)
+void mock_plugin::handle_load(bot&)
 {
 	push("handle_load");
 }
 
-void mock_plugin::handle_message(irccd&, const message_event& event)
+void mock_plugin::handle_message(bot&, const message_event& event)
 {
 	push("handle_message", { event });
 }
 
-void mock_plugin::handle_me(irccd&, const me_event& event)
+void mock_plugin::handle_me(bot&, const me_event& event)
 {
 	push("handle_me", { event });
 }
 
-void mock_plugin::handle_mode(irccd&, const mode_event& event)
+void mock_plugin::handle_mode(bot&, const mode_event& event)
 {
 	push("handle_mode", { event });
 }
 
-void mock_plugin::handle_names(irccd&, const names_event& event)
+void mock_plugin::handle_names(bot&, const names_event& event)
 {
 	push("handle_names", { event });
 }
 
-void mock_plugin::handle_nick(irccd&, const nick_event& event)
+void mock_plugin::handle_nick(bot&, const nick_event& event)
 {
 	push("handle_nick", { event });
 }
 
-void mock_plugin::handle_notice(irccd&, const notice_event& event)
+void mock_plugin::handle_notice(bot&, const notice_event& event)
 {
 	push("handle_notice", { event });
 }
 
-void mock_plugin::handle_part(irccd&, const part_event& event)
+void mock_plugin::handle_part(bot&, const part_event& event)
 {
 	push("handle_part", { event });
 }
 
-void mock_plugin::handle_reload(irccd&)
+void mock_plugin::handle_reload(bot&)
 {
 	push("handle_reload");
 }
 
-void mock_plugin::handle_topic(irccd&, const topic_event& event)
+void mock_plugin::handle_topic(bot&, const topic_event& event)
 {
 	push("handle_topic", { event });
 }
 
-void mock_plugin::handle_unload(irccd&)
+void mock_plugin::handle_unload(bot&)
 {
 	push("handle_unload");
 }
 
-void mock_plugin::handle_whois(irccd&, const whois_event& event)
+void mock_plugin::handle_whois(bot&, const whois_event& event)
 {
 	push("handle_whois", { event });
 }
--- a/libirccd-test/irccd/test/mock_plugin.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/mock_plugin.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -33,7 +33,7 @@
 /**
  * \brief Mock plugin.
  */
-class mock_plugin : public plugin, public mock {
+class mock_plugin : public daemon::plugin, public mock {
 private:
 	map options_;
 	map formats_;
@@ -43,149 +43,149 @@
 	using plugin::plugin;
 
 	/**
-	 * \copydoc plugin::get_name
+	 * \copydoc daemon::plugin::get_name
 	 */
 	auto get_name() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_author
+	 * \copydoc daemon::plugin::get_author
 	 */
 	auto get_author() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_license
+	 * \copydoc daemon::plugin::get_license
 	 */
 	auto get_license() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_summary
+	 * \copydoc daemon::plugin::get_summary
 	 */
 	auto get_summary() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_version
+	 * \copydoc daemon::plugin::get_version
 	 */
 	auto get_version() const noexcept -> std::string_view override;
 
 	/**
-	 * \copydoc plugin::get_options
+	 * \copydoc daemon::plugin::get_options
 	 */
 	auto get_options() const -> map override;
 
 	/**
-	 * \copydoc plugin::set_options
+	 * \copydoc daemon::plugin::set_options
 	 */
 	void set_options(const map& map) override;
 
 	/**
-	 * \copydoc plugin::get_formats
+	 * \copydoc daemon::plugin::get_formats
 	 */
 	auto get_formats() const -> map override;
 
 	/**
-	 * \copydoc plugin::set_formats
+	 * \copydoc daemon::plugin::set_formats
 	 */
 	void set_formats(const map& map) override;
 
 	/**
-	 * \copydoc plugin::get_paths
+	 * \copydoc daemon::plugin::get_paths
 	 */
 	auto get_paths() const -> map override;
 
 	/**
-	 * \copydoc plugin::set_paths
+	 * \copydoc daemon::plugin::set_paths
 	 */
 	void set_paths(const map& map) override;
 
 	/**
-	 * \copydoc plugin::handle_command
+	 * \copydoc daemon::plugin::handle_command
 	 */
-	void handle_command(irccd& irccd, const message_event& event) override;
+	void handle_command(daemon::bot& bot, const daemon::message_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_connect
+	 * \copydoc daemon::plugin::handle_connect
 	 */
-	void handle_connect(irccd& irccd, const connect_event& event) override;
+	void handle_connect(daemon::bot& bot, const daemon::connect_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_disconnect
+	 * \copydoc daemon::plugin::handle_disconnect
 	 */
-	void handle_disconnect(irccd& irccd, const disconnect_event& event) override;
+	void handle_disconnect(daemon::bot& bot, const daemon::disconnect_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_invite
+	 * \copydoc daemon::plugin::handle_invite
 	 */
-	void handle_invite(irccd& irccd, const invite_event& event) override;
+	void handle_invite(daemon::bot& bot, const daemon::invite_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_join
+	 * \copydoc daemon::plugin::handle_join
 	 */
-	void handle_join(irccd& irccd, const join_event& event) override;
+	void handle_join(daemon::bot& bot, const daemon::join_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_kick
+	 * \copydoc daemon::plugin::handle_kick
 	 */
-	void handle_kick(irccd& irccd, const kick_event& event) override;
+	void handle_kick(daemon::bot& bot, const daemon::kick_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_load
+	 * \copydoc daemon::plugin::handle_load
 	 */
-	void handle_load(irccd& irccd) override;
+	void handle_load(daemon::bot& bot) override;
 
 	/**
-	 * \copydoc plugin::handle_message
+	 * \copydoc daemon::plugin::handle_message
 	 */
-	void handle_message(irccd& irccd, const message_event& event) override;
+	void handle_message(daemon::bot& bot, const daemon::message_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_me
+	 * \copydoc daemon::plugin::handle_me
 	 */
-	void handle_me(irccd& irccd, const me_event& event) override;
+	void handle_me(daemon::bot& bot, const daemon::me_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_mode
+	 * \copydoc daemon::plugin::handle_mode
 	 */
-	void handle_mode(irccd& irccd, const mode_event& event) override;
+	void handle_mode(daemon::bot& bot, const daemon::mode_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_names
+	 * \copydoc daemon::plugin::handle_names
 	 */
-	void handle_names(irccd& irccd, const names_event& event) override;
+	void handle_names(daemon::bot& bot, const daemon::names_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_nick
+	 * \copydoc daemon::plugin::handle_nick
 	 */
-	void handle_nick(irccd& irccd, const nick_event& event) override;
+	void handle_nick(daemon::bot& bot, const daemon::nick_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_notice
+	 * \copydoc daemon::plugin::handle_notice
 	 */
-	void handle_notice(irccd& irccd, const notice_event& event) override;
+	void handle_notice(daemon::bot& bot, const daemon::notice_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_part
+	 * \copydoc daemon::plugin::handle_part
 	 */
-	void handle_part(irccd& irccd, const part_event& event) override;
+	void handle_part(daemon::bot& bot, const daemon::part_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_reload
+	 * \copydoc daemon::plugin::handle_reload
 	 */
-	void handle_reload(irccd& irccd) override;
+	void handle_reload(daemon::bot& bot) override;
 
 	/**
-	 * \copydoc plugin::handle_topic
+	 * \copydoc daemon::plugin::handle_topic
 	 */
-	void handle_topic(irccd& irccd, const topic_event& event) override;
+	void handle_topic(daemon::bot& bot, const daemon::topic_event& event) override;
 
 	/**
-	 * \copydoc plugin::handle_unload
+	 * \copydoc daemon::plugin::handle_unload
 	 */
-	void handle_unload(irccd& irccd) override;
+	void handle_unload(daemon::bot& bot) override;
 
 	/**
-	 * \copydoc plugin::handle_whois
+	 * \copydoc daemon::plugin::handle_whois
 	 */
-	void handle_whois(irccd& irccd, const whois_event& event) override;
+	void handle_whois(daemon::bot& bot, const daemon::whois_event& event) override;
 };
 
 } // !irccd::test
--- a/libirccd-test/irccd/test/mock_server.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd-test/irccd/test/mock_server.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -33,7 +33,7 @@
 /**
  * \brief Mock server.
  */
-class mock_server : public server, public mock {
+class mock_server : public daemon::server, public mock {
 public:
 	/**
 	 * Inherited constructors.
@@ -41,42 +41,42 @@
 	using server::server;
 
 	/**
-	 * \copydoc server::connect
+	 * \copydoc daemon::server::connect
 	 */
 	void connect(connect_handler handler) noexcept override;
 
 	/**
-	 * \copydoc server::disconnect
+	 * \copydoc daemon::server::disconnect
 	 */
 	void disconnect() noexcept override;
 
 	/**
-	 * \copydoc server::invite
+	 * \copydoc daemon::server::invite
 	 */
 	void invite(std::string_view target, std::string_view channel) override;
 
 	/**
-	 * \copydoc server::join
+	 * \copydoc daemon::server::join
 	 */
 	void join(std::string_view channel, std::string_view password = "") override;
 
 	/**
-	 * \copydoc server::kick
+	 * \copydoc daemon::server::kick
 	 */
 	void kick(std::string_view target, std::string_view channel, std::string_view reason = "") override;
 
 	/**
-	 * \copydoc server::me
+	 * \copydoc daemon::server::me
 	 */
 	void me(std::string_view target, std::string_view message) override;
 
 	/**
-	 * \copydoc server::message
+	 * \copydoc daemon::server::message
 	 */
 	void message(std::string_view target, std::string_view message) override;
 
 	/**
-	 * \copydoc server::mode
+	 * \copydoc daemon::server::mode
 	 */
 	void mode(std::string_view channel,
                   std::string_view mode,
@@ -85,32 +85,32 @@
                   std::string_view mask = "") override;
 
 	/**
-	 * \copydoc server::names
+	 * \copydoc daemon::server::names
 	 */
 	void names(std::string_view channel) override;
 
 	/**
-	 * \copydoc server::notice
+	 * \copydoc daemon::server::notice
 	 */
 	void notice(std::string_view target, std::string_view message) override;
 
 	/**
-	 * \copydoc server::part
+	 * \copydoc daemon::server::part
 	 */
 	void part(std::string_view channel, std::string_view reason = "") override;
 
 	/**
-	 * \copydoc server::send
+	 * \copydoc daemon::server::send
 	 */
 	void send(std::string_view raw) override;
 
 	/**
-	 * \copydoc server::topic
+	 * \copydoc daemon::server::topic
 	 */
 	void topic(std::string_view channel, std::string_view topic) override;
 
 	/**
-	 * \copydoc server::whois
+	 * \copydoc daemon::server::whois
 	 */
 	void whois(std::string_view target) override;
 };
--- a/libirccd/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/libirccd/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -18,49 +18,65 @@
 
 project(libirccd)
 
+find_package(Boost 1.60 REQUIRED QUIET COMPONENTS filesystem system)
+
 set(
 	SOURCES
-	${libirccd_SOURCE_DIR}/irccd/daemon/command.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/command.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/dynlib_plugin.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/dynlib_plugin.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/irc.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/irc.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/irccd.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/irccd.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/logger.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/logger.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/plugin.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/plugin.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/plugin_service.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/plugin_service.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/rule.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/rule.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/rule_service.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/rule_service.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/rule_util.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/rule_util.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/server.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/server.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/server_service.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/server_service.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/server_util.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/server_util.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_client.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_client.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_server.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_server.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_service.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_service.hpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_util.cpp
-	${libirccd_SOURCE_DIR}/irccd/daemon/transport_util.hpp
+	${libirccd_SOURCE_DIR}/irccd/acceptor.hpp
+	${libirccd_SOURCE_DIR}/irccd/config.cpp
+	${libirccd_SOURCE_DIR}/irccd/config.hpp
+	${libirccd_SOURCE_DIR}/irccd/connector.hpp
+	${libirccd_SOURCE_DIR}/irccd/fs_util.cpp
+	${libirccd_SOURCE_DIR}/irccd/fs_util.hpp
+	${libirccd_SOURCE_DIR}/irccd/ini.cpp
+	${libirccd_SOURCE_DIR}/irccd/ini.hpp
+	${libirccd_SOURCE_DIR}/irccd/ini_util.hpp
+	${libirccd_SOURCE_DIR}/irccd/json_util.cpp
+	${libirccd_SOURCE_DIR}/irccd/json_util.hpp
+	${libirccd_SOURCE_DIR}/irccd/options.cpp
+	${libirccd_SOURCE_DIR}/irccd/options.hpp
+	${libirccd_SOURCE_DIR}/irccd/stream.hpp
+	${libirccd_SOURCE_DIR}/irccd/string_util.cpp
+	${libirccd_SOURCE_DIR}/irccd/string_util.hpp
+	${libirccd_SOURCE_DIR}/irccd/system.cpp
+	${libirccd_SOURCE_DIR}/irccd/system.hpp
+	${libirccd_SOURCE_DIR}/irccd/xdg.hpp
 )
 
+set(
+	LIBRARIES
+	${CMAKE_DL_LIBS}
+	libjson
+	Threads::Threads
+	Boost::filesystem
+	Boost::system
+)
+
+if (CMAKE_SYSTEM_NAME MATCHES Windows)
+	list(APPEND LIBRARIES mswsock shlwapi ws2_32)
+elseif (CMAKE_SYSTEM_NAME MATCHES "Linux")
+	#
+	# Disable epoll in boost until it get fixed:
+	# https://github.com/boostorg/asio/issues/150
+	#
+	list(APPEND FLAGS "BOOST_ASIO_DISABLE_EPOLL")
+elseif (APPLE)
+	list(APPEND LIBRARIES resolv)
+endif ()
+
+if (IRCCD_HAVE_SSL)
+	list(APPEND LIBRARIES OpenSSL::Crypto OpenSSL::SSL)
+endif ()
+
 irccd_define_library(
 	TARGET libirccd
 	EXPORT
-	HEADERS ${libirccd_SOURCE_DIR}/irccd/daemon
+	FLAGS ${FLAGS}
+	HEADERS ${libirccd_SOURCE_DIR}/irccd/
 	SOURCES ${SOURCES}
-	LIBRARIES libirccd-core
-	PUBLIC_INCLUDES $<BUILD_INTERFACE:${libirccd_SOURCE_DIR}>
+	LIBRARIES ${LIBRARIES}
+	PUBLIC_INCLUDES
+		$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}>
+		$<BUILD_INTERFACE:${libirccd_SOURCE_DIR}>
+		$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
 )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/acceptor.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,516 @@
+/*
+ * acceptor.hpp -- abstract stream acceptor interface
+ *
+ * Copyright (c) 2013-2018 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_ACCEPTOR_HPP
+#define IRCCD_ACCEPTOR_HPP
+
+/**
+ * \file acceptor.hpp
+ * \brief Abstract stream acceptor interface.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <cassert>
+#include <functional>
+#include <memory>
+#include <system_error>
+
+#include <boost/asio.hpp>
+#include <boost/filesystem/path.hpp>
+
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
+
+#include "stream.hpp"
+
+namespace irccd {
+
+/**
+ * \brief Abstract stream acceptor interface.
+ * \ingroup core-acceptors
+ *
+ * This class is used to wait a new client in an asynchronous manner. Derived
+ * classes must implement a non-blocking accept function.
+ */
+class acceptor {
+public:
+	/**
+	 * \brief Accept completion handler.
+	 */
+	using handler = std::function<void (std::error_code, std::shared_ptr<stream>)>;
+
+public:
+	/**
+	 * Default constructor.
+	 */
+	acceptor() = default;
+
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~acceptor() = default;
+
+	/**
+	 * Start asynchronous accept.
+	 *
+	 * Once the client is accepted, the original acceptor must be kept until it
+	 * is destroyed.
+	 *
+	 * \pre another accept operation must not be running
+	 * \pre handler != nullptr
+	 * \param handler the handler
+	 */
+	virtual void accept(handler handler) = 0;
+};
+
+// {{{ basic_socket_acceptor
+
+/**
+ * \brief Convenient acceptor owner.
+ * \ingroup core-acceptors
+ */
+template <typename Acceptor>
+class basic_socket_acceptor : public acceptor {
+public:
+	/**
+	 * Underlying socket type.
+	 */
+	using socket_type = typename Acceptor::protocol_type::socket;
+
+private:
+#if !defined(NDEBUG)
+	bool is_accepting_{false};
+#endif
+
+protected:
+	/**
+	 * \brief The I/O context.
+	 */
+	boost::asio::io_context& service_;
+
+	/**
+	 * \brief The underlying acceptor.
+	 */
+	Acceptor acceptor_;
+
+public:
+	/**
+	 * Construct a basic_socket_acceptor.
+	 *
+	 * \param service the I/O context
+	 */
+	basic_socket_acceptor(boost::asio::io_context& service);
+
+	/**
+	 * Construct a basic_socket_acceptor with a already bound native
+	 * acceptor.
+	 *
+	 * \param service the I/O context
+	 * \param acceptor the acceptor
+	 */
+	basic_socket_acceptor(boost::asio::io_context& service, Acceptor acceptor) noexcept;
+
+	/**
+	 * Get the I/O context.
+	 *
+	 * \return the context
+	 */
+	auto get_service() const noexcept -> const boost::asio::io_context&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the context
+	 */
+	auto get_service() noexcept -> boost::asio::io_context&;
+
+	/**
+	 * Get the underlying acceptor.
+	 *
+	 * \return the acceptor
+	 */
+	auto get_acceptor() const noexcept -> const Acceptor&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the acceptor
+	 */
+	auto get_acceptor() noexcept -> Acceptor&;
+
+	/**
+	 * Accept a new client.
+	 *
+	 * \pre another accept call must not be running
+	 * \param sc the socket type
+	 * \param handler the handler
+	 * \note implemented for SocketAcceptor concept
+	 */
+	template <typename Socket, typename Handler>
+	void accept(Socket& sc, Handler handler);
+};
+
+template <typename Acceptor>
+inline basic_socket_acceptor<Acceptor>::basic_socket_acceptor(boost::asio::io_context& service)
+	: service_(service)
+	, acceptor_(service)
+{
+}
+
+template <typename Acceptor>
+inline basic_socket_acceptor<Acceptor>::basic_socket_acceptor(boost::asio::io_context& service, Acceptor acceptor) noexcept
+	: service_(service)
+	, acceptor_(std::move(acceptor))
+{
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_service() const noexcept -> const boost::asio::io_context&
+{
+	return service_;
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_service() noexcept -> boost::asio::io_context&
+{
+	return service_;
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_acceptor() const noexcept -> const Acceptor&
+{
+	return acceptor_;
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_acceptor() noexcept -> Acceptor&
+{
+	return acceptor_;
+}
+
+template <typename Acceptor>
+template <typename Socket, typename Handler>
+inline void basic_socket_acceptor<Acceptor>::accept(Socket& sc, Handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_accepting_);
+
+	is_accepting_ = true;
+#endif
+
+	assert(acceptor_.is_open());
+
+	acceptor_.async_accept(sc, [this, handler] (auto code) {
+#if !defined(NDEBUG)
+		is_accepting_ = false;
+#endif
+		(void)this;
+		handler(std::move(code));
+	});
+}
+
+// }}}
+
+// {{{ ip_acceptor
+
+/**
+ * \brief TCP/IP acceptor.
+ * \ingroup core-acceptors
+ */
+class ip_acceptor : public basic_socket_acceptor<boost::asio::ip::tcp::acceptor> {
+private:
+	void open(bool ipv4, bool ipv6);
+	void set(bool ipv4, bool ipv6);
+	void bind(const std::string& address, std::uint16_t port, bool ipv4, bool ipv6);
+
+public:
+	/**
+	 * Construct a TCP/IP acceptor.
+	 *
+	 * If both ipv4 and ipv6 are set, the acceptor will listen on the two
+	 * protocols.
+	 *
+	 * To listen to any address, you can use "*" as address argument.
+	 *
+	 * \pre at least ipv4 or ipv6 must be true
+	 * \param service the I/O service
+	 * \param address the address to bind or * for any
+	 * \param port the port number
+	 * \param ipv4 enable ipv4
+	 * \param ipv6 enable ipv6
+	 */
+	ip_acceptor(boost::asio::io_context& service,
+	            std::string address,
+	            std::uint16_t port,
+	            bool ipv4 = true,
+	            bool ipv6 = true);
+
+	/**
+	 * Inherited constructors.
+	 */
+	using basic_socket_acceptor::basic_socket_acceptor;
+
+	/**
+	 * Inherited functions.
+	 */
+	using basic_socket_acceptor::accept;
+
+	/**
+	 * \copydoc acceptor::accept
+	 */
+	void accept(handler handler) override;
+};
+
+inline void ip_acceptor::open(bool ipv4, bool ipv6)
+{
+	using boost::asio::ip::tcp;
+
+	if (ipv6)
+		acceptor_.open(tcp::v6());
+	else
+		acceptor_.open(tcp::v4());
+}
+
+inline void ip_acceptor::set(bool ipv4, bool ipv6)
+{
+	using boost::asio::socket_base;
+	using boost::asio::ip::v6_only;
+
+	if (ipv6)
+		acceptor_.set_option(v6_only(!ipv4));
+
+	acceptor_.set_option(socket_base::reuse_address(true));
+}
+
+inline void ip_acceptor::bind(const std::string& address, std::uint16_t port, bool ipv4, bool ipv6)
+{
+	using boost::asio::ip::make_address_v4;
+	using boost::asio::ip::make_address_v6;
+	using boost::asio::ip::tcp;
+
+	tcp::endpoint ep;
+
+	if (address == "*")
+		ep = tcp::endpoint(ipv6 ? tcp::v6() : tcp::v4(), port);
+	else if (ipv6)
+		ep = tcp::endpoint(make_address_v6(address), port);
+	else
+		ep = tcp::endpoint(make_address_v4(address), port);
+
+	acceptor_.bind(ep);
+	acceptor_.listen();
+}
+
+inline ip_acceptor::ip_acceptor(boost::asio::io_context& service,
+                                std::string address,
+                                std::uint16_t port,
+                                bool ipv4,
+                                bool ipv6)
+	: basic_socket_acceptor(service)
+{
+	open(ipv4, ipv6);
+	set(ipv4, ipv6);
+	bind(address, port, ipv4, ipv6);
+}
+
+inline void ip_acceptor::accept(handler handler)
+{
+	auto stream = std::make_shared<ip_stream>(service_);
+
+	basic_socket_acceptor::accept(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+// }}}
+
+// {{{ local_acceptor
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Local acceptor.
+ * \ingroup core-acceptors
+ * \note Only available if BOOST_ASIO_HAS_LOCAL_SOCKETS is defined
+ */
+class local_acceptor : public basic_socket_acceptor<boost::asio::local::stream_protocol::acceptor> {
+public:
+	/**
+	 * Construct a local acceptor.
+	 *
+	 * \param service the I/O service
+	 * \param path the unix socket file
+	 */
+	local_acceptor(boost::asio::io_context& service,
+	               const boost::filesystem::path& path);
+
+	/**
+	 * Inherited constructors.
+	 */
+	using basic_socket_acceptor::basic_socket_acceptor;
+
+	/**
+	 * Inherited functions.
+	 */
+	using basic_socket_acceptor::accept;
+
+	/**
+	 * \copydoc acceptor::accept
+	 */
+	void accept(handler handler) override;
+};
+
+inline local_acceptor::local_acceptor(boost::asio::io_context& service,
+                                      const boost::filesystem::path& path)
+	: basic_socket_acceptor(service)
+{
+	using boost::asio::socket_base;
+
+	std::remove(path.string().c_str());
+
+	acceptor_.open();
+	acceptor_.set_option(socket_base::reuse_address(true));
+	acceptor_.bind({ path.string() });
+	acceptor_.listen();
+}
+
+inline void local_acceptor::accept(handler handler)
+{
+	auto stream = std::make_shared<local_stream>(service_);
+
+	basic_socket_acceptor::accept(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+#endif
+
+// }}}
+
+// {{{ tls_acceptor
+
+#if defined(IRCCD_HAVE_SSL)
+
+/**
+ * \brief TLS/SSL acceptors.
+ * \ingroup core-acceptors
+ * \tparam SocketAcceptor the socket connector (e.g. ip_acceptor)
+ *
+ * Wrap a SocketAcceptor object.
+ *
+ * The SocketAcceptor object must have the following types:
+ *
+ * ```cpp
+ * using socket_type = implementation-defined
+ * ```
+ *
+ * The following function:
+ *
+ * ```cpp
+ * template <typename Handler>
+ * void accept(socket_type& sc, Handler handler);
+ *
+ * auto get_context() -> boost::asio::io_context&
+ * ```
+ *
+ * The Handler callback must have the signature
+ * `void f(const std::error_code&)`.
+ */
+template <typename SocketAcceptor>
+class tls_acceptor : public acceptor {
+private:
+	using socket_type = typename SocketAcceptor::socket_type;
+
+	std::shared_ptr<boost::asio::ssl::context> context_;
+	SocketAcceptor acceptor_;
+
+public:
+	/**
+	 * Construct a secure layer transport server.
+	 *
+	 * \param context the SSL context
+	 * \param args the arguments to SocketAcceptor constructor
+	 */
+	template <typename... Args>
+	tls_acceptor(boost::asio::ssl::context context, Args&&... args);
+
+	/**
+	 * \copydoc acceptor::accept
+	 */
+	void accept(handler handler) override;
+};
+
+template <typename SocketAcceptor>
+template <typename... Args>
+inline tls_acceptor<SocketAcceptor>::tls_acceptor(boost::asio::ssl::context context, Args&&... args)
+	: context_(std::make_shared<boost::asio::ssl::context>(std::move(context)))
+	, acceptor_(std::forward<Args>(args)...)
+{
+}
+
+template <typename SocketAcceptor>
+inline void tls_acceptor<SocketAcceptor>::accept(handler handler)
+{
+	auto client = std::make_shared<tls_stream<socket_type>>(acceptor_.get_service(), context_);
+
+	acceptor_.accept(client->get_socket().lowest_layer(), [handler, client] (auto code) {
+		using boost::asio::ssl::stream_base;
+
+		if (code) {
+			handler(std::move(code), nullptr);
+			return;
+		}
+
+		client->get_socket().async_handshake(stream_base::server, [handler, client] (auto code) {
+			if (code)
+				handler(std::move(code), nullptr);
+			else
+				handler(std::move(code), std::move(client));
+		});
+	});
+}
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_ip_acceptor = tls_acceptor<ip_acceptor>;
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_local_acceptor = tls_acceptor<local_acceptor>;
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+#endif // !IRCCD_HAVE_SSL
+
+// }}}
+
+} // !irccd
+
+#endif // !IRCCD_ACCEPTOR_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/config.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,49 @@
+/*
+ * config.cpp -- irccd configuration loader
+ *
+ * Copyright (c) 2013-2018 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 <boost/filesystem.hpp>
+
+#include "config.hpp"
+#include "system.hpp"
+
+namespace irccd {
+
+auto config::search(std::string_view name) -> std::optional<config>
+{
+	for (const auto& path : sys::config_filenames(name)) {
+		boost::system::error_code ec;
+
+		if (boost::filesystem::exists(path, ec) && !ec)
+			return config(path);
+	}
+
+	return std::nullopt;
+}
+
+config::config(std::string path)
+	: document(path.empty() ? ini::document() : ini::read_file(path))
+	, path_(std::move(path))
+{
+}
+
+auto config::get_path() const noexcept -> const std::string&
+{
+	return path_;
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/config.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,67 @@
+/*
+ * config.hpp -- irccd configuration loader
+ *
+ * Copyright (c) 2013-2018 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_CONFIG_HPP
+#define IRCCD_CONFIG_HPP
+
+/**
+ * \file config.hpp
+ * \brief Read .ini configuration file for irccd
+ */
+
+#include <optional>
+#include <string_view>
+
+#include "ini.hpp"
+
+namespace irccd {
+
+/**
+ * \brief Read .ini configuration file for irccd
+ */
+class config : public ini::document {
+private:
+	std::string path_;
+
+public:
+	/**
+	 * Search the configuration file into the standard defined paths.
+	 *
+	 * \param name the file name
+	 * \return the config or empty if not found
+	 */
+	static auto search(std::string_view name) -> std::optional<config>;
+
+	/**
+	 * Load the configuration from the specified path.
+	 *
+	 * \param path the path
+	 */
+	config(std::string path = "");
+
+	/**
+	 * Get the path to the configuration file.
+	 *
+	 * \return the path
+	 */
+	auto get_path() const noexcept -> const std::string&;
+};
+
+} // !irccd
+
+#endif // !IRCCD_CONFIG_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/connector.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,446 @@
+/*
+ * connector.hpp -- abstract connection interface
+ *
+ * Copyright (c) 2013-2018 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_CONNECTOR_HPP
+#define IRCCD_CONNECTOR_HPP
+
+/**
+ * \file connector.hpp
+ * \brief Abstract connection interface.
+ */
+
+#include <cassert>
+#include <functional>
+#include <memory>
+#include <system_error>
+
+#include <boost/asio.hpp>
+
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
+
+#include <boost/filesystem/path.hpp>
+
+#include "stream.hpp"
+
+namespace irccd {
+
+/**
+ * \brief Abstract connection interface.
+ * \ingroup core-connectors
+ *
+ * This class is used to connect to a stream end point (usually sockets) in an
+ * asynchronous manner.
+ *
+ * Derived class must implement non-blocking connect function.
+ */
+class connector {
+public:
+	/**
+	 * \brief Connect completion handler.
+	 */
+	using handler = std::function<void (std::error_code, std::shared_ptr<stream>)>;
+
+	/**
+	 * Default constructor.
+	 */
+	connector() = default;
+
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~connector() = default;
+
+	/**
+	 * Start asynchronous connect.
+	 *
+	 * Once the client is connected, the original acceptor must be kept until it
+	 * is destroyed.
+	 *
+	 * \pre another connect operation must not be running
+	 * \pre handler != nullptr
+	 * \param handler the handler
+	 */
+	virtual void connect(handler handler) = 0;
+};
+
+// {{{ socket_connector_base
+
+/**
+ * \brief Provide convenient functions for connectors.
+ * \ingroup core-connectors
+ */
+class socket_connector_base : public connector {
+protected:
+	/**
+	 * \brief The I/O service.
+	 */
+	boost::asio::io_context& service_;
+
+public:
+	/**
+	 * Construct the connector
+	 *
+	 * \param service the service
+	 */
+	socket_connector_base(boost::asio::io_context& service);
+
+	/**
+	 * Get the I/O service.
+	 *
+	 * \return the service
+	 */
+	auto get_service() const noexcept -> const boost::asio::io_context&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the service
+	 */
+	auto get_service() noexcept -> boost::asio::io_context&;
+};
+
+inline socket_connector_base::socket_connector_base(boost::asio::io_context& service)
+	: service_(service)
+{
+}
+
+inline auto socket_connector_base::get_service() const noexcept -> const boost::asio::io_context&
+{
+	return service_;
+}
+
+inline auto socket_connector_base::get_service() noexcept -> boost::asio::io_context&
+{
+	return service_;
+}
+
+// }}}
+
+// {{{ ip_connector
+
+/**
+ * \brief TCP/IP connector.
+ * \ingroup core-connectors
+ */
+class ip_connector : public socket_connector_base {
+public:
+	/**
+	 * Underlying socket type.
+	 */
+	using socket_type = boost::asio::ip::tcp::socket;
+
+private:
+	boost::asio::ip::tcp::resolver resolver_;
+
+	std::string hostname_;
+	std::string port_;
+
+	bool ipv4_;
+	bool ipv6_;
+
+#if !defined(NDEBUG)
+	bool is_connecting_{false};
+#endif
+
+	template <typename Handler>
+	void resolve(Handler handler);
+
+public:
+	/**
+	 * Construct the TCP/IP connector.
+	 *
+	 * \pre at least ipv4 or ipv6 must be true
+	 * \param service the I/O context
+	 * \param hostname the hostname
+	 * \param port the port or service name
+	 * \param ipv4 enable IPv4
+	 * \param ipv6 enable IPv6
+	 */
+	ip_connector(boost::asio::io_context& service,
+	             std::string hostname,
+	             std::string port,
+	             bool ipv4 = true,
+	             bool ipv6 = true) noexcept;
+
+	/**
+	 * Connect to the given socket.
+	 *
+	 * \param sc the socket type
+	 * \param handler the handler
+	 * \note implemented for SocketConnector concept
+	 */
+	template <typename Socket, typename Handler>
+	void connect(Socket& sc, Handler handler);
+
+	/**
+	 * \copydoc connector::connect
+	 */
+	void connect(handler handler);
+};
+
+template <typename Handler>
+inline void ip_connector::resolve(Handler handler)
+{
+	using boost::asio::ip::tcp;
+
+	if (ipv6_ && ipv4_)
+		resolver_.async_resolve(hostname_, port_, handler);
+	else if (ipv6_)
+		resolver_.async_resolve(tcp::v6(), hostname_, port_, handler);
+	else
+		resolver_.async_resolve(tcp::v4(), hostname_, port_, handler);
+}
+
+inline ip_connector::ip_connector(boost::asio::io_context& service,
+                                  std::string hostname,
+                                  std::string port,
+                                  bool ipv4,
+                                  bool ipv6) noexcept
+	: socket_connector_base(service)
+	, resolver_(service)
+	, hostname_(std::move(hostname))
+	, port_(std::move(port))
+	, ipv4_(ipv4)
+	, ipv6_(ipv6)
+{
+	assert(!hostname_.empty());
+	assert(!port_.empty());
+	assert(ipv4 || ipv6);
+}
+
+template <typename Socket, typename Handler>
+inline void ip_connector::connect(Socket& sc, Handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_connecting_);
+
+	is_connecting_ = true;
+#endif
+
+	resolve([this, &sc, handler] (auto code, auto res) {
+#if !defined(NDEBUG)
+		is_connecting_ = false;
+#endif
+		(void)this;
+
+		if (code) {
+			handler(std::move(code));
+			return;
+		}
+
+		async_connect(sc, res, [handler] (auto code, auto) {
+			handler(std::move(code));
+		});
+	});
+}
+
+inline void ip_connector::connect(handler handler)
+{
+	auto stream = std::make_shared<ip_stream>(service_);
+
+	connect(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+// }}}
+
+// {{{ local_connector
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Unix domain connector.
+ * \ingroup core-connectors
+ */
+class local_connector : public socket_connector_base {
+public:
+	/**
+	 * Underlying socket type.
+	 */
+	using socket_type = boost::asio::local::stream_protocol::socket;
+
+private:
+	boost::filesystem::path path_;
+
+#if !defined(NDEBUG)
+	bool is_connecting_{false};
+#endif
+
+public:
+	/**
+	 * Construct a local connector.
+	 *
+	 * \param service the service
+	 * \param path the path to the file
+	 */
+	local_connector(boost::asio::io_context& service,
+	                boost::filesystem::path path) noexcept;
+
+	/**
+	 * Connect to the given socket.
+	 *
+	 * \param sc the socket type
+	 * \param handler the handler
+	 * \note implemented for SocketConnector concept
+	 */
+	template <typename Socket, typename Handler>
+	void connect(Socket& sc, Handler handler) noexcept;
+
+	/**
+	 * \copydoc connector::connect
+	 */
+	void connect(handler handler);
+};
+
+inline local_connector::local_connector(boost::asio::io_context& service,
+                                        boost::filesystem::path path) noexcept
+	: socket_connector_base(service)
+	, path_(std::move(path))
+{
+}
+
+template <typename Socket, typename Handler>
+inline void local_connector::connect(Socket& sc, Handler handler) noexcept
+{
+#if !defined(NDEBUG)
+	assert(!is_connecting_);
+
+	is_connecting_ = true;
+#endif
+
+	sc.async_connect({ path_.string() }, [this, handler] (auto code) {
+#if !defined(NDEBUG)
+		is_connecting_ = false;
+#endif
+		(void)this;
+		handler(std::move(code));
+	});
+}
+
+inline void local_connector::connect(handler handler)
+{
+	auto stream = std::make_shared<local_stream>(service_);
+
+	connect(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+// }}}
+
+// {{{ tls_connector
+
+#if defined(IRCCD_HAVE_SSL)
+
+/**
+ * \brief TLS/SSL connectors.
+ * \ingroup core-connectors
+ * \tparam SocketConnector the socket connector (e.g. ip_connector)
+ */
+template <typename SocketConnector>
+class tls_connector : public connector {
+public:
+	/**
+	 * \brief the underlying socket type.
+	 */
+	using socket_type = typename SocketConnector::socket_type;
+
+private:
+	std::shared_ptr<boost::asio::ssl::context> context_;
+	SocketConnector connector_;
+
+public:
+	/**
+	 * Construct a secure layer transport server.
+	 *
+	 * \param context the SSL context
+	 * \param args the arguments to SocketConnector constructor
+	 */
+	template <typename... Args>
+	tls_connector(boost::asio::ssl::context context, Args&&... args);
+
+	/**
+	 * \copydoc connector::connect
+	 */
+	void connect(handler handler) override;
+};
+
+template <typename SocketConnector>
+template <typename... Args>
+inline tls_connector<SocketConnector>::tls_connector(boost::asio::ssl::context context, Args&&... args)
+	: context_(std::make_shared<boost::asio::ssl::context>(std::move(context)))
+	, connector_(std::forward<Args>(args)...)
+{
+}
+
+template <typename SocketConnector>
+inline void tls_connector<SocketConnector>::connect(handler handler)
+{
+	using boost::asio::ssl::stream_base;
+
+	assert(handler);
+
+	auto stream = std::make_shared<tls_stream<socket_type>>(connector_.get_service(), context_);
+
+	connector_.connect(stream->get_socket().lowest_layer(), [handler, stream] (auto code) {
+		if (code) {
+			handler(code, nullptr);
+			return;
+		}
+
+		stream->get_socket().async_handshake(stream_base::client, [handler, stream] (auto code) {
+			if (code)
+				handler(std::move(code), nullptr);
+			else
+				handler(std::move(code), std::move(stream));
+		});
+	});
+}
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_ip_connector = tls_connector<ip_connector>;
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_local_connector = tls_connector<local_connector>;
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+#endif // !IRCCD_HAVE_SSL
+
+// }}}
+
+} // !irccd
+
+#endif // !IRCCD_CONNECTOR_HPP
--- a/libirccd/irccd/daemon/command.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,856 +0,0 @@
-/*
- * command.cpp -- remote command
- *
- * Copyright (c) 2013-2018 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 <irccd/string_util.hpp>
-
-#include "command.hpp"
-#include "irccd.hpp"
-#include "plugin.hpp"
-#include "plugin_service.hpp"
-#include "rule.hpp"
-#include "rule_service.hpp"
-#include "rule_util.hpp"
-#include "server.hpp"
-#include "server_service.hpp"
-#include "server_util.hpp"
-#include "transport_client.hpp"
-
-using namespace std::string_literals;
-
-namespace irccd {
-
-namespace {
-
-void exec_set(transport_client& client, plugin& plugin, const nlohmann::json& args)
-{
-	assert(args.count("value") > 0);
-
-	const auto var = args.find("variable");
-	const auto value = args.find("value");
-
-	if (var == args.end() || !var->is_string())
-		throw irccd_error(irccd_error::error::incomplete_message);
-	if (value == args.end() || !value->is_string())
-		throw irccd_error(irccd_error::error::incomplete_message);
-
-	auto config = plugin.get_options();
-
-	config[*var] = *value;
-	plugin.set_options(config);
-	client.success("plugin-config");
-}
-
-void exec_get(transport_client& client, plugin& plugin, const nlohmann::json& args)
-{
-	auto variables = nlohmann::json::object();
-	auto var = args.find("variable");
-
-	if (var != args.end() && var->is_string())
-		variables[var->get<std::string>()] = plugin.get_options()[*var];
-	else
-		for (const auto& pair : plugin.get_options())
-			variables[pair.first] = pair.second;
-
-	/*
-	 * Don't put all variables into the response, put them into a sub
-         * property 'variables' instead.
-	 *
-	 * It's easier for the client to iterate over all.
-	 */
-	client.write({
-		{ "command",    "plugin-config" },
-		{ "variables",  variables       }
-	});
-}
-
-template <typename T>
-auto bind() noexcept -> command::constructor
-{
-	return [] () noexcept {
-		return std::make_unique<T>();
-	};
-}
-
-} // !namespace
-
-const std::vector<command::constructor> command::registry{
-	bind<plugin_config_command>(),
-	bind<plugin_info_command>(),
-	bind<plugin_list_command>(),
-	bind<plugin_load_command>(),
-	bind<plugin_reload_command>(),
-	bind<plugin_unload_command>(),
-	bind<rule_add_command>(),
-	bind<rule_edit_command>(),
-	bind<rule_info_command>(),
-	bind<rule_info_command>(),
-	bind<rule_list_command>(),
-	bind<rule_move_command>(),
-	bind<rule_remove_command>(),
-	bind<server_connect_command>(),
-	bind<server_disconnect_command>(),
-	bind<server_info_command>(),
-	bind<server_invite_command>(),
-	bind<server_join_command>(),
-	bind<server_kick_command>(),
-	bind<server_list_command>(),
-	bind<server_me_command>(),
-	bind<server_message_command>(),
-	bind<server_mode_command>(),
-	bind<server_nick_command>(),
-	bind<server_notice_command>(),
-	bind<server_part_command>(),
-	bind<server_reconnect_command>(),
-	bind<server_topic_command>(),
-};
-
-// {{{ plugin_config_command
-
-auto plugin_config_command::get_name() const noexcept -> std::string_view
-{
-	return "plugin-config";
-}
-
-void plugin_config_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("plugin");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw plugin_error(plugin_error::invalid_identifier);
-
-	const auto plugin = irccd.plugins().require(*id);
-
-	if (args.count("value") > 0)
-		exec_set(client, *plugin, args);
-	else
-		exec_get(client, *plugin, args);
-}
-
-// }}}
-
-// {{{ plugin_info_command
-
-auto plugin_info_command::get_name() const noexcept -> std::string_view
-{
-	return "plugin-info";
-}
-
-void plugin_info_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("plugin");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw plugin_error(plugin_error::invalid_identifier);
-
-	const auto plugin = irccd.plugins().require(*id);
-
-	client.write({
-		{ "command",    "plugin-info"                           },
-		{ "author",     std::string(plugin->get_author())       },
-		{ "license",    std::string(plugin->get_license())      },
-		{ "summary",    std::string(plugin->get_summary())      },
-		{ "version",    std::string(plugin->get_version())      }
-	});
-}
-
-// }}}
-
-// {{{ plugin_list_command
-
-auto plugin_list_command::get_name() const noexcept -> std::string_view
-{
-	return "plugin-list";
-}
-
-void plugin_list_command::exec(irccd& irccd, transport_client& client, const document&)
-{
-	auto list = nlohmann::json::array();
-
-	for (const auto& plg : irccd.plugins().list())
-		list += plg->get_id();
-
-	client.write({
-		{ "command",    "plugin-list"   },
-		{ "list",       list            }
-	});
-}
-
-// }}}
-
-// {{{ plugin_load_command
-
-auto plugin_load_command::get_name() const noexcept -> std::string_view
-{
-	return "plugin-load";
-}
-
-void plugin_load_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("plugin");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw plugin_error(plugin_error::invalid_identifier);
-
-	irccd.plugins().load(*id, "");
-	client.success("plugin-load");
-}
-
-// }}}
-
-// {{{ plugin_reload_command
-
-auto plugin_reload_command::get_name() const noexcept -> std::string_view
-{
-	return "plugin-reload";
-}
-
-void plugin_reload_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("plugin");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw plugin_error(plugin_error::invalid_identifier);
-
-	irccd.plugins().reload(*id);
-	client.success("plugin-reload");
-}
-
-// }}}
-
-// {{{ plugin_unload_command
-
-auto plugin_unload_command::get_name() const noexcept -> std::string_view
-{
-	return "plugin-unload";
-}
-
-void plugin_unload_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("plugin");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw plugin_error(plugin_error::invalid_identifier);
-
-	irccd.plugins().unload(*id);
-	client.success("plugin-unload");
-}
-
-// }}}
-
-// {{{ rule_add_command
-
-auto rule_add_command::get_name() const noexcept -> std::string_view
-{
-	return "rule-add";
-}
-
-void rule_add_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto index = args.optional<unsigned>("index", irccd.rules().list().size());
-
-	if (!index || *index > irccd.rules().list().size())
-		throw rule_error(rule_error::error::invalid_index);
-
-	irccd.rules().insert(rule_util::from_json(args), *index);
-	client.success("rule-add");
-}
-
-// }}}
-
-// {{{ rule_edit_command
-
-auto rule_edit_command::get_name() const noexcept -> std::string_view
-{
-	return "rule-edit";
-}
-
-void rule_edit_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	static const auto updateset = [] (auto& set, auto args, const auto& key) {
-		for (const auto& v : args["remove-"s + key]) {
-			if (v.is_string())
-				set.erase(v.template get<std::string>());
-		}
-		for (const auto& v : args["add-"s + key]) {
-			if (v.is_string())
-				set.insert(v.template get<std::string>());
-		}
-	};
-
-	const auto index = args.get<unsigned>("index");
-
-	if (!index)
-		throw rule_error(rule_error::invalid_index);
-
-	// Create a copy to avoid incomplete edition in case of errors.
-	auto rule = irccd.rules().require(*index);
-
-	updateset(rule.channels, args, "channels");
-	updateset(rule.events, args, "events");
-	updateset(rule.plugins, args, "plugins");
-	updateset(rule.servers, args, "servers");
-
-	auto action = args.find("action");
-
-	if (action != args.end()) {
-		if (!action->is_string())
-			throw rule_error(rule_error::error::invalid_action);
-
-		if (action->get<std::string>() == "accept")
-			rule.action = rule::action_type::accept;
-		else if (action->get<std::string>() == "drop")
-			rule.action = rule::action_type::drop;
-		else
-			throw rule_error(rule_error::invalid_action);
-	}
-
-	// All done, sync the rule.
-	irccd.rules().require(*index) = rule;
-	client.success("rule-edit");
-}
-
-// }}}
-
-// {{{ rule_info_command
-
-auto rule_info_command::get_name() const noexcept -> std::string_view
-{
-	return "rule-info";
-}
-
-void rule_info_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto index = args.get<unsigned>("index");
-
-	if (!index)
-		throw rule_error(rule_error::invalid_index);
-
-	auto json = rule_util::to_json(irccd.rules().require(*index));
-
-	json.push_back({"command", "rule-info"});
-	client.write(std::move(json));
-}
-
-// }}}
-
-// {{{ rule_list_command
-
-auto rule_list_command::get_name() const noexcept -> std::string_view
-{
-	return "rule-list";
-}
-
-void rule_list_command::exec(irccd& irccd, transport_client& client, const document&)
-{
-	auto array = nlohmann::json::array();
-
-	for (const auto& rule : irccd.rules().list())
-		array.push_back(rule_util::to_json(rule));
-
-	client.write({
-		{ "command",    "rule-list"             },
-		{ "list",       std::move(array)        }
-	});
-}
-
-// }}}
-
-// {{{ rule_move_command
-
-auto rule_move_command::get_name() const noexcept -> std::string_view
-{
-	return "rule-move";
-}
-
-void rule_move_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto from = args.get<unsigned>("from");
-	const auto to = args.get<unsigned>("to");
-
-	if (!from || !to)
-		throw rule_error(rule_error::invalid_index);
-
-	/*
-	 * Examples of moves
-	 * --------------------------------------------------------------
-	 *
-	 * Before: [0] [1] [2]
-	 *
-	 * from = 0
-	 * to   = 2
-	 *
-	 * After:  [1] [2] [0]
-	 *
-	 * --------------------------------------------------------------
-	 *
-	 * Before: [0] [1] [2]
-	 *
-	 * from = 2
-	 * to   = 0
-	 *
-	 * After:  [2] [0] [1]
-	 *
-	 * --------------------------------------------------------------
-	 *
-	 * Before: [0] [1] [2]
-	 *
-	 * from = 0
-	 * to   = 123
-	 *
-	 * After:  [1] [2] [0]
-	 */
-
-	// Ignore dumb input.
-	if (*from == *to) {
-		client.success("rule-move");
-		return;
-	}
-
-	if (*from >= irccd.rules().list().size())
-		throw rule_error(rule_error::error::invalid_index);
-
-	const auto save = irccd.rules().list()[*from];
-
-	irccd.rules().remove(*from);
-	irccd.rules().insert(save, *to > irccd.rules().list().size() ? irccd.rules().list().size() : *to);
-	client.success("rule-move");
-}
-
-// }}}
-
-// {{{ rule_remove_command
-
-auto rule_remove_command::get_name() const noexcept -> std::string_view
-{
-	return "rule-remove";
-}
-
-void rule_remove_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto index = args.get<unsigned>("index");
-
-	if (!index || *index >= irccd.rules().list().size())
-		throw rule_error(rule_error::invalid_index);
-
-	irccd.rules().remove(*index);
-	client.success("rule-remove");
-}
-
-// }}}
-
-// {{{ server_connect_command
-
-auto server_connect_command::get_name() const noexcept -> std::string_view
-{
-	return "server-connect";
-}
-
-void server_connect_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	auto server = server_util::from_json(irccd.get_service(), args);
-
-	if (irccd.servers().has(server->get_id()))
-		throw server_error(server_error::already_exists);
-
-	irccd.servers().add(std::move(server));
-	client.success("server-connect");
-}
-
-// }}}
-
-// {{{ server_disconnect_command
-
-auto server_disconnect_command::get_name() const noexcept -> std::string_view
-{
-	return "server-disconnect";
-}
-
-void server_disconnect_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto it = args.find("server");
-
-	if (it == args.end())
-		irccd.servers().clear();
-	else {
-		if (!it->is_string() || !string_util::is_identifier(it->get<std::string>()))
-			throw server_error(server_error::invalid_identifier);
-
-		const auto name = it->get<std::string>();
-
-		irccd.servers().require(name);
-		irccd.servers().remove(name);
-	}
-
-	client.success("server-disconnect");
-}
-
-// }}}
-
-// {{{ server_info_command
-
-auto server_info_command::get_name() const noexcept -> std::string_view
-{
-	return "server-info";
-}
-
-void server_info_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-
-	const auto server = irccd.servers().require(*id);
-
-	// Construct the JSON response.
-	auto response = document::object();
-
-	// General stuff.
-	response.push_back({"command", "server-info"});
-	response.push_back({"name", server->get_id()});
-	response.push_back({"hostname", server->get_hostname()});
-	response.push_back({"port", server->get_port()});
-	response.push_back({"nickname", server->get_nickname()});
-	response.push_back({"username", server->get_username()});
-	response.push_back({"realname", server->get_realname()});
-	response.push_back({"channels", server->get_channels()});
-
-	// Optional stuff.
-	response.push_back({"ipv4", static_cast<bool>(server->get_options() & server::options::ipv4)});
-	response.push_back({"ipv6", static_cast<bool>(server->get_options() & server::options::ipv6)});
-	response.push_back({"ssl", static_cast<bool>(server->get_options() & server::options::ssl)});
-
-	client.write(response);
-}
-
-// }}}
-
-// {{{ server_invite_command
-
-auto server_invite_command::get_name() const noexcept -> std::string_view
-{
-	return "server-invite";
-}
-
-void server_invite_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto target = args.get<std::string>("target");
-	const auto channel = args.get<std::string>("channel");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!target || target->empty())
-		throw server_error(server_error::invalid_nickname);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-
-	irccd.servers().require(*id)->invite(*target, *channel);
-	client.success("server-invite");
-}
-
-// }}}
-
-// {{{ server_join_command
-
-auto server_join_command::get_name() const noexcept -> std::string_view
-{
-	return "server-join";
-}
-
-void server_join_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("channel");
-	const auto password = args.optional<std::string>("password", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!password)
-		throw server_error(server_error::invalid_password);
-
-	irccd.servers().require(*id)->join(*channel, *password);
-	client.success("server-join");
-}
-
-// }}}
-
-// {{{ server_kick_command
-
-auto server_kick_command::get_name() const noexcept -> std::string_view
-{
-	return "server-kick";
-}
-
-void server_kick_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto target = args.get<std::string>("target");
-	const auto channel = args.get<std::string>("channel");
-	const auto reason = args.optional<std::string>("reason", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!target || target->empty())
-		throw server_error(server_error::invalid_nickname);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!reason)
-		throw server_error(server_error::invalid_message);
-
-	irccd.servers().require(*id)->kick(*target, *channel, *reason);
-	client.success("server-kick");
-}
-
-// }}}
-
-// {{{ server_list_command
-
-auto server_list_command::get_name() const noexcept -> std::string_view
-{
-	return "server-list";
-}
-
-void server_list_command::exec(irccd& irccd, transport_client& client, const document&)
-{
-	auto json = nlohmann::json::object();
-	auto list = nlohmann::json::array();
-
-	for (const auto& server : irccd.servers().list())
-		list.push_back(server->get_id());
-
-	client.write({
-		{ "command",    "server-list"   },
-		{ "list",       std::move(list) }
-	});
-}
-
-// }}}
-
-// {{{ server_me_command
-
-auto server_me_command::get_name() const noexcept -> std::string_view
-{
-	return "server-me";
-}
-
-void server_me_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("target");
-	const auto message = args.optional<std::string>("message", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!message)
-		throw server_error(server_error::invalid_message);
-
-	irccd.servers().require(*id)->me(*channel, *message);
-	client.success("server-me");
-}
-
-// }}}
-
-// {{{ server_message_command
-
-auto server_message_command::get_name() const noexcept -> std::string_view
-{
-	return "server-message";
-}
-
-void server_message_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("target");
-	const auto message = args.optional<std::string>("message", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!message)
-		throw server_error(server_error::invalid_message);
-
-	irccd.servers().require(*id)->message(*channel, *message);
-	client.success("server-message");
-}
-
-// }}}
-
-// {{{ server_mode_command
-
-auto server_mode_command::get_name() const noexcept -> std::string_view
-{
-	return "server-mode";
-}
-
-void server_mode_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("channel");
-	const auto mode = args.get<std::string>("mode");
-	const auto limit = args.optional<std::string>("limit", "");
-	const auto user = args.optional<std::string>("user", "");
-	const auto mask = args.optional<std::string>("mask", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!mode || mode->empty())
-		throw server_error(server_error::invalid_mode);
-	if (!limit || !user || !mask)
-		throw server_error(server_error::invalid_mode);
-
-	irccd.servers().require(*id)->mode(*channel, *mode, *limit, *user, *mask);
-	client.success("server-mode");
-}
-
-// }}}
-
-// {{{ server_nick_command
-
-auto server_nick_command::get_name() const noexcept -> std::string_view
-{
-	return "server-nick";
-}
-
-void server_nick_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto nick = args.get<std::string>("nickname");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!nick || nick->empty())
-		throw server_error(server_error::invalid_nickname);
-
-	irccd.servers().require(*id)->set_nickname(*nick);
-	client.success("server-nick");
-}
-
-// }}}
-
-// {{{ server_notice_command
-
-auto server_notice_command::get_name() const noexcept -> std::string_view
-{
-	return "server-notice";
-}
-
-void server_notice_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("target");
-	const auto message = args.optional<std::string>("message", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!message)
-		throw server_error(server_error::invalid_message);
-
-	irccd.servers().require(*id)->notice(*channel, *message);
-	client.success("server-notice");
-}
-
-// }}}
-
-// {{{ server_part_command
-
-auto server_part_command::get_name() const noexcept -> std::string_view
-{
-	return "server-part";
-}
-
-void server_part_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("channel");
-	const auto reason = args.optional<std::string>("reason", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!reason)
-		throw server_error(server_error::invalid_message);
-
-	irccd.servers().require(*id)->part(*channel, *reason);
-	client.success("server-part");
-}
-
-// }}}
-
-// {{{ server_reconnect_command
-
-auto server_reconnect_command::get_name() const noexcept -> std::string_view
-{
-	return "server-reconnect";
-}
-
-void server_reconnect_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto it = args.find("server");
-
-	if (it == args.end())
-		irccd.servers().reconnect();
-	else {
-		if (!it->is_string() || !string_util::is_identifier(it->get<std::string>()))
-			throw server_error(server_error::invalid_identifier);
-
-		irccd.servers().reconnect(it->get<std::string>());
-	}
-
-	client.success("server-reconnect");
-}
-
-// }}}
-
-// {{{ server_topic_command
-
-auto server_topic_command::get_name() const noexcept -> std::string_view
-{
-	return "server-topic";
-}
-
-void server_topic_command::exec(irccd& irccd, transport_client& client, const document& args)
-{
-	const auto id = args.get<std::string>("server");
-	const auto channel = args.get<std::string>("channel");
-	const auto topic = args.optional<std::string>("topic", "");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!channel || channel->empty())
-		throw server_error(server_error::invalid_channel);
-	if (!topic)
-		throw server_error(server_error::invalid_message);
-
-	irccd.servers().require(*id)->topic(*channel, *topic);
-	client.success("server-topic");
-}
-
-// }}}
-
-} // !irccd
--- a/libirccd/irccd/daemon/command.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,804 +0,0 @@
-/*
- * command.hpp -- remote command
- *
- * Copyright (c) 2013-2018 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_DAEMON_COMMAND_HPP
-#define IRCCD_DAEMON_COMMAND_HPP
-
-/**
- * \file command.hpp
- * \brief Remote commands.
- */
-
-/**
- * \defgroup commands Commands
- * \brief Remote transport commands.
- *
- * The remote commands are server-side functions that can be called by clients
- * connected to irccd.
- *
- * Example, the \ref irccd::server_message_command is used to send a message.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <functional>
-#include <memory>
-#include <string_view>
-#include <vector>
-
-#include <irccd/json_util.hpp>
-
-namespace irccd {
-
-class irccd;
-class transport_client;
-
-// {{{ command
-
-/**
- * \brief Server side remote command
- * \ingroup commands
- */
-class command {
-public:
-	/**
-	 * \brief Convenient alias.
-	 */
-	using document = json_util::deserializer;
-
-	/**
-	 * \brief Command constructor factory.
-	 */
-	using constructor = std::function<auto () -> std::unique_ptr<command>>;
-
-	/**
-	 * \brief Registry of all commands.
-	 */
-	static const std::vector<constructor> registry;
-
-	/**
-	 * Default destructor virtual.
-	 */
-	virtual ~command() = default;
-
-	/**
-	 * Return the command name, must not have spaces.
-	 *
-	 * \return the command name
-	 */
-	virtual auto get_name() const noexcept -> std::string_view = 0;
-
-	/**
-	 * Execute the command.
-	 *
-	 * If the command throw an exception, the error is sent to the client so be
-	 * careful about sensitive information.
-	 *
-	 * The implementation should use client.success() or client.error() to send
-	 * some data.
-	 *
-	 * \param irccd the irccd instance
-	 * \param client the client
-	 * \param args the client arguments
-	 */
-	virtual void exec(irccd& irccd, transport_client& client, const document& args) = 0;
-};
-
-// }}}
-
-// {{{ plugin_config_command
-
-/**
- * \brief Implementation of plugin-config transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - plugin_error::not_found
- */
-class plugin_config_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ plugin_info_command
-
-/**
- * \brief Implementation of plugin-info transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - plugin_error::not_found
- */
-class plugin_info_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ plugin_list_command
-
-/**
- * \brief Implementation of plugin-list transport command.
- * \ingroup commands
- */
-class plugin_list_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ plugin_load_command
-
-/**
- * \brief Implementation of plugin-load transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - plugin_error::already_exists
- * - plugin_error::not_found
- * - plugin_error::exec_error
- */
-class plugin_load_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ plugin_reload_command
-
-/**
- * \brief Implementation of plugin-reload transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - plugin_error::not_found
- * - plugin_error::exec_error
- */
-class plugin_reload_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ plugin_unload_command
-
-/**
- * \brief Implementation of plugin-unload transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - plugin_error::not_found
- * - plugin_error::exec_error
- */
-class plugin_unload_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ rule_add_command
-
-/**
- * \brief Implementation of rule-add transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - rule_error::invalid_action
- */
-class rule_add_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ rule_edit_command
-
-/**
- * \brief Implementation of rule-edit transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - rule_error::invalid_index
- * - rule_error::invalid_action
- */
-class rule_edit_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ rule_info_command
-
-/**
- * \brief Implementation of rule-info transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - rule_error::invalid_index
- */
-class rule_info_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ rule_list_command
-
-/**
- * \brief Implementation of rule-list transport command.
- * \ingroup commands
- */
-class rule_list_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ rule_move_command
-
-/**
- * \brief Implementation of rule-move transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - rule_error::invalid_index
- */
-class rule_move_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ rule_remove_command
-
-/**
- * \brief Implementation of rule-remove transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - rule_error::invalid_index
- */
-class rule_remove_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_connect_command
-
-/**
- * \brief Implementation of server-connect transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::already_exists,
- * - server_error::invalid_hostname,
- * - server_error::invalid_identifier,
- * - server_error::invalid_port_number,
- * - server_error::ssl_disabled.
- */
-class server_connect_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_disconnect_command
-
-/**
- * \brief Implementation of server-disconnect transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_disconnect_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_info_command
-
-/**
- * \brief Implementation of server-info transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_info_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_invite_command
-
-/**
- * \brief Implementation of server-invite transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::invalid_nickname,
- * - server_error::not_found.
- */
-class server_invite_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_join_command
-
-/**
- * \brief Implementation of server-join transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_join_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_kick_command
-
-/**
- * \brief Implementation of server-kick transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::invalid_nickname,
- * - server_error::not_found.
- */
-class server_kick_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_list_command
-
-/**
- * \brief Implementation of server-list transport command.
- * \ingroup commands
- */
-class server_list_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_me_command
-
-/**
- * \brief Implementation of server-me transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_me_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_message_command
-
-/**
- * \brief Implementation of server-message transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_message_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_mode_command
-
-/**
- * \brief Implementation of server-mode transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::invalid_mode,
- * - server_error::not_found.
- */
-class server_mode_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_nick_command
-
-/**
- * \brief Implementation of server-nick transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_identifier,
- * - server_error::invalid_nickname,
- * - server_error::not_found.
- */
-class server_nick_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_notice_command
-
-/**
- * \brief Implementation of server-notice transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_notice_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_part_command
-
-/**
- * \brief Implementation of server-part transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_part_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_reconnect_command
-
-/**
- * \brief Implementation of server-reconnect transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_reconnect_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-// {{{ server_topic_command
-
-/**
- * \brief Implementation of server-topic transport command.
- * \ingroup commands
- *
- * Replies:
- *
- * - server_error::invalid_channel,
- * - server_error::invalid_identifier,
- * - server_error::not_found.
- */
-class server_topic_command : public command {
-public:
-	/**
-	 * \copydoc command::get_name
-	 */
-	auto get_name() const noexcept -> std::string_view override;
-
-	/**
-	 * \copydoc command::exec
-	 */
-	void exec(irccd& irccd, transport_client& client, const document& args) override;
-};
-
-// }}}
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_COMMAND_HPP
--- a/libirccd/irccd/daemon/dynlib_plugin.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/*
- * dynlib_plugin.cpp -- native plugin implementation
- *
- * Copyright (c) 2013-2018 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 <algorithm>
-
-#include <boost/dll.hpp>
-#include <boost/filesystem.hpp>
-#include <boost/predef/os.h>
-#include <boost/format.hpp>
-
-#define BOOST_DLL_FORCE_ALIAS_INSTANTIATION
-#include "dynlib_plugin.hpp"
-
-#if BOOST_OS_WINDOWS
-#	define DYNLIB_EXTENSION ".dll"
-#elif BOOST_OS_MACOS
-#	define DYNLIB_EXTENSION ".dylib"
-#else
-#	define DYNLIB_EXTENSION ".so"
-#endif
-
-using boost::format;
-using boost::str;
-
-namespace irccd {
-
-namespace {
-
-auto symbol(std::string_view path) -> std::pair<std::string, std::string>
-{
-	auto id = boost::filesystem::path(std::string(path)).stem().string();
-
-	// Remove forbidden characters.
-	id.erase(std::remove_if(id.begin(), id.end(), [] (auto c) {
-		return !isalnum(c) && c != '-' && c != '_';
-	}), id.end());
-
-	// Transform - to _.
-	std::transform(id.begin(), id.end(), id.begin(), [] (auto c) noexcept {
-		return c == '-' ? '_' : c;
-	});
-
-	return {
-		str(format("irccd_abi_%1%") % id),
-		str(format("irccd_init_%1%") % id)
-	};
-}
-
-} // !namespace
-
-dynlib_plugin_loader::dynlib_plugin_loader(std::vector<std::string> directories) noexcept
-	: plugin_loader(std::move(directories), { DYNLIB_EXTENSION })
-{
-}
-
-auto dynlib_plugin_loader::open(std::string_view id, std::string_view path) -> std::shared_ptr<plugin>
-{
-	const std::string idstr(id);
-	const std::string pathstr(path);
-
-	const auto [ abisym, initsym ] = symbol(pathstr);
-
-	using abisym_func_type = version ();
-	using initsym_func_type = std::unique_ptr<plugin> (std::string);
-
-	const auto abi = boost::dll::import_alias<abisym_func_type>(pathstr, abisym);
-	const auto init = boost::dll::import_alias<initsym_func_type>(pathstr, initsym);
-
-	// The abi version is reset after new major version, check for both.
-	const version current;
-
-	if (current.major != abi().major || current.abi != abi().abi)
-		throw plugin_error(plugin_error::exec_error, idstr, "incompatible version");
-
-	auto plg = init(idstr);
-
-	if (!plg)
-		throw plugin_error(plugin_error::exec_error, idstr, "invalid plugin");
-
-	/*
-	 * We need to keep a reference to `init' variable for the whole plugin
-	 * lifetime.
-	 */
-	return std::shared_ptr<plugin>(plg.release(), [init] (auto ptr) mutable {
-		delete ptr;
-	});
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/dynlib_plugin.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/*
- * dynlib_plugin.hpp -- native plugin implementation
- *
- * Copyright (c) 2013-2018 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_DAEMON_DYNLIB_PLUGIN_HPP
-#define IRCCD_DAEMON_DYNLIB_PLUGIN_HPP
-
-/**
- * \file dynlib_plugin.hpp
- * \brief Native plugin implementation.
- */
-
-#define BOOST_DLL_FORCE_ALIAS_INSTANTIATION
-#include <boost/dll.hpp>
-
-#include "plugin.hpp"
-
-namespace irccd {
-
-/**
- * \ingroup plugins
- * \brief Implementation for searching native plugins.
- */
-class dynlib_plugin_loader : public plugin_loader {
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param directories optional directories to search, if empty use defaults.
-	 */
-	dynlib_plugin_loader(std::vector<std::string> directories = {}) noexcept;
-
-	/**
-	 * \copydoc plugin_loader::open
-	 */
-	auto open(std::string_view id, std::string_view file) -> std::shared_ptr<plugin> override;
-};
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_DYNLIB_PLUGIN_HPP
--- a/libirccd/irccd/daemon/irc.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,300 +0,0 @@
-/*
- * irc.cpp -- low level IRC functions
- *
- * Copyright (c) 2013-2018 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 <cassert>
-#include <iterator>
-#include <sstream>
-
-#include "irc.hpp"
-
-using std::errc;
-using std::flush;
-using std::isspace;
-using std::istreambuf_iterator;
-using std::istringstream;
-using std::move;
-using std::ostream;
-using std::string;
-using std::string_view;
-using std::vector;
-
-using boost::asio::async_connect;
-using boost::asio::async_read_until;
-using boost::asio::async_write;
-using boost::asio::io_service;
-using boost::asio::ip::tcp;
-
-#if defined(IRCCD_HAVE_SSL)
-
-using boost::asio::ssl::stream_base;
-
-#endif
-
-namespace irccd::irc {
-
-auto message::get(unsigned short index) const noexcept -> const string&
-{
-	static const string dummy;
-
-	return (index >= args.size()) ? dummy : args[index];
-}
-
-auto message::is_ctcp(unsigned short index) const noexcept -> bool
-{
-	const auto a = get(index);
-
-	if (a.empty())
-		return false;
-
-	return a.front() == 0x01 && a.back() == 0x01;
-}
-
-auto message::ctcp(unsigned short index) const -> string
-{
-	assert(is_ctcp(index));
-
-	return args[index].substr(1, args[index].size() - 1);
-}
-
-auto message::parse(const string& line) -> message
-{
-	istringstream iss(line);
-	string prefix;
-
-	if (line.empty())
-		return {};
-
-	// Prefix.
-	if (line[0] == ':') {
-		iss.ignore(1);
-		iss >> prefix;
-		iss.ignore(1);
-	}
-
-	// Command.
-	string command;
-	iss >> command;
-	iss.ignore(1);
-
-	// Arguments.
-	vector<std::string> args;
-	istreambuf_iterator<char> it(iss), end;
-
-	while (it != end) {
-		std::string arg;
-
-		if (*it == ':')
-			arg = string(++it, end);
-		else {
-			while (!isspace(*it) && it != end)
-				arg.push_back(*it++);
-
-			// Skip space after param.
-			if (it != end)
-				++it;
-		}
-
-		args.push_back(move(arg));
-	}
-
-	return { move(prefix), move(command), move(args) };
-}
-
-auto user::parse(string_view line) -> user
-{
-	if (line.empty())
-		return { "", "" };
-
-	const auto pos = line.find("!");
-
-	if (pos == string::npos)
-		return { string(line), "" };
-
-	return { string(line.substr(0, pos)), string(line.substr(pos + 1)) };
-}
-
-void connection::handshake(const connect_handler& handler)
-{
-	if (!ssl_) {
-		handler({});
-		return;
-	}
-
-#if defined(IRCCD_HAVE_SSL)
-	ssl_socket_.async_handshake(stream_base::client, [handler] (auto code) {
-		handler(std::move(code));
-	});
-#endif
-}
-
-void connection::connect(const tcp::resolver::results_type& endpoints, const connect_handler& handler)
-{
-	async_connect(socket_, endpoints, [this, handler] (auto code, auto) {
-		if (code) {
-			handler(move(code));
-			return;
-		}
-
-		handshake(handler);
-	});
-}
-
-void connection::resolve(string_view hostname, string_view port, const connect_handler& handler)
-{
-	auto chain = [this, handler] (auto code, auto eps) {
-		if (code)
-			handler(std::move(code));
-		else
-			connect(eps, std::move(handler));
-	};
-
-	if (ipv6_ && ipv4_)
-		resolver_.async_resolve(hostname, port, move(chain));
-	else if (ipv6_)
-		resolver_.async_resolve(tcp::v6(), hostname, port, move(chain));
-	else
-		resolver_.async_resolve(tcp::v4(), hostname, port, move(chain));
-}
-
-connection::connection(io_service& service)
-	: service_(service)
-	, resolver_(service)
-{
-}
-
-void connection::use_ipv4(bool enable) noexcept
-{
-	ipv4_ = enable;
-}
-
-void connection::use_ipv6(bool enable) noexcept
-{
-	ipv6_ = enable;
-}
-
-void connection::use_ssl(bool enable) noexcept
-{
-	ssl_ = enable;
-}
-
-void connection::connect(string_view hostname, string_view service, connect_handler handler)
-{
-#if !defined(IRCCD_HAVE_SSL)
-	assert(!ssl_);
-#endif
-#if !defined(NDEBUG)
-	assert(!is_connecting_);
-
-	is_connecting_ = true;
-#endif
-	assert(handler);
-	assert(ipv4_ || ipv6_);
-
-	auto chain = [this, handler] (auto code) {
-#if !defined(NDEBUG)
-		is_connecting_ = false;
-#endif
-		(void)this;
-
-		handler(move(code));
-	};
-
-	resolve(hostname, service, move(chain));
-}
-
-void connection::recv(recv_handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_receiving_);
-
-	is_receiving_ = true;
-#endif
-
-	auto chain = [this, handler] (auto code, auto xfer) {
-#if !defined(NDEBUG)
-		is_receiving_ = false;
-#endif
-		(void)this;
-
-		if (code == boost::asio::error::not_found)
-			return handler(make_error_code(errc::argument_list_too_long), message());
-		if (code == boost::asio::error::eof || xfer == 0)
-			return handler(make_error_code(errc::connection_reset), message());
-		else if (code)
-			return handler(move(code), message());
-
-		string data;
-
-		// 1. Convert the buffer safely.
-		try {
-			data = string(
-				buffers_begin(input_.data()),
-				buffers_begin(input_.data()) + xfer - 2
-			);
-
-			input_.consume(xfer);
-		} catch (...) {
-			return handler(make_error_code(errc::not_enough_memory), message());
-		}
-
-		handler(move(code), message::parse(data));
-	};
-
-#if defined(IRCCD_HAVE_SSL)
-	if (ssl_)
-		async_read_until(ssl_socket_, input_, "\r\n", move(chain));
-	else
-#endif
-		async_read_until(socket_, input_, "\r\n", move(chain));
-}
-
-void connection::send(string_view message, send_handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_sending_);
-
-	is_sending_ = true;
-#endif
-
-	auto chain = [this, handler] (auto code, auto xfer) {
-#if !defined(NDEBUG)
-		is_sending_ = false;
-#endif
-		(void)this;
-
-		if (code == boost::asio::error::eof || xfer == 0)
-			return handler(make_error_code(errc::connection_reset));
-
-		handler(move(code));
-	};
-
-	ostream out(&output_);
-
-	out << message;
-	out << "\r\n";
-	out << flush;
-
-#if defined(IRCCD_HAVE_SSL)
-	if (ssl_)
-		async_write(ssl_socket_, output_, move(chain));
-	else
-#endif
-		async_write(socket_, output_, move(chain));
-}
-
-} // !irccd::irc
--- a/libirccd/irccd/daemon/irc.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1390 +0,0 @@
-/*
- * irc.hpp -- low level IRC functions
- *
- * Copyright (c) 2013-2018 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_IRC_HPP
-#define IRCCD_IRC_HPP
-
-/**
- * \file irc.hpp
- * \brief Low level IRC functions.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <functional>
-#include <string>
-#include <string_view>
-#include <utility>
-#include <vector>
-
-#include <boost/asio.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-#	include <boost/asio/ssl.hpp>
-#endif
-
-namespace irccd::irc {
-
-/**
- * \brief Describe errors.
- *
- * See [RFC1459 (6.1)](https://tools.ietf.org/html/rfc1459#section-6.1).
- */
-enum class err {
-	/**
-	 * ERR_NOSUCHNICK
-	 *
-	 * "<nickname> :No such nick/channel"
-	 *
-	 * Used to indicate the nickname parameter supplied to a
-	 * command is currently unused.
-	 */
-	nosuchnick = 401,
-
-	/**
-	 * ERR_NOSUCHSERVER
-	 *
-	 * "<server name> :No such server"
-	 *
-	 * Used to indicate the server name given currently
-	 * doesn't exist.
-	 */
-	nosuchserver = 402,
-
-	/**
-	 * ERR_NOSUCHCHANNEL
-	 *
-	 * "<channel name> :No such channel"
-	 *
-	 * Used to indicate the given channel name is invalid.
-	 */
-	nosuchchannel = 403,
-
-	/**
-	 * ERR_CANNOTSENDTOCHAN
-	 *
-	 * "<channel name> :Cannot send to channel"
-	 *
-	 * Sent to a user who is either (a) not on a channel
-	 * which is mode +n or (b) not a chanop (or mode +v) on
-	 * a channel which has mode +m set and is trying to send
-	 * a PRIVMSG message to that channel.
-	 */
-	cannotsendtochan = 404,
-
-	/**
-	 * ERR_TOOMANYCHANNELS
-	 *
-	 * "<channel name> :You have joined too many channels"
-	 *
-	 * Sent to a user when they have joined the maximum
-	 * number of allowed channels and they try to join
-	 * another channel.
-	 */
-	toomanychannels = 405,
-
-	/**
-	 * ERR_WASNOSUCHNICK
-	 *
-	 * "<nickname> :There was no such nickname"
-	 *
-	 * Returned by WHOWAS to indicate there is no history
-	 * information for that nickname.
-	 */
-	wasnosuchnick = 406,
-
-	/**
-	 * ERR_TOOMANYTARGETS
-	 *
-	 * "<target> :Duplicate recipients. No message delivered"
-	 *
-	 * Returned to a client which is attempting to send a
-	 * PRIVMSG/NOTICE using the user@host destination format
-	 * and for a user@host which has several occurrences.
-	 */
-	toomanytargets = 407,
-
-	/**
-	 * ERR_NOORIGIN
-	 *
-	 * ":No origin specified"
-	 *
-	 * PING or PONG message missing the originator parameter
-	 * which is required since these commands must work
-	 * without valid prefixes.
-	 */
-	noorigin = 409,
-
-	/**
-	 * ERR_NORECIPIENT
-	 *
-	 * ":No recipient given (<command>)"
-	 */
-	norecipient = 411,
-
-	/**
-	 * ERR_NOTEXTTOSEND
-	 *
-	 * ":No text to send"
-	 */
-	notexttosend = 412,
-
-	/**
-	 * ERR_NOTOPLEVEL
-	 *
-	 * "<mask> :No toplevel domain specified"
-	 */
-	notoplevel = 413,
-
-	/**
-	 * ERR_WILDTOPLEVEL
-	 *
-	 * "<mask> :Wildcard in toplevel domain"
-	 *
-	 * are returned by PRIVMSG to indicate that
-	 * the message wasn't delivered for some reason.
-	 * ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that
-	 * are returned when an invalid use of
-	 * "PRIVMSG $<server>" or "PRIVMSG #<host>" is attempted.
-	 */
-	wildtoplevel = 414,
-
-	/**
-	 * ERR_UNKNOWNCOMMAND
-	 *
-	 * "<command> :Unknown command"
-	 *
-	 * Returned to a registered client to indicate that the
-	 * command sent is unknown by the server.
-	 */
-	unknowncommand = 421,
-
-	/**
-	 * ERR_NOMOTD
-	 *
-	 * ":MOTD File is missing"
-	 *
-	 * Server's MOTD file could not be opened by the server.
-	 */
-	nomotd = 422,
-
-	/**
-	 * ERR_NOADMININFO
-	 *
-	 * "<server> :No administrative info available"
-	 *
-	 * Returned by a server in response to an ADMIN message
-	 * when there is an error in finding the appropriate
-	 * information.
-	 */
-	noadmininfo = 423,
-
-	/**
-	 * ERR_FILEERROR
-	 *
-	 * ":File error doing <file op> on <file>"
-	 *
-	 * Generic error message used to report a failed file
-	 * operation during the processing of a message.
-	 */
-	fileerror = 424,
-
-	/**
-	 * ERR_NONICKNAMEGIVEN
-	 *
-	 * ":No nickname given"
-	 *
-	 * Returned when a nickname parameter expected for a
-	 * command and isn't found.
-	 */
-	nonicknamegiven = 431,
-
-	/**
-	 * ERR_ERRONEUSNICKNAME
-	 *
-	 * "<nick> :Erroneus nickname"
-	 *
-	 * Returned after receiving a NICK message which contains
-	 * characters which do not fall in the defined set.  See
-	 * section x.x.x for details on valid nicknames.
-	 */
-	erroneusnickname = 432,
-
-	/**
-	 * ERR_NICKNAMEINUSE
-	 *
-	 * "<nick> :Nickname is already in use"
-	 *
-	 * Returned when a NICK message is processed that results
-	 * in an attempt to change to a currently existing
-	 * nickname.
-	 */
-	nicknameinuse = 433,
-
-	/**
-	 * ERR_NICKCOLLISION
-	 *
-	 * "<nick> :Nickname collision KILL"
-	 *
-	 * Returned by a server to a client when it detects a
-	 * nickname collision (registered of a NICK that
-	 * already exists by another server).
-	 */
-	nickcollision = 436,
-
-	/**
-	 * ERR_USERNOTINCHANNEL
-	 *
-	 * "<nick> <channel> :They aren't on that channel"
-	 *
-	 * Returned by the server to indicate that the target
-	 * user of the command is not on the given channel.
-	 */
-	usernotinchannel = 441,
-
-	/**
-	 * ERR_NOTONCHANNEL
-	 *
-	 * "<channel> :You're not on that channel"
-	 *
-	 * Returned by the server whenever a client tries to
-	 * perform a channel effecting command for which the
-	 * client isn't a member.
-	 */
-	notonchannel = 442,
-
-	/**
-	 * ERR_USERONCHANNEL
-	 *
-	 * "<user> <channel> :is already on channel"
-	 *
-	 * Returned when a client tries to invite a user to a
-	 * channel they are already on.
-	 */
-	useronchannel = 443,
-
-	/**
-	 * ERR_NOLOGIN
-	 *
-	 * "<user> :User not logged in"
-	 *
-	 * Returned by the summon after a SUMMON command for a
-	 * user was unable to be performed since they were not
-	 * logged in.
-	 */
-	nologin = 444,
-
-	/**
-	 * ERR_SUMMONDISABLED
-	 *
-	 * ":SUMMON has been disabled"
-	 *
-	 * Returned as a response to the SUMMON command.  Must be
-	 * returned by any server which does not implement it.
-	 */
-	summondisabled = 445,
-
-	/**
-	 * ERR_USERSDISABLED
-	 *
-	 * ":USERS has been disabled"
-	 *
-	 * Returned as a response to the USERS command.  Must be
-	 * returned by any server which does not implement it.
-	 */
-	usersdisabled = 446,
-
-	/**
-	 * ERR_NOTREGISTERED
-	 *
-	 * ":You have not registered"
-	 *
-	 * Returned by the server to indicate that the client
-	 * must be registered before the server will allow it
-	 * to be parsed in detail.
-	 */
-	notregistered = 451,
-
-	/**
-	 * ERR_NEEDMOREPARAMS
-	 *
-	 * "<command> :Not enough parameters"
-	 *
-	 * Returned by the server by numerous commands to
-	 * indicate to the client that it didn't supply enough
-	 * parameters.
-	 */
-	needmoreparams = 461,
-
-	/**
-	 * ERR_ALREADYREGISTRED
-	 *
-	 * ":You may not reregister"
-	 *
-	 * Returned by the server to any link which tries to
-	 * change part of the registered details (such as
-	 * password or user details from second USER message).
-	 */
-	alreadyregistred = 462,
-
-	/**
-	 * ERR_NOPERMFORHOST
-	 *
-	 * ":Your host isn't among the privileged"
-	 *
-	 * Returned to a client which attempts to register with
-	 * a server which does not been setup to allow
-	 * connections from the host the attempted connection
-	 * is tried.
-	 */
-	nopermforhost = 463,
-
-	/**
-	 * ERR_PASSWDMISMATCH
-	 *
-	 * ":Password incorrect"
-	 *
-	 * Returned to indicate a failed attempt at registering
-	 * a connection for which a password was required and
-	 * was either not given or incorrect.
-	 */
-	passwdmismatch = 464,
-
-	/**
-	 * ERR_YOUREBANNEDCREEP
-	 *
-	 * ":You are banned from this server"
-	 *
-	 * Returned after an attempt to connect and register
-	 * yourself with a server which has been setup to
-	 * explicitly deny connections to you.
-	 */
-	yourebannedcreep = 465,
-
-	/**
-	 * ERR_KEYSET
-	 *
-	 * "<channel> :Channel key already set"
-	 */
-	keyset = 467,
-
-	/**
-	 * ERR_CHANNELISFULL
-	 *
-	 * "<channel> :Cannot join channel (+l)"
-	 */
-	channelisfull = 471,
-
-	/**
-	 * ERR_UNKNOWNMODE
-	 *
-	 * "<char> :is unknown mode char to me"
-	 */
-	unknownmode = 472,
-
-	/**
-	 * ERR_INVITEONLYCHAN
-	 *
-	 * "<channel> :Cannot join channel (+i)"
-	 */
-	inviteonlychan = 473,
-
-	/**
-	 * ERR_BANNEDFROMCHAN
-	 *
-	 * "<channel> :Cannot join channel (+b)"
-	 */
-	bannedfromchan = 474,
-
-	/**
-	 * ERR_BADCHANNELKEY
-	 *
-	 * "<channel> :Cannot join channel (+k)"
-	 */
-	badchannelkey = 475,
-
-	/**
-	 * ERR_NOPRIVILEGES
-	 *
-	 * ":Permission Denied- You're not an IRC operator"
-	 *
-	 * Any command requiring operator privileges to operate
-	 * must return this error to indicate the attempt was
-	 * unsuccessful.
-	 */
-	noprivileges = 481,
-
-	/**
-	 * ERR_CHANOPRIVSNEEDED
-	 *
-	 * "<channel> :You're not channel operator"
-	 *
-	 * Any command requiring 'chanop' privileges (such as
-	 * MODE messages) must return this error if the client
-	 * making the attempt is not a chanop on the specified
-	 * channel.
-	 */
-	chanoprivsneeded = 482,
-
-	/**
-	 * ERR_CANTKILLSERVER
-	 *
-	 * ":You cant kill a server!"
-	 *
-	 * Any attempts to use the KILL command on a server
-	 * are to be refused and this error returned directly
-	 * to the client.
-	 */
-	cantkillserver = 483,
-
-	/**
-	 * ERR_NOOPERHOST
-	 *
-	 * ":No O-lines for your host"
-	 *
-	 * If a client sends an OPER message and the server has
-	 * not been configured to allow connections from the
-	 * client's host as an operator, this error must be
-	 * returned.
-	 */
-	nooperhost = 491,
-
-	/**
-	 * ERR_UMODEUNKNOWNFLAG
-	 *
-	 * ":Unknown MODE flag"
-	 *
-	 * Returned by the server to indicate that a MODE
-	 * message was sent with a nickname parameter and that
-	 * the a mode flag sent was not recognized.
-	 */
-	umodeunknownflag = 501,
-
-	/**
-	 * ERR_USERSDONTMATCH
-	 *
-	 * ":Cant change mode for other users"
-	 *
-	 * Error sent to any user trying to view or change the
-	 * user mode for a user other than themselves.
-	 */
-	usersdontmatch = 502
-};
-
-/**
- * \brief Describe numeric replies.
- *
- * See [RFC1459 (6.2)](https://tools.ietf.org/html/rfc1459#section-6.2).
- */
-enum class rpl {
-	/**
-	 * RPL_NONE
-	 *
-	 * Dummy reply number. Not used.
-	 */
-	none = 300,
-
-	/**
-	 * RPL_USERHOST
-	 *
-	 * ":[<reply>{<space><reply>}]"
-	 *
-	 * Reply format used by USERHOST to list replies to
-	 * the query list.  The reply string is composed as
-	 * follows:
-	 *
-	 * <reply> ::= <nick>['*'] '=' <'+'|'-'><hostname>
-	 *
-	 * The '*' indicates whether the client has registered
-	 * as an Operator.  The '-' or '+' characters represent
-	 * whether the client has set an AWAY message or not
-	 * respectively.
-	 */
-	userhost = 302,
-
-	/**
-	 * RPL_ISON
-	 *
-	 * ":[<nick> {<space><nick>}]"
-	 *
-	 * Reply format used by ISON to list replies to the
-	 * query list.
-	 */
-	ison = 303,
-
-	/**
-	 * RPL_AWAY
-	 *
-	 * "<nick> :<away message>"
-	 */
-	away = 301,
-
-	/**
-	 * RPL_UNAWAY
-	 *
-	 * ":You are no longer marked as being away"
-	 */
-	unaway = 305,
-
-	/**
-	 * RPL_NOWAWAY
-	 *
-	 * ":You have been marked as being away"
-	 *
-	 * These replies are used with the AWAY command (if
-	 * allowed).  RPL_AWAY is sent to any client sending a
-	 * PRIVMSG to a client which is away.  RPL_AWAY is only
-	 * sent by the server to which the client is connected.
-	 * Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the
-	 * client removes and sets an AWAY message.
-	 */
-	nowaway = 306,
-
-	/**
-	 * RPL_WHOISUSER
-	 *
-	 * "<nick> <user> <host> * :<real name>"
-	 */
-	whoisuser = 311,
-
-	/**
-	 * RPL_WHOISSERVER
-	 *
-	 * "<nick> <server> :<server info>"
-	 */
-	whoisserver = 312,
-
-	/**
-	 * RPL_WHOISOPERATOR
-	 *
-	 * "<nick> :is an IRC operator"
-	 */
-	whoisoperator = 313,
-
-	/**
-	 * RPL_WHOISIDLE
-	 *
-	 * "<nick> <integer> :seconds idle"
-	 */
-	whoisidle = 317,
-
-	/**
-	 * RPL_ENDOFWHOIS
-	 *
-	 * "<nick> :End of /WHOIS list"
-	 */
-	endofwhois = 318,
-
-	/**
-	 * RPL_WHOISCHANNELS
-	 *
-	 * "<nick> :{[@|+]<channel><space>}"
-	 *
-	 * Replies 311 - 313, 317 - 319 are all replies
-	 * generated in response to a WHOIS message.  Given that
-	 * there are enough parameters present, the answering
-	 * server must either formulate a reply out of the above
-	 * numerics (if the query nick is found) or return an
-	 * error reply.  The '*' in RPL_WHOISUSER is there as
-	 * the literal character and not as a wild card.  For
-	 * each reply set, only RPL_WHOISCHANNELS may appear
-	 * more than once (for long lists of channel names).
-	 * The '@' and '+' characters next to the channel name
-	 * indicate whether a client is a channel operator or
-	 * has been granted permission to speak on a moderated
-	 * channel.  The RPL_ENDOFWHOIS reply is used to mark
-	 * the end of processing a WHOIS message.
-	 */
-	whoischannels = 319,
-
-	/**
-	 * RPL_WHOWASUSER
-	 *
-	 * "<nick> <user> <host> * :<real name>"
-	 */
-	whowasuser = 314,
-
-	/**
-	 * RPL_ENDOFWHOWAS
-	 *
-	 * "<nick> :End of WHOWAS"
-	 *
-	 * When replying to a WHOWAS message, a server must use
-	 * the replies RPL_WHOWASUSER, RPL_WHOISSERVER or
-	 * ERR_WASNOSUCHNICK for each nickname in the presented
-	 * list.  At the end of all reply batches, there must
-	 * be RPL_ENDOFWHOWAS (even if there was only one reply
-	 * and it was an error).
-	 */
-	endofwhowas = 369,
-
-	/**
-	 * RPL_LISTSTART
-	 *
-	 * "Channel :Users  Name"
-	 */
-	liststart = 321,
-
-	/**
-	 * RPL_LIST
-	 *
-	 * "<channel> <# visible> :<topic>"
-	 */
-	list = 322,
-
-	/**
-	 * RPL_LISTEND
-	 *
-	 * ":End of /LIST"
-	 *
-	 * Replies RPL_LISTSTART, RPL_LIST, RPL_LISTEND mark
-	 * the start, actual replies with data and end of the
-	 * server's response to a LIST command.  If there are
-	 * no channels available to return, only the start
-	 * and end reply must be sent.
-	 */
-	listend = 323,
-
-	/**
-	 * RPL_CHANNELMODEIS
-	 *
-	 * "<channel> <mode> <mode params>"
-	 */
-	channelmodeis = 324,
-
-	/**
-	 * RPL_NOTOPIC
-	 *
-	 * "<channel> :No topic is set"
-	 */
-	notopic = 331,
-
-	/**
-	 * RPL_TOPIC
-	 *
-	 * "<channel> :<topic>"
-	 *
-	 * When sending a TOPIC message to determine the
-	 * channel topic, one of two replies is sent.  If
-	 * the topic is set, RPL_TOPIC is sent back else
-	 * RPL_NOTOPIC.
-	 */
-	topic = 332,
-
-	/**
-	 * RPL_INVITING
-	 *
-	 * "<channel> <nick>"
-	 *
-	 * Returned by the server to indicate that the
-	 * attempted INVITE message was successful and is
-	 * being passed onto the end client.
-	 */
-	inviting = 341,
-
-	/**
-	 * RPL_SUMMONING
-	 *
-	 * "<user> :Summoning user to IRC"
-	 *
-	 * Returned by a server answering a SUMMON message to
-	 * indicate that it is summoning that user.
-	 */
-	summoning = 342,
-
-	/**
-	 * RPL_VERSION
-	 *
-	 * "<version>.<debuglevel> <server> :<comments>"
-	 *
-	 * Reply by the server showing its version details.
-	 * The <version> is the version of the software being
-	 * used (including any patchlevel revisions) and the
-	 * <debuglevel> is used to indicate if the server is
-	 * running in "debug mode".
-	 *
-	 * The "comments" field may contain any comments about
-	 * the version or further version details.
-	 */
-	version = 351,
-
-	/**
-	 * RPL_WHOREPLY
-	 *
-	 * "<channel> <user> <host> <server> <nick> \
-	 *  <H|G>[*][@|+] :<hopcount> <real name>"
-	 */
-	whoreply = 352,
-
-	/**
-	 * RPL_ENDOFWHO
-	 *
-	 * "<name> :End of /WHO list"
-	 *
-	 * The RPL_WHOREPLY and RPL_ENDOFWHO pair are used
-	 * to answer a WHO message.  The RPL_WHOREPLY is only
-	 * sent if there is an appropriate match to the WHO
-	 * query.  If there is a list of parameters supplied
-	 * with a WHO message, a RPL_ENDOFWHO must be sent
-	 * after processing each list item with <name> being
-	 * the item.
-	 */
-	endofwho = 315,
-
-	/**
-	 * RPL_NAMREPLY
-	 *
-	 * "<channel> :[[@|+]<nick> [[@|+]<nick> [...]]]"
-	 */
-	namreply = 353,
-
-	/**
-	 * RPL_ENDOFNAMES
-	 *
-	 * "<channel> :End of /NAMES list"
-	 *
-	 * To reply to a NAMES message, a reply pair consisting
-	 * of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the
-	 * server back to the client.  If there is no channel
-	 * found as in the query, then only RPL_ENDOFNAMES is
-	 * returned.  The exception to this is when a NAMES
-	 * message is sent with no parameters and all visible
-	 * channels and contents are sent back in a series of
-	 * RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark
-	 * the end.
-	 */
-	endofnames = 366,
-
-	/**
-	 * RPL_LINKS
-	 *
-	 * "<mask> <server> :<hopcount> <server info>"
-	 */
-	links = 364,
-
-	/**
-	 * RPL_ENDOFLINKS
-	 *
-	 * "<mask> :End of /LINKS list"
-	 *
-	 * In replying to the LINKS message, a server must send
-	 * replies back using the RPL_LINKS numeric and mark the
-	 * end of the list using an RPL_ENDOFLINKS reply.
-	 */
-	endoflinks = 365,
-
-	/**
-	 * RPL_BANLIST
-	 *
-	 * "<channel> <banid>"
-	 */
-	banlist = 367,
-
-	/**
-	 * RPL_ENDOFBANLIST
-	 *
-	 * "<channel> :End of channel ban list"
-	 *
-	 * When listing the active 'bans' for a given channel,
-	 * a server is required to send the list back using the
-	 * RPL_BANLIST and RPL_ENDOFBANLIST messages.  A separate
-	 * RPL_BANLIST is sent for each active banid.  After the
-	 * banids have been listed (or if none present) a
-	 * RPL_ENDOFBANLIST must be sent.
-	 */
-	endofbanlist = 368,
-
-	/**
-	 * RPL_INFO
-	 *
-	 * ":<string>"
-	 */
-	info = 371,
-
-	/**
-	 * RPL_ENDOFINFO
-	 *
-	 * ":End of /INFO list"
-	 *
-	 * A server responding to an INFO message is required to
-	 * send all its 'info' in a series of RPL_INFO messages
-	 * with a RPL_ENDOFINFO reply to indicate the end of the
-	 * replies.
-	 */
-	endofinfo = 374,
-
-	/**
-	 * RPL_MOTDSTART
-	 *
-	 * ":- <server> Message of the day - "
-	 */
-	motdstart = 375,
-
-	/**
-	 * RPL_MOTD
-	 *
-	 * ":- <text>"
-	 */
-	motd = 372,
-
-	/**
-	 * RPL_ENDOFMOTD
-	 *
-	 * ":End of /MOTD command"
-	 *
-	 * When responding to the MOTD message and the MOTD file
-	 * is found, the file is displayed line by line, with
-	 * each line no longer than 80 characters, using
-	 * RPL_MOTD format replies.  These should be surrounded
-	 * by a RPL_MOTDSTART (before the RPL_MOTDs) and an
-	 * RPL_ENDOFMOTD (after).
-	 */
-	endofmotd = 376,
-
-	/**
-	 * RPL_YOUREOPER
-	 *
-	 * ":You are now an IRC operator"
-	 *
-	 * RPL_YOUREOPER is sent back to a client which has
-	 * just successfully issued an OPER message and gained
-	 * operator status.
-	 */
-	youreoper = 381,
-
-	/**
-	 * RPL_REHASHING
-	 *
-	 * "<config file> :Rehashing"
-	 *
-	 * If the REHASH option is used and an operator sends
-	 * a REHASH message, an RPL_REHASHING is sent back to
-	 * the operator.
-	 */
-	rehashing = 382,
-
-	/**
-	 * RPL_TIME
-	 *
-	 * "<server> :<string showing server's local time>"
-	 *
-	 * When replying to the TIME message, a server must send
-	 * the reply using the RPL_TIME format above.  The string
-	 * showing the time need only contain the correct day and
-	 * time there.  There is no further requirement for the
-	 * time string.
-	 */
-	time = 391,
-
-	/**
-	 * RPL_USERSSTART
-	 *
-	 * ":UserID   Terminal  Host"
-	 */
-	userstart = 392,
-
-	/**
-	 * RPL_USERS
-	 *
-	 * ":%-8s %-9s %-8s"
-	 */
-	users = 393,
-
-	/**
-	 * RPL_ENDOFUSERS
-	 *
-	 * ":End of users"
-	 */
-	endofusers = 394,
-
-	/**
-	 * RPL_NOUSERS
-	 *
-	 * ":Nobody logged in"
-	 *
-	 * If the USERS message is handled by a server, the
-	 * replies RPL_USERSTART, RPL_USERS, RPL_ENDOFUSERS and
-	 * RPL_NOUSERS are used.  RPL_USERSSTART must be sent
-	 * first, following by either a sequence of RPL_USERS
-	 * or a single RPL_NOUSER.  Following this is
-	 * RPL_ENDOFUSERS.
-	 */
-	nousers = 395,
-
-	/**
-	 * RPL_TRACELINK
-	 *
-	 * "Link <version & debug level> <destination> <next server>"
-	 */
-	tracelink = 200,
-
-	/**
-	 * RPL_TRACECONNECTING
-	 *
-	 * "Try. <class> <server>"
-	 */
-	traceconnecting = 201,
-
-	/**
-	 * RPL_TRACEHANDSHAKE
-	 *
-	 * "H.S. <class> <server>"
-	 */
-	tracehandshake = 202,
-
-	/**
-	 * RPL_TRACEUNKNOWN
-	 *
-	 * "???? <class> [<client IP address in dot form>]"
-	 */
-	traceunknown = 203,
-
-	/**
-	 * RPL_TRACEOPERATOR
-	 *
-	 * "Oper <class> <nick>"
-	 */
-	traceoperator = 204,
-
-	/**
-	 * RPL_TRACEUSER
-	 *
-	 * "User <class> <nick>"
-	 */
-	traceuser = 205,
-
-	/**
-	 * RPL_TRACESERVER
-	 *
-	 * "Serv <class> <int>S <int>C <server> \
-	 *  <nick!user|*!*>@<host|server>
-	 */
-	traceserver = 206,
-
-	/**
-	 * RPL_TRACENEWTYPE
-	 *
-	 * "<newtype> 0 <client name>"
-	 */
-	tracenewtype = 208,
-
-	/**
-	 * RPL_TRACELOG
-	 *
-	 * "File <logfile> <debug level>"
-	 *
-	 * The RPL_TRACE* are all returned by the server in
-	 * response to the TRACE message.  How many are
-	 * returned is dependent on the the TRACE message and
-	 * whether it was sent by an operator or not.  There
-	 * is no predefined order for which occurs first.
-	 * Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and
-	 * RPL_TRACEHANDSHAKE are all used for connections
-	 * which have not been fully established and are either
-	 * unknown, still attempting to connect or in the
-	 * process of completing the 'server handshake'.
-	 * RPL_TRACELINK is sent by any server which handles
-	 * a TRACE message and has to pass it on to another
-	 * server.  The list of RPL_TRACELINKs sent in
-	 * response to a TRACE command traversing the IRC
-	 * network should reflect the actual connectivity of
-	 * the servers themselves along that path.
-	 * RPL_TRACENEWTYPE is to be used for any connection
-	 * which does not fit in the other categories but is
-	 * being displayed anyway.
-	 */
-	tracelog = 261,
-
-	/**
-	 * RPL_STATSLINKINFO
-	 *
-	 * "<linkname> <sendq> <sent messages> \
-	 *  <sent bytes> <received messages> \
-	 *  <received bytes> <time open>"
-	 */
-	statslinkinfo = 211,
-
-	/**
-	 * RPL_STATSCOMMANDS
-	 *
-	 * "<command> <count>"
-	 */
-	statscommands = 212,
-
-	/**
-	 * RPL_STATSCLINE
-	 *
-	 * "C <host> * <name> <port> <class>"
-	 */
-	statscline = 213,
-
-	/**
-	 * RPL_STATSNLINE
-	 *
-	 * "N <host> * <name> <port> <class>"
-	 */
-	statsnline = 214,
-
-	/**
-	 * RPL_STATSILINE
-	 *
-	 * "I <host> * <host> <port> <class>"
-	 */
-	statsiline = 215,
-
-	/**
-	 * RPL_STATSKLINE
-	 *
-	 * K <host> * <username> <port> <class>"
-	 */
-	statskline = 216,
-
-	/**
-	 * RPL_STATSYLINE
-	 *
-	 * "Y <class> <ping frequency> <connect frequency> <max sendq>"
-	 */
-	statsyline = 218,
-
-	/**
-	 * RPL_ENDOFSTATS
-	 *
-	 * "<stats letter> :End of /STATS report"
-	 */
-	endofstats = 219,
-
-	/**
-	 * RPL_STATSLLINE
-	 *
-	 * "L <hostmask> * <servername> <maxdepth>"
-	 */
-	statslline = 241,
-
-	/**
-	 * RPL_STATSUPTIME
-	 *
-	 * ":Server Up %d days %d:%02d:%02d"
-	 */
-	statsuptime = 242,
-
-	/**
-	 * RPL_STATSOLINE
-	 *
-	 * "O <hostmask> * <name>"
-	 */
-	statsoline = 243,
-
-	/**
-	 * RPL_STATSHLINE
-	 *
-	 * "H <hostmask> * <servername>"
-	 */
-	statshline = 244,
-
-	/**
-	 * RPL_UMODEIS
-	 *
-	 * "<user mode string>"
-	 *
-	 * To answer a query about a client's own mode,
-	 * RPL_UMODEIS is sent back.
-	 */
-	umodeis = 221,
-
-	/**
-	 * RPL_LUSERCLIENT
-	 *
-	 * ":There are <integer> users and <integer> \
-	 *  invisible on <integer> servers"
-	 */
-	luserclient = 251,
-
-	/**
-	 * RPL_LUSEROP
-	 *
-	 * "<integer> :operator(s) online"
-	 */
-	luserop = 252,
-
-	/**
-	 * RPL_LUSERUNKNOWN
-	 *
-	 * "<integer> :unknown connection(s)"
-	 */
-	luserunknown = 253,
-
-	/**
-	 * RPL_LUSERCHANNELS
-	 *
-	 * "<integer> :channels formed"
-	 */
-	luserchannels = 254,
-
-	/**
-	 * RPL_LUSERME
-	 *
-	 * ":I have <integer> clients and <integer> servers"
-	 *
-	 * In processing an LUSERS message, the server
-	 * sends a set of replies from RPL_LUSERCLIENT,
-	 * RPL_LUSEROP, RPL_USERUNKNOWN,
-	 * RPL_LUSERCHANNELS and RPL_LUSERME.  When
-	 * replying, a server must send back
-	 * RPL_LUSERCLIENT and RPL_LUSERME.  The other
-	 * replies are only sent back if a non-zero count
-	 * is found for them.
-	 */
-	luserme = 255,
-
-	/**
-	 * RPL_ADMINME
-	 *
-	 * "<server> :Administrative info"
-	 */
-	adminme = 256,
-
-	/**
-	 * RPL_ADMINLOC1
-	 *
-	 * ":<admin info>"
-	 */
-	adminloc1 = 257,
-
-	/**
-	 * RPL_ADMINLOC2
-	 *
-	 * ":<admin info>"
-	 */
-	adminloc2 = 258,
-
-	/**
-	 * RPL_ADMINEMAIL
-	 *
-	 * ":<admin info>"
-	 *
-	 * When replying to an ADMIN message, a server
-	 * is expected to use replies RLP_ADMINME
-	 * through to RPL_ADMINEMAIL and provide a text
-	 * message with each.  For RPL_ADMINLOC1 a
-	 * description of what city, state and country
-	 * the server is in is expected, followed by
-	 * details of the university and department
-	 * (RPL_ADMINLOC2) and finally the administrative
-	 * contact for the server (an email address here
-	 * is required) in RPL_ADMINEMAIL.
-	 */
-	adminemail = 259
-};
-
-/**
- * \brief Describe a IRC message
- */
-struct message {
-	std::string prefix;             //!< optional prefix
-	std::string command;            //!< command (maybe string or code)
-	std::vector<std::string> args;  //!< parameters
-
-	/**
-	 * Check if the command is of the given enum number.
-	 *
-	 * \param e the code
-	 * \return true if command is a number and equals to e
-	 */
-	template <typename Enum>
-	auto is(Enum e) const noexcept -> bool
-	{
-		try {
-			return std::stoi(command) == static_cast<int>(e);
-		} catch (...) {
-			return false;
-		}
-	}
-
-	/**
-	 * Convenient function that returns an empty string if the nth argument is
-	 * not defined.
-	 *
-	 * \param index the index
-	 * \return a string or empty if out of bounds
-	 */
-	auto get(unsigned short index) const noexcept -> const std::string&;
-
-	/**
-	 * Tells if the message is a CTCP.
-	 *
-	 * \param index the param index (maybe out of bounds)
-	 * \return true if CTCP
-	 */
-	auto is_ctcp(unsigned short index) const noexcept -> bool;
-
-	/**
-	 * Parse a CTCP message.
-	 *
-	 * \pre is_ctcp(index)
-	 * \param index the param index
-	 * \return the CTCP command
-	 */
-	auto ctcp(unsigned short index) const -> std::string;
-
-	/**
-	 * Parse a IRC message.
-	 *
-	 * \param line the buffer content (without `\r\n`)
-	 * \return the message (maybe empty if line is empty)
-	 */
-	static auto parse(const std::string& line) -> message;
-};
-
-/**
- * \brief Describe a user.
- */
-struct user {
-	std::string nick;       //!< The nickname
-	std::string host;       //!< The hostname
-
-	/**
-	 * Parse a nick/host combination.
-	 *
-	 * \param line the line to parse
-	 * \return a user
-	 */
-	static auto parse(std::string_view line) -> user;
-};
-
-/**
- * \brief Abstract connection to a server.
- */
-class connection {
-public:
-	/**
-	 * Handler for connecting.
-	 */
-	using connect_handler = std::function<void (std::error_code)>;
-
-	/**
-	 * Handler for receiving.
-	 */
-	using recv_handler = std::function<void (std::error_code, message)>;
-
-	/**
-	 * Handler for sending.
-	 */
-	using send_handler = std::function<void (std::error_code)>;
-
-private:
-	boost::asio::io_context& service_;
-	boost::asio::ip::tcp::socket socket_{service_};
-	boost::asio::ip::tcp::resolver resolver_{service_};
-	boost::asio::streambuf input_{1024};
-	boost::asio::streambuf output_;
-
-	bool ipv4_{true};
-	bool ipv6_{true};
-	bool ssl_{false};
-
-#if defined(IRCCD_HAVE_SSL)
-	boost::asio::ssl::context context_{boost::asio::ssl::context::tlsv12};
-	boost::asio::ssl::stream<boost::asio::ip::tcp::socket&> ssl_socket_{socket_, context_};
-#endif
-
-#if !defined(NDEBUG)
-	bool is_connecting_{false};
-	bool is_receiving_{false};
-	bool is_sending_{false};
-#endif
-
-	void handshake(const connect_handler&);
-	void connect(const boost::asio::ip::tcp::resolver::results_type&, const connect_handler&);
-	void resolve(std::string_view, std::string_view, const connect_handler&);
-
-public:
-	/**
-	 * Default constructor.
-	 *
-	 * \param service the I/O service
-	 */
-	connection(boost::asio::io_service& service);
-
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~connection() = default;
-
-	/**
-	 * Enable IPv4
-	 *
-	 * \param enable true to enable
-	 */
-	void use_ipv4(bool enable = true) noexcept;
-
-	/**
-	 * Enable IPv6
-	 *
-	 * \param enable true to enable
-	 */
-	void use_ipv6(bool enable = true) noexcept;
-
-	/**
-	 * Enable TLS.
-	 *
-	 * \pre IRCCD_HAVE_SSL must be defined
-	 * \param enable true to enable
-	 */
-	void use_ssl(bool enable = true) noexcept;
-
-	/**
-	 * Connect to the host.
-	 *
-	 * \pre handler the handler
-	 * \pre another connect operation must not be running
-	 * \pre ipv4 or ipv6 must be set
-	 * \param hostname the hostname
-	 * \param service the service or port number
-	 * \param handler the non-null handler
-	 */
-	void connect(std::string_view hostname, std::string_view service, connect_handler handler);
-
-	/**
-	 * Start receiving data.
-	 *
-	 * The handler must not throw exceptions and `this` must be valid in the
-	 * lifetime of the handler.
-	 *
-	 * \pre another recv operation must not be running
-	 * \pre handler != nullptr
-	 * \param handler the handler to call
-	 */
-	void recv(recv_handler handler);
-
-	/**
-	 * Start sending data.
-	 *
-	 * The handler must not throw exceptions and `this` must be valid in the
-	 * lifetime of the handler.
-	 *
-	 * \pre another send operation must not be running
-	 * \pre handler != nullptr
-	 * \param message the raw message
-	 * \param handler the handler to call
-	 */
-	void send(std::string_view message, send_handler handler);
-};
-
-} // !irccd::irc
-
-#endif // !IRCCD_IRC_HPP
--- a/libirccd/irccd/daemon/irccd.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,324 +0,0 @@
-/*
- * irccd.cpp -- main irccd class
- *
- * Copyright (c) 2013-2018 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 <fstream>
-
-#include <boost/predef/os.h>
-
-#include <irccd/string_util.hpp>
-#include <irccd/system.hpp>
-
-#include "irccd.hpp"
-#include "logger.hpp"
-#include "plugin_service.hpp"
-#include "rule_service.hpp"
-#include "server_service.hpp"
-#include "transport_service.hpp"
-
-namespace irccd {
-
-namespace {
-
-class format_filter : public logger::filter {
-private:
-	std::string info_;
-	std::string warning_;
-	std::string debug_;
-
-	auto convert(const std::string&,
-	             std::string_view,
-	             std::string_view,
-	             std::string_view) const -> std::string;
-
-public:
-	format_filter(std::string info, std::string warning, std::string debug) noexcept;
-
-	auto pre_debug(std::string_view,
-	               std::string_view,
-	               std::string_view) const -> std::string override;
-
-	auto pre_info(std::string_view,
-	              std::string_view,
-	              std::string_view) const -> std::string override;
-
-	auto pre_warning(std::string_view,
-	                 std::string_view,
-	                 std::string_view) const -> std::string override;
-};
-
-auto format_filter::convert(const std::string& tmpl,
-                            std::string_view category,
-                            std::string_view component,
-                            std::string_view message) const -> std::string
-{
-	if (tmpl.empty())
-		return pre(category, component, message);
-
-	string_util::subst params;
-
-	params.flags &= ~(string_util::subst_flags::irc_attrs);
-	params.flags |= string_util::subst_flags::shell_attrs;
-	params.keywords.emplace("category", std::string(category));
-	params.keywords.emplace("component", std::string(component));
-	params.keywords.emplace("message", std::string(message));
-
-	return string_util::format(tmpl, params);
-}
-
-format_filter::format_filter(std::string info, std::string warning, std::string debug) noexcept
-	: info_(std::move(info))
-	, warning_(std::move(warning))
-	, debug_(std::move(debug))
-{
-}
-
-auto format_filter::pre_debug(std::string_view category,
-                              std::string_view component,
-                              std::string_view message) const -> std::string
-{
-	return convert(debug_, category, component, message);
-}
-
-auto format_filter::pre_info(std::string_view category,
-                             std::string_view component,
-                             std::string_view message) const -> std::string
-{
-	return convert(info_, category, component, message);
-}
-
-auto format_filter::pre_warning(std::string_view category,
-                                std::string_view component,
-                                std::string_view message) const -> std::string
-{
-	return convert(warning_, category, component, message);
-}
-
-} // !namespace
-
-void irccd::load_logs_file(const ini::section& sc)
-{
-	/*
-	 * TODO: improve that with CMake options.
-	 */
-#if BOOST_OS_WINDOWS
-	std::string normal = "log.txt";
-	std::string errors = "errors.txt";
-#else
-	std::string normal = "/var/log/irccd/log.txt";
-	std::string errors = "/var/log/irccd/errors.txt";
-#endif
-
-	ini::section::const_iterator it;
-
-	if ((it = sc.find("path-logs")) != sc.end())
-		normal = it->get_value();
-	if ((it = sc.find("path-errors")) != sc.end())
-		errors = it->get_value();
-
-	try {
-		sink_ = std::make_unique<logger::file_sink>(std::move(normal), std::move(errors));
-	} catch (const std::exception& ex) {
-		sink_->warning("logs", "") << ex.what() << std::endl;
-	}
-}
-
-void irccd::load_logs_syslog()
-{
-#if defined(IRCCD_HAVE_SYSLOG)
-	sink_ = std::make_unique<logger::syslog_sink>();
-#else
-	sink_->warning("logs", "") << "logs: syslog is not available on this platform" << std::endl;
-#endif // !IRCCD_HAVE_SYSLOG
-}
-
-void irccd::load_logs()
-{
-	const auto sc = config_.get("logs");
-
-	if (sc.empty())
-		return;
-
-	sink_->set_verbose(string_util::is_identifier(sc.get("verbose").get_value()));
-
-	const auto type = sc.get("type").get_value();
-
-	if (!type.empty()) {
-		// Console is the default, no test case.
-		if (type == "file")
-			load_logs_file(sc);
-		else if (type == "syslog")
-			load_logs_syslog();
-		else if (type != "console")
-			sink_->warning("logs", "") << "invalid log type '" << type << std::endl;
-	}
-}
-
-void irccd::load_formats()
-{
-	const auto sc = config_.get("format");
-
-	if (sc.empty())
-		return;
-
-	sink_->set_filter(std::make_unique<format_filter>(
-		sc.get("info").get_value(),
-		sc.get("warning").get_value(),
-		sc.get("debug").get_value()
-	));
-}
-
-irccd::irccd(boost::asio::io_service& service, std::string config)
-	: config_(std::move(config))
-	, service_(service)
-	, sink_(std::make_unique<logger::console_sink>())
-	, server_service_(std::make_unique<server_service>(*this))
-	, tpt_service_(std::make_unique<transport_service>(*this))
-	, rule_service_(std::make_unique<rule_service>(*this))
-	, plugin_service_(std::make_unique<plugin_service>(*this))
-{
-}
-
-irccd::~irccd() = default;
-
-auto irccd::get_config() const noexcept -> const config&
-{
-	return config_;
-}
-
-void irccd::set_config(config cfg) noexcept
-{
-	config_ = std::move(cfg);
-}
-
-auto irccd::get_service() const noexcept -> const boost::asio::io_service&
-{
-	return service_;
-}
-
-auto irccd::get_service() noexcept -> boost::asio::io_service&
-{
-	return service_;
-}
-
-auto irccd::get_log() const noexcept -> const logger::sink&
-{
-	return *sink_;
-}
-
-auto irccd::get_log() noexcept -> logger::sink&
-{
-	return *sink_;
-}
-
-auto irccd::servers() noexcept -> server_service&
-{
-	return *server_service_;
-}
-
-auto irccd::transports() noexcept -> transport_service&
-{
-	return *tpt_service_;
-}
-
-auto irccd::rules() noexcept -> rule_service&
-{
-	return *rule_service_;
-}
-
-auto irccd::plugins() noexcept -> plugin_service&
-{
-	return *plugin_service_;
-}
-
-void irccd::set_log(std::unique_ptr<logger::sink> sink) noexcept
-{
-	assert(sink);
-
-	sink_ = std::move(sink);
-}
-
-void irccd::load() noexcept
-{
-	/*
-	 * Order matters, please be careful when changing this.
-	 *
-	 * 1. Open logs as early as possible to use the defined outputs on any
-	 *    loading errors.
-	 */
-
-	// [logs] and [format] sections.
-	load_logs();
-	load_formats();
-
-	if (!loaded_)
-		sink_->info("irccd", "") << "loading configuration from " << config_.get_path() << std::endl;
-	else
-		sink_->info("irccd", "") << "reloading configuration" << std::endl;
-
-	if (!loaded_)
-		tpt_service_->load(config_);
-
-	server_service_->load(config_);
-	plugin_service_->load(config_);
-	rule_service_->load(config_);
-
-	// Mark as loaded.
-	loaded_ = true;
-}
-
-auto irccd_category() noexcept -> const std::error_category&
-{
-	static const class category : public std::error_category {
-	public:
-		auto name() const noexcept -> const char* override
-		{
-			return "irccd";
-		}
-
-		auto message(int e) const -> std::string override
-		{
-			switch (static_cast<irccd_error::error>(e)) {
-			case irccd_error::error::not_irccd:
-				return "daemon is not irccd instance";
-			case irccd_error::error::incompatible_version:
-				return "major version is incompatible";
-			case irccd_error::error::auth_required:
-				return "authentication is required";
-			case irccd_error::error::invalid_auth:
-				return "invalid authentication";
-			case irccd_error::error::invalid_message:
-				return "invalid message";
-			case irccd_error::error::invalid_command:
-				return "invalid command";
-			case irccd_error::error::incomplete_message:
-				return "command requires more arguments";
-			default:
-				return "no error";
-			}
-		}
-	} category;
-
-	return category;
-}
-
-auto make_error_code(irccd_error::error e) noexcept -> std::error_code
-{
-	return { static_cast<int>(e), irccd_category() };
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/irccd.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,260 +0,0 @@
-/*
- * irccd.hpp -- main irccd class
- *
- * Copyright (c) 2013-2018 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_DAEMON_IRCCD_HPP
-#define IRCCD_DAEMON_IRCCD_HPP
-
-/**
- * \file irccd.hpp
- * \brief Base class for irccd front end.
- */
-
-/**
- * \defgroup services Services
- * \brief All irccd services.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <memory>
-#include <system_error>
-
-#include <boost/asio/io_service.hpp>
-
-#include <irccd/config.hpp>
-
-/**
- * \brief Main irccd namespace
- */
-namespace irccd {
-
-namespace logger {
-
-class sink;
-
-} // !logger
-
-class plugin_service;
-class rule_service;
-class server_service;
-class transport_service;
-
-/**
- * \brief Irccd main instance.
- */
-class irccd {
-private:
-	// Configuration.
-	config config_;
-
-	// Main io service.
-	boost::asio::io_service& service_;
-
-	// Tells if the configuration has already been called.
-	bool loaded_{false};
-
-	// Custom logger.
-	std::unique_ptr<logger::sink> sink_;
-
-	// Services.
-	std::unique_ptr<server_service> server_service_;
-	std::unique_ptr<transport_service> tpt_service_;
-	std::unique_ptr<rule_service> rule_service_;
-	std::unique_ptr<plugin_service> plugin_service_;
-
-	// Not copyable and not movable because services have references to irccd.
-	irccd(const irccd&) = delete;
-	irccd(irccd&&) = delete;
-
-	void operator=(const irccd&) = delete;
-	void operator=(irccd&&) = delete;
-
-	// Load functions.
-	void load_logs_file(const ini::section&);
-	void load_logs_syslog();
-	void load_logs();
-	void load_formats();
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * This only create a barebone irccd instance.
-	 *
-	 * \param service the service
-	 * \param config the optional path to the configuration.
-	 * \see load_all
-	 * \see load_config
-	 */
-	irccd(boost::asio::io_service& service, std::string config = "");
-
-	/**
-	 * Default destructor.
-	 */
-	~irccd();
-
-	/**
-	 * Get the current configuration.
-	 *
-	 * \return the configuration
-	 */
-	auto get_config() const noexcept -> const config&;
-
-	/**
-	 * Set the configuration.
-	 *
-	 * \param cfg the new config
-	 */
-	void set_config(config cfg) noexcept;
-
-	/**
-	 * Get the underlying io service.
-	 *
-	 * \return the service
-	 */
-	auto get_service() const noexcept -> const boost::asio::io_service&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the service
-	 */
-	auto get_service() noexcept -> boost::asio::io_service&;
-
-	/**
-	 * Access the logger.
-	 *
-	 * \return the logger
-	 */
-	auto get_log() const noexcept -> const logger::sink&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the logger
-	 */
-	auto get_log() noexcept -> logger::sink&;
-
-	/**
-	 * Set the logger.
-	 *
-	 * \pre sink != nullptr
-	 * \param sink the new sink
-	 */
-	void set_log(std::unique_ptr<logger::sink> sink) noexcept;
-
-	/**
-	 * Access the server service.
-	 *
-	 * \return the service
-	 */
-	auto servers() noexcept -> server_service&;
-
-	/**
-	 * Access the transport service.
-	 *
-	 * \return the service
-	 */
-	auto transports() noexcept -> transport_service&;
-
-	/**
-	 * Access the rule service.
-	 *
-	 * \return the service
-	 */
-	auto rules() noexcept -> rule_service&;
-
-	/**
-	 * Access the plugin service.
-	 *
-	 * \return the service
-	 */
-	auto plugins() noexcept -> plugin_service&;
-
-	/**
-	 * Load and re-apply the configuration to the daemon.
-	 */
-	void load() noexcept;
-};
-
-/**
- * \brief Irccd error.
- */
-class irccd_error : public std::system_error {
-public:
-	/**
-	 * \brief Irccd related errors.
-	 */
-	enum error {
-		//!< No error.
-		no_error = 0,
-
-		//!< The connected peer is not irccd.
-		not_irccd,
-
-		//!< The irccd version is too different.
-		incompatible_version,
-
-		//!< Authentication was required but not issued.
-		auth_required,
-
-		//!< Authentication was invalid.
-		invalid_auth,
-
-		//!< The message was not a valid JSON object.
-		invalid_message,
-
-		//!< The specified command does not exist.
-		invalid_command,
-
-		//!< The specified command requires more arguments.
-		incomplete_message,
-	};
-
-	/**
-	 * Inherited constructors.
-	 */
-	using system_error::system_error;
-};
-
-/**
- * Get the irccd error category singleton.
- *
- * \return the singleton
- */
-auto irccd_category() noexcept -> const std::error_category&;
-
-/**
- * Create a std::error_code from irccd_error::error enum.
- *
- * \param e the error code
- * \return the error code
- */
-auto make_error_code(irccd_error::error e) noexcept -> std::error_code;
-
-} // !irccd
-
-namespace std {
-
-template <>
-struct is_error_code_enum<irccd::irccd_error::error> : public std::true_type {
-};
-
-} // !std
-
-#endif // !IRCCD_DAEMON_IRCCD_HPP
--- a/libirccd/irccd/daemon/logger.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
-/*
- * logger.cpp -- irccd logging
- *
- * Copyright (c) 2013-2018 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 <cassert>
-#include <fstream>
-#include <iostream>
-#include <streambuf>
-
-#include "logger.hpp"
-
-#if defined(IRCCD_HAVE_SYSLOG)
-#	include <syslog.h>
-#endif // !IRCCD_HAVE_SYSLOG
-
-namespace irccd::logger {
-
-void logger::debug(const std::string& line)
-{
-	// Print only in debug mode, the buffer is flushed anyway.
-#if !defined(NDEBUG)
-	parent_.write_debug(parent_.filter_->pre_debug(category_, component_, line));
-#else
-	(void)line;
-#endif
-}
-
-void logger::info(const std::string& line)
-{
-	// Print only if verbose, the buffer will be flushed anyway.
-	if (parent_.verbose_)
-		parent_.write_info(parent_.filter_->pre_info(category_, component_, line));
-}
-
-void logger::warning(const std::string& line)
-{
-	parent_.write_warning(parent_.filter_->pre_warning(category_, component_, line));
-}
-
-logger::logger(sink& parent, level level, std::string_view category, std::string_view component) noexcept
-	: std::ostream(this)
-	, level_(level)
-	, parent_(parent)
-	, category_(category)
-	, component_(component)
-{
-	assert(level >= level::debug && level <= level::warning);
-}
-
-int logger::sync()
-{
-	std::string buffer = str();
-	std::string::size_type pos;
-
-	while ((pos = buffer.find("\n")) != std::string::npos) {
-		auto line = buffer.substr(0, pos);
-
-		// Remove this line.
-		buffer.erase(buffer.begin(), buffer.begin() + pos + 1);
-
-		switch (level_) {
-		case level::debug:
-			debug(line);
-			break;
-		case level::info:
-			info(line);
-			break;
-		case level::warning:
-			warning(line);
-			break;
-		default:
-			break;
-		}
-	}
-
-	str(buffer);
-
-	return 0;
-}
-
-void console_sink::write_info(const std::string& line)
-{
-	std::cout << line << std::endl;
-}
-
-void console_sink::write_warning(const std::string& line)
-{
-	std::cerr << line << std::endl;
-}
-
-void console_sink::write_debug(const std::string& line)
-{
-	std::cout << line << std::endl;
-}
-
-file_sink::file_sink(std::string normal, std::string errors)
-	: output_normal_(std::move(normal))
-	, output_error_(std::move(errors))
-{
-}
-
-void file_sink::write_info(const std::string& line)
-{
-	std::ofstream(output_normal_, std::ofstream::out | std::ofstream::app) << line << std::endl;
-}
-
-void file_sink::write_warning(const std::string& line)
-{
-	std::ofstream(output_error_, std::ofstream::out | std::ofstream::app) << line << std::endl;
-}
-
-void file_sink::write_debug(const std::string& line)
-{
-	std::ofstream(output_normal_, std::ofstream::out | std::ofstream::app) << line << std::endl;
-}
-
-void silent_sink::write_info(const std::string&)
-{
-}
-
-void silent_sink::write_warning(const std::string&)
-{
-}
-
-void silent_sink::write_debug(const std::string&)
-{
-}
-
-#if defined(IRCCD_HAVE_SYSLOG)
-
-syslog_sink::syslog_sink()
-{
-	openlog("irccd", LOG_PID, LOG_DAEMON);
-}
-
-syslog_sink::~syslog_sink()
-{
-	closelog();
-}
-
-void syslog_sink::write_info(const std::string& line)
-{
-	syslog(LOG_INFO | LOG_USER, "%s", line.c_str());
-}
-
-void syslog_sink::write_warning(const std::string& line)
-{
-	syslog(LOG_WARNING | LOG_USER, "%s", line.c_str());
-}
-
-void syslog_sink::write_debug(const std::string& line)
-{
-	syslog(LOG_DEBUG | LOG_USER, "%s", line.c_str());
-}
-
-#endif // !IRCCD_HAVE_SYSLOG
-
-sink::sink()
-	: filter_(new filter)
-{
-}
-
-auto sink::is_verbose() const noexcept -> bool
-{
-	return verbose_;
-}
-
-void sink::set_verbose(bool mode) noexcept
-{
-	verbose_ = mode;
-}
-
-void sink::set_filter(std::unique_ptr<filter> filter) noexcept
-{
-	assert(filter);
-
-	filter_ = std::move(filter);
-}
-
-auto sink::info(std::string_view category, std::string_view component) -> logger
-{
-	return logger(*this, logger::level::info, category, component);;
-}
-
-auto sink::warning(std::string_view category, std::string_view component) -> logger
-{
-	return logger(*this, logger::level::warning, category, component);;
-}
-
-auto sink::debug(std::string_view category, std::string_view component) -> logger
-{
-	return logger(*this, logger::level::debug, category, component);;
-}
-
-auto filter::pre(std::string_view category,
-                 std::string_view component,
-                 std::string_view message) const -> std::string
-{
-	std::ostringstream oss;
-
-	oss << category;
-
-	if (!component.empty())
-		oss << " " << component;
-
-	oss << ": ";
-	oss << message;
-
-	return oss.str();
-}
-
-auto filter::pre_debug(std::string_view category,
-                       std::string_view component,
-                       std::string_view message) const -> std::string
-{
-	return pre(category, component, message);
-}
-
-auto filter::pre_info(std::string_view category,
-                      std::string_view component,
-                      std::string_view message) const -> std::string
-{
-	return pre(category, component, message);
-}
-
-auto filter::pre_warning(std::string_view category,
-                         std::string_view component,
-                         std::string_view message) const -> std::string
-{
-	return pre(category, component, message);
-}
-
-} // !irccd::logger
--- a/libirccd/irccd/daemon/logger.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,474 +0,0 @@
-/*
- * logger.hpp -- irccd logging
- *
- * Copyright (c) 2013-2018 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_DAEMON_LOGGER_HPP
-#define IRCCD_DAEMON_LOGGER_HPP
-
-/**
- * \file logger.hpp
- * \brief Logging facilities.
- */
-
-/**
- * \brief Irccd logging system.
- * \defgroup logger Loggers
- */
-
-/**
- * \brief Predefined logger sinks.
- * \defgroup logger-sinks Predefined logger sinks
- * \ingroup logger
- */
-
-/**
- * \brief Specialized loggable traits.
- * \defgroup logger-traits Specialized loggable traits
- * \ingroup logger
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <memory>
-#include <sstream>
-#include <string>
-#include <string_view>
-#include <utility>
-
-namespace irccd::logger {
-
-class filter;
-class sink;
-
-/**
- * \brief Traits for loggable objects.
- * \ingroup logger-traits
- *
- * Specialize this structure and add the following static functions to be able
- * to log object with convenience:
- *
- * ## get_category
- *
- * The get_category function should return a single word that describe the
- * message entry category.
- *
- * Synopsis:
- *
- * ```cpp
- * static auto get_category(const T&) noexcept -> std::string_view;
- * ```
- *
- * ## get_component
- *
- * The get_component function should return the identifier or any valid
- * information about the given object that is useful for the user.
- *
- * If no information could be provided, an empty string can be returned.
- *
- * Synopsis:
- *
- * ```cpp
- * static auto get_component(const T&) noexcept -> std::string_view;
- * ```
- */
-template <typename T>
-struct loggable_traits;
-
-/**
- * \brief Logger object.
- * \ingroup logger
- */
-class logger : public std::ostream, public std::stringbuf {
-private:
-	/**
-	 * \brief Make sink friend.
-	 */
-	friend class sink;
-
-	enum class level {
-		debug,
-		info,
-		warning
-	} level_;
-
-	sink& parent_;
-
-	std::string_view category_;
-	std::string_view component_;
-
-	void debug(const std::string&);
-	void info(const std::string&);
-	void warning(const std::string&);
-	auto sync() -> int override;
-	logger(sink&, level, std::string_view, std::string_view) noexcept;
-};
-
-/**
- * \brief Interface to implement new logger mechanisms.
- * \ingroup logger-sinks
- *
- * Derive from this class and implement write_info, write_warning and
- * write_debug functions.
- *
- * \see file_sink
- * \see console_sink
- * \see syslog_sink
- * \see silent_sink
- */
-class sink {
-private:
-	/**
-	 * \brief Make logger friend.
-	 */
-	friend class logger;
-
-	// User options.
-	bool verbose_{false};
-	std::unique_ptr<filter> filter_;
-
-protected:
-	/**
-	 * Write a debug message.
-	 *
-	 * This function is called only if NDEBUG is not defined.
-	 *
-	 * \param line the data
-	 * \see log::debug
-	 */
-	virtual void write_debug(const std::string& line) = 0;
-
-	/**
-	 * Write a information message.
-	 *
-	 * The function is called only if verbose is true.
-	 *
-	 * \param line the data
-	 * \see log::info
-	 */
-	virtual void write_info(const std::string& line) = 0;
-
-	/**
-	 * Write an error message.
-	 *
-	 * This function is always called.
-	 *
-	 * \param line the data
-	 * \see log::warning
-	 */
-	virtual void write_warning(const std::string& line) = 0;
-
-public:
-	/**
-	 * Default constructor.
-	 */
-	sink();
-
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~sink() = default;
-
-	/**
-	 * Tells if logger is verbose.
-	 *
-	 * \return true if verbose
-	 */
-	auto is_verbose() const noexcept -> bool;
-
-	/**
-	 * Set the verbosity mode.
-	 *
-	 * \param mode the new mode
-	 */
-	void set_verbose(bool mode) noexcept;
-
-	/**
-	 * Set an optional filter.
-	 *
-	 * \pre filter must not be null
-	 * \param filter the filter
-	 */
-	void set_filter(std::unique_ptr<filter> filter) noexcept;
-
-	/**
-	 * Get the stream for informational messages.
-	 *
-	 * If message is specified, a new line character is appended.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \return the output stream
-	 * \note Has no effect if verbose is set to false.
-	 */
-	auto info(std::string_view category, std::string_view component) -> logger;
-
-	/**
-	 * Convenient function with loggable objects.
-	 *
-	 * \param loggable the loggable object
-	 * \return the output stream
-	 * \see loggable_traits
-	 */
-	template <typename Loggable>
-	auto info(const Loggable& loggable) -> logger
-	{
-		return info(
-			loggable_traits<Loggable>::get_category(loggable),
-			loggable_traits<Loggable>::get_component(loggable)
-		);
-	}
-
-	/**
-	 * Get the stream for warnings.
-	 *
-	 * If message is specified, a new line character is appended.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \return the output stream
-	 */
-	auto warning(std::string_view category, std::string_view component) -> logger;
-
-	/**
-	 * Convenient function with loggable objects.
-	 *
-	 * \param loggable the loggable object
-	 * \return the output stream
-	 * \see loggable_traits
-	 */
-	template <typename Loggable>
-	auto warning(const Loggable& loggable) -> logger
-	{
-		return warning(
-			loggable_traits<Loggable>::get_category(loggable),
-			loggable_traits<Loggable>::get_component(loggable)
-		);
-	}
-
-	/**
-	 * Get the stream for debug messages.
-	 *
-	 * If message is specified, a new line character is appended.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \return the output stream
-	 * \note Has no effect if compiled in release mode.
-	 */
-	auto debug(std::string_view category, std::string_view component) -> logger;
-
-	/**
-	 * Convenient function with loggable objects.
-	 *
-	 * \param loggable the loggable object
-	 * \return the output stream
-	 * \see loggable_traits
-	 */
-	template <typename Loggable>
-	auto debug(const Loggable& loggable) -> logger
-	{
-		return debug(
-			loggable_traits<Loggable>::get_category(loggable),
-			loggable_traits<Loggable>::get_component(loggable)
-		);
-	}
-};
-
-/**
- * \brief Filter messages before printing them.
- * \ingroup logger
- */
-class filter {
-private:
-public:
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~filter() = default;
-
-	/**
-	 * Default function called for each virtual ones.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \param message the message
-	 * \return default formatted message
-	 */
-	auto pre(std::string_view category,
-	         std::string_view component,
-	         std::string_view message) const -> std::string;
-
-
-	/**
-	 * Update the debug message.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \param message the message
-	 * \return the message
-	 */
-	virtual auto pre_debug(std::string_view category,
-	                       std::string_view component,
-	                       std::string_view message) const -> std::string;
-
-	/**
-	 * Update the information message.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \param message the message
-	 * \return the updated message
-	 */
-	virtual auto pre_info(std::string_view category,
-	                      std::string_view component,
-	                      std::string_view message) const -> std::string;
-
-	/**
-	 * Update the warning message.
-	 *
-	 * \param category the category subsystem
-	 * \param component the optional component
-	 * \param message the message
-	 * \return the updated message
-	 */
-	virtual auto pre_warning(std::string_view category,
-	                         std::string_view component,
-	                         std::string_view message) const -> std::string;
-};
-
-/**
- * \brief Logger implementation for console output using std::cout and
- *        std::cerr.
- * \ingroup logger-sinks
- */
-class console_sink : public sink {
-protected:
-	/**
-	 * \copydoc sink::write_debug
-	 */
-	void write_debug(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_info
-	 */
-	void write_info(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_warning
-	 */
-	void write_warning(const std::string& line) override;
-};
-
-/**
- * \brief Output to a files.
- * \ingroup logger-sinks
- */
-class file_sink : public sink {
-private:
-	std::string output_normal_;
-	std::string output_error_;
-
-protected:
-	/**
-	 * \copydoc sink::write_debug
-	 */
-	void write_debug(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_info
-	 */
-	void write_info(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_warning
-	 */
-	void write_warning(const std::string& line) override;
-
-public:
-	/**
-	 * Outputs to files.
-	 *
-	 * \param normal the path to the normal logs
-	 * \param errors the path to the errors logs
-	 */
-	file_sink(std::string normal, std::string errors);
-};
-
-/**
- * \brief Use to disable logs.
- * \ingroup logger-sinks
- *
- * Useful for unit tests when some classes may emits log.
- */
-class silent_sink : public sink {
-protected:
-	/**
-	 * \copydoc sink::write_debug
-	 */
-	void write_debug(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_info
-	 */
-	void write_info(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_warning
-	 */
-	void write_warning(const std::string& line) override;
-};
-
-#if defined(IRCCD_HAVE_SYSLOG)
-
-/**
- * \brief Implements logger into syslog.
- * \ingroup logger-sinks
- */
-class syslog_sink : public sink {
-protected:
-	/**
-	 * \copydoc sink::write_debug
-	 */
-	void write_debug(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_info
-	 */
-	void write_info(const std::string& line) override;
-
-	/**
-	 * \copydoc sink::write_warning
-	 */
-	void write_warning(const std::string& line) override;
-
-public:
-	/**
-	 * Open the syslog.
-	 */
-	syslog_sink();
-
-	/**
-	 * Close the syslog.
-	 */
-	~syslog_sink();
-};
-
-#endif // !IRCCD_HAVE_SYSLOG
-
-} // !irccd::logger
-
-#endif // !IRCCD_DAEMON_LOGGER_HPP
--- a/libirccd/irccd/daemon/plugin.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,277 +0,0 @@
-/*
- * plugin.cpp -- irccd JavaScript plugin interface
- *
- * Copyright (c) 2013-2018 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 <cassert>
-#include <sstream>
-
-#include <boost/filesystem.hpp>
-
-#include <irccd/system.hpp>
-#include <irccd/string_util.hpp>
-
-#include "plugin.hpp"
-
-namespace irccd {
-
-plugin::plugin(std::string id) noexcept
-	: id_(std::move(id))
-{
-	assert(string_util::is_identifier(id_));
-}
-
-auto plugin::get_id() const noexcept -> const std::string&
-{
-	return id_;
-}
-
-auto plugin::get_author() const noexcept -> std::string_view
-{
-	return "unknown";
-}
-
-auto plugin::get_license() const noexcept -> std::string_view
-{
-	return "unknown";
-}
-
-auto plugin::get_summary() const noexcept -> std::string_view
-{
-	return "unknown";
-}
-
-auto plugin::get_version() const noexcept -> std::string_view
-{
-	return "unknown";
-}
-
-auto plugin::get_options() const -> map
-{
-	return {};
-}
-
-void plugin::set_options(const map&)
-{
-}
-
-auto plugin::get_formats() const -> map
-{
-	return {};
-}
-
-void plugin::set_formats(const map&)
-{
-}
-
-auto plugin::get_paths() const -> map
-{
-	return {};
-}
-
-void plugin::set_paths(const map&)
-{
-}
-
-void plugin::handle_command(irccd&, const message_event&)
-{
-}
-
-void plugin::handle_connect(irccd&, const connect_event&)
-{
-}
-
-void plugin::handle_disconnect(irccd&, const disconnect_event&)
-{
-}
-
-void plugin::handle_invite(irccd&, const invite_event&)
-{
-}
-
-void plugin::handle_join(irccd&, const join_event&)
-{
-}
-
-void plugin::handle_kick(irccd&, const kick_event&)
-{
-}
-
-void plugin::handle_load(irccd&)
-{
-}
-
-void plugin::handle_message(irccd&, const message_event&)
-{
-}
-
-void plugin::handle_me(irccd&, const me_event&)
-{
-}
-
-void plugin::handle_mode(irccd&, const mode_event&)
-{
-}
-
-void plugin::handle_names(irccd&, const names_event&)
-{
-}
-
-void plugin::handle_nick(irccd&, const nick_event&)
-{
-}
-
-void plugin::handle_notice(irccd&, const notice_event&)
-{
-}
-
-void plugin::handle_part(irccd&, const part_event&)
-{
-}
-
-void plugin::handle_reload(irccd&)
-{
-}
-
-void plugin::handle_topic(irccd&, const topic_event&)
-{
-}
-
-void plugin::handle_unload(irccd&)
-{
-}
-
-void plugin::handle_whois(irccd&, const whois_event&)
-{
-}
-
-plugin_loader::plugin_loader(std::vector<std::string> directories,
-                             std::vector<std::string> extensions) noexcept
-	: directories_(std::move(directories))
-	, extensions_(std::move(extensions))
-{
-}
-
-auto plugin_loader::is_supported(std::string_view path) noexcept -> bool
-{
-	const std::string name(path);
-
-	for (const auto& ext : extensions_)
-		if (boost::filesystem::path(name).extension() == ext)
-			return true;
-
-	/*
-	 * If extensions are not specified, let plugin_loader::open a chance
-	 * to be called anyway.
-	 */
-	return extensions_.empty();
-}
-
-auto plugin_loader::find(std::string_view name) -> std::shared_ptr<plugin>
-{
-	std::vector<std::string> filenames;
-
-	if (directories_.empty())
-		filenames = sys::plugin_filenames(std::string(name), extensions_);
-	else {
-		for (const auto& dir : directories_)
-			for (const auto& ext : extensions_)
-				filenames.push_back(dir + std::string("/") + std::string(name) + ext);
-	}
-
-	for (const auto& candidate : filenames) {
-		boost::system::error_code ec;
-
-		if (!boost::filesystem::exists(candidate, ec) || ec)
-			continue;
-
-		auto plugin = open(name, candidate);
-
-		if (plugin)
-			return plugin;
-	}
-
-	return nullptr;
-}
-
-plugin_error::plugin_error(error errc, std::string_view name, std::string_view message)
-	: system_error(make_error_code(errc))
-	, name_(std::move(name))
-	, message_(std::move(message))
-{
-	std::ostringstream oss;
-
-	oss << code().message();
-
-	std::istringstream iss(message_);
-	std::string line;
-
-	while (getline(iss, line))
-		oss << "\n" << line;
-
-	what_ = oss.str();
-}
-
-auto plugin_error::get_name() const noexcept -> const std::string&
-{
-	return name_;
-}
-
-auto plugin_error::get_message() const noexcept -> const std::string&
-{
-	return message_;
-}
-
-auto plugin_error::what() const noexcept -> const char*
-{
-	return what_.c_str();
-}
-
-auto plugin_category() -> const std::error_category&
-{
-	static const class category : public std::error_category {
-	public:
-		auto name() const noexcept -> const char* override
-		{
-			return "plugin";
-		}
-
-		auto message(int e) const -> std::string override
-		{
-			switch (static_cast<plugin_error::error>(e)) {
-			case plugin_error::not_found:
-				return "plugin not found";
-			case plugin_error::invalid_identifier:
-				return "invalid plugin identifier";
-			case plugin_error::exec_error:
-				return "plugin exec error";
-			case plugin_error::already_exists:
-				return "plugin already exists";
-			default:
-				return "no error";
-			}
-		}
-	} category;
-
-	return category;
-}
-
-auto make_error_code(plugin_error::error e) -> std::error_code
-{
-	return { static_cast<int>(e), plugin_category() };
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/plugin.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,485 +0,0 @@
-/*
- * plugin.hpp -- irccd JavaScript plugin interface
- *
- * Copyright (c) 2013-2018 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_DAEMON_PLUGIN_HPP
-#define IRCCD_DAEMON_PLUGIN_HPP
-
-/**
- * \file plugin.hpp
- * \brief irccd plugins
- */
-
-/**
- * \defgroup plugins Plugins
- * \brief Plugin management.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <memory>
-#include <string>
-#include <string_view>
-#include <system_error>
-#include <unordered_map>
-#include <vector>
-
-namespace irccd {
-
-class irccd;
-
-struct connect_event;
-struct disconnect_event;
-struct invite_event;
-struct join_event;
-struct kick_event;
-struct me_event;
-struct message_event;
-struct mode_event;
-struct names_event;
-struct nick_event;
-struct notice_event;
-struct part_event;
-struct topic_event;
-struct whois_event;
-
-/**
- * \ingroup plugins
- * \brief Abstract plugin.
- *
- * A plugin is identified by name and can be loaded and unloaded at runtime.
- */
-class plugin : public std::enable_shared_from_this<plugin> {
-public:
-	/**
-	 * Map for key/value pairs.
-	 *
-	 * Used in options, formats and paths.
-	 */
-	using map = std::unordered_map<std::string, std::string>;
-
-private:
-	std::string id_;
-
-public:
-	/**
-	 * Construct a plugin.
-	 *
-	 * \pre id must be a valid identifier
-	 * \param id the plugin id
-	 */
-	plugin(std::string id) noexcept;
-
-	/**
-	 * Temporary, close all timers.
-	 */
-	virtual ~plugin() = default;
-
-	/**
-	 * Get user unique id.
-	 *
-	 * \return the plugin id
-	 */
-	auto get_id() const noexcept -> const std::string&;
-
-	/**
-	 * Get the plugin name.
-	 *
-	 * \return the plugin name
-	 */
-	virtual auto get_name() const noexcept -> std::string_view = 0;
-
-	/**
-	 * Get the author.
-	 *
-	 * \return the author
-	 */
-	virtual auto get_author() const noexcept -> std::string_view;
-
-	/**
-	 * Get the license.
-	 *
-	 * \return the license
-	 */
-	virtual auto get_license() const noexcept -> std::string_view;
-
-	/**
-	 * Get the summary.
-	 *
-	 * \return the summary
-	 */
-	virtual auto get_summary() const noexcept -> std::string_view;
-
-	/**
-	 * Get the version.
-	 *
-	 * \return the version
-	 */
-	virtual auto get_version() const noexcept -> std::string_view;
-
-	/**
-	 * Get all options.
-	 *
-	 * \return options
-	 */
-	virtual auto get_options() const -> map;
-
-	/**
-	 * Set all options.
-	 *
-	 * \param map the options
-	 */
-	virtual void set_options(const map& map);
-
-	/**
-	 * Get all formats.
-	 *
-	 * \return formats
-	 */
-	virtual auto get_formats() const -> map;
-
-	/**
-	 * Set all formats.
-	 *
-	 * \param map the formats
-	 */
-	virtual void set_formats(const map& map);
-
-	/**
-	 * Get all paths.
-	 *
-	 * \return paths
-	 */
-	virtual auto get_paths() const -> map;
-
-	/**
-	 * Set all paths.
-	 *
-	 * \param map the paths
-	 */
-	virtual void set_paths(const map& map);
-
-	/**
-	 * On channel message. This event will call onMessage or
-	 * onCommand if the messages starts with the command character
-	 * plus the plugin name.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_command(irccd& irccd, const message_event& event);
-
-	/**
-	 * On successful connection.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_connect(irccd& irccd, const connect_event& event);
-
-	/**
-	 * On disconnection.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_disconnect(irccd& irccd, const disconnect_event& event);
-
-	/**
-	 * On invitation.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_invite(irccd& irccd, const invite_event& event);
-
-	/**
-	 * On join.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_join(irccd& irccd, const join_event& event);
-
-	/**
-	 * On kick.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_kick(irccd& irccd, const kick_event& event);
-
-	/**
-	 * On load.
-	 *
-	 * \param irccd the irccd instance
-	 */
-	virtual void handle_load(irccd& irccd);
-
-	/**
-	 * On channel message.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_message(irccd& irccd, const message_event& event);
-
-	/**
-	 * On CTCP Action.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_me(irccd& irccd, const me_event& event);
-
-	/**
-	 * On user mode change.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_mode(irccd& irccd, const mode_event& event);
-
-	/**
-	 * On names listing.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_names(irccd& irccd, const names_event& event);
-
-	/**
-	 * On nick change.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_nick(irccd& irccd, const nick_event& event);
-
-	/**
-	 * On user notice.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_notice(irccd& irccd, const notice_event& event);
-
-	/**
-	 * On part.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_part(irccd& irccd, const part_event& event);
-
-	/**
-	 * On reload.
-	 *
-	 * \param irccd the irccd instance
-	 */
-	virtual void handle_reload(irccd& irccd);
-
-	/**
-	 * On topic change.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_topic(irccd& irccd, const topic_event& event);
-
-	/**
-	 * On unload.
-	 *
-	 * \param irccd the irccd instance
-	 */
-	virtual void handle_unload(irccd& irccd);
-
-	/**
-	 * On whois information.
-	 *
-	 * \param irccd the irccd instance
-	 * \param event the event
-	 */
-	virtual void handle_whois(irccd& irccd, const whois_event& event);
-};
-
-/**
- * \ingroup plugins
- * \brief Abstract interface for searching plugins.
- *
- * This class is used to make loading of plugins extensible, the plugin_service
- * knows some predefined plugins loaders and use them to search for available
- * plugins.
- *
- * This makes easier to implement new plugins or new ways of loading them.
- *
- * \see dynlib_plugin_loader
- * \see js_plugin_loader
- */
-class plugin_loader {
-private:
-	std::vector<std::string> directories_;
-	std::vector<std::string> extensions_;
-
-public:
-	/**
-	 * Construct the loader with a predefined set of directories and
-	 * extensions.
-	 *
-	 * If directories is not specified, a sensible default list of system
-	 * and user paths are searched.
-	 *
-	 * \pre !extensions.empty()
-	 * \param directories optional list of directories to search
-	 * \param extensions optional list of extensions
-	 */
-	plugin_loader(std::vector<std::string> directories,
-	              std::vector<std::string> extensions) noexcept;
-
-	/**
-	 * Virtual destructor defaulted.
-	 */
-	virtual ~plugin_loader() = default;
-
-	/**
-	 * Tells if the plugin should be opened by checking file extension.
-	 *
-	 * \param path the path
-	 * \return true if the extension matches
-	 */
-	virtual auto is_supported(std::string_view path) noexcept -> bool;
-
-	/**
-	 * Try to open the plugin specified by path.
-	 *
-	 * The implementation must test if the plugin is suitable for opening, by
-	 * testing extension for example.
-	 *
-	 * \param id the plugin identifier
-	 * \param file the file path
-	 * \return the plugin
-	 * \throw plugin_error on errors
-	 */
-	virtual auto open(std::string_view id, std::string_view file) -> std::shared_ptr<plugin> = 0;
-
-	/**
-	 * Search for a plugin named by this id.
-	 *
-	 * \param id the plugin id
-	 * \return the plugin
-	 * \throw plugin_error on errors
-	 */
-	virtual auto find(std::string_view id) -> std::shared_ptr<plugin>;
-};
-
-/**
- * \ingroup plugins
- * \brief Plugin error.
- */
-class plugin_error : public std::system_error {
-public:
-	/**
-	 * \brief Plugin related errors.
-	 */
-	enum error {
-		//!< No error.
-		no_error = 0,
-
-		//!< The specified identifier is invalid.
-		invalid_identifier,
-
-		//!< The specified plugin is not found.
-		not_found,
-
-		//!< The plugin was unable to run the function.
-		exec_error,
-
-		//!< The plugin is already loaded.
-		already_exists,
-	};
-
-private:
-	std::string name_;
-	std::string message_;
-	std::string what_;
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param code the error code
-	 * \param name the plugin name
-	 * \param message the optional message (e.g. error from plugin)
-	 */
-	plugin_error(error code, std::string_view name = "", std::string_view message = "");
-
-	/**
-	 * Get the plugin name.
-	 *
-	 * \return the name
-	 */
-	auto get_name() const noexcept -> const std::string&;
-
-	/**
-	 * Get the additional message.
-	 *
-	 * \return the message
-	 */
-	auto get_message() const noexcept -> const std::string&;
-
-	/**
-	 * Get message appropriate for use with logger.
-	 *
-	 * \return the error message
-	 */
-	auto what() const noexcept -> const char* override;
-};
-
-/**
- * Get the plugin error category singleton.
- *
- * \return the singleton
- */
-auto plugin_category() -> const std::error_category&;
-
-/**
- * Create a std::error_code from plugin_error::error enum.
- *
- * \param e the error code
- * \return the error code
- */
-auto make_error_code(plugin_error::error e) -> std::error_code;
-
-} // !irccd
-
-/**
- * \cond IRCCD_HIDDEN_SYMBOLS
- */
-
-namespace std {
-
-template <>
-struct is_error_code_enum<irccd::plugin_error::error> : public std::true_type {
-};
-
-} // !std
-
-/**
- * \endcond
- */
-
-#endif // !IRCCD_DAEMON_PLUGIN_HPP
--- a/libirccd/irccd/daemon/plugin_service.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,283 +0,0 @@
-/*
- * plugin_service.cpp -- plugin service
- *
- * Copyright (c) 2013-2018 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 <boost/format.hpp>
-
-#include <irccd/config.hpp>
-#include <irccd/string_util.hpp>
-#include <irccd/system.hpp>
-
-#include "irccd.hpp"
-#include "logger.hpp"
-#include "plugin_service.hpp"
-
-using boost::format;
-using boost::str;
-
-namespace irccd {
-
-namespace {
-
-auto to_map(const config& conf, const std::string& section) -> plugin::map
-{
-	plugin::map ret;
-
-	for (const auto& opt : conf.get(section))
-		ret.emplace(opt.get_key(), opt.get_value());
-
-	return ret;
-}
-
-} // !namespace
-
-plugin_service::plugin_service(irccd& irccd) noexcept
-	: irccd_(irccd)
-{
-}
-
-plugin_service::~plugin_service()
-{
-	for (const auto& plg : plugins_) {
-		try {
-			plg->handle_unload(irccd_);
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning(*plg) << ex.what() << std::endl;
-		}
-	}
-}
-
-auto plugin_service::list() const noexcept -> plugins
-{
-	return plugins_;
-}
-
-auto plugin_service::has(std::string_view id) const noexcept -> bool
-{
-	return get(id) != nullptr;
-}
-
-auto plugin_service::get(std::string_view id) const noexcept -> std::shared_ptr<plugin>
-{
-	const auto find = [id] (const auto& plg) {
-		return plg->get_id() == id;
-	};
-
-	if (const auto it = std::find_if(plugins_.begin(), plugins_.end(), find); it != plugins_.end())
-		return *it;
-
-	return nullptr;
-}
-
-auto plugin_service::require(std::string_view id) const -> std::shared_ptr<plugin>
-{
-	auto plugin = get(id);
-
-	if (!plugin)
-		throw plugin_error(plugin_error::not_found, id);
-
-	return plugin;
-}
-
-void plugin_service::add(std::shared_ptr<plugin> plugin)
-{
-	assert(plugin);
-
-	plugins_.push_back(std::move(plugin));
-}
-
-void plugin_service::add_loader(std::unique_ptr<plugin_loader> loader)
-{
-	assert(loader);
-
-	loaders_.push_back(std::move(loader));
-}
-
-auto plugin_service::get_options(std::string_view id) -> plugin::map
-{
-	return to_map(irccd_.get_config(), str(format("plugin.%1%") % id));
-}
-
-auto plugin_service::get_formats(std::string_view id) -> plugin::map
-{
-	return to_map(irccd_.get_config(), str(format("format.%1%") % id));
-}
-
-auto plugin_service::get_paths(std::string_view id) -> plugin::map
-{
-	auto defaults = to_map(irccd_.get_config(), "paths");
-	auto paths = to_map(irccd_.get_config(), str(format("paths.%1%") % id));
-
-	// Fill defaults paths.
-	if (!defaults.count("cache"))
-		defaults.emplace("cache", sys::cachedir().string());
-	if (!defaults.count("data"))
-		defaults.emplace("data", sys::datadir().string());
-	if (!defaults.count("config"))
-		defaults.emplace("config", sys::sysconfdir().string());
-
-	const auto join = [id] (auto path) {
-		return (boost::filesystem::path(path) / "plugin" / std::string(id)).string();
-	};
-
-	// Now fill missing fields.
-	if (!paths.count("cache"))
-		paths.emplace("cache", join(defaults["cache"]));
-	if (!paths.count("data"))
-		paths.emplace("data", join(defaults["data"]));
-	if (!paths.count("config"))
-		paths.emplace("config", join(defaults["config"]));
-
-	return paths;
-}
-
-auto plugin_service::open(std::string_view id, std::string_view path) -> std::shared_ptr<plugin>
-{
-	for (const auto& loader : loaders_) {
-		if (!loader->is_supported(path))
-			continue;
-
-		auto plugin = loader->open(id, path);
-
-		if (plugin)
-			return plugin;
-	}
-
-	return nullptr;
-}
-
-auto plugin_service::find(std::string_view id) -> std::shared_ptr<plugin>
-{
-	for (const auto& loader : loaders_) {
-		try {
-			auto plugin = loader->find(id);
-
-			if (plugin)
-				return plugin;
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning("plugin", id) << ex.what() << std::endl;
-		}
-	}
-
-	return nullptr;
-}
-
-void plugin_service::load(std::string_view id, std::string_view path)
-{
-	if (has(id))
-		throw plugin_error(plugin_error::already_exists, id);
-
-	std::shared_ptr<plugin> plugin;
-
-	if (path.empty())
-		plugin = find(id);
-	else
-		plugin = open(id, std::move(path));
-
-	if (!plugin)
-		throw plugin_error(plugin_error::not_found, id);
-
-	plugin->set_options(get_options(id));
-	plugin->set_formats(get_formats(id));
-	plugin->set_paths(get_paths(id));
-
-	exec(plugin, &plugin::handle_load, irccd_);
-	add(plugin);
-
-	irccd_.get_log().info(*plugin) << "loaded version " << plugin->get_version() << std::endl;
-}
-
-void plugin_service::reload(std::string_view id)
-{
-	auto plugin = get(id);
-
-	if (!plugin)
-		throw plugin_error(plugin_error::not_found, id);
-
-	exec(plugin, &plugin::handle_reload, irccd_);
-}
-
-void plugin_service::unload(std::string_view id)
-{
-	const auto find = [id] (const auto& plg) {
-		return plg->get_id() == id;
-	};
-
-	const auto it = std::find_if(plugins_.begin(), plugins_.end(), find);
-
-	if (it == plugins_.end())
-		throw plugin_error(plugin_error::not_found, id);
-
-	// Erase first, in case of throwing.
-	const auto save = *it;
-
-	plugins_.erase(it);
-	exec(save, &plugin::handle_unload, irccd_);
-}
-
-void plugin_service::clear() noexcept
-{
-	while (plugins_.size() > 0) {
-		const auto plugin = plugins_[0];
-
-		try {
-			unload(plugin->get_id());
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning(*plugin) << ex.what() << std::endl;
-		}
-	}
-}
-
-void plugin_service::load(const config& cfg) noexcept
-{
-	for (const auto& option : cfg.get("plugins")) {
-		if (!string_util::is_identifier(option.get_key()))
-			continue;
-
-		auto id = option.get_key();
-		auto p = get(id);
-
-		// Reload the plugin if already loaded.
-		if (p) {
-			p->set_options(get_options(id));
-			p->set_formats(get_formats(id));
-			p->set_paths(get_paths(id));
-		} else {
-			try {
-				load(id, option.get_value());
-			} catch (const std::exception& ex) {
-				irccd_.get_log().warning("plugin", id) << ex.what() << std::endl;
-			}
-		}
-	}
-}
-
-namespace logger {
-
-auto loggable_traits<plugin>::get_category(const plugin&) -> std::string_view
-{
-	return "plugin";
-}
-
-auto loggable_traits<plugin>::get_component(const plugin& plugin) -> std::string_view
-{
-	return plugin.get_id();
-}
-
-} // !logger
-
-} // !irccd
--- a/libirccd/irccd/daemon/plugin_service.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-/*
- * plugin_service.hpp -- plugin service
- *
- * Copyright (c) 2013-2018 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_DAEMON_PLUGIN_SERVICE_HPP
-#define IRCCD_DAEMON_PLUGIN_SERVICE_HPP
-
-/**
- * \file plugin_service.hpp
- * \brief Plugin service.
- */
-
-#include <cassert>
-#include <memory>
-#include <string>
-#include <string_view>
-#include <vector>
-
-#include "plugin.hpp"
-
-namespace irccd {
-
-class irccd;
-class config;
-
-/**
- * \brief Manage plugins.
- * \ingroup plugins
- * \ingroup services
- */
-class plugin_service {
-public:
-	/**
-	 * \brief Map of plugins.
-	 */
-	using plugins = std::vector<std::shared_ptr<plugin>>;
-
-	/**
-	 * \brief List of loaders.
-	 */
-	using plugin_loaders = std::vector<std::unique_ptr<plugin_loader>>;
-
-private:
-	irccd& irccd_;
-	plugins plugins_;
-	plugin_loaders loaders_;
-
-public:
-	/**
-	 * Create the plugin service.
-	 *
-	 * \param irccd the irccd instance
-	 */
-	plugin_service(irccd& irccd) noexcept;
-
-	/**
-	 * Destroy plugins.
-	 */
-	virtual ~plugin_service();
-
-	/**
-	 * Get the list of plugins.
-	 *
-	 * \return the list of plugins
-	 */
-	auto list() const noexcept -> plugins;
-
-	/**
-	 * Check if a plugin is loaded.
-	 *
-	 * \param id the plugin id
-	 * \return true if has plugin
-	 */
-	auto has(std::string_view id) const noexcept -> bool;
-
-	/**
-	 * Get a loaded plugin or null if not found.
-	 *
-	 * \param id the plugin id
-	 * \return the plugin or empty one if not found
-	 */
-	auto get(std::string_view id) const noexcept -> std::shared_ptr<plugin>;
-
-	/**
-	 * Find a loaded plugin.
-	 *
-	 * \param id the plugin id
-	 * \return the plugin
-	 * \throw plugin_error on errors
-	 */
-	auto require(std::string_view id) const -> std::shared_ptr<plugin>;
-
-	/**
-	 * Add the specified plugin to the registry.
-	 *
-	 * \pre plg != nullptr
-	 * \param plg the plugin
-	 * \note the plugin is only added to the list, no action is performed on it
-	 */
-	void add(std::shared_ptr<plugin> plg);
-
-	/**
-	 * Add a loader.
-	 *
-	 * \pre loader != nullptr
-	 * \param loader the loader
-	 */
-	void add_loader(std::unique_ptr<plugin_loader> loader);
-
-	/**
-	 * Get the configuration for the specified plugin.
-	 *
-	 * \param id the plugin id
-	 * \return the configuration
-	 */
-	auto get_options(std::string_view id) -> plugin::map;
-
-	/**
-	 * Get the formats for the specified plugin.
-	 *
-	 * \param id the plugin id
-	 * \return the formats
-	 */
-	auto get_formats(std::string_view id) -> plugin::map;
-
-	/**
-	 * Get the paths for the specified plugin.
-	 *
-	 * If none is defined, return the default ones.
-	 *
-	 * \param id the plugin id
-	 * \return the paths
-	 */
-	auto get_paths(std::string_view id) -> plugin::map;
-
-	/**
-	 * Generic function for opening the plugin at the given path.
-	 *
-	 * This function will search for every pluginLoader and call open() on it,
-	 * the first one that success will be returned.
-	 *
-	 * \param id the plugin id
-	 * \param path the path to the file
-	 * \return the plugin or nullptr on failures
-	 */
-	auto open(std::string_view id, std::string_view path) -> std::shared_ptr<plugin>;
-
-	/**
-	 * Generic function for finding a plugin.
-	 *
-	 * \param id the plugin id
-	 * \return the plugin or nullptr on failures
-	 */
-	auto find(std::string_view id) -> std::shared_ptr<plugin>;
-
-	/**
-	 * Convenient wrapper that loads a plugin, call handle_load and add it
-	 * to the registry.
-	 *
-	 * Any errors are printed using logger.
-	 *
-	 * \param id the plugin id
-	 * \param path the optional path (searched if empty)
-	 */
-	void load(std::string_view id, std::string_view path = "");
-
-	/**
-	 * Unload a plugin and remove it.
-	 *
-	 * \param id the plugin id
-	 */
-	void unload(std::string_view id);
-
-	/**
-	 * Reload a plugin by calling onReload.
-	 *
-	 * \param id the plugin id
-	 * \throw std::exception on failures
-	 */
-	void reload(std::string_view id);
-
-	/**
-	 * Call a plugin function and throw an exception with the following errors:
-	 *
-	 *   - plugin_error::not_found if not loaded
-	 *   - plugin_error::exec_error if function failed
-	 *
-	 * \pre plugin != nullptr
-	 * \param plugin the plugin
-	 * \param fn the plugin member function (pointer to member)
-	 * \param args the arguments to pass
-	 */
-	template <typename Func, typename... Args>
-	void exec(std::shared_ptr<plugin> plugin, Func fn, Args&&... args)
-	{
-		assert(plugin);
-
-		// TODO: replace with C++17 std::invoke.
-		try {
-			((*plugin).*(fn))(std::forward<Args>(args)...);
-		} catch (const std::exception& ex) {
-			throw plugin_error(plugin_error::exec_error, plugin->get_name(), ex.what());
-		} catch (...) {
-			throw plugin_error(plugin_error::exec_error, plugin->get_name());
-		}
-	}
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \param name the plugin name
-	 * \param fn the plugin member function (pointer to member)
-	 * \param args the arguments to pass
-	 */
-	template <typename Func, typename... Args>
-	void exec(const std::string& name, Func fn, Args&&... args)
-	{
-		auto plugin = find(name);
-
-		if (!plugin)
-			throw plugin_error(plugin_error::not_found, plugin->get_name());
-
-		exec(plugin, fn, std::forward<Args>(args)...);
-	}
-
-	/**
-	 * Remove all plugins.
-	 */
-	void clear() noexcept;
-
-	/**
-	 * Load all plugins.
-	 *
-	 * \param cfg the config
-	 */
-	void load(const config& cfg) noexcept;
-};
-
-namespace logger {
-
-template <typename T>
-struct loggable_traits;
-
-/**
- * \brief Implement Loggable traits for plugin.
- * \ingroup logger-traits
- */
-template <>
-struct loggable_traits<plugin> {
-	/**
-	 * Return "plugin"
-	 *
-	 * \param plugin the plugin
-	 * \return the category
-	 */
-	static auto get_category(const plugin& plugin) -> std::string_view;
-
-	/**
-	 * Return the plugin id.
-	 *
-	 * \param plugin the plugin
-	 * \return the plugin id
-	 */
-	static auto get_component(const plugin& plugin) -> std::string_view;
-};
-
-} // !logger
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_PLUGIN_SERVICE_HPP
--- a/libirccd/irccd/daemon/rule.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/*
- * rule.cpp -- rule for server and channels
- *
- * Copyright (c) 2013-2018 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 <algorithm>
-#include <cctype>
-
-#include "rule.hpp"
-
-namespace irccd {
-
-auto rule::match_set(const set& set, const std::string& value) const noexcept -> bool
-{
-	return set.empty() || set.count(value) == 1;
-}
-
-auto rule::match(std::string_view server,
-                 std::string_view channel,
-                 std::string_view nick,
-                 std::string_view plugin,
-                 std::string_view event) const noexcept -> bool
-{
-	const auto tolower = [] (auto str) noexcept -> std::string {
-		std::string ret(str);
-		std::transform(ret.begin(), ret.end(), ret.begin(), ::tolower);
-		return ret;
-	};
-
-	return match_set(servers, tolower(server)) &&
-	       match_set(channels, tolower(channel)) &&
-	       match_set(origins, tolower(nick)) &&
-	       match_set(plugins, tolower(plugin)) &&
-	       match_set(events, std::string(event));
-}
-
-auto rule_category() -> const std::error_category&
-{
-	static const class category : public std::error_category {
-	public:
-		auto name() const noexcept -> const char* override
-		{
-			return "rule";
-		}
-
-		auto message(int e) const -> std::string override
-		{
-			switch (static_cast<rule_error::error>(e)) {
-			case rule_error::invalid_action:
-				return "invalid rule action";
-			case rule_error::invalid_index:
-				return "invalid rule index";
-			default:
-				return "no error";
-			}
-		}
-	} category;
-
-	return category;
-}
-
-auto make_error_code(rule_error::error e) -> std::error_code
-{
-	return { static_cast<int>(e), rule_category() };
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/rule.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-/*
- * rule.hpp -- rule for server and channels
- *
- * Copyright (c) 2013-2018 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_DAEMON_RULE_HPP
-#define IRCCD_DAEMON_RULE_HPP
-
-/**
- * \file rule.hpp
- * \brief Rule description
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <cassert>
-#include <set>
-#include <string>
-#include <system_error>
-
-namespace irccd {
-
-/**
- * \brief Manage rule to activate or deactive events.
- */
-struct rule {
-	/**
-	 * List of criterias.
-	 */
-	using set = std::set<std::string>;
-
-	/**
-	 * \brief Rule action type.
-	 */
-	enum class action_type {
-		accept,         //!< The event is accepted (default)
-		drop            //!< The event is dropped
-	};
-
-	set servers;            //!< The list of servers
-	set channels;           //!< The list of channels
-	set origins;            //!< The list of originators
-	set plugins;            //!< The list of plugins
-	set events;             //!< The list of events
-
-	/**
-	 * The action.
-	 */
-	action_type action{action_type::accept};
-
-	/**
-	 * Check if a set contains the value and return true if it is or return
-	 * true if value is empty (which means applicable).
-	 *
-	 * \param set the set to test
-	 * \param value the value
-	 * \return true if match
-	 */
-	auto match_set(const set& set, const std::string& value) const noexcept -> bool;
-
-	/**
-	 * Check if that rule apply for the given criterias.
-	 *
-	 * \param server the server
-	 * \param channel the channel
-	 * \param origin the origin
-	 * \param plugin the plugin
-	 * \param event the event
-	 * \return true if match
-	 */
-	auto match(std::string_view server,
-	           std::string_view channel,
-	           std::string_view origin,
-	           std::string_view plugin,
-	           std::string_view event) const noexcept -> bool;
-};
-
-/**
- * \brief Rule error.
- */
-class rule_error : public std::system_error {
-public:
-	/**
-	 * \brief Rule related errors.
-	 */
-	enum error {
-		//!< No error.
-		no_error = 0,
-
-		//!< Invalid action given.
-		invalid_action,
-
-		//!< Invalid rule index.
-		invalid_index,
-	};
-
-	/**
-	 * Inherited constructors.
-	 */
-	using system_error::system_error;
-};
-
-/**
- * Get the rule error category singleton.
- *
- * \return the singleton
- */
-auto rule_category() -> const std::error_category&;
-
-/**
- * Create a std::error_code from rule_error::error enum.
- *
- * \param e the error code
- * \return the error code
- */
-auto make_error_code(rule_error::error e) -> std::error_code;
-
-} // !irccd
-
-/**
- * \cond IRCCD_HIDDEN_SYMBOLS
- */
-
-namespace std {
-
-template <>
-struct is_error_code_enum<irccd::rule_error::error> : public std::true_type {
-};
-
-} // !std
-
-/**
- * \endcond
- */
-
-#endif // !IRCCD_DAEMON_RULE_HPP
--- a/libirccd/irccd/daemon/rule_service.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-/*
- * rule_service.cpp -- rule service
- *
- * Copyright (c) 2013-2018 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 <stdexcept>
-
-#include <irccd/config.hpp>
-#include <irccd/string_util.hpp>
-
-#include "irccd.hpp"
-#include "logger.hpp"
-#include "rule_service.hpp"
-#include "rule_util.hpp"
-
-namespace irccd {
-
-rule_service::rule_service(irccd &irccd)
-	: irccd_(irccd)
-{
-}
-
-auto rule_service::list() const noexcept -> const std::vector<rule>&
-{
-	return rules_;
-}
-
-void rule_service::add(rule rule)
-{
-	rules_.push_back(std::move(rule));
-}
-
-void rule_service::insert(rule rule, unsigned position)
-{
-	assert(position <= rules_.size());
-
-	rules_.insert(rules_.begin() + position, std::move(rule));
-}
-
-void rule_service::remove(unsigned position)
-{
-	assert(position < rules_.size());
-
-	rules_.erase(rules_.begin() + position);
-}
-
-auto rule_service::require(unsigned position) const -> const rule&
-{
-	if (position >= rules_.size())
-		throw rule_error(rule_error::invalid_index);
-
-	return rules_[position];
-}
-
-auto rule_service::require(unsigned position) -> rule&
-{
-	if (position >= rules_.size())
-		throw rule_error(rule_error::invalid_index);
-
-	return rules_[position];
-}
-
-auto rule_service::solve(std::string_view server,
-                         std::string_view channel,
-                         std::string_view origin,
-                         std::string_view plugin,
-                         std::string_view event) noexcept -> bool
-{
-	bool result = true;
-
-	for (const auto& rule : rules_)
-		if (rule.match(server, channel, origin, plugin, event))
-			result = rule.action == rule::action_type::accept;
-
-	return result;
-}
-
-void rule_service::load(const config& cfg) noexcept
-{
-	rules_.clear();
-
-	for (const auto& section : cfg) {
-		if (section.get_key() != "rule")
-			continue;
-
-		try {
-			rules_.push_back(rule_util::from_config(section));
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning("rule", "") << ex.what() << std::endl;
-		}
-	}
-}
-
-namespace logger {
-
-auto loggable_traits<rule>::get_category(const rule&) -> std::string_view
-{
-	return "rule";
-}
-
-auto loggable_traits<rule>::get_component(const rule&) -> std::string_view
-{
-	return "";
-}
-
-} // !logger
-
-} // !irccd
--- a/libirccd/irccd/daemon/rule_service.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-/*
- * rule_service.hpp -- rule service
- *
- * Copyright (c) 2013-2018 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_DAEMON_RULE_SERVICE_HPP
-#define IRCCD_DAEMON_RULE_SERVICE_HPP
-
-/**
- * \file rule_service.hpp
- * \brief Rule service.
- */
-
-#include <vector>
-
-#include <json.hpp>
-
-#include "rule.hpp"
-
-namespace irccd {
-
-class config;
-class irccd;
-
-/**
- * \brief Store and solve rules.
- * \ingroup services
- */
-class rule_service {
-private:
-	irccd& irccd_;
-	std::vector<rule> rules_;
-
-public:
-	/**
-	 * Create the rule service.
-	 *
-	 * \param instance the irccd instance
-	 */
-	rule_service(irccd& instance);
-
-	/**
-	 * Get the list of rules.
-	 *
-	 * \return the list of rules
-	 */
-	auto list() const noexcept -> const std::vector<rule>&;
-
-	/**
-	 * Append a rule.
-	 *
-	 * \param rule the rule to append
-	 */
-	void add(rule rule);
-
-	/**
-	 * Insert a new rule at the specified position.
-	 *
-	 * \param rule the rule
-	 * \param position the position
-	 */
-	void insert(rule rule, unsigned position);
-
-	/**
-	 * Remove a new rule from the specified position.
-	 *
-	 * \pre position must be valid
-	 * \param position the position
-	 */
-	void remove(unsigned position);
-
-	/**
-	 * Get a rule at the specified index or throw an exception if not found.
-	 *
-	 * \param position the position
-	 * \return the rule
-	 * \throw std::out_of_range if position is invalid
-	 */
-	auto require(unsigned position) const -> const rule&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \copydoc require
-	 */
-	auto require(unsigned position) -> rule&;
-
-	/**
-	 * Resolve the action to execute with the specified list of rules.
-	 *
-	 * \param server the server name
-	 * \param channel the channel name
-	 * \param origin the origin
-	 * \param plugin the plugin name
-	 * \param event the event name (e.g onKick)
-	 * \return true if the plugin must be called
-	 */
-	auto solve(std::string_view server,
-	           std::string_view channel,
-	           std::string_view origin,
-	           std::string_view plugin,
-	           std::string_view event) noexcept -> bool;
-
-	/**
-	 * Load rules from the configuration.
-	 *
-	 * \param cfg the config
-	 */
-	void load(const config& cfg) noexcept;
-};
-
-namespace logger {
-
-template <typename T>
-struct loggable_traits;
-
-/**
- * \brief Specialization for rule.
- * \ingroup logger-traits
- */
-template <>
-struct loggable_traits<rule> {
-	/**
-	 * Get 'rule' category.
-	 *
-	 * \param rule the rule
-	 * \return rule
-	 */
-	static auto get_category(const rule& rule) -> std::string_view;
-
-	/**
-	 * Returns nothing for the moment.
-	 *
-	 * \param rule the rule
-	 * \return nothing
-	 */
-	static auto get_component(const rule& rule) -> std::string_view;
-};
-
-} // !logger
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_RULE_SERVICE_HPP
--- a/libirccd/irccd/daemon/rule_util.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-/*
- * rule_util.cpp -- rule utilities
- *
- * Copyright (c) 2013-2018 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 <irccd/ini.hpp>
-
-#include "rule.hpp"
-#include "rule_util.hpp"
-
-namespace irccd::rule_util {
-
-auto from_config(const ini::section& sc) -> rule
-{
-	// Simple converter from std::vector to std::unordered_set.
-	const auto toset = [] (const auto& v) {
-		return std::set<std::string>(v.begin(), v.end());
-	};
-
-	rule::set servers, channels, origins, plugins, events;
-	rule::action_type action = rule::action_type::accept;
-
-	// Get the sets.
-	ini::section::const_iterator it;
-
-	if ((it = sc.find("servers")) != sc.end())
-		servers = toset(*it);
-	if ((it = sc.find("channels")) != sc.end())
-		channels = toset(*it);
-	if ((it = sc.find("origins")) != sc.end())
-		origins = toset(*it);
-	if ((it = sc.find("plugins")) != sc.end())
-		plugins = toset(*it);
-	if ((it = sc.find("channels")) != sc.end())
-		channels = toset(*it);
-	if ((it = sc.find("events")) != sc.end())
-		events = toset(*it);
-
-	// Get the action.
-	auto actionstr = sc.get("action").get_value();
-
-	if (actionstr == "drop")
-		action = rule::action_type::drop;
-	else if (actionstr == "accept")
-		action = rule::action_type::accept;
-	else
-		throw rule_error(rule_error::invalid_action);
-
-	return {
-		std::move(servers),
-		std::move(channels),
-		std::move(origins),
-		std::move(plugins),
-		std::move(events),
-		action
-	};
-}
-
-auto from_json(const nlohmann::json& json) -> rule
-{
-	const auto toset = [] (auto object, auto name) {
-		rule::set result;
-
-		for (const auto& s : object[name])
-			if (s.is_string())
-				result.insert(s.template get<std::string>());
-
-		return result;
-	};
-	const auto toaction = [] (const auto& object, const auto& name) {
-		const auto v = object.find(name);
-
-		if (v == object.end() || !v->is_string())
-			throw rule_error(rule_error::invalid_action);
-
-		const auto s = v->template get<std::string>();
-
-		if (s == "accept")
-			return rule::action_type::accept;
-		if (s == "drop")
-			return rule::action_type::drop;
-
-		throw rule_error(rule_error::invalid_action);
-	};
-
-	return {
-		toset(json, "servers"),
-		toset(json, "channels"),
-		toset(json, "origins"),
-		toset(json, "plugins"),
-		toset(json, "events"),
-		toaction(json, "action")
-	};
-}
-
-auto get_index(const nlohmann::json& json, const std::string& key) -> unsigned
-{
-	const auto index = json.find(key);
-
-	if (index == json.end() || !index->is_number_unsigned())
-		throw rule_error(rule_error::invalid_index);
-
-	return index->get<unsigned>();
-}
-
-auto to_json(const rule& rule) -> nlohmann::json
-{
-	const auto join = [] (const auto& set) {
-		auto array = nlohmann::json::array();
-
-		for (const auto& entry : set)
-			array.push_back(entry);
-
-		return array;
-	};
-	const auto str = [] (auto action) {
-		switch (action) {
-		case rule::action_type::accept:
-			return "accept";
-		default:
-			return "drop";
-		}
-	};
-
-	return {
-		{ "servers",    join(rule.servers)      },
-		{ "channels",   join(rule.channels)     },
-		{ "origins",    join(rule.origins)      },
-		{ "plugins",    join(rule.plugins)      },
-		{ "events",     join(rule.events)       },
-		{ "action",     str(rule.action)        }
-	};
-}
-
-} // !irccd::rule_util
--- a/libirccd/irccd/daemon/rule_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/*
- * rule_util.hpp -- rule utilities
- *
- * Copyright (c) 2013-2018 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_DAEMON_RULE_UTIL_HPP
-#define IRCCD_DAEMON_RULE_UTIL_HPP
-
-/**
- * \file rule_util.hpp
- * \brief Rule utilities.
- */
-
-#include <json.hpp>
-
-namespace irccd {
-
-namespace ini {
-
-class section;
-
-} // !ini
-
-class config;
-
-struct rule;
-
-/**
- * \brief Rule utilities.
- */
-namespace rule_util {
-
-/**
- * Load a rule from a JSON object.
- *
- * For possible use in transport commands or Javascript API.
- *
- * \pre json.is_object()
- * \param json the JSON object
- * \return the new rule
- * \throw rule_error on errors
- */
-auto from_json(const nlohmann::json& json) -> rule;
-
-/**
- * Load a rule from a INI section.
- *
- * \param sc the ini section
- * \return the rule
- * \throw rule_error on errors
- */
-auto from_config(const ini::section& sc) -> rule;
-
-/**
- * Convert a rule into a JSON object.
- *
- * \param rule the rule
- * \throw the JSON representation
- * \return the JSON representation
- */
-auto to_json(const rule& rule) -> nlohmann::json;
-
-} // !rule_util
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_RULE_UTIL_HPP
--- a/libirccd/irccd/daemon/server.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,896 +0,0 @@
-/*
- * server.cpp -- an IRC server
- *
- * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
-
-#include <boost/predef/os.h>
-#include <boost/format.hpp>
-
-#include <algorithm>
-#include <cerrno>
-#include <cstring>
-#include <stdexcept>
-
-#if !BOOST_OS_WINDOWS
-#	include <sys/types.h>
-#	include <netinet/in.h>
-#	include <arpa/nameser.h>
-#	include <resolv.h>
-#endif
-
-#include <irccd/json_util.hpp>
-#include <irccd/string_util.hpp>
-
-#include "server.hpp"
-
-using boost::format;
-using boost::str;
-
-namespace irccd {
-
-namespace {
-
-/*
- * clean_prefix
- * ------------------------------------------------------------------
- *
- * Remove the user prefix only if it is present in the mode table, for example
- * removes @ from @irccd if and only if @ is a character mode (e.g. operator).
- */
-auto clean_prefix(const std::map<channel_mode, char>& modes, std::string nickname) -> std::string
-{
-	if (nickname.length() == 0)
-		return nickname;
-
-	for (const auto& pair : modes)
-		if (nickname[0] == pair.second)
-			nickname.erase(0, 1);
-
-	return nickname;
-}
-
-/*
- * isupport_extract_prefixes
- * ------------------------------------------------------------------
- *
- * Read modes from the IRC event numeric.
- */
-auto isupport_extract_prefixes(const std::string& line) -> std::map<channel_mode, char>
-{
-	// FIXME: what if line has different size?
-	std::pair<char, char> table[16];
-	std::string buf = line.substr(7);
-	std::map<channel_mode, char> modes;
-
-	for (int i = 0; i < 16; ++i)
-		table[i] = std::make_pair(-1, -1);
-
-	int j = 0;
-	bool read_modes = true;
-
-	for (size_t i = 0; i < buf.size(); ++i) {
-		if (buf[i] == '(')
-			continue;
-		if (buf[i] == ')') {
-			j = 0;
-			read_modes = false;
-			continue;
-		}
-
-		if (read_modes)
-			table[j++].first = buf[i];
-		else
-			table[j++].second = buf[i];
-	}
-
-	// Put these as a map of mode to prefix.
-	for (int i = 0; i < 16; ++i) {
-		auto key = static_cast<channel_mode>(table[i].first);
-		auto value = table[i].second;
-
-		modes.emplace(key, value);
-	}
-
-	return modes;
-}
-
-} // !namespace
-
-auto server::dispatch_connect(const irc::message&, const recv_handler& handler) -> bool
-{
-	state_ = state::connected;
-	handler({}, connect_event{shared_from_this()});
-
-	for (const auto& channel : rchannels_)
-		join(channel.name, channel.password);
-
-	return true;
-}
-
-auto server::dispatch_endofnames(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	/*
-	 * Called when end of name listing has finished on a channel.
-	 *
-	 * params[0] == originator
-	 * params[1] == channel
-	 * params[2] == End of NAMES list
-	 */
-	if (msg.args.size() < 3 || msg.get(1) == "")
-		return false;
-
-	const auto it = names_map_.find(msg.get(1));
-
-	if (it != names_map_.end()) {
-		handler({}, names_event{
-			shared_from_this(),
-			msg.get(1),
-			std::vector<std::string>(it->second.begin(), it->second.end())
-		});
-
-		names_map_.erase(it);
-	}
-
-	return true;
-}
-
-auto server::dispatch_endofwhois(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	/*
-	 * Called when whois is finished.
-	 *
-	 * params[0] == originator
-	 * params[1] == nickname
-	 * params[2] == End of WHOIS list
-	 */
-	const auto it = whois_map_.find(msg.get(1));
-
-	if (it != whois_map_.end()) {
-		handler({}, whois_event{shared_from_this(), it->second});
-		whois_map_.erase(it);
-	}
-
-	return true;
-}
-
-auto server::dispatch_invite(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	// If join-invite is set, join the channel.
-	if ((options_ & options::join_invite) == options::join_invite && is_self(msg.get(0)))
-		join(msg.get(1));
-
-	handler({}, invite_event{shared_from_this(), msg.prefix, msg.get(1), msg.get(0)});
-
-	return true;
-}
-
-auto server::dispatch_isupport(const irc::message& msg) -> bool
-{
-	for (unsigned int i = 0; i < msg.args.size(); ++i) {
-		if (msg.get(i).compare(0, 6, "PREFIX") == 0) {
-			modes_ = isupport_extract_prefixes(msg.get(i));
-			break;
-		}
-	}
-
-	return false;
-}
-
-auto server::dispatch_join(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	if (is_self(msg.prefix))
-		jchannels_.insert(msg.get(0));
-
-	handler({}, join_event{shared_from_this(), msg.prefix, msg.get(0)});
-
-	return true;
-}
-
-auto server::dispatch_kick(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	if (is_self(msg.get(1))) {
-		// Remove the channel from the joined list.
-		jchannels_.erase(msg.get(0));
-
-		// Rejoin the channel if the option has been set and I was kicked.
-		if ((options_ & options::auto_rejoin) == options::auto_rejoin)
-			join(msg.get(0));
-	}
-
-	handler({}, kick_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1), msg.get(2)});
-
-	return true;
-}
-
-auto server::dispatch_mode(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	handler({}, mode_event{
-		shared_from_this(),
-		msg.prefix,
-		msg.get(0),
-		msg.get(1),
-		msg.get(2),
-		msg.get(3),
-		msg.get(4)
-	});
-
-	return true;
-}
-
-auto server::dispatch_namreply(const irc::message& msg) -> bool
-{
-	/*
-	 * Called multiple times to list clients on a channel.
-	 *
-	 * params[0] == originator
-	 * params[1] == '='
-	 * params[2] == channel
-	 * params[3] == list of users with their prefixes
-	 *
-	 * IDEA for the future: maybe give the appropriate mode as a second
-	 * parameter in onNames.
-	 */
-	if (msg.args.size() < 4 || msg.get(2) == "" || msg.get(3) == "")
-		return false;
-
-	auto users = string_util::split(msg.get(3), " \t");
-
-	// The listing may add some prefixes, remove them if needed.
-	for (const auto& u : users)
-		names_map_[msg.get(2)].insert(clean_prefix(modes_, u));
-
-	return false;
-}
-
-auto server::dispatch_nick(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	// Update our nickname.
-	if (is_self(msg.prefix))
-		nickname_ = msg.get(0);
-
-	handler({}, nick_event{shared_from_this(), msg.prefix, msg.get(0)});
-
-	return true;
-}
-
-auto server::dispatch_notice(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	handler({}, notice_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
-
-	return true;
-}
-
-auto server::dispatch_part(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	// Remove the channel from the joined list if I left a channel.
-	if (is_self(msg.prefix))
-		jchannels_.erase(msg.get(1));
-
-	handler({}, part_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
-
-	return true;
-}
-
-auto server::dispatch_ping(const irc::message& msg) -> bool
-{
-	assert(msg.command == "PING");
-
-	send(str(format("PONG %1%") % msg.get(0)));
-
-	return false;
-}
-
-auto server::dispatch_privmsg(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	assert(msg.command == "PRIVMSG");
-
-	if (msg.is_ctcp(1)) {
-		auto cmd = msg.ctcp(1);
-
-		if (cmd.compare(0, 6, "ACTION") == 0)
-			handler({}, me_event{shared_from_this(), msg.prefix, msg.get(0), cmd.substr(7)});
-		else
-			return false;
-	} else
-		handler({}, message_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
-
-	return true;
-}
-
-auto server::dispatch_topic(const irc::message& msg, const recv_handler& handler) -> bool
-{
-	assert(msg.command == "TOPIC");
-
-	handler({}, topic_event{shared_from_this(), msg.get(0), msg.get(1), msg.get(2)});
-
-	return true;
-}
-
-auto server::dispatch_whoischannels(const irc::message& msg) -> bool
-{
-	/*
-	 * Called when we have received channels for one user.
-	 *
-	 * params[0] == originator
-	 * params[1] == nickname
-	 * params[2] == list of channels with their prefixes
-	 */
-	if (msg.args.size() < 3 || msg.get(1) == "" || msg.get(2) == "")
-		return false;
-
-	auto it = whois_map_.find(msg.get(1));
-
-	if (it != whois_map_.end()) {
-		auto channels = string_util::split(msg.get(2), " \t");
-
-		// Clean their prefixes.
-		for (auto& s : channels)
-			s = clean_prefix(modes_, s);
-
-		it->second.channels = std::move(channels);
-	}
-
-	return false;
-}
-
-auto server::dispatch_whoisuser(const irc::message& msg) -> bool
-{
-	/*
-	 * Called when whois information has been partially received.
-	 *
-	 * params[0] == originator
-	 * params[1] == nickname
-	 * params[2] == username
-	 * params[3] == hostname
-	 * params[4] == * (no idea what is that)
-	 * params[5] == realname
-	 */
-	if (msg.args.size() < 6 || msg.get(1) == "" || msg.get(2) == "" || msg.get(3) == "" || msg.get(5) == "")
-		return false;
-
-	whois_info info;
-
-	info.nick = msg.get(1);
-	info.user = msg.get(2);
-	info.hostname = msg.get(3);
-	info.realname = msg.get(5);
-
-	whois_map_.emplace(info.nick, info);
-
-	return false;
-}
-
-auto server::dispatch(const irc::message& message, const recv_handler& handler) -> bool
-{
-	bool handled = false;
-
-	if (message.is(5))
-		handled = dispatch_isupport(message);
-	else if (message.is(irc::err::nomotd) || message.is(irc::rpl::endofmotd))
-		handled = dispatch_connect(message, handler);
-	else if (message.command == "INVITE")
-		handled = dispatch_invite(message, handler);
-	else if (message.command == "JOIN")
-		handled = dispatch_join(message, handler);
-	else if (message.command == "KICK")
-		handled = dispatch_kick(message, handler);
-	else if (message.command == "MODE")
-		handled = dispatch_mode(message, handler);
-	else if (message.command == "NICK")
-		handled = dispatch_nick(message, handler);
-	else if (message.command == "NOTICE")
-		handled = dispatch_notice(message, handler);
-	else if (message.command == "TOPIC")
-		handled = dispatch_topic(message, handler);
-	else if (message.command == "PART")
-		handled = dispatch_part(message, handler);
-	else if (message.command == "PING")
-		handled = dispatch_ping(message);
-	else if (message.command == "PRIVMSG")
-		handled = dispatch_privmsg(message, handler);
-	else if (message.is(irc::rpl::namreply))
-		handled = dispatch_namreply(message);
-	else if (message.is(irc::rpl::endofnames))
-		handled = dispatch_endofnames(message, handler);
-	else if (message.is(irc::rpl::endofwhois))
-		handled = dispatch_endofwhois(message, handler);
-	else if (message.is(irc::rpl::whoischannels))
-		handled = dispatch_whoischannels(message);
-	else if (message.is(irc::rpl::whoisuser))
-		handled = dispatch_whoisuser(message);
-
-	return handled;
-}
-
-void server::handle_send(const std::error_code& code)
-{
-	/*
-	 * We don't notify server_service in case of error because in any case the
-	 * pending recv() will complete with an error.
-	 */
-	queue_.pop_front();
-
-	if (!code)
-		flush();
-}
-
-void server::handle_recv(const std::error_code& code,
-                         const irc::message& message,
-                         const recv_handler& handler)
-{
-	/*
-	 * Once a message is received, dispatch it to individual dispatch_*
-	 * functions. If the function calls handler by itself it returns true
-	 * otherwise we call handler with no event to tell the caller the message
-	 * has arrived and allowed to call recv() again.
-	 */
-	if (code) {
-		disconnect();
-		handler(std::move(code), event(std::monostate()));
-	} else if (!dispatch(message, handler))
-		handler({}, std::monostate{});
-}
-
-void server::recv(recv_handler handler) noexcept
-{
-	const auto self = shared_from_this();
-
-	conn_->recv([this, handler, self, c = conn_] (auto code, auto message) {
-		handle_recv(std::move(code), message, handler);
-	});
-}
-
-void server::flush()
-{
-	if (queue_.empty())
-		return;
-
-	const auto self = shared_from_this();
-
-	conn_->send(queue_.front(), [this, self, c = conn_] (auto code) {
-		handle_send(std::move(code));
-	});
-}
-
-void server::identify()
-{
-	state_ = state::identifying;
-
-	if (!password_.empty())
-		send(str(format("PASS %1%") % password_));
-
-	send(str(format("NICK %1%") % nickname_));
-	send(str(format("USER %1% unknown unknown :%2%") % username_ % realname_));
-}
-
-void server::handle_wait(const std::error_code& code, const connect_handler& handler)
-{
-	if (code && code != std::errc::operation_canceled)
-		handler(code);
-}
-
-void server::handle_connect(const std::error_code& code, const connect_handler& handler)
-{
-	timer_.cancel();
-
-	if (code)
-		disconnect();
-	else
-		identify();
-
-	handler(code);
-}
-
-server::server(boost::asio::io_service& service, std::string id, std::string hostname)
-	: id_(std::move(id))
-	, hostname_(std::move(hostname))
-	, options_(options::ipv4 | options::ipv6)
-	, service_(service)
-	, timer_(service)
-{
-	assert(!hostname_.empty());
-}
-
-server::~server()
-{
-	conn_ = nullptr;
-	state_ = state::disconnected;
-}
-
-auto server::get_state() const noexcept -> state
-{
-	return state_;
-}
-
-auto server::get_id() const noexcept -> const std::string&
-{
-	return id_;
-}
-
-auto server::get_hostname() const noexcept -> const std::string&
-{
-	return hostname_;
-}
-
-auto server::get_password() const noexcept -> const std::string&
-{
-	return password_;
-}
-
-void server::set_password(std::string password) noexcept
-{
-	password_ = std::move(password);
-}
-
-auto server::get_port() const noexcept -> std::uint16_t
-{
-	return port_;
-}
-
-void server::set_port(std::uint16_t port) noexcept
-{
-	port_ = port;
-}
-
-auto server::get_options() const noexcept -> options
-{
-	return options_;
-}
-
-void server::set_options(options flags) noexcept
-{
-#if !defined(IRCCD_HAVE_SSL)
-	assert((flags & options::ssl) != options::ssl);
-#endif
-
-	options_ = flags;
-}
-
-auto server::get_nickname() const noexcept -> const std::string&
-{
-	return nickname_;
-}
-
-void server::set_nickname(std::string nickname)
-{
-	if (state_ == state::connected)
-		send(str(format("NICK %1%") % nickname));
-	else
-		nickname_ = std::move(nickname);
-}
-
-auto server::get_username() const noexcept -> const std::string&
-{
-	return username_;
-}
-
-void server::set_username(std::string name) noexcept
-{
-	username_ = std::move(name);
-}
-
-auto server::get_realname() const noexcept -> const std::string&
-{
-	return realname_;
-}
-
-void server::set_realname(std::string realname) noexcept
-{
-	realname_ = std::move(realname);
-}
-
-auto server::get_ctcp_version() const noexcept -> const std::string&
-{
-	return ctcpversion_;
-}
-
-void server::set_ctcp_version(std::string ctcpversion)
-{
-	ctcpversion_ = std::move(ctcpversion);
-}
-
-auto server::get_command_char() const noexcept -> const std::string&
-{
-	return command_char_;
-}
-
-void server::set_command_char(std::string command_char) noexcept
-{
-	assert(!command_char.empty());
-
-	command_char_ = std::move(command_char);
-}
-
-auto server::get_reconnect_delay() const noexcept -> std::uint16_t
-{
-	return recodelay_;
-}
-
-void server::set_reconnect_delay(std::uint16_t reconnect_delay) noexcept
-{
-	recodelay_ = reconnect_delay;
-}
-
-auto server::get_ping_timeout() const noexcept -> std::uint16_t
-{
-	return timeout_;
-}
-
-void server::set_ping_timeout(std::uint16_t ping_timeout) noexcept
-{
-	timeout_ = ping_timeout;
-}
-
-auto server::get_channels() const noexcept -> const std::set<std::string>&
-{
-	return jchannels_;
-}
-
-auto server::is_self(std::string_view target) const noexcept -> bool
-{
-	return nickname_ == irc::user::parse(target).nick;
-}
-
-void server::connect(connect_handler handler) noexcept
-{
-	assert(state_ == state::disconnected);
-	assert((options_ & options::ipv4) == options::ipv4 || (options_ & options::ipv6) == options::ipv6);
-
-	/*
-	 * This is needed if irccd is started before DHCP or if DNS cache is
-	 * outdated.
-	 */
-#if !BOOST_OS_WINDOWS
-	(void)res_init();
-#endif
-
-	conn_ = std::make_unique<irc::connection>(service_);
-	conn_->use_ssl((options_ & options::ssl) == options::ssl);
-	conn_->use_ipv4((options_ & options::ipv4) == options::ipv4);
-	conn_->use_ipv6((options_ & options::ipv6) == options::ipv6);
-
-	jchannels_.clear();
-	state_ = state::connecting;
-
-	timer_.expires_from_now(boost::posix_time::seconds(timeout_));
-	timer_.async_wait([this, handler] (auto code) {
-		handle_wait(code, handler);
-	});
-
-	const auto self = shared_from_this();
-
-	conn_->connect(hostname_, std::to_string(port_), [this, handler, c = conn_] (auto code) {
-		handle_connect(code, handler);
-	});
-}
-
-void server::disconnect() noexcept
-{
-	conn_ = nullptr;
-	state_ = state::disconnected;
-	queue_.clear();
-}
-
-void server::invite(std::string_view target, std::string_view channel)
-{
-	assert(!target.empty());
-	assert(!channel.empty());
-
-	send(str(format("INVITE %1% %2%") % target % channel));
-}
-
-void server::join(std::string_view channel, std::string_view password)
-{
-	assert(!channel.empty());
-
-	auto it = std::find_if(rchannels_.begin(), rchannels_.end(), [&] (const auto& c) {
-		return c.name == channel;
-	});
-
-	if (it == rchannels_.end())
-		rchannels_.push_back({ std::string(channel), std::string(password) });
-	else
-		*it = { std::string(channel), std::string(password) };
-
-	if (state_ == state::connected) {
-		if (password.empty())
-			send(str(format("JOIN %1%") % channel));
-		else
-			send(str(format("JOIN %1% :%2%") % channel % password));
-	}
-}
-
-void server::kick(std::string_view target, std::string_view channel, std::string_view reason)
-{
-	assert(!target.empty());
-	assert(!channel.empty());
-
-	if (!reason.empty())
-		send(str(format("KICK %1% %2% :%3%") % channel % target % reason));
-	else
-		send(str(format("KICK %1% %2%") % channel % target));
-}
-
-void server::me(std::string_view target, std::string_view message)
-{
-	assert(!target.empty());
-	assert(!message.empty());
-
-	send(str(format("PRIVMSG %1% :\x01" "ACTION %2%\x01") % target % message));
-}
-
-void server::message(std::string_view target, std::string_view message)
-{
-	assert(!target.empty());
-	assert(!message.empty());
-
-	send(str(format("PRIVMSG %1% :%2%") % target % message));
-}
-
-void server::mode(std::string_view channel,
-                  std::string_view mode,
-                  std::string_view limit,
-                  std::string_view user,
-                  std::string_view mask)
-{
-	assert(!channel.empty());
-	assert(!mode.empty());
-
-	std::ostringstream oss;
-
-	oss << "MODE " << channel << " " << mode;
-
-	if (!limit.empty())
-		oss << " " << limit;
-	if (!user.empty())
-		oss << " " << user;
-	if (!mask.empty())
-		oss << " " << mask;
-
-	send(oss.str());
-}
-
-void server::names(std::string_view channel)
-{
-	assert(!channel.empty());
-
-	send(str(format("NAMES %1%") % channel));
-}
-
-void server::notice(std::string_view target, std::string_view message)
-{
-	assert(!target.empty());
-	assert(!message.empty());
-
-	send(str(format("NOTICE %1% :%2%") % target % message));
-}
-
-void server::part(std::string_view channel, std::string_view reason)
-{
-	assert(!channel.empty());
-
-	if (!reason.empty())
-		send(str(format("PART %1% :%2%") % channel % reason));
-	else
-		send(str(format("PART %1%") % channel));
-}
-
-void server::send(std::string_view raw)
-{
-	assert(!raw.empty());
-
-	if (state_ == state::identifying || state_ == state::connected) {
-		const auto in_progress = queue_.size() > 0;
-
-		queue_.push_back(std::string(raw));
-
-		if (!in_progress)
-			flush();
-	} else
-		queue_.push_back(std::string(raw));
-}
-
-void server::topic(std::string_view channel, std::string_view topic)
-{
-	assert(!channel.empty());
-
-	if (!topic.empty())
-		send(str(format("TOPIC %1% :%2%") % channel % topic));
-	else
-		send(str(format("TOPIC %1%") % channel));
-}
-
-void server::whois(std::string_view target)
-{
-	assert(!target.empty());
-
-	send(str(format("WHOIS %1% %2%") % target % target));
-}
-
-server_error::server_error(error code) noexcept
-	: system_error(make_error_code(code))
-{
-}
-
-auto server_category() -> const std::error_category&
-{
-	static const class category : public std::error_category {
-	public:
-		auto name() const noexcept -> const char* override
-		{
-			return "server";
-		}
-
-		auto message(int e) const -> std::string override
-		{
-			switch (static_cast<server_error::error>(e)) {
-			case server_error::not_found:
-				return "server not found";
-			case server_error::invalid_identifier:
-				return "invalid server identifier";
-			case server_error::not_connected:
-				return "server is not connected";
-			case server_error::already_connected:
-				return "server is already connected";
-			case server_error::already_exists:
-				return "server already exists";
-			case server_error::invalid_port:
-				return "invalid port number specified";
-			case server_error::invalid_reconnect_delay:
-				return "invalid reconnect delay number";
-			case server_error::invalid_hostname:
-				return "invalid hostname";
-			case server_error::invalid_channel:
-				return "invalid or empty channel";
-			case server_error::invalid_mode:
-				return "invalid or empty mode";
-			case server_error::invalid_nickname:
-				return "invalid nickname";
-			case server_error::invalid_username:
-				return "invalid username";
-			case server_error::invalid_realname:
-				return "invalid realname";
-			case server_error::invalid_password:
-				return "invalid password";
-			case server_error::invalid_ping_timeout:
-				return "invalid ping timeout";
-			case server_error::invalid_ctcp_version:
-				return "invalid CTCP VERSION";
-			case server_error::invalid_command_char:
-				return "invalid character command";
-			case server_error::invalid_message:
-				return "invalid message";
-			case server_error::ssl_disabled:
-				return "ssl is not enabled";
-			case server_error::invalid_family:
-				return "invalid family";
-			default:
-				return "no error";
-			}
-		}
-	} category;
-
-	return category;
-}
-
-auto make_error_code(server_error::error e) -> std::error_code
-{
-	return { static_cast<int>(e), server_category() };
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/server.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,917 +0,0 @@
-/*
- * server.hpp -- an IRC server
- *
- * Copyright (c) 2013-2018 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_DAEMON_SERVER_HPP
-#define IRCCD_DAEMON_SERVER_HPP
-
-/**
- * \file server.hpp
- * \brief IRC Server.
- */
-
-/**
- * \defgroup servers Servers
- * \brief Everything you need for IRC server connection
- */
-
-/**
- * \defgroup events Events
- * \brief Describe all IRC events.
- * \ingroup servers
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <cstdint>
-#include <deque>
-#include <functional>
-#include <map>
-#include <memory>
-#include <set>
-#include <string>
-#include <variant>
-#include <vector>
-
-#include <json.hpp>
-
-#include "irc.hpp"
-
-namespace irccd {
-
-class server;
-
-/**
- * \brief Prefixes for nicknames.
- * \ingroup servers
- */
-enum class channel_mode {
-	creator         = 'O',                  //!< Channel creator
-	half_op         = 'h',                  //!< Half operator
-	op              = 'o',                  //!< Channel operator
-	protection      = 'a',                  //!< Unkillable
-	voiced          = 'v'                   //!< Voice power
-};
-
-/**
- * \brief A channel to join with an optional password.
- * \ingroup servers
- */
-struct channel {
-	std::string name;                       //!< the channel to join
-	std::string password;                   //!< the optional password
-};
-
-/**
- * \brief Describe a whois information.
- * \ingroup events
- */
-struct whois_info {
-	std::string nick;                       //!< user's nickname
-	std::string user;                       //!< user's user
-	std::string hostname;                   //!< hostname
-	std::string realname;                   //!< realname
-	std::vector<std::string> channels;      //!< the channels where the user is
-};
-
-/**
- * \brief Connection success event.
- * \ingroup events
- */
-struct connect_event {
-	std::shared_ptr<class server> server;   //!< The server.
-};
-
-/**
- * \brief Connection success event.
- * \ingroup events
- */
-struct disconnect_event {
-	std::shared_ptr<class server> server;   //!< The server.
-};
-
-/**
- * \brief Invite event.
- * \ingroup events
- */
-struct invite_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-	std::string nickname;                   //!< The nickname (you).
-};
-
-/**
- * \brief Join event.
- * \ingroup events
- */
-struct join_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-};
-
-/**
- * \brief Kick event.
- * \ingroup events
- */
-struct kick_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-	std::string target;                     //!< The target.
-	std::string reason;                     //!< The reason (Optional).
-};
-
-/**
- * \brief Message event.
- * \ingroup events
- */
-struct message_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-	std::string message;                    //!< The message.
-};
-
-/**
- * \brief CTCP action event.
- * \ingroup events
- */
-struct me_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-	std::string message;                    //!< The message.
-};
-
-/**
- * \brief Mode event.
- * \ingroup events
- */
-struct mode_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel or target.
-	std::string mode;                       //!< The mode.
-	std::string limit;                      //!< The optional limit.
-	std::string user;                       //!< The optional user.
-	std::string mask;                       //!< The optional ban mask.
-};
-
-/**
- * \brief Names listing event.
- * \ingroup events
- */
-struct names_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string channel;                    //!< The channel.
-	std::vector<std::string> names;         //!< The names.
-};
-
-/**
- * \brief Nick change event.
- * \ingroup events
- */
-struct nick_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string nickname;                   //!< The new nickname.
-};
-
-/**
- * \brief Notice event.
- * \ingroup events
- */
-struct notice_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel or target.
-	std::string message;                    //!< The message.
-};
-
-/**
- * \brief Part event.
- * \ingroup events
- */
-struct part_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-	std::string reason;                     //!< The reason.
-};
-
-/**
- * \brief Topic event.
- * \ingroup events
- */
-struct topic_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	std::string origin;                     //!< The originator.
-	std::string channel;                    //!< The channel.
-	std::string topic;                      //!< The topic message.
-};
-
-/**
- * \brief Whois event.
- * \ingroup events
- */
-struct whois_event {
-	std::shared_ptr<class server> server;   //!< The server.
-	whois_info whois;                       //!< The whois information.
-};
-
-/**
- * \brief Store all possible events.
- * \ingroup events
- */
-using event = std::variant<
-	std::monostate,
-	connect_event,
-	disconnect_event,
-	invite_event,
-	join_event,
-	kick_event,
-	me_event,
-	message_event,
-	mode_event,
-	names_event,
-	nick_event,
-	notice_event,
-	part_event,
-	topic_event,
-	whois_event
->;
-
-/**
- * \brief The class that connect to a IRC server.
- * \ingroup server
- *
- * This class is higher level than irc connection, it does identify process,
- * parsing message, translating messages and queue'ing user requests.
- */
-class server : public std::enable_shared_from_this<server> {
-public:
-	/**
-	 * Completion handler once network connection is complete.
-	 */
-	using connect_handler = std::function<void (std::error_code)>;
-
-	/**
-	 * Completion handler once a network message has arrived.
-	 */
-	using recv_handler = std::function<void (std::error_code, event)>;
-
-	/**
-	 * \brief Various options for server.
-	 */
-	enum class options : std::uint8_t {
-		none            = 0,            //!< No options
-		ipv4            = (1 << 0),     //!< Connect using IPv4
-		ipv6            = (1 << 1),     //!< Connect using IPv6
-		ssl             = (1 << 2),     //!< Use SSL
-		auto_rejoin     = (1 << 3),     //!< Auto rejoin a kick
-		auto_reconnect  = (1 << 4),     //!< Auto reconnect on disconnection
-		join_invite     = (1 << 5)      //!< Join a channel on invitation
-	};
-
-	/**
-	 * \brief Describe current server state.
-	 */
-	enum class state : std::uint8_t {
-		disconnected,                   //!< not connected at all,
-		connecting,                     //!< network connection in progress,
-		identifying,                    //!< sending nick, user and password commands,
-		connected                       //!< ready for use.
-	};
-
-protected:
-	/**
-	 * \brief Server state.
-	 */
-	state state_{state::disconnected};
-
-private:
-	// Requested and joined channels.
-	std::vector<channel> rchannels_;
-	std::set<std::string> jchannels_;
-
-	// Identifier.
-	std::string id_;
-
-	// Connection information.
-	std::string hostname_;
-	std::string password_;
-	std::uint16_t port_{6667};
-	options options_;
-
-	// Identity.
-	std::string nickname_{"irccd"};
-	std::string username_{"irccd"};
-	std::string realname_{"IRC Client Daemon"};
-	std::string ctcpversion_{"IRC Client Daemon"};
-
-	// Settings.
-	std::string command_char_{"!"};
-	std::uint16_t recodelay_{30};
-	std::uint16_t timeout_{1000};
-
-	// Server information.
-	std::map<channel_mode, char> modes_;
-
-	// Misc.
-	boost::asio::io_service& service_;
-	boost::asio::deadline_timer timer_;
-	std::shared_ptr<irc::connection> conn_;
-	std::deque<std::string> queue_;
-	std::map<std::string, std::set<std::string>> names_map_;
-	std::map<std::string, whois_info> whois_map_;
-
-	auto dispatch_connect(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_endofnames(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_endofwhois(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_invite(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_isupport(const irc::message&) -> bool;
-	auto dispatch_join(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_kick(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_mode(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_namreply(const irc::message&) -> bool;
-	auto dispatch_nick(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_notice(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_part(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_ping(const irc::message&) -> bool;
-	auto dispatch_privmsg(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_topic(const irc::message&, const recv_handler&) -> bool;
-	auto dispatch_whoischannels(const irc::message&) -> bool;
-	auto dispatch_whoisuser(const irc::message&) -> bool;
-	auto dispatch(const irc::message&, const recv_handler&) -> bool;
-
-	// I/O and connection.
-	void flush();
-	void identify();
-	void handle_send(const std::error_code&);
-	void handle_recv(const std::error_code&, const irc::message&, const recv_handler&);
-	void handle_wait(const std::error_code&, const connect_handler&);
-	void handle_connect(const std::error_code&, const connect_handler&);
-
-public:
-	/**
-	 * Construct a server.
-	 *
-	 * \pre !host.empty()
-	 * \param service the service
-	 * \param id the identifier
-	 * \param hostname the hostname
-	 */
-	server(boost::asio::io_service& service, std::string id, std::string hostname = "localhost");
-
-	/**
-	 * Destructor. Close the connection if needed.
-	 */
-	virtual ~server();
-
-	/**
-	 * Get the current server state.
-	 *
-	 * \return the state
-	 */
-	auto get_state() const noexcept -> state;
-
-	/**
-	 * Get the server identifier.
-	 *
-	 * \return the id
-	 */
-	auto get_id() const noexcept -> const std::string&;
-
-	/**
-	 * Get the hostname.
-	 *
-	 * \return the hostname
-	 */
-	auto get_hostname() const noexcept -> const std::string&;
-
-	/**
-	 * Get the password.
-	 *
-	 * \return the password
-	 */
-	auto get_password() const noexcept -> const std::string&;
-
-	/**
-	 * Set the password.
-	 *
-	 * An empty password means no password.
-	 *
-	 * \param password the password
-	 */
-	void set_password(std::string password) noexcept;
-
-	/**
-	 * Get the port.
-	 *
-	 * \return the port
-	 */
-	auto get_port() const noexcept -> std::uint16_t;
-
-	/**
-	 * Set the port.
-	 *
-	 * \param port the port
-	 */
-	void set_port(std::uint16_t port) noexcept;
-
-	/**
-	 * Get the options flags.
-	 *
-	 * \return the flags
-	 */
-	auto get_options() const noexcept -> options;
-
-	/**
-	 * Set the options flags.
-	 *
-	 * \param flags the flags
-	 */
-	void set_options(options flags) noexcept;
-
-	/**
-	 * Get the nickname.
-	 *
-	 * \return the nickname
-	 */
-	auto get_nickname() const noexcept -> const std::string&;
-
-	/**
-	 * Set the nickname.
-	 *
-	 * If the server is connected, send a nickname command to the IRC server,
-	 * otherwise change it instantly.
-	 *
-	 * \param nickname the nickname
-	 */
-	void set_nickname(std::string nickname);
-
-	/**
-	 * Get the username.
-	 *
-	 * \return the username
-	 */
-	auto get_username() const noexcept -> const std::string&;
-
-	/**
-	 * Set the username.
-	 *
-	 * \param name the username
-	 * \note the username will be changed on the next connection
-	 */
-	void set_username(std::string name) noexcept;
-
-	/**
-	 * Get the realname.
-	 *
-	 * \return the realname
-	 */
-	auto get_realname() const noexcept -> const std::string&;
-
-	/**
-	 * Set the realname.
-	 *
-	 * \param realname the username
-	 * \note the username will be changed on the next connection
-	 */
-	void set_realname(std::string realname) noexcept;
-
-	/**
-	 * Get the CTCP version.
-	 *
-	 * \return the CTCP version
-	 */
-	auto get_ctcp_version() const noexcept -> const std::string&;
-
-	/**
-	 * Set the CTCP version.
-	 *
-	 * \param ctcpversion the version
-	 */
-	void set_ctcp_version(std::string ctcpversion);
-
-	/**
-	 * Get the command character.
-	 *
-	 * \return the character
-	 */
-	auto get_command_char() const noexcept -> const std::string&;
-
-	/**
-	 * Set the command character.
-	 *
-	 * \pre !command_char_.empty()
-	 * \param command_char the command character
-	 */
-	void set_command_char(std::string command_char) noexcept;
-
-	/**
-	 * Get the reconnection delay before retrying.
-	 *
-	 * \return the number of seconds
-	 */
-	auto get_reconnect_delay() const noexcept -> std::uint16_t;
-
-	/**
-	 * Set the number of seconds before retrying.
-	 *
-	 * \param reconnect_delay the number of seconds
-	 */
-	void set_reconnect_delay(std::uint16_t reconnect_delay) noexcept;
-
-	/**
-	 * Get the ping timeout.
-	 *
-	 * \return the ping timeout
-	 */
-	auto get_ping_timeout() const noexcept -> std::uint16_t;
-
-	/**
-	 * Set the ping timeout before considering a server as dead.
-	 *
-	 * \param ping_timeout the delay in seconds
-	 */
-	void set_ping_timeout(std::uint16_t ping_timeout) noexcept;
-
-	/**
-	 * Get the list of channels joined.
-	 *
-	 * \return the channels
-	 */
-	auto get_channels() const noexcept -> const std::set<std::string>&;
-
-	/**
-	 * Determine if the nickname is the bot itself.
-	 *
-	 * \param nick the nickname to check
-	 * \return true if it is the bot
-	 */
-	auto is_self(std::string_view nick) const noexcept -> bool;
-
-	/**
-	 * Start connecting.
-	 *
-	 * This only initiate TCP connection and/or SSL handshaking, the identifying
-	 * process may take some time and you must repeatedly call recv() to wait
-	 * for connect_event.
-	 *
-	 * \pre handler != nullptr
-	 * \param handler the completion handler
-	 * \note the server must be kept alive until completion
-	 */
-	virtual void connect(connect_handler handler) noexcept;
-
-	/**
-	 * Force disconnection.
-	 */
-	virtual void disconnect() noexcept;
-
-	/**
-	 * Receive next event.
-	 *
-	 * \pre handler != nullptr
-	 * \param handler the handler
-	 * \note the server must be kept alive until completion
-	 */
-	virtual void recv(recv_handler handler) noexcept;
-
-	/**
-	 * Invite a user to a channel.
-	 *
-	 * \param target the target nickname
-	 * \param channel the channel
-	 */
-	virtual void invite(std::string_view target, std::string_view channel);
-
-	/**
-	 * Join a channel, the password is optional and can be kept empty.
-	 *
-	 * \param channel the channel to join
-	 * \param password the optional password
-	 */
-	virtual void join(std::string_view channel, std::string_view password = "");
-
-	/**
-	 * Kick someone from the channel. Please be sure to have the rights
-	 * on that channel because errors won't be reported.
-	 *
-	 * \param target the target to kick
-	 * \param channel from which channel
-	 * \param reason the optional reason
-	 */
-	virtual void kick(std::string_view target,
-	                  std::string_view channel,
-	                  std::string_view reason = "");
-
-	/**
-	 * Send a CTCP Action as known as /me. The target may be either a
-	 * channel or a nickname.
-	 *
-	 * \param target the nickname or the channel
-	 * \param message the message
-	 */
-	virtual void me(std::string_view target, std::string_view message);
-
-	/**
-	 * Send a message to the specified target or channel.
-	 *
-	 * \param target the target
-	 * \param message the message
-	 */
-	virtual void message(std::string_view target, std::string_view message);
-
-	/**
-	 * Change channel/user mode.
-	 *
-	 * \param channel the channel or nickname
-	 * \param mode the mode
-	 * \param limit the optional limit
-	 * \param user the optional user
-	 * \param mask the optional ban mask
-	 */
-	virtual void mode(std::string_view channel,
-	                  std::string_view mode,
-	                  std::string_view limit = "",
-	                  std::string_view user = "",
-	                  std::string_view mask = "");
-
-	/**
-	 * Request the list of names.
-	 *
-	 * \param channel the channel
-	 */
-	virtual void names(std::string_view channel);
-
-	/**
-	 * Send a private notice.
-	 *
-	 * \param target the target
-	 * \param message the notice message
-	 */
-	virtual void notice(std::string_view target, std::string_view message);
-
-	/**
-	 * Part from a channel.
-	 *
-	 * Please note that the reason is not supported on all servers so if you
-	 * want portability, don't provide it.
-	 *
-	 * \param channel the channel to leave
-	 * \param reason the optional reason
-	 */
-	virtual void part(std::string_view channel, std::string_view reason = "");
-
-	/**
-	 * Send a raw message to the IRC server. You don't need to add
-	 * message terminators.
-	 *
-	 * If the server is not yet connected, the command is postponed and will be
-	 * ran when ready.
-	 *
-	 * \param raw the raw message (without `\r\n\r\n`)
-	 */
-	virtual void send(std::string_view raw);
-
-	/**
-	 * Change the channel topic.
-	 *
-	 * \param channel the channel
-	 * \param topic the desired topic
-	 */
-	virtual void topic(std::string_view channel, std::string_view topic);
-
-	/**
-	 * Request for whois information.
-	 *
-	 * \param target the target nickname
-	 */
-	virtual void whois(std::string_view target);
-};
-
-/**
- * \cond IRCCD_HIDDEN_SYMBOLS
- */
-
-/**
- * Apply bitwise XOR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator^(server::options v1, server::options v2) noexcept -> server::options
-{
-	return static_cast<server::options>(static_cast<unsigned>(v1) ^ static_cast<unsigned>(v2));
-}
-
-/**
- * Apply bitwise AND.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator&(server::options v1, server::options v2) noexcept -> server::options
-{
-	return static_cast<server::options>(static_cast<unsigned>(v1) & static_cast<unsigned>(v2));
-}
-
-/**
- * Apply bitwise OR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator|(server::options v1, server::options v2) noexcept -> server::options
-{
-	return static_cast<server::options>(static_cast<unsigned>(v1) | static_cast<unsigned>(v2));
-}
-
-/**
- * Apply bitwise NOT.
- *
- * \param v the value
- * \return the complement
- */
-inline auto operator~(server::options v) noexcept -> server::options
-{
-	return static_cast<server::options>(~static_cast<unsigned>(v));
-}
-
-/**
- * Assign bitwise OR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator|=(server::options& v1, server::options v2) noexcept -> server::options&
-{
-	return v1 = v1 | v2;
-}
-
-/**
- * Assign bitwise AND.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator&=(server::options& v1, server::options v2) noexcept -> server::options&
-{
-	return v1 = v1 & v2;
-}
-
-/**
- * Assign bitwise XOR.
- *
- * \param v1 the first value
- * \param v2 the second value
- * \return the new value
- */
-inline auto operator^=(server::options& v1, server::options v2) noexcept -> server::options&
-{
-	return v1 = v1 ^ v2;
-}
-
-/**
- * \endcond
- */
-
-/**
- * \brief Server error.
- */
-class server_error : public std::system_error {
-public:
-	/**
-	 * \brief Server related errors.
-	 */
-	enum error {
-		//!< No error.
-		no_error = 0,
-
-		//!< The specified server was not found.
-		not_found,
-
-		//!< The specified identifier is invalid.
-		invalid_identifier,
-
-		//!< The server is not connected.
-		not_connected,
-
-		//!< The server is already connected.
-		already_connected,
-
-		//!< Server with same name already exists.
-		already_exists,
-
-		//!< The specified port number is invalid.
-		invalid_port,
-
-		//!< The specified reconnect delay number is invalid.
-		invalid_reconnect_delay,
-
-		//!< The specified host was invalid.
-		invalid_hostname,
-
-		//!< The channel was empty or invalid.
-		invalid_channel,
-
-		//!< The mode given was empty.
-		invalid_mode,
-
-		//!< The nickname was empty or invalid.
-		invalid_nickname,
-
-		//!< The username was empty or invalid.
-		invalid_username,
-
-		//!< The realname was empty or invalid.
-		invalid_realname,
-
-		//!< Invalid password property.
-		invalid_password,
-
-		//!< Invalid ping timeout.
-		invalid_ping_timeout,
-
-		//!< Invalid ctcp version.
-		invalid_ctcp_version,
-
-		//!< Invalid command character.
-		invalid_command_char,
-
-		//!< Message (PRIVMSG) was invalid
-		invalid_message,
-
-		//!< SSL was requested but is disabled.
-		ssl_disabled,
-
-		//!< IPv4 or IPv6 must be defined.
-		invalid_family
-	};
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param code the error code
-	 */
-	server_error(error code) noexcept;
-};
-
-/**
- * Get the server error category singleton.
- *
- * \return the singleton
- */
-auto server_category() -> const std::error_category&;
-
-/**
- * Create a std::error_code from server_error::error enum.
- *
- * \param e the error code
- * \return the error code
- */
-auto make_error_code(server_error::error e) -> std::error_code;
-
-} // !irccd
-
-/**
- * \cond IRCCD_HIDDEN_SYMBOLS
- */
-
-namespace std {
-
-template <>
-struct is_error_code_enum<irccd::server_error::error> : public std::true_type {
-};
-
-} // !std
-
-/**
- * \endcond
- */
-
-#endif // !IRCCD_DAEMON_SERVER_HPP
--- a/libirccd/irccd/daemon/server_service.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,689 +0,0 @@
-/*
- * server_service.cpp -- server service
- *
- * Copyright (c) 2013-2018 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 <irccd/json_util.hpp>
-#include <irccd/string_util.hpp>
-
-#include "irccd.hpp"
-#include "logger.hpp"
-#include "plugin_service.hpp"
-#include "rule_service.hpp"
-#include "server.hpp"
-#include "server_service.hpp"
-#include "server_util.hpp"
-#include "transport_service.hpp"
-
-namespace irccd {
-
-namespace {
-
-class dispatcher {
-private:
-	irccd& irccd_;
-
-	template <typename EventNameFunc, typename ExecFunc>
-	void dispatch(std::string_view, std::string_view, std::string_view, EventNameFunc&&, ExecFunc);
-
-public:
-	dispatcher(irccd& irccd);
-	void operator()(const std::monostate&);
-	void operator()(const connect_event&);
-	void operator()(const disconnect_event&);
-	void operator()(const invite_event&);
-	void operator()(const join_event&);
-	void operator()(const kick_event&);
-	void operator()(const message_event&);
-	void operator()(const me_event&);
-	void operator()(const mode_event&);
-	void operator()(const names_event&);
-	void operator()(const nick_event&);
-	void operator()(const notice_event&);
-	void operator()(const part_event&);
-	void operator()(const topic_event&);
-	void operator()(const whois_event&);
-};
-
-template <typename EventNameFunc, typename ExecFunc>
-void dispatcher::dispatch(std::string_view server,
-                          std::string_view origin,
-                          std::string_view target,
-                          EventNameFunc&& name_func,
-                          ExecFunc exec_func)
-{
-	for (const auto& plugin : irccd_.plugins().list()) {
-		const auto eventname = name_func(*plugin);
-		const auto allowed = irccd_.rules().solve(server, target, origin, plugin->get_name(), eventname);
-
-		if (!allowed) {
-			irccd_.get_log().debug("rule", "") << "event skipped on match" << std::endl;
-			continue;
-		}
-
-		irccd_.get_log().debug("rule", "") << "event allowed" << std::endl;
-
-		try {
-			exec_func(*plugin);
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning(*plugin) << ex.what() << std::endl;
-		}
-	}
-}
-
-dispatcher::dispatcher(irccd& irccd)
-	: irccd_(irccd)
-{
-}
-
-void dispatcher::operator()(const std::monostate&)
-{
-}
-
-void dispatcher::operator()(const connect_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onConnect" << std::endl;
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onConnect"             },
-		{ "server",     ev.server->get_id()     }
-	}));
-
-	dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "",
-		[=] (plugin&) -> std::string {
-			return "onConnect";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_connect(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const disconnect_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onDisconnect" << std::endl;
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onDisconnect"          },
-		{ "server",     ev.server->get_id()     }
-	}));
-
-	dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "",
-		[=] (plugin&) -> std::string {
-			return "onDisconnect";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_disconnect(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const invite_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onInvite:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  target: " << ev.nickname << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onInvite"              },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onInvite";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_invite(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const join_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onJoin:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onJoin"                },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onJoin";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_join(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const kick_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onKick:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  target: " << ev.target << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  reason: " << ev.reason << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onKick"                },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              },
-		{ "target",     ev.target               },
-		{ "reason",     ev.reason               }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onKick";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_kick(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const message_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onMessage:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  message: " << ev.message << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onMessage"             },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              },
-		{ "message",    ev.message              }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin& plugin) -> std::string {
-			return server_util::message_type::parse(
-				ev.message,
-				ev.server->get_command_char(),
-				plugin.get_id()
-			).type == server_util::message_type::is_command ? "onCommand" : "onMessage";
-		},
-		[=] (plugin& plugin) mutable {
-			auto copy = ev;
-			auto pack = server_util::message_type::parse(
-				copy.message,
-				copy.server->get_command_char(),
-				plugin.get_id()
-			);
-
-			copy.message = pack.message;
-
-			if (pack.type == server_util::message_type::is_command)
-				plugin.handle_command(irccd_, copy);
-			else
-				plugin.handle_message(irccd_, copy);
-		}
-	);
-}
-
-void dispatcher::operator()(const me_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onMe:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  target: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  message: " << ev.message << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onMe"                  },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "target",     ev.channel              },
-		{ "message",    ev.message              }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onMe";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_me(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const mode_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onMode" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  mode: " << ev.mode << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  limit: " << ev.limit << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  user: " << ev.user << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  mask: " << ev.mask << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onMode"                },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              },
-		{ "mode",       ev.mode                 },
-		{ "limit",      ev.limit                },
-		{ "user",       ev.user                 },
-		{ "mask",       ev.mask                 }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, /* channel */ "",
-		[=] (plugin &) -> std::string {
-			return "onMode";
-		},
-		[=] (plugin &plugin) {
-			plugin.handle_mode(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const names_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onNames:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  names: " << string_util::join(ev.names.begin(), ev.names.end(), ", ") << std::endl;
-
-	auto names = nlohmann::json::array();
-
-	for (const auto& v : ev.names)
-		names.push_back(v);
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onNames"               },
-		{ "server",     ev.server->get_id()     },
-		{ "channel",    ev.channel              },
-		{ "names",      std::move(names)        }
-	}));
-
-	dispatch(ev.server->get_id(), /* origin */ "", ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onNames";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_names(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const nick_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onNick:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  nickname: " << ev.nickname << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onNick"                },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "nickname",   ev.nickname             }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, /* channel */ "",
-		[=] (plugin&) -> std::string {
-			return "onNick";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_nick(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const notice_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onNotice:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  message: " << ev.message << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onNotice"              },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              },
-		{ "message",    ev.message              }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, /* channel */ "",
-		[=] (plugin&) -> std::string {
-			return "onNotice";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_notice(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const part_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onPart:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  reason: " << ev.reason << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onPart"                },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              },
-		{ "reason",     ev.reason               }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onPart";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_part(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const topic_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onTopic:" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  origin: " << ev.origin << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channel: " << ev.channel << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  topic: " << ev.topic << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onTopic"               },
-		{ "server",     ev.server->get_id()     },
-		{ "origin",     ev.origin               },
-		{ "channel",    ev.channel              },
-		{ "topic",      ev.topic                }
-	}));
-
-	dispatch(ev.server->get_id(), ev.origin, ev.channel,
-		[=] (plugin&) -> std::string {
-			return "onTopic";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_topic(irccd_, ev);
-		}
-	);
-}
-
-void dispatcher::operator()(const whois_event& ev)
-{
-	irccd_.get_log().debug(*ev.server) << "event onWhois" << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  nickname: " << ev.whois.nick << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  username: " << ev.whois.user << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  hostname: " << ev.whois.hostname << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  realname: " << ev.whois.realname << std::endl;
-	irccd_.get_log().debug(*ev.server) << "  channels: " << string_util::join(ev.whois.channels, ", ") << std::endl;
-
-	irccd_.transports().broadcast(nlohmann::json::object({
-		{ "event",      "onWhois"               },
-		{ "server",     ev.server->get_id()     },
-		{ "nickname",   ev.whois.nick           },
-		{ "username",   ev.whois.user           },
-		{ "hostname",   ev.whois.hostname       },
-		{ "realname",   ev.whois.realname       }
-	}));
-
-	dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "",
-		[=] (plugin&) -> std::string {
-			return "onWhois";
-		},
-		[=] (plugin& plugin) {
-			plugin.handle_whois(irccd_, ev);
-		}
-	);
-}
-
-} // !namespace
-
-void server_service::handle_error(const std::shared_ptr<server>& server,
-                                  const std::error_code& code)
-{
-	assert(server);
-
-	irccd_.get_log().warning(*server) << code.message() << std::endl;
-	irccd_.get_log().warning(*server) << int(server->get_options()) << std::endl;
-
-	if ((server->get_options() & server::options::auto_reconnect) != server::options::auto_reconnect)
-		remove(server->get_id());
-	else {
-		irccd_.get_log().info(*server) << "reconnecting in "
-			<< server->get_reconnect_delay() << " second(s)" << std::endl;
-		wait(server);
-	}
-}
-
-void server_service::handle_wait(const std::shared_ptr<server>& server, const std::error_code& code)
-{
-	/*
-	 * The timer runs on his own control, it will complete either if the delay
-	 * was reached, there was an error or if the io_context was called to cancel
-	 * all pending operations.
-	 *
-	 * This means while the timer is running someone may already have ask a
-	 * server for explicit reconnection (e.g. remote command, plugin). Thus we
-	 * check for server state and if it is still present in service.
-	 */
-	if (code && code != std::errc::operation_canceled) {
-		irccd_.get_log().warning(*server) << code.message() << std::endl;
-		return;
-	}
-
-	if (server->get_state() == server::state::connected || !has(server->get_id()))
-		return;
-
-	connect(server);
-}
-
-void server_service::handle_recv(const std::shared_ptr<server>& server,
-                                 const std::error_code& code,
-                                 const event& event)
-{
-	assert(server);
-
-	if (code)
-		handle_error(server, code);
-	else {
-		recv(server);
-		std::visit(dispatcher(irccd_), event);
-	}
-}
-
-void server_service::handle_connect(const std::shared_ptr<server>& server, const std::error_code& code)
-{
-	if (code)
-		handle_error(server, code);
-	else
-		recv(server);
-}
-
-void server_service::wait(const std::shared_ptr<server>& server)
-{
-	assert(server);
-
-	auto timer = std::make_shared<boost::asio::deadline_timer>(irccd_.get_service());
-
-	timer->expires_from_now(boost::posix_time::seconds(server->get_reconnect_delay()));
-	timer->async_wait([this, server, timer] (auto code) {
-		handle_wait(server, code);
-	});
-}
-
-void server_service::recv(const std::shared_ptr<server>& server)
-{
-	assert(server);
-
-	server->recv([this, server] (auto code, auto event) {
-		handle_recv(server, code, event);
-	});
-}
-
-void server_service::connect(const std::shared_ptr<server>& server)
-{
-	assert(server);
-
-	server->connect([this, server] (auto code) {
-		handle_connect(server, code);
-	});
-}
-
-server_service::server_service(irccd &irccd)
-	: irccd_(irccd)
-{
-}
-
-auto server_service::list() const noexcept -> const std::vector<std::shared_ptr<server>>&
-{
-	return servers_;
-}
-
-auto server_service::has(const std::string& name) const noexcept -> bool
-{
-	return std::count_if(servers_.begin(), servers_.end(), [&] (const auto& server) {
-		return server->get_id() == name;
-	}) > 0;
-}
-
-void server_service::add(std::shared_ptr<server> server)
-{
-	assert(server);
-	assert(!has(server->get_id()));
-
-	servers_.push_back(server);
-	connect(server);
-}
-
-auto server_service::get(std::string_view name) const noexcept -> std::shared_ptr<server>
-{
-	const auto it = std::find_if(servers_.begin(), servers_.end(), [&] (const auto& server) {
-		return server->get_id() == name;
-	});
-
-	if (it == servers_.end())
-		return nullptr;
-
-	return *it;
-}
-
-auto server_service::require(std::string_view name) const -> std::shared_ptr<server>
-{
-	if (!string_util::is_identifier(name))
-		throw server_error(server_error::invalid_identifier);
-
-	const auto s = get(name);
-
-	if (!s)
-		throw server_error(server_error::not_found);
-
-	return s;
-}
-
-void server_service::disconnect(std::string_view id)
-{
-	const auto s = require(id);
-
-	s->disconnect();
-	dispatcher{irccd_}(disconnect_event{s});
-}
-
-void server_service::reconnect(std::string_view id)
-{
-	disconnect(id);
-	connect(require(id));
-}
-
-void server_service::reconnect()
-{
-	for (const auto& s : servers_) {
-		try {
-			s->disconnect();
-			dispatcher{irccd_}(disconnect_event{s});
-			connect(s);
-		} catch (const server_error& ex) {
-			irccd_.get_log().warning(*s) << ex.what() << std::endl;
-		}
-	}
-}
-
-void server_service::remove(std::string_view name)
-{
-	const auto it = std::find_if(servers_.begin(), servers_.end(), [&] (const auto& server) {
-		return server->get_id() == name;
-	});
-
-	if (it != servers_.end()) {
-		(*it)->disconnect();
-		servers_.erase(it);
-	}
-}
-
-void server_service::clear() noexcept
-{
-	/*
-	 * Copy the array, because disconnect() may trigger on_die signal which
-	 * erase the server from itself.
-	 */
-	const auto save = servers_;
-
-	for (const auto& server : save)
-		server->disconnect();
-
-	servers_.clear();
-}
-
-void server_service::load(const config& cfg) noexcept
-{
-	for (const auto& section : cfg) {
-		if (section.get_key() != "server")
-			continue;
-
-		const auto id = section.get("name").get_value();
-
-		try {
-			auto server = server_util::from_config(irccd_.get_service(), section);
-
-			if (has(server->get_id()))
-				throw server_error(server_error::already_exists);
-
-			add(std::move(server));
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning("server", id) << ex.what() << std::endl;
-		}
-	}
-}
-
-namespace logger {
-
-auto loggable_traits<server>::get_category(const server&) -> std::string_view
-{
-	return "server";
-}
-
-auto loggable_traits<server>::get_component(const server& sv) -> std::string_view
-{
-	return sv.get_id();
-}
-
-} // !logger
-
-} // !irccd
--- a/libirccd/irccd/daemon/server_service.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,182 +0,0 @@
-/*
- * server_service.hpp -- server service
- *
- * Copyright (c) 2013-2018 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_DAEMON_SERVER_SERVICE_HPP
-#define IRCCD_DAEMON_SERVER_SERVICE_HPP
-
-/**
- * \file server_service.hpp
- * \brief Server service.
- */
-
-#include <memory>
-#include <system_error>
-#include <vector>
-
-#include "server.hpp"
-
-namespace irccd {
-
-class config;
-class irccd;
-
-/**
- * \brief Manage IRC servers.
- * \ingroup services
- */
-class server_service {
-private:
-	irccd& irccd_;
-	std::vector<std::shared_ptr<server>> servers_;
-
-	void handle_error(const std::shared_ptr<server>&, const std::error_code&);
-	void handle_wait(const std::shared_ptr<server>&, const std::error_code&);
-	void handle_recv(const std::shared_ptr<server>&, const std::error_code&, const event&);
-	void handle_connect(const std::shared_ptr<server>&, const std::error_code&);
-
-	void wait(const std::shared_ptr<server>&);
-	void recv(const std::shared_ptr<server>&);
-	void connect(const std::shared_ptr<server>&);
-
-public:
-	/**
-	 * Create the server service.
-	 *
-	 * \param instance the irccd instance
-	 */
-	server_service(irccd& instance);
-
-	/**
-	 * Get the list of servers
-	 *
-	 * \return the servers
-	 */
-	auto list() const noexcept -> const std::vector<std::shared_ptr<server>>&;
-
-	/**
-	 * Check if a server exists.
-	 *
-	 * \param name the name
-	 * \return true if exists
-	 */
-	auto has(const std::string& name) const noexcept -> bool;
-
-	/**
-	 * Add a new server to the application.
-	 *
-	 * \pre hasServer must return false
-	 * \param sv the server
-	 */
-	void add(std::shared_ptr<server> sv);
-
-	/**
-	 * Get a server or empty one if not found
-	 *
-	 * \param name the server name
-	 * \return the server or empty one if not found
-	 */
-	auto get(std::string_view name) const noexcept -> std::shared_ptr<server>;
-
-	/**
-	 * Find a server from a JSON object.
-	 *
-	 * \param name the server name
-	 * \return the server
-	 * \throw server_error on errors
-	 */
-	auto require(std::string_view name) const -> std::shared_ptr<server>;
-
-	/**
-	 * Force disconnection, this also call plugin::handle_disconnect handler.
-	 *
-	 * \param id the server id
-	 * \throw server_error on errors
-	 */
-	void disconnect(std::string_view id);
-
-	/**
-	 * Force reconnection, this also call plugin::handle_disconnect handler.
-	 *
-	 * \param id the server id
-	 * \return the server
-	 * \throw server_error on errors
-	 */
-	void reconnect(std::string_view id);
-
-	/**
-	 * Force reconnection of all servers.
-	 */
-	void reconnect();
-
-	/**
-	 * Remove a server from the irccd instance.
-	 *
-	 * The server if any, will be disconnected.
-	 *
-	 * \param name the server name
-	 */
-	void remove(std::string_view name);
-
-	/**
-	 * Remove all servers.
-	 *
-	 * All servers will be disconnected.
-	 */
-	void clear() noexcept;
-
-	/**
-	 * Load servers from the configuration.
-	 *
-	 * \param cfg the config
-	 */
-	void load(const config& cfg) noexcept;
-};
-
-namespace logger {
-
-template <typename T>
-struct loggable_traits;
-
-/**
- * \brief Specialization for server.
- * \ingroup logger-traits
- */
-template <>
-struct loggable_traits<server> {
-	/**
-	 * Get 'server' category.
-	 *
-	 * \param server the server
-	 * \return server
-	 */
-	static auto get_category(const server& server) -> std::string_view;
-
-	/**
-	 * Get the server name.
-	 *
-	 * \param server the server
-	 * \return the server
-	 */
-	static auto get_component(const server& server) -> std::string_view;
-};
-
-} // !logger
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_SERVER_SERVICE_HPP
--- a/libirccd/irccd/daemon/server_util.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,281 +0,0 @@
-/*
- * server_util.cpp -- server utilities
- *
- * Copyright (c) 2013-2018 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 <algorithm>
-
-#include <irccd/config.hpp>
-#include <irccd/ini_util.hpp>
-#include <irccd/json_util.hpp>
-#include <irccd/string_util.hpp>
-
-#include "server.hpp"
-#include "server_util.hpp"
-
-using irccd::json_util::deserializer;
-
-namespace irccd::server_util {
-
-namespace {
-
-void toggle(server& s, server::options opt, bool value) noexcept
-{
-	if (value)
-		s.set_options(s.get_options() | opt);
-	else
-		s.set_options(s.get_options() & ~(opt));
-}
-
-void from_config_load_identity(server& sv, const ini::section& sc)
-{
-	const auto username = ini_util::optional_string(sc, "username", sv.get_username());
-	const auto realname = ini_util::optional_string(sc, "realname", sv.get_realname());
-	const auto nickname = ini_util::optional_string(sc, "nickname", sv.get_nickname());
-	const auto ctcp_version = ini_util::optional_string(sc, "ctcp-version", sv.get_ctcp_version());
-
-	if (username.empty())
-		throw server_error(server_error::invalid_username);
-	if (realname.empty())
-		throw server_error(server_error::invalid_realname);
-	if (nickname.empty())
-		throw server_error(server_error::invalid_nickname);
-	if (ctcp_version.empty())
-		throw server_error(server_error::invalid_ctcp_version);
-
-	sv.set_username(username);
-	sv.set_realname(realname);
-	sv.set_nickname(nickname);
-	sv.set_ctcp_version(ctcp_version);
-}
-
-void from_config_load_channels(server& sv, const ini::section& sc)
-{
-	for (const auto& s : sc.get("channels")) {
-		channel channel;
-
-		if (auto pos = s.find(":") != std::string::npos) {
-			channel.name = s.substr(0, pos);
-			channel.password = s.substr(pos + 1);
-		} else
-			channel.name = s;
-
-		sv.join(channel.name, channel.password);
-	}
-}
-
-void from_config_load_flags(server& sv, const ini::section& sc)
-{
-	const auto ssl = sc.find("ssl");
-	const auto auto_rejoin = sc.find("auto-rejoin");
-	const auto auto_reconnect = sc.find("auto-reconnect");
-	const auto join_invite = sc.find("join-invite");
-	const auto ipv4 = sc.find("ipv4");
-	const auto ipv6 = sc.find("ipv6");
-
-	if (ssl != sc.end())
-		toggle(sv, server::options::ssl, string_util::is_boolean(ssl->get_value()));
-	if (auto_rejoin != sc.end())
-		toggle(sv, server::options::auto_rejoin, string_util::is_boolean(auto_rejoin->get_value()));
-	if (auto_reconnect != sc.end())
-		toggle(sv, server::options::auto_reconnect, string_util::is_boolean(auto_reconnect->get_value()));
-	if (join_invite != sc.end())
-		toggle(sv, server::options::join_invite, string_util::is_boolean(join_invite->get_value()));
-	if (ipv4 != sc.end())
-		toggle(sv, server::options::ipv4, string_util::is_boolean(ipv4->get_value()));
-	if (ipv6 != sc.end())
-		toggle(sv, server::options::ipv6, string_util::is_boolean(ipv6->get_value()));
-
-	if ((sv.get_options() & server::options::ipv4) != server::options::ipv4 &&
-	    (sv.get_options() & server::options::ipv6) != server::options::ipv6)
-		throw server_error(server_error::invalid_family);
-}
-
-void from_config_load_numeric_parameters(server& sv, const ini::section& sc)
-{
-	const auto port = ini_util::optional_uint<std::uint16_t>(sc, "port", sv.get_port());
-	const auto ping_timeout = ini_util::optional_uint<uint16_t>(sc, "ping-timeout", sv.get_ping_timeout());
-	const auto reco_timeout = ini_util::optional_uint<uint16_t>(sc, "auto-reconnect-delay", sv.get_reconnect_delay());
-
-	if (!port)
-		throw server_error(server_error::invalid_port);
-	if (!ping_timeout)
-		throw server_error(server_error::invalid_ping_timeout);
-	if (!reco_timeout)
-		throw server_error(server_error::invalid_reconnect_delay);
-
-	sv.set_port(*port);
-	sv.set_ping_timeout(*ping_timeout);
-	sv.set_reconnect_delay(*reco_timeout);
-}
-
-void from_config_load_options(server& sv, const ini::section& sc)
-{
-	const auto password = ini_util::optional_string(sc, "password", "");
-	const auto command_char = ini_util::optional_string(sc, "command-char", sv.get_command_char());
-
-	sv.set_password(password);
-	sv.set_command_char(command_char);
-}
-
-void from_json_load_general(server& sv, const deserializer& parser)
-{
-	const auto port = parser.optional<std::uint16_t>("port", sv.get_port());
-	const auto nickname = parser.optional<std::string>("nickname", sv.get_nickname());
-	const auto realname = parser.optional<std::string>("realname", sv.get_realname());
-	const auto username = parser.optional<std::string>("username", sv.get_username());
-	const auto ctcp_version = parser.optional<std::string>("ctcpVersion", sv.get_ctcp_version());
-	const auto command = parser.optional<std::string>("commandChar", sv.get_command_char());
-	const auto password = parser.optional<std::string>("password", sv.get_password());
-
-	if (!port || *port > std::numeric_limits<std::uint16_t>::max())
-		throw server_error(server_error::invalid_port);
-	if (!nickname)
-		throw server_error(server_error::invalid_nickname);
-	if (!realname)
-		throw server_error(server_error::invalid_realname);
-	if (!username)
-		throw server_error(server_error::invalid_username);
-	if (!ctcp_version)
-		throw server_error(server_error::invalid_ctcp_version);
-	if (!command)
-		throw server_error(server_error::invalid_command_char);
-	if (!password)
-		throw server_error(server_error::invalid_password);
-
-	sv.set_port(*port);
-	sv.set_nickname(*nickname);
-	sv.set_realname(*realname);
-	sv.set_username(*username);
-	sv.set_ctcp_version(*ctcp_version);
-	sv.set_command_char(*command);
-	sv.set_password(*password);
-}
-
-void from_json_load_options(server& sv, const deserializer& parser)
-{
-	const auto auto_rejoin = parser.get<bool>("autoRejoin");
-	const auto join_invite = parser.get<bool>("joinInvite");
-	const auto ssl = parser.get<bool>("ssl");
-	const auto ipv4 = parser.optional<bool>("ipv4", true);
-	const auto ipv6 = parser.optional<bool>("ipv6", true);
-
-	if (!ipv4 || !ipv6)
-		throw server_error(server_error::invalid_family);
-
-	toggle(sv, server::options::ipv4, *ipv4);
-	toggle(sv, server::options::ipv6, *ipv6);
-	toggle(sv, server::options::auto_rejoin, *auto_rejoin);
-	toggle(sv, server::options::join_invite, *join_invite);
-	toggle(sv, server::options::ssl, *ssl);
-
-#if !defined(IRCCD_HAVE_SSL)
-	if ((server::get_options() & server::options::ssl) == server::options::ssl)
-		throw server_error(server_error::ssl_disabled);
-#endif
-
-	// Verify that at least IPv4 or IPv6 is set.
-	if ((sv.get_options() & server::options::ipv4) != server::options::ipv4 &&
-	    (sv.get_options() & server::options::ipv6) != server::options::ipv6)
-		throw server_error(server_error::invalid_family);
-}
-
-} // !namespace
-
-auto message_type::parse(std::string_view message,
-                         std::string_view cchar,
-                         std::string_view plugin) -> message_type
-{
-	auto result = std::string(message);
-	auto cc = std::string(cchar);
-	auto name = std::string(plugin);
-	auto type = is_message;
-
-	// handle special commands "!<plugin> command"
-	if (cc.length() > 0) {
-		auto pos = result.find_first_of(" \t");
-		auto fullcommand = cc + name;
-
-		/*
-		 * If the message that comes is "!foo" without spaces we
-		 * compare the command char + the plugin name. If there
-		 * is a space, we check until we find a space, if not
-		 * typing "!foo123123" will trigger foo plugin.
-		 */
-		if (pos == std::string::npos)
-			type = result == fullcommand ? is_command : is_message;
-		else if (result.length() >= fullcommand.length() && result.compare(0, pos, fullcommand) == 0)
-			type = is_command;
-
-		if (type == is_command) {
-			/*
-			 * If no space is found we just set the message to "" otherwise
-			 * the plugin name will be passed through onCommand
-			 */
-			if (pos == std::string::npos)
-				result = "";
-			else
-				result = message.substr(pos + 1);
-		}
-	}
-
-	return {type, result};
-}
-
-auto from_json(boost::asio::io_service& service, const nlohmann::json& object) -> std::shared_ptr<server>
-{
-	// Mandatory parameters.
-	const deserializer parser(object);
-	const auto id = parser.get<std::string>("name");
-	const auto hostname = parser.get<std::string>("hostname");
-
-	if (!id || !string_util::is_identifier(*id))
-		throw server_error(server_error::invalid_identifier);
-	if (!hostname || hostname->empty())
-		throw server_error(server_error::invalid_hostname);
-
-	const auto sv = std::make_shared<server>(service, *id, *hostname);
-
-	from_json_load_general(*sv, parser);
-	from_json_load_options(*sv, parser);
-
-	return sv;
-}
-
-auto from_config(boost::asio::io_service& service,
-                 const ini::section& sc) -> std::shared_ptr<server>
-{
-	// Mandatory parameters.
-	const auto id = sc.get("name");
-	const auto hostname = sc.get("hostname");
-
-	if (!string_util::is_identifier(id.get_value()))
-		throw server_error(server_error::invalid_identifier);
-	if (hostname.get_value().empty())
-		throw server_error(server_error::invalid_hostname);
-
-	const auto sv = std::make_shared<server>(service, id.get_value(), hostname.get_value());
-
-	from_config_load_channels(*sv, sc);
-	from_config_load_flags(*sv, sc);
-	from_config_load_numeric_parameters(*sv, sc);
-	from_config_load_options(*sv, sc);
-	from_config_load_identity(*sv, sc);
-
-	return sv;
-}
-
-} // !irccd::server_util
--- a/libirccd/irccd/daemon/server_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-/*
- * server_util.hpp -- server utilities
- *
- * Copyright (c) 2013-2018 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_DAEMON_SERVER_UTIL_HPP
-#define IRCCD_DAEMON_SERVER_UTIL_HPP
-
-/**
- * \file server_util.hpp
- * \brief Server utilities.
- */
-
-#include <memory>
-#include <string_view>
-
-#include <boost/asio/io_service.hpp>
-
-#include <json.hpp>
-
-namespace irccd {
-
-namespace ini {
-
-class section;
-
-} // !ini
-
-class config;
-class server;
-
-/**
- * \brief Server utilities.
- */
-namespace server_util {
-
-/**
- * \brief Pack a message and its type
- *
- * On channels and queries, you may have a special command or a standard message
- * depending on the beginning of the message.
- *
- * Example: `!reminder help' may invoke the command event if a plugin reminder
- * exists.
- */
-struct message_type {
-	/**
-	 * \brief Describe which type of message has been received.
-	 */
-	enum kind {
-		is_command,     //!< special command
-		is_message      //!< standard message
-	};
-
-	/**
-	 * Message kind.
-	 */
-	kind type;
-
-	/**
-	 * Message content.
-	 */
-	std::string message;
-
-	/**
-	 * Parse IRC message and determine if it's a command or a simple message.
-	 *
-	 * If it's a command, the plugin invocation command is removed from the
-	 * original message, otherwise it is copied verbatime.
-	 *
-	 * \param message the message line
-	 * \param cchar the command char (e.g '!')
-	 * \param plugin the plugin name
-	 * \return the pair
-	 */
-	static auto parse(std::string_view message,
-	                  std::string_view cchar,
-	                  std::string_view plugin) -> message_type;
-};
-
-/**
- * Convert a JSON object to a server.
- *
- * \param service the io service
- * \param object the object
- * \return the server
- * \throw server_error on errors
- */
-auto from_json(boost::asio::io_service& service,
-               const nlohmann::json& object) -> std::shared_ptr<server>;
-
-/**
- * Convert a INI section to a server.
- *
- * \param service the io service
- * \param sc the server section
- * \return the server
- * \throw server_error on errors
- */
-auto from_config(boost::asio::io_service& service,
-                 const ini::section& sc) -> std::shared_ptr<server>;
-
-} // !server_util
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_SERVER_UTIL_HPP
--- a/libirccd/irccd/daemon/transport_client.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/*
- * transport_client.cpp -- server side transport clients
- *
- * Copyright (c) 2013-2018 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 <cassert>
-
-#include "transport_client.hpp"
-#include "transport_server.hpp"
-
-namespace irccd {
-
-void transport_client::flush()
-{
-	if (queue_.empty())
-		return;
-
-	const auto self = shared_from_this();
-
-	stream_->send(queue_.front().first, [this, self] (auto code) {
-		if (queue_.front().second)
-			queue_.front().second(code);
-
-		queue_.pop_front();
-
-		if (code)
-			erase();
-		else
-			flush();
-	});
-}
-
-void transport_client::erase()
-{
-	state_ = state::closing;
-
-	if (auto parent = parent_.lock())
-		parent->get_clients().erase(shared_from_this());
-}
-
-transport_client::transport_client(std::weak_ptr<transport_server> server,
-                                   std::shared_ptr<stream> stream) noexcept
-	: parent_(server)
-	, stream_(std::move(stream))
-{
-	assert(stream_);
-}
-
-auto transport_client::get_state() const noexcept -> state
-{
-	return state_;
-}
-
-void transport_client::set_state(state state) noexcept
-{
-	state_ = state;
-}
-
-void transport_client::read(stream::recv_handler handler)
-{
-	assert(handler);
-
-	if (state_ != state::closing) {
-		const auto self = shared_from_this();
-
-		stream_->recv([this, self, handler] (auto code, auto msg) {
-			handler(code, msg);
-
-			if (code)
-				erase();
-		});
-	}
-}
-
-void transport_client::write(nlohmann::json json, stream::send_handler handler)
-{
-	const auto in_progress = queue_.size() > 0;
-
-	queue_.emplace_back(std::move(json), std::move(handler));
-
-	if (!in_progress)
-		flush();
-}
-
-void transport_client::success(const std::string& cname, stream::send_handler handler)
-{
-	assert(!cname.empty());
-
-	write({{ "command", cname }}, std::move(handler));
-}
-
-void transport_client::error(std::error_code code, stream::send_handler handler)
-{
-	error(std::move(code), "", std::move(handler));
-}
-
-void transport_client::error(std::error_code code, std::string_view cname, stream::send_handler handler)
-{
-	assert(code);
-
-	auto json = nlohmann::json::object({
-		{ "error",              code.value()            },
-		{ "errorCategory",      code.category().name()  },
-		{ "errorMessage",       code.message()          }
-	});
-
-	// TODO: check newer version of JSON for string_view support.
-	if (!cname.empty())
-		json["command"] = std::string(cname);
-
-	const auto self = shared_from_this();
-
-	write(std::move(json), [this, handler, self] (auto code) {
-		erase();
-
-		if (handler)
-			handler(code);
-	});
-
-	state_ = state::closing;
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/transport_client.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/*
- * transport_client.hpp -- server side transport clients
- *
- * Copyright (c) 2013-2018 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_DAEMON_TRANSPORT_CLIENT_HPP
-#define IRCCD_DAEMON_TRANSPORT_CLIENT_HPP
-
-/**
- * \file transport_client.hpp
- * \brief Server side transport clients.
- */
-
-#include <deque>
-#include <memory>
-#include <string_view>
-
-#include <irccd/stream.hpp>
-
-namespace irccd {
-
-class transport_server;
-
-/**
- * \brief Abstract transport client class.
- * \ingroup transports
- *
- * This class is responsible of receiving/sending data.
- */
-class transport_client : public std::enable_shared_from_this<transport_client> {
-public:
-	/**
-	 * Client state.
-	 */
-	enum class state {
-		authenticating,         //!< client is authenticating
-		ready,                  //!< client is ready
-		closing                 //!< client is closing
-	};
-
-private:
-	state state_{state::authenticating};
-	std::weak_ptr<transport_server> parent_;
-	std::shared_ptr<stream> stream_;
-	std::deque<std::pair<nlohmann::json, stream::send_handler>> queue_;
-
-	void flush();
-	void erase();
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \pre stream != nullptr
-	 * \param server the parent
-	 * \param stream the I/O stream
-	 */
-	transport_client(std::weak_ptr<transport_server> server,
-	                 std::shared_ptr<stream> stream) noexcept;
-
-	/**
-	 * Get the current client state.
-	 *
-	 * \return the state
-	 */
-	auto get_state() const noexcept -> state;
-
-	/**
-	 * Set the client state.
-	 *
-	 * \param state the new state
-	 */
-	void set_state(state state) noexcept;
-
-	/**
-	 * Start receiving if not closed.
-	 *
-	 * Possible error codes:
-	 *
-	 *   - std::errc::network_down in case of errors,
-	 *   - std::errc::invalid_argument if the JSON message is invalid,
-	 *   - std::errc::not_enough_memory in case of memory failure.
-	 *
-	 * \pre handler != nullptr
-	 * \param handler the handler
-	 * \warning Another read operation MUST NOT be running.
-	 */
-	void read(stream::recv_handler handler);
-
-	/**
-	 * Start sending if not closed.
-	 *
-	 * Possible error codes:
-	 *
-	 *   - boost::system::errc::network_down in case of errors,
-	 *
-	 * \pre json.is_object()
-	 * \param json the json message
-	 * \param handler the optional handler
-	 * \note If a write operation is running, it is postponed once ready.
-	 */
-	void write(nlohmann::json json, stream::send_handler handler = nullptr);
-
-	/**
-	 * Convenient success message.
-	 *
-	 * \param command the command name
-	 * \param handler the optional handler
-	 * \note If a write operation is running, it is postponed once ready.
-	 */
-	void success(const std::string& command, stream::send_handler handler = nullptr);
-
-	/**
-	 * Send an error code to the client.
-	 *
-	 * \pre code is not 0
-	 * \param code the error code
-	 * \param handler the optional handler
-	 * \note If a write operation is running, it is postponed once ready.
-	 */
-	void error(std::error_code code, stream::send_handler handler = nullptr);
-
-	/**
-	 * Send an error code to the client.
-	 *
-	 * \pre code is not 0
-	 * \param code the error code
-	 * \param command the command name
-	 * \param handler the optional handler
-	 * \note If a write operation is running, it is postponed once ready.
-	 */
-	void error(std::error_code code, std::string_view command, stream::send_handler handler = nullptr);
-};
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_TRANSPORT_CLIENT_HPP
--- a/libirccd/irccd/daemon/transport_server.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,192 +0,0 @@
-/*
- * transport_server.cpp -- server side transports
- *
- * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
-
-#include <cassert>
-#include <system_error>
-
-#include <irccd/json_util.hpp>
-
-#include "irccd.hpp"
-#include "transport_client.hpp"
-#include "transport_server.hpp"
-
-namespace irccd {
-
-void transport_server::do_auth(std::shared_ptr<transport_client> client, accept_handler handler)
-{
-	assert(client);
-	assert(handler);
-
-	client->read([this, client, handler] (auto code, auto message) {
-		if (code) {
-			handler(std::move(code), std::move(client));
-			return;
-		}
-
-		const json_util::deserializer doc(message);
-		const auto command = doc.get<std::string>("command");
-		const auto password = doc.get<std::string>("password");
-
-		if (!command || *command != "auth") {
-			client->error(irccd_error::auth_required);
-			code = irccd_error::auth_required;
-		} else if (!password || *password != password_) {
-			client->error(irccd_error::invalid_auth);
-			code = irccd_error::invalid_auth;
-		} else {
-			clients_.insert(client);
-			client->set_state(transport_client::state::ready);
-			client->success("auth");
-			code = irccd_error::no_error;
-		}
-
-		handler(std::move(code), std::move(client));
-	});
-}
-
-void transport_server::do_greetings(std::shared_ptr<transport_client> client, accept_handler handler)
-{
-	assert(client);
-	assert(handler);
-
-	const auto greetings = nlohmann::json({
-		{ "program",    "irccd"                 },
-		{ "major",      IRCCD_VERSION_MAJOR     },
-		{ "minor",      IRCCD_VERSION_MINOR     },
-		{ "patch",      IRCCD_VERSION_PATCH     },
-#if defined(IRCCD_HAVE_JS)
-		{ "javascript", true                    },
-#endif
-#if defined(IRCCD_HAVE_SSL)
-		{ "ssl",        true                    },
-#endif
-	});
-
-	client->write(greetings, [this, client, handler] (auto code) {
-		if (code) {
-			handler(std::move(code), std::move(client));
-			return;
-		}
-
-		if (!password_.empty())
-			do_auth(std::move(client), std::move(handler));
-		else {
-			clients_.insert(client);
-			client->set_state(transport_client::state::ready);
-			handler(std::move(code), std::move(client));
-		}
-	});
-}
-
-transport_server::transport_server(std::unique_ptr<acceptor> acceptor) noexcept
-	: acceptor_(std::move(acceptor))
-{
-	assert(acceptor_);
-}
-
-auto transport_server::get_clients() const noexcept -> const client_set&
-{
-	return clients_;
-}
-
-auto transport_server::get_clients() noexcept -> client_set&
-{
-	return clients_;
-}
-
-auto transport_server::get_password() const noexcept -> const std::string&
-{
-	return password_;
-}
-
-void transport_server::set_password(std::string password) noexcept
-{
-	password_ = std::move(password);
-}
-
-void transport_server::accept(accept_handler handler)
-{
-	acceptor_->accept([this, handler] (auto code, auto stream) {
-		if (code) {
-			handler(code, nullptr);
-			return;
-		}
-
-		do_greetings(
-			std::make_shared<transport_client>(shared_from_this(), std::move(stream)),
-			std::move(handler)
-		);
-	});
-}
-
-transport_error::transport_error(error code) noexcept
-	: system_error(make_error_code(code))
-{
-}
-
-auto transport_category() noexcept -> const std::error_category&
-{
-	static const class category : public std::error_category {
-	public:
-		auto name() const noexcept -> const char* override
-		{
-			return "transport";
-		}
-
-		auto message(int e) const -> std::string override
-		{
-			switch (static_cast<transport_error::error>(e)) {
-			case transport_error::auth_required:
-				return "authentication required";
-			case transport_error::invalid_auth:
-				return "invalid authentication";
-			case transport_error::invalid_port:
-				return "invalid port";
-			case transport_error::invalid_address:
-				return "invalid address";
-			case transport_error::invalid_hostname:
-				return "invalid hostname";
-			case transport_error::invalid_path:
-				return "invalid socket path";
-			case transport_error::invalid_family:
-				return "invalid family";
-			case transport_error::invalid_certificate:
-				return "invalid certificate";
-			case transport_error::invalid_private_key:
-				return "invalid private key";
-			case transport_error::ssl_disabled:
-				return "ssl is not enabled";
-			case transport_error::not_supported:
-				return "transport not supported";
-			default:
-				return "no error";
-			}
-		}
-	} category;
-
-	return category;
-};
-
-auto make_error_code(transport_error::error e) noexcept -> std::error_code
-{
-	return { static_cast<int>(e), transport_category() };
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/transport_server.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-/*
- * transport_server.hpp -- server side transports
- *
- * Copyright (c) 2013-2018 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_DAEMON_TRANSPORT_SERVER_HPP
-#define IRCCD_DAEMON_TRANSPORT_SERVER_HPP
-
-/**
- * \file transport_server.hpp
- * \brief Server side transports.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <functional>
-#include <memory>
-#include <unordered_set>
-#include <type_traits>
-
-#include <irccd/acceptor.hpp>
-
-namespace irccd {
-
-class transport_client;
-
-/**
- * \brief Abstract transport server class.
- * \ingroup transports
- *
- * The transport_server class is an abstract interface that waits for clients to
- * connect and store them locally. It does not know the underlying
- * implementation so derived classes may be implemented in any shape of form.
- *
- * As only constraint the implementation must provide an asynchronous operation
- * to avoid blocking the daemon.
- *
- * The derived class only have to implement accept function which is only
- * responsible of getting a client ready for I/O (receiving and sending), the
- * transport_server does authentication and greeting by itself.
- *
- * # Accept procedure
- *
- * The connection procedure looks like this:
- *
- * ~~~
- *            o (transport_server::accept is called)
- *            |
- *            v                          [error]
- *   +-----------------------------+                  +---------------------+
- *   | asynchronous accept process |----------------->| client is discarded |
- *   +-----------------------------+                  +---------------------+
- *            |                                                          ^
- *            | [success]                                                |
- *            v                                                          |
- *   +-----------------------------------------+  [error while sending]  |
- *   | sending irccd information to the client |------------------------>+
- *   +-----------------------------------------+                         |
- *     |              |                                                  |
- *     |              | [authentication required]                        |
- *     |              |                                                  |
- *     |              v                    [error or invalid password]   |
- *     |      +-------------------------+         +------------+         |
- *     |      | wait for authentication |-------->| send error |-------->+
- *     |      +-------------------------+         +------------+         ^
- *     |              |                                                  |
- *     |              | [correct password]                               |
- *     v              v                                                  |
- *   +---------------------------------------+  [incorrect]              |
- *   | client is added to the list and ready ]-------------------------- +
- *   +---------------------------------------+
- * ~~~
- *
- * # I/O procedures
- *
- * Each client has a reference to its parent, since operations are asynchronous,
- * they maintain their lifetime by themselve to update the parent list on
- * errors.
- *
- * See the following diagram:
- *
- * ```
- *       o (transport_client::recv or send is called) o
- *       |                                            |
- *       | [no operations in queue]                   | [operation in progress]
- *       |                                            v
- *       |                                    +---------------+
- *       |                                    | push in queue |
- *       |                                    +---------------+
- *       |
- *       |
- *       |                                [pending operations in queue]
- *       |<-----------------------------------------------+
- *       |                                                ^
- *       |                                                |
- *       v                             [success]          |
- *   +-------------------------------+           +-------------------+
- *   | asynchronous operation starts |---------->| handler is called |
- *   +-------------------------------+           +-------------------+
- *       |
- *       v [error]
- *   +--------------------------------------+
- *   | handler is called with an error code |
- *   +--------------------------------------+
- *       |
- *       v
- *   +----------------------------------+
- *   | client delete itself from parent |
- *   +----------------------------------+
- * ```
- *
- * \see transport_client
- * \see transport_service
- */
-class transport_server : public std::enable_shared_from_this<transport_server> {
-public:
-	/**
-	 * Set of clients.
-	 */
-	using client_set = std::unordered_set<std::shared_ptr<transport_client>>;
-
-	/**
-	 * Accept completion handler.
-	 */
-	using accept_handler = std::function<void (std::error_code, std::shared_ptr<transport_client>)>;
-
-private:
-	client_set clients_;
-	std::unique_ptr<acceptor> acceptor_;
-	std::string password_;
-
-	void do_auth(std::shared_ptr<transport_client>, accept_handler);
-	void do_greetings(std::shared_ptr<transport_client>, accept_handler);
-
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \pre acceptor != nullptr
-	 * \param acceptor the stream acceptor
-	 */
-	transport_server(std::unique_ptr<acceptor> acceptor) noexcept;
-
-	/**
-	 * Get the clients.
-	 *
-	 * \return the clients
-	 */
-	auto get_clients() const noexcept -> const client_set&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the clients
-	 */
-	auto get_clients() noexcept -> client_set&;
-
-	/**
-	 * Get the current password, empty string means no password.
-	 *
-	 * \return the password
-	 */
-	auto get_password() const noexcept -> const std::string&;
-
-	/**
-	 * Set an optional password, empty string means no password.
-	 *
-	 * \param password the password
-	 */
-	void set_password(std::string password) noexcept;
-
-	/**
-	 * Accept a client.
-	 *
-	 * Also perform greetings and authentication under the hood. On success, the
-	 * client is added into the server and is ready to use.
-	 *
-	 * \pre handler != nullptr
-	 * \param handler the completion handler
-	 */
-	void accept(accept_handler handler);
-};
-
-/**
- * \brief Transport error.
- */
-class transport_error : public std::system_error {
-public:
-	/**
-	 * \brief Transport related errors.
-	 */
-	enum error {
-		//!< No error.
-		no_error = 0,
-
-		//!< Authentication is required.
-		auth_required,
-
-		//!< Authentication was invalid.
-		invalid_auth,
-
-		//!< Invalid TCP/IP port.
-		invalid_port,
-
-		//!< Invalid TCP/IP address.
-		invalid_address,
-
-		//!< The specified host was invalid.
-		invalid_hostname,
-
-		//!< Invalid unix local path.
-		invalid_path,
-
-		//!< Invalid IPv4/IPv6 family.
-		invalid_family,
-
-		//!< Invalid certificate given.
-		invalid_certificate,
-
-		//!< Invalid private key given.
-		invalid_private_key,
-
-		//!< SSL was requested but is disabled.
-		ssl_disabled,
-
-		//!< Kind of transport not supported on this platform.
-		not_supported
-	};
-
-	/**
-	 * Constructor.
-	 *
-	 * \param code the error code
-	 */
-	transport_error(error code) noexcept;
-};
-
-/**
- * Get the transport error category singleton.
- *
- * \return the singleton
- */
-auto transport_category() noexcept -> const std::error_category&;
-
-/**
- * Create a std::error_code from server_error::error enum.
- *
- * \param e the error code
- * \return the error code
- */
-auto make_error_code(transport_error::error e) noexcept -> std::error_code;
-
-} // !irccd
-
-namespace std {
-
-template <>
-struct is_error_code_enum<irccd::transport_error::error> : public std::true_type {
-};
-
-} // !std
-
-#endif // !IRCCD_DAEMON_TRANSPORT_SERVER_HPP
--- a/libirccd/irccd/daemon/transport_service.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/*
- * transport_service.cpp -- transport service
- *
- * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
-
-#include <cassert>
-
-#include <irccd/json_util.hpp>
-
-#include "command.hpp"
-#include "irccd.hpp"
-#include "logger.hpp"
-#include "transport_client.hpp"
-#include "transport_server.hpp"
-#include "transport_service.hpp"
-#include "transport_util.hpp"
-
-namespace irccd {
-
-void transport_service::handle_command(std::shared_ptr<transport_client> tc, const nlohmann::json& object)
-{
-	assert(object.is_object());
-
-	const json_util::deserializer doc(object);
-	const auto name = doc.get<std::string>("command");
-
-	if (!name) {
-		tc->error(irccd_error::invalid_message);
-		return;
-	}
-
-	const auto cmd = std::find_if(commands_.begin(), commands_.end(), [&] (const auto& cptr) {
-		return cptr->get_name() == *name;
-	});
-
-	if (cmd == commands_.end())
-		tc->error(irccd_error::invalid_command, *name);
-	else {
-		try {
-			(*cmd)->exec(irccd_, *tc, doc);
-		} catch (const std::system_error& ex) {
-			tc->error(ex.code(), (*cmd)->get_name());
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning("transport", "")
-				<< "unknown error not reported: "
-				<< ex.what() << std::endl;
-		}
-	}
-}
-
-void transport_service::do_recv(std::shared_ptr<transport_client> tc)
-{
-	tc->read([this, tc] (auto code, auto json) {
-		switch (static_cast<std::errc>(code.value())) {
-		case std::errc::not_connected:
-			irccd_.get_log().info("transport", "") << "client disconnected" << std::endl;
-			break;
-		case std::errc::invalid_argument:
-			tc->error(irccd_error::invalid_message);
-			break;
-		default:
-			// Other errors.
-			if (!code) {
-				handle_command(tc, json);
-
-				if (tc->get_state() == transport_client::state::ready)
-					do_recv(std::move(tc));
-			}
-
-			break;
-		}
-	});
-}
-
-void transport_service::do_accept(transport_server& ts)
-{
-	ts.accept([this, &ts] (auto code, auto client) {
-		if (!code) {
-			do_accept(ts);
-			do_recv(std::move(client));
-
-			irccd_.get_log().info("transport", "") << "new client connected" << std::endl;
-		}
-	});
-}
-
-transport_service::transport_service(irccd& irccd) noexcept
-	: irccd_(irccd)
-{
-}
-
-transport_service::~transport_service() noexcept = default;
-
-auto transport_service::get_commands() const noexcept -> const commands&
-{
-	return commands_;
-}
-
-auto transport_service::get_commands() noexcept -> commands&
-{
-	return commands_;
-}
-
-void transport_service::add(std::shared_ptr<transport_server> ts)
-{
-	assert(ts);
-
-	do_accept(*ts);
-	servers_.push_back(std::move(ts));
-}
-
-void transport_service::broadcast(const nlohmann::json& json)
-{
-	assert(json.is_object());
-
-	for (const auto& servers : servers_)
-		for (const auto& client : servers->get_clients())
-			client->write(json);
-}
-
-void transport_service::load(const config& cfg) noexcept
-{
-	for (const auto& section : cfg) {
-		if (section.get_key() != "transport")
-			continue;
-
-		try {
-			add(transport_util::from_config(irccd_.get_service(), section));
-		} catch (const std::exception& ex) {
-			irccd_.get_log().warning("transport", "") << ex.what() << std::endl;
-		}
-	}
-}
-
-} // !irccd
--- a/libirccd/irccd/daemon/transport_service.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/*
- * transport_service.hpp -- transport service
- *
- * Copyright (c) 2013-2018 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_DAEMON_TRANSPORT_SERVICE_HPP
-#define IRCCD_DAEMON_TRANSPORT_SERVICE_HPP
-
-/**
- * \file transport_service.hpp
- * \brief Transport service.
- */
-
-/**
- * \defgroup transports Transports
- * \brief Transport client and servers
- */
-
-#include <memory>
-#include <vector>
-
-#include <json.hpp>
-
-namespace irccd {
-
-class command;
-class config;
-class irccd;
-class transport_client;
-class transport_server;
-
-/**
- * \brief Manage transport servers and clients.
- * \ingroup services
- * \ingroup transports
- */
-class transport_service {
-public:
-	/**
-	 * \brief the list of transport commands.
-	 */
-	using commands = std::vector<std::unique_ptr<command>>;
-
-	/**
-	 * \brief The list of transport acceptors.
-	 */
-	using servers = std::vector<std::shared_ptr<transport_server>>;
-
-private:
-	irccd& irccd_;
-	commands commands_;
-	servers servers_;
-
-	void handle_command(std::shared_ptr<transport_client>, const nlohmann::json&);
-	void do_recv(std::shared_ptr<transport_client>);
-	void do_accept(transport_server&);
-
-public:
-	/**
-	 * Create the transport service.
-	 *
-	 * \param irccd the irccd instance
-	 */
-	transport_service(irccd& irccd) noexcept;
-
-	/**
-	 * Default destructor.
-	 */
-	~transport_service() noexcept;
-
-	/**
-	 * Get underlying commands.
-	 *
-	 * \return the commands
-	 */
-	auto get_commands() const noexcept -> const commands&;
-
-	/**
-	 * Get underlying commands.
-	 *
-	 * \return the commands
-	 */
-	auto get_commands() noexcept -> commands&;
-
-	/**
-	 * Add a transport server.
-	 *
-	 * \param ts the transport server
-	 */
-	void add(std::shared_ptr<transport_server> ts);
-
-	/**
-	 * Send data to all clients.
-	 *
-	 * \pre object.is_object()
-	 * \param object the json object
-	 */
-	void broadcast(const nlohmann::json& object);
-
-	/**
-	 * Load transports from the configuration.
-	 *
-	 * \param cfg the config
-	 */
-	void load(const config& cfg) noexcept;
-};
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_TRANSPORT_SERVICE_HPP
--- a/libirccd/irccd/daemon/transport_util.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +0,0 @@
-/*
- * transport_util.cpp -- transport utilities
- *
- * Copyright (c) 2013-2018 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 <irccd/sysconfig.hpp>
-
-#include <cassert>
-
-#include <irccd/acceptor.hpp>
-#include <irccd/ini_util.hpp>
-#include <irccd/string_util.hpp>
-
-#include "transport_util.hpp"
-#include "transport_server.hpp"
-
-namespace asio = boost::asio;
-
-namespace irccd::transport_util {
-
-namespace {
-
-auto from_config_load_ip_protocols(const ini::section& sc) -> std::pair<bool, bool>
-{
-	bool ipv4 = true, ipv6 = true;
-
-	if (const auto it = sc.find("ipv4"); it != sc.end())
-		ipv4 = string_util::is_boolean(it->get_value());
-	if (const auto it = sc.find("ipv6"); it != sc.end())
-		ipv6 = string_util::is_boolean(it->get_value());
-
-	if (!ipv4 && !ipv6)
-		throw transport_error(transport_error::invalid_family);
-
-	return { ipv4, ipv6 };
-}
-
-#if defined(IRCCD_HAVE_SSL)
-
-auto from_config_load_ssl(const ini::section& sc) -> boost::asio::ssl::context
-{
-	const auto key = sc.get("key").get_value();
-	const auto cert = sc.get("certificate").get_value();
-
-	if (key.empty())
-		throw transport_error(transport_error::invalid_private_key);
-	if (cert.empty())
-		throw transport_error(transport_error::invalid_certificate);
-
-	boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv12);
-
-	ctx.use_private_key_file(key, boost::asio::ssl::context::pem);
-	ctx.use_certificate_file(cert, boost::asio::ssl::context::pem);
-
-	return ctx;
-}
-
-#endif // !IRCCD_HAVE_SSL
-
-auto from_config_load_ip(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<acceptor>
-{
-	assert(sc.get_key() == "transport");
-
-	const auto port = ini_util::get_uint<std::uint16_t>(sc, "port");
-	const auto address = ini_util::optional_string(sc, "address", "*");
-	const auto [ ipv4, ipv6 ] = from_config_load_ip_protocols(sc);
-
-	if (!port)
-		throw transport_error(transport_error::invalid_port);
-	if (address.empty())
-		throw transport_error(transport_error::invalid_address);
-
-	if (string_util::is_boolean(sc.get("ssl").get_value()))
-#if !defined(IRCCD_HAVE_SSL)
-	throw transport_error(transport_error::ssl_disabled);
-#else
-		return std::make_unique<tls_ip_acceptor>(from_config_load_ssl(sc),
-			service, address, *port, ipv4, ipv6);
-#endif // !IRCCD_HAVE_SSL
-
-	return std::make_unique<ip_acceptor>(service, address, *port, ipv4, ipv6);
-}
-
-auto from_config_load_local(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<acceptor>
-{
-	assert(sc.get_key() == "transport");
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-	const auto path = sc.get("path").get_value();
-
-	if (path.empty())
-		throw transport_error(transport_error::invalid_path);
-
-	if (string_util::is_boolean(sc.get("ssl").get_value()))
-#if !defined(IRCCD_HAVE_SSL)
-	throw transport_error(transport_error::ssl_disabled);
-#else
-		return std::make_unique<tls_local_acceptor>(from_config_load_ssl(sc), service, path);
-#endif // !IRCCD_HAVE_SSL
-
-	return std::make_unique<local_acceptor>(service, path);
-#else
-	(void)service;
-	(void)sc;
-
-	throw transport_error(transport_error::not_supported);
-#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
-}
-
-} // !namespace
-
-auto from_config(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<transport_server>
-{
-	assert(sc.get_key() == "transport");
-
-	const auto type = sc.get("type").get_value();
-	const auto password = sc.get("password").get_value();
-
-	if (type.empty())
-		throw transport_error(transport_error::not_supported);
-
-	std::unique_ptr<acceptor> acceptor;
-
-	if (type == "ip")
-		acceptor = from_config_load_ip(service, sc);
-	else if (type == "unix")
-		acceptor = from_config_load_local(service, sc);
-	else
-		throw transport_error(transport_error::not_supported);
-
-	auto transport = std::make_unique<transport_server>(std::move(acceptor));
-
-	transport->set_password(password);
-
-	return transport;
-}
-
-} // !irccd::transport_util
--- a/libirccd/irccd/daemon/transport_util.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
- * transport_util.hpp -- transport utilities
- *
- * Copyright (c) 2013-2018 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.
- */
-
-/**
- * \file transport_util.hpp
- * \brief Transport utilities.
- */
-
-#ifndef IRCCD_DAEMON_TRANSPORT_UTIL_HPP
-#define IRCCD_DAEMON_TRANSPORT_UTIL_HPP
-
-/*
- * \file transport_util.hpp
- * \brief Transport utilities.
- */
-
-#include <memory>
-
-#include <boost/asio/io_service.hpp>
-
-namespace irccd {
-
-class transport_server;
-
-namespace ini {
-
-class section;
-
-} // !ini
-
-/*
- * \brief Transport utilities.
- */
-namespace transport_util {
-
-/**
- * Load a transport from a [transport] configuration section.
- *
- * \param service the IO service
- * \param sc the configuration
- * \throw transport_error on errors
- * \return the transport
- */
-auto from_config(boost::asio::io_service& service,
-                 const ini::section& sc) -> std::unique_ptr<transport_server>;
-
-} // !transport_util
-
-} // !irccd
-
-#endif // !IRCCD_DAEMON_TRANSPORT_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/fs_util.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,59 @@
+/*
+ * fs_util.cpp -- filesystem utilities
+ *
+ * Copyright (c) 2013-2018 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 "fs_util.hpp"
+
+namespace irccd::fs_util {
+
+// {{{ base_name
+
+auto base_name(const std::string& path) -> std::string
+{
+	return boost::filesystem::path(path).filename().string();
+}
+
+// }}}
+
+// {{{ dir_name
+
+auto dir_name(const std::string& path) -> std::string
+{
+	return boost::filesystem::path(path).parent_path().string();
+}
+
+// }}}
+
+// {{{ find
+
+auto find(const std::string& base, const std::string& name, bool recursive) -> std::string
+{
+	return find_if(base, recursive, [&] (const auto& entry) {
+		return entry.path().filename().string() == name;
+	});
+}
+
+auto find(const std::string& base, const std::regex& regex, bool recursive) -> std::string
+{
+	return find_if(base, recursive, [&] (const auto& entry) {
+		return std::regex_match(entry.path().filename().string(), regex);
+	});
+}
+
+// }}}
+
+} // !irccd::fs_util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/fs_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,129 @@
+/*
+ * fs_util.hpp -- filesystem utilities
+ *
+ * Copyright (c) 2013-2018 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_FS_UTIL_HPP
+#define IRCCD_FS_UTIL_HPP
+
+/**
+ * \file fs_util.hpp
+ * \brief Filesystem utilities.
+ */
+
+#include <regex>
+#include <string>
+
+#include <boost/filesystem.hpp>
+
+/**
+ * \brief Filesystem utilities.
+ */
+namespace irccd::fs_util {
+
+// {{{ base_name
+
+/**
+ * Get the base name from a path.
+ *
+ * Example, base_name("/etc/foo.conf") returns foo.conf
+ *
+ * \param path the path
+ * \return the base name
+ */
+auto base_name(const std::string& path) -> std::string;
+
+// }}}
+
+// {{{ dir_name
+
+/**
+ * Get the parent directory from a path.
+ *
+ * Example, dir_name("/etc/foo.conf") returns /etc
+ *
+ * \param path the path
+ * \return the parent directory
+ */
+auto dir_name(const std::string& path) -> std::string;
+
+// }}}
+
+// {{{ find_if
+
+/**
+ * Search an item recursively.
+ *
+ * The predicate must have the following signature:
+ *  void f(const boost::filesystem::directory_entry& entry)
+ *
+ * Where:
+ *   - base is the current parent directory in the tree
+ *   - entry is the current entry
+ *
+ * \param base the base directory
+ * \param predicate the predicate
+ * \param recursive true to do recursive search
+ * \return the full path name to the file or empty string if never found
+ * \throw boost::system::system_error on errors
+ */
+template <typename Predicate>
+auto find_if(const std::string& base, bool recursive, Predicate&& predicate) -> std::string
+{
+	const auto find = [&] (auto it) -> std::string {
+		for (const auto& entry : it)
+			if (predicate(entry))
+				return entry.path().string();
+
+		return "";
+	};
+
+	return recursive
+		? find(boost::filesystem::recursive_directory_iterator(base))
+		: find(boost::filesystem::directory_iterator(base));
+}
+
+// }}}
+
+// {{{ find
+
+/**
+ * Find a file by name recursively.
+ *
+ * \param base the base directory
+ * \param name the file name
+ * \param recursive true to do recursive search
+ * \return the full path name to the file or empty string if never found
+ * \throw boost::system::system_error on errors
+ */
+auto find(const std::string& base, const std::string& name, bool recursive = false) -> std::string;
+
+/**
+ * Overload by regular expression.
+ *
+ * \param base the base directory
+ * \param regex the regular expression
+ * \param recursive true to do recursive search
+ * \return the full path name to the file or empty string if never found
+ * \throw boost::system::system_error on errors
+ */
+auto find(const std::string& base, const std::regex& regex, bool recursive = false) -> std::string;
+
+// }}}
+
+} // !irccd::fs_util
+
+#endif // !IRCCD_FS_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/ini.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,638 @@
+/*
+ * ini.cpp -- extended .ini file parser
+ *
+ * Copyright (c) 2013-2018 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 <cassert>
+#include <cctype>
+#include <cstring>
+#include <iostream>
+#include <iterator>
+#include <fstream>
+#include <sstream>
+#include <stdexcept>
+
+// for PathIsRelative.
+#if defined(_WIN32)
+#  include <Shlwapi.h>
+#endif
+
+#include "ini.hpp"
+
+using namespace std::string_literals;
+
+namespace irccd::ini {
+
+namespace {
+
+using stream_iterator = std::istreambuf_iterator<char>;
+using token_iterator = std::vector<token>::const_iterator;
+
+auto is_absolute(const std::string& path) noexcept -> bool
+{
+#if defined(_WIN32)
+	return !PathIsRelative(path.c_str());
+#else
+	return path.size() > 0 && path[0] == '/';
+#endif
+}
+
+auto is_quote(char c) noexcept -> bool
+{
+	return c == '\'' || c == '"';
+}
+
+auto is_space(char c) noexcept -> bool
+{
+	// Custom version because std::isspace includes \n as space.
+	return c == ' ' || c == '\t';
+}
+
+auto is_list(char c) noexcept -> bool
+{
+	return c == '(' || c == ')' || c == ',';
+}
+
+auto is_reserved(char c) noexcept -> bool
+{
+	return is_list(c) || is_quote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '=';
+}
+
+void analyse_line(unsigned& line, unsigned& column, stream_iterator& it) noexcept
+{
+	assert(*it == '\n');
+
+	++ line;
+	++ it;
+	column = 0;
+}
+
+void analyse_comment(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
+{
+	assert(*it == '#');
+
+	while (it != end && *it != '\n') {
+		++ column;
+		++ it;
+	}
+}
+
+void analyse_spaces(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
+{
+	assert(is_space(*it));
+
+	while (it != end && is_space(*it)) {
+		++ column;
+		++ it;
+	}
+}
+
+void analyse_list(tokens& list, unsigned line, unsigned& column, stream_iterator& it) noexcept
+{
+	assert(is_list(*it));
+
+	switch (*it++) {
+	case '(':
+		list.emplace_back(token::list_begin, line, column++);
+		break;
+	case ')':
+		list.emplace_back(token::list_end, line, column++);
+		break;
+	case ',':
+		list.emplace_back(token::comma, line, column++);
+		break;
+	default:
+		break;
+	}
+}
+
+void analyse_section(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+	assert(*it == '[');
+
+	std::string value;
+	unsigned save = column;
+
+	// Read section name.
+	++ it;
+	while (it != end && *it != ']') {
+		if (*it == '\n')
+			throw exception(line, column, "section not terminated, missing ']'");
+		if (is_reserved(*it))
+			throw exception(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'");
+
+		++ column;
+		value += *it++;
+	}
+
+	if (it == end)
+		throw exception(line, column, "section name expected after '[', got <EOF>");
+	if (value.empty())
+		throw exception(line, column, "empty section name");
+
+	// Remove ']'.
+	++ it;
+
+	list.emplace_back(token::section, line, save, std::move(value));
+}
+
+void analyse_assign(tokens& list, unsigned& line, unsigned& column, stream_iterator& it)
+{
+	assert(*it == '=');
+
+	list.push_back({ token::assign, line, column++ });
+	++ it;
+}
+
+void analyse_quoted_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+	std::string value;
+	unsigned save = column;
+	char quote = *it++;
+
+	while (it != end && *it != quote) {
+		// TODO: escape sequence
+		++ column;
+		value += *it++;
+	}
+
+	if (it == end)
+		throw exception(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>");
+
+	// Remove quote.
+	++ it;
+
+	list.push_back({ token::quoted_word, line, save, std::move(value) });
+}
+
+void analyse_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+	assert(!is_reserved(*it));
+
+	std::string value;
+	unsigned save = column;
+
+	while (it != end && !std::isspace(*it) && !is_reserved(*it)) {
+		++ column;
+		value += *it++;
+	}
+
+	list.push_back({ token::word, line, save, std::move(value) });
+}
+
+void analyse_include(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+	assert(*it == '@');
+
+	std::string include;
+	unsigned save = column;
+
+	// Read include.
+	++ it;
+	while (it != end && !is_space(*it)) {
+		++ column;
+		include += *it++;
+	}
+
+	if (include == "include")
+		list.push_back({ token::include, line, save });
+	else if (include == "tryinclude")
+		list.push_back({ token::tryinclude, line, save });
+	else
+		throw exception(line, column, "expected include or tryinclude after '@' token");
+}
+
+void parse_option_value_simple(option& option, token_iterator& it)
+{
+	assert(it->get_type() == token::word || it->get_type() == token::quoted_word);
+
+	option.push_back((it++)->get_value());
+}
+
+void parse_option_value_list(option& option, token_iterator& it, token_iterator end)
+{
+	assert(it->get_type() == token::list_begin);
+
+	token_iterator save = it++;
+
+	while (it != end && it->get_type() != token::list_end) {
+		switch (it->get_type()) {
+		case token::comma:
+			// Previous must be a word.
+			if (it[-1].get_type() != token::word && it[-1].get_type() != token::quoted_word)
+				throw exception(it->get_line(), it->get_column(),
+				                "unexpected comma after '"s + it[-1].get_value() + "'");
+
+			++ it;
+			break;
+		case token::word:
+		case token::quoted_word:
+			option.push_back((it++)->get_value());
+			break;
+		default:
+			throw exception(it->get_line(), it->get_column(), "unexpected '"s + it[-1].get_value() + "' in list construct");
+			break;
+		}
+	}
+
+	if (it == end)
+		throw exception(save->get_line(), save->get_column(), "unterminated list construct");
+
+	// Remove ).
+	++ it;
+}
+
+void parse_option(section& sc, token_iterator& it, token_iterator end)
+{
+	option option(it->get_value());
+	token_iterator save(it);
+
+	// No '=' or something else?
+	if (++it == end)
+		throw exception(save->get_line(), save->get_column(), "expected '=' assignment, got <EOF>");
+	if (it->get_type() != token::assign)
+		throw exception(it->get_line(), it->get_column(), "expected '=' assignment, got " + it->get_value());
+
+	// Empty options are allowed so just test for words.
+	if (++it != end) {
+		if (it->get_type() == token::word || it->get_type() == token::quoted_word)
+			parse_option_value_simple(option, it);
+		else if (it->get_type() == token::list_begin)
+			parse_option_value_list(option, it, end);
+	}
+
+	sc.push_back(std::move(option));
+}
+
+void parse_include(document& doc, const std::string& path, token_iterator& it, token_iterator end, bool required)
+{
+	token_iterator save(it);
+
+	if (++it == end)
+		throw exception(save->get_line(), save->get_column(), "expected file name after '@include' statement, got <EOF>");
+	if (it->get_type() != token::word && it->get_type() != token::quoted_word)
+		throw exception(it->get_line(), it->get_column(),
+		                "expected file name after '@include' statement, got "s + it->get_value());
+
+	std::string value = (it++)->get_value();
+	std::string file;
+
+	if (!is_absolute(value)) {
+#if defined(_WIN32)
+		file = path + "\\" + value;
+#else
+		file = path + "/" + value;
+#endif
+	} else
+		file = value;
+
+	try {
+		/*
+		 * If required is set to true, we have @include, otherwise the non-fatal
+		 * @tryinclude keyword.
+		 */
+		for (const auto& sc : read_file(file))
+			doc.push_back(sc);
+	} catch (...) {
+		if (required)
+			throw;
+	}
+}
+
+void parse_section(document& doc, token_iterator& it, token_iterator end)
+{
+	section sc(it->get_value());
+
+	// Skip [section].
+	++ it;
+
+	// Read until next section.
+	while (it != end && it->get_type() != token::section) {
+		if (it->get_type() != token::word)
+			throw exception(it->get_line(), it->get_column(),
+			                "unexpected token '"s + it->get_value() + "' in section definition");
+
+		parse_option(sc, it, end);
+	}
+
+	doc.push_back(std::move(sc));
+}
+
+} // !namespace
+
+exception::exception(unsigned line, unsigned column, std::string msg) noexcept
+	: line_(line)
+	, column_(column)
+	, message_(std::move(msg))
+{
+}
+
+auto exception::line() const noexcept -> unsigned
+{
+	return line_;
+}
+
+auto exception::column() const noexcept -> unsigned
+{
+	return column_;
+}
+
+auto exception::what() const noexcept -> const char*
+{
+	return message_.c_str();
+}
+
+token::token(type type, unsigned line, unsigned column, std::string value) noexcept
+	: type_(type)
+	, line_(line)
+	, column_(column)
+{
+	switch (type) {
+	case include:
+		value_ = "@include";
+		break;
+	case tryinclude:
+		value_ = "@tryinclude";
+		break;
+	case section:
+	case word:
+	case quoted_word:
+		value_ = value;
+		break;
+	case assign:
+		value_ = "=";
+		break;
+	case list_begin:
+		value_ = "(";
+		break;
+	case list_end:
+		value_ = ")";
+		break;
+	case comma:
+		value_ = ",";
+		break;
+	default:
+		break;
+	}
+}
+
+auto token::get_type() const noexcept -> type
+{
+	return type_;
+}
+
+auto token::get_line() const noexcept -> unsigned
+{
+	return line_;
+}
+
+auto token::get_column() const noexcept -> unsigned
+{
+	return column_;
+}
+
+auto token::get_value() const noexcept -> const std::string&
+{
+	return value_;
+}
+
+option::option(std::string key) noexcept
+	: std::vector<std::string>()
+	, key_(std::move(key))
+{
+	assert(!key_.empty());
+}
+
+option::option(std::string key, std::string value) noexcept
+	: key_(std::move(key))
+{
+	assert(!key_.empty());
+
+	push_back(std::move(value));
+}
+
+option::option(std::string key, std::vector<std::string> values) noexcept
+	: std::vector<std::string>(std::move(values))
+	, key_(std::move(key))
+{
+	assert(!key_.empty());
+}
+
+auto option::get_key() const noexcept -> const std::string&
+{
+	return key_;
+}
+
+auto option::get_value() const noexcept -> const std::string&
+{
+	static std::string dummy;
+
+	return empty() ? dummy : (*this)[0];
+}
+
+section::section(std::string key) noexcept
+	: key_(std::move(key))
+{
+	assert(!key_.empty());
+}
+
+auto section::get_key() const noexcept -> const std::string&
+{
+	return key_;
+}
+
+auto section::contains(std::string_view key) const noexcept -> bool
+{
+	return find(key) != end();
+}
+
+auto section::get(std::string_view key) const noexcept -> option
+{
+	auto it = find(key);
+
+	if (it == end())
+		return option(std::string(key));
+
+	return *it;
+}
+
+auto section::find(std::string_view key) noexcept -> iterator
+{
+	return std::find_if(begin(), end(), [&] (const auto& o) {
+		return o.get_key() == key;
+	});
+}
+
+auto section::find(std::string_view key) const noexcept -> const_iterator
+{
+	return std::find_if(cbegin(), cend(), [&] (const auto& o) {
+		return o.get_key() == key;
+	});
+}
+
+auto section::operator[](std::string_view key) -> option&
+{
+	assert(contains(key));
+
+	return *find(key);
+}
+
+auto section::operator[](std::string_view key) const -> const option&
+{
+	assert(contains(key));
+
+	return *find(key);
+}
+
+auto document::contains(std::string_view key) const noexcept -> bool
+{
+	return find(key) != end();
+}
+
+auto document::get(std::string_view key) const noexcept -> section
+{
+	auto it = find(key);
+
+	if (it == end())
+		return section(std::string(key));
+
+	return *it;
+}
+
+auto document::find(std::string_view key) noexcept -> iterator
+{
+	return std::find_if(begin(), end(), [&] (const auto& o) {
+		return o.get_key() == key;
+	});
+}
+
+auto document::find(std::string_view key) const noexcept -> const_iterator
+{
+	return std::find_if(cbegin(), cend(), [&] (const auto& o) {
+		return o.get_key() == key;
+	});
+}
+
+auto document::operator[](std::string_view key) -> section&
+{
+	assert(contains(key));
+
+	return *find(key);
+}
+
+auto document::operator[](std::string_view key) const -> const section&
+{
+	assert(contains(key));
+
+	return *find(key);
+}
+
+tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end)
+{
+	tokens list;
+	unsigned line = 1;
+	unsigned column = 0;
+
+	while (it != end) {
+		if (*it == '\n')
+			analyse_line(line, column, it);
+		else if (*it == '#')
+			analyse_comment(column, it, end);
+		else if (*it == '[')
+			analyse_section(list, line, column, it, end);
+		else if (*it == '=')
+			analyse_assign(list, line, column, it);
+		else if (is_space(*it))
+			analyse_spaces(column, it, end);
+		else if (*it == '@')
+			analyse_include(list, line, column, it, end);
+		else if (is_quote(*it))
+			analyse_quoted_word(list, line, column, it, end);
+		else if (is_list(*it))
+			analyse_list(list, line, column, it);
+		else
+			analyse_word(list, line, column, it, end);
+	}
+
+	return list;
+}
+
+tokens analyse(std::istream& stream)
+{
+	return analyse(std::istreambuf_iterator<char>(stream), {});
+}
+
+document parse(const tokens& tokens, const std::string& path)
+{
+	document doc;
+	token_iterator it = tokens.cbegin();
+	token_iterator end = tokens.cend();
+
+	while (it != end) {
+		switch (it->get_type()) {
+		case token::include:
+			parse_include(doc, path, it, end, true);
+			break;
+		case token::tryinclude:
+			parse_include(doc, path, it, end, false);
+			break;
+		case token::section:
+			parse_section(doc, it, end);
+			break;
+		default:
+			throw exception(it->get_line(), it->get_column(),
+			                "unexpected '"s + it->get_value() + "' on root document");
+		}
+	}
+
+	return doc;
+}
+
+document read_file(const std::string& filename)
+{
+	// Get parent path.
+	auto parent = filename;
+	auto pos = parent.find_last_of("/\\");
+
+	if (pos != std::string::npos)
+		parent.erase(pos);
+	else
+		parent = ".";
+
+	std::ifstream input(filename);
+
+	if (!input)
+		throw exception(0, 0, std::strerror(errno));
+
+	return parse(analyse(input), parent);
+}
+
+document read_string(const std::string& buffer)
+{
+	std::istringstream iss(buffer);
+
+	return parse(analyse(iss));
+}
+
+void dump(const tokens& tokens)
+{
+	for (const token& token: tokens) {
+		// TODO: add better description
+		std::cout << token.get_line() << ":" << token.get_column() << ": " << token.get_value() << std::endl;
+	}
+}
+
+} // !irccd::ini
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/ini.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,493 @@
+/*
+ * ini.hpp -- extended .ini file parser
+ *
+ * Copyright (c) 2013-2018 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_INI_HPP
+#define IRCCD_INI_HPP
+
+/**
+ * \file ini.hpp
+ * \brief Extended .ini file parser.
+ * \author David Demelier <markand@malikania.fr>
+ * \version 2.0.0
+ */
+
+/**
+ * \page Ini Ini
+ * \brief Extended .ini file parser.
+ * - \subpage ini-syntax
+ */
+
+/**
+ * \page ini-syntax Syntax
+ * \brief File syntax.
+ *
+ * The syntax is similar to most of `.ini` implementations as:
+ *
+ * - a section is delimited by `[name]` can be redefined multiple times,
+ * - an option **must** always be defined in a section,
+ * - empty options must be surrounded by quotes,
+ * - lists can not include trailing commas,
+ * - include statements must always live at the beginning of files
+ *   (in no sections),
+ * - comments start with # until the end of line,
+ * - options with spaces **must** use quotes.
+ *
+ * # Basic file
+ *
+ * ````ini
+ * # This is a comment.
+ * [section]
+ * option1 = value1
+ * option2 = "value 2 with spaces"	# comment is also allowed here
+ * ````
+ *
+ * # Redefinition
+ *
+ * Sections can be redefined multiple times and are kept the order they are
+ * seen.
+ *
+ * ````ini
+ * [section]
+ * value = "1"
+ *
+ * [section]
+ * value = "2"
+ * ````
+ *
+ * The ini::document object will contains two ini::section.
+ *
+ * # Lists
+ *
+ * Lists are defined using `()` and commas, like values, they may have quotes.
+ *
+ * ````ini
+ * [section]
+ * names = ( "x1", "x2" )
+ *
+ * # This is also allowed.
+ * biglist = (
+ *   "abc",
+ *   "def"
+ * )
+ * ````
+ *
+ * # Include statement
+ *
+ * You can split a file into several pieces, if the include statement contains a
+ * relative path, the path will be relative to the current file being parsed.
+ *
+ * You **must** use the include statement before any section.
+ *
+ * If the file contains spaces, use quotes.
+ *
+ * ````ini
+ * # main.conf
+ * @include "foo.conf"
+ *
+ * # foo.conf
+ * [section]
+ * option1 = value1
+ * ````
+ */
+
+#include <algorithm>
+#include <exception>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <vector>
+
+/**
+ * \brief Namespace for ini related classes.
+ */
+namespace irccd::ini {
+
+class document;
+
+/**
+ * \brief exception in a file.
+ */
+class exception : public std::exception {
+private:
+	unsigned line_;
+	unsigned column_;
+	std::string message_;
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param line the line
+	 * \param column the column
+	 * \param msg the message
+	 */
+	exception(unsigned line, unsigned column, std::string msg) noexcept;
+
+	/**
+	 * Get the line number.
+	 *
+	 * \return the line
+	 */
+	auto line() const noexcept -> unsigned;
+
+	/**
+	 * Get the column number.
+	 *
+	 * \return the column
+	 */
+	auto column() const noexcept -> unsigned;
+
+	/**
+	 * Return the raw exception message (no line and column shown).
+	 *
+	 * \return the exception message
+	 */
+	auto what() const noexcept -> const char* override;
+};
+
+/**
+ * \brief Describe a token read in the .ini source.
+ *
+ * This class can be used when you want to parse a .ini file yourself.
+ *
+ * \see analyse
+ */
+class token {
+public:
+	/**
+	 * \brief token type.
+	 */
+	enum type {
+		include,                //!< include statement
+		tryinclude,             //!< tryinclude statement
+		section,                //!< [section]
+		word,                   //!< word without quotes
+		quoted_word,            //!< word with quotes
+		assign,                 //!< = assignment
+		list_begin,             //!< begin of list (
+		list_end,               //!< end of list )
+		comma                   //!< list separation
+	};
+
+private:
+	type type_;
+	unsigned line_;
+	unsigned column_;
+	std::string value_;
+
+public:
+	/**
+	 * Construct a token.
+	 *
+	 * \param type the type
+	 * \param line the line
+	 * \param column the column
+	 * \param value the value
+	 */
+	token(type type, unsigned line, unsigned column, std::string value = "") noexcept;
+
+	/**
+	 * Get the type.
+	 *
+	 * \return the type
+	 */
+	auto get_type() const noexcept -> type;
+
+	/**
+	 * Get the line.
+	 *
+	 * \return the line
+	 */
+	auto get_line() const noexcept -> unsigned;
+
+	/**
+	 * Get the column.
+	 *
+	 * \return the column
+	 */
+	auto get_column() const noexcept -> unsigned;
+
+	/**
+	 * Get the value. For words, quoted words and section, the value is the
+	 * content. Otherwise it's the characters parsed.
+	 *
+	 * \return the value
+	 */
+	auto get_value() const noexcept -> const std::string&;
+};
+
+/**
+ * List of tokens in order they are analyzed.
+ */
+using tokens = std::vector<token>;
+
+/**
+ * \brief option definition.
+ */
+class option : public std::vector<std::string> {
+private:
+	std::string key_;
+
+public:
+	/**
+	 * Construct an empty option.
+	 *
+	 * \pre key must not be empty
+	 * \param key the key
+	 */
+	option(std::string key) noexcept;
+
+	/**
+	 * Construct a single option.
+	 *
+	 * \pre key must not be empty
+	 * \param key the key
+	 * \param value the value
+	 */
+	option(std::string key, std::string value) noexcept;
+
+	/**
+	 * Construct a list option.
+	 *
+	 * \pre key must not be empty
+	 * \param key the key
+	 * \param values the values
+	 */
+	option(std::string key, std::vector<std::string> values) noexcept;
+
+	/**
+	 * Get the option key.
+	 *
+	 * \return the key
+	 */
+	auto get_key() const noexcept -> const std::string&;
+
+	/**
+	 * Get the option value.
+	 *
+	 * \return the value
+	 */
+	auto get_value() const noexcept -> const std::string&;
+};
+
+/**
+ * \brief Section that contains one or more options.
+ */
+class section : public std::vector<option> {
+private:
+	std::string key_;
+
+public:
+	/**
+	 * Construct a section with its name.
+	 *
+	 * \pre key must not be empty
+	 * \param key the key
+	 */
+	section(std::string key) noexcept;
+
+	/**
+	 * Get the section key.
+	 *
+	 * \return the key
+	 */
+	auto get_key() const noexcept -> const std::string&;
+
+	/**
+	 * Check if the section contains a specific option.
+	 *
+	 * \param key the option key
+	 * \return true if the option exists
+	 */
+	auto contains(std::string_view key) const noexcept -> bool;
+
+	/**
+	 * Find an option or return an empty one if not found.
+	 *
+	 * \param key the key
+	 * \return the option or empty one if not found
+	 */
+	auto get(std::string_view key) const noexcept -> option;
+
+	/**
+	 * Find an option by key and return an iterator.
+	 *
+	 * \param key the key
+	 * \return the iterator or end() if not found
+	 */
+	auto find(std::string_view key) noexcept -> iterator;
+
+	/**
+	 * Find an option by key and return an iterator.
+	 *
+	 * \param key the key
+	 * \return the iterator or end() if not found
+	 */
+	auto find(std::string_view key) const noexcept -> const_iterator;
+
+	/**
+	 * Access an option at the specified key.
+	 *
+	 * \param key the key
+	 * \return the option
+	 * \pre contains(key) must return true
+	 */
+	auto operator[](std::string_view key) -> option&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \param key the key
+	 * \return the option
+	 * \pre contains(key) must return true
+	 */
+	auto operator[](std::string_view key) const -> const option&;
+
+	/**
+	 * Inherited operators.
+	 */
+	using std::vector<option>::operator[];
+};
+
+/**
+ * \brief Ini document description.
+ * \see read_file
+ * \see read_string
+ */
+class document : public std::vector<section> {
+public:
+	/**
+	 * Check if a document has a specific section.
+	 *
+	 * \param key the key
+	 * \return true if the document contains the section
+	 */
+	auto contains(std::string_view key) const noexcept -> bool;
+
+	/**
+	 * Find a section or return an empty one if not found.
+	 *
+	 * \param key the key
+	 * \return the section or empty one if not found
+	 */
+	auto get(std::string_view key) const noexcept -> section;
+
+	/**
+	 * Find a section by key and return an iterator.
+	 *
+	 * \param key the key
+	 * \return the iterator or end() if not found
+	 */
+	auto find(std::string_view key) noexcept -> iterator;
+
+	/**
+	 * Find a section by key and return an iterator.
+	 *
+	 * \param key the key
+	 * \return the iterator or end() if not found
+	 */
+	auto find(std::string_view key) const noexcept -> const_iterator;
+
+	/**
+	 * Access a section at the specified key.
+	 *
+	 * \param key the key
+	 * \return the section
+	 * \pre contains(key) must return true
+	 */
+	auto operator[](std::string_view key) -> section&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \param key the key
+	 * \return the section
+	 * \pre contains(key) must return true
+	 */
+	auto operator[](std::string_view key) const -> const section&;
+
+	/**
+	 * Inherited operators.
+	 */
+	using std::vector<section>::operator[];
+};
+
+/**
+ * Analyse a stream and detect potential syntax errors. This does not parse the
+ * file like including other files in include statement.
+ *
+ * It does only analysis, for example if an option is defined under no section,
+ * this does not trigger an exception while it's invalid.
+ *
+ * \param it the iterator
+ * \param end where to stop
+ * \return the list of tokens
+ * \throws exception on errors
+ */
+auto analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end) -> tokens;
+
+/**
+ * Overloaded function for stream.
+ *
+ * \param stream the stream
+ * \return the list of tokens
+ * \throws exception on errors
+ */
+auto analyse(std::istream& stream) -> tokens;
+
+/**
+ * Parse the produced tokens.
+ *
+ * \param tokens the tokens
+ * \param path the parent path
+ * \return the document
+ * \throw exception on errors
+ */
+auto parse(const tokens& tokens, const std::string& path = ".") -> document;
+
+/**
+ * Parse a file.
+ *
+ * \param filename the file name
+ * \return the document
+ * \throw exception on errors
+ */
+auto read_file(const std::string& filename) -> document;
+
+/**
+ * Parse a string.
+ *
+ * If the string contains include statements, they are relative to the current
+ * working directory.
+ *
+ * \param buffer the buffer
+ * \return the document
+ * \throw exception on exceptions
+ */
+auto read_string(const std::string& buffer) -> document;
+
+/**
+ * Show all tokens and their description.
+ *
+ * \param tokens the tokens
+ */
+void dump(const tokens& tokens);
+
+} // !irccd::ini
+
+#endif // !IRCCD_INI_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/ini_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,97 @@
+/*
+ * ini_util.hpp -- ini utilities
+ *
+ * Copyright (c) 2013-2018 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_INI_UTIL_HPP
+#define IRCCD_INI_UTIL_HPP
+
+/**
+ * \file ini_util.hpp
+ * \brief Ini utilities.
+ */
+
+#include <optional>
+
+#include "ini.hpp"
+#include "string_util.hpp"
+
+namespace irccd {
+
+/**
+ * \brief Ini utilities.
+ */
+namespace ini_util {
+
+/**
+ * Get an unsigned integer from the configuration section.
+ *
+ * \param sc the section
+ * \param name the option name
+ * \return the value or none if not able to convert
+ */
+template <typename Int>
+inline auto get_uint(const ini::section& sc, std::string_view name) noexcept -> std::optional<Int>
+{
+	return string_util::to_uint<Int>(sc.get(name).get_value());
+}
+
+/**
+ * Get an optional string or the default value if not given.
+ *
+ * \param sc the section
+ * \param name the option name
+ * \param def the default value
+ * \return the value or def if not found
+ */
+inline auto optional_string(const ini::section& sc,
+                            std::string_view name,
+                            std::string_view def) noexcept -> std::string
+{
+	const auto it = sc.find(name);
+
+	if (it == sc.end())
+		return std::string(def);
+
+	return it->get_value();
+}
+
+/**
+ * Get an optional unsigned integer from the configuration section.
+ *
+ * \param sc the section
+ * \param name the option name
+ * \param def the default value
+ * \return the value or none if not able to convert
+ */
+template <typename Int>
+inline auto optional_uint(const ini::section& sc,
+                          std::string_view name,
+                          Int def) noexcept -> std::optional<Int>
+{
+	const auto it = sc.find(name);
+
+	if (it == sc.end())
+		return def;
+
+	return string_util::to_uint<Int>(it->get_value());
+}
+
+} // !ini_util
+
+} // !irccd
+
+#endif // !IRCCD_INI_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/json_util.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,163 @@
+/*
+ * json_util.cpp -- utilities for JSON
+ *
+ * Copyright (c) 2018 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 <limits>
+#include <type_traits>
+
+#include "json_util.hpp"
+
+using nlohmann::json;
+
+namespace irccd::json_util {
+
+namespace {
+
+template <typename Int>
+auto clampi(const json& value) noexcept -> std::optional<Int>
+{
+	static_assert(std::is_signed<Int>::value, "Int must be signed");
+
+	if (!value.is_number_integer())
+		return std::nullopt;
+
+	const auto ret = value.get<std::int64_t>();
+
+	if (ret < std::numeric_limits<Int>::min() || ret > std::numeric_limits<Int>::max())
+		return std::nullopt;
+
+	return static_cast<Int>(ret);
+}
+
+template <typename Int>
+auto clampu(const json& value) noexcept -> std::optional<Int>
+{
+	static_assert(std::is_unsigned<Int>::value, "Int must be unsigned");
+
+	if (!value.is_number_unsigned())
+		return std::nullopt;
+
+	const auto ret = value.get<std::uint64_t>();
+
+	if (ret > std::numeric_limits<Int>::max())
+		return std::nullopt;
+
+	return static_cast<Int>(ret);
+}
+
+} // !namespace
+
+auto type_traits<bool>::get(const json& value) noexcept -> std::optional<bool>
+{
+	if (!value.is_boolean())
+		return std::nullopt;
+
+	return value.get<bool>();
+}
+
+auto type_traits<double>::get(const json& value) noexcept -> std::optional<double>
+{
+	if (!value.is_number_float())
+		return std::nullopt;
+
+	return value.get<double>();
+}
+
+auto type_traits<std::string>::get(const json& value) -> std::optional<std::string>
+{
+	if (!value.is_string())
+		return std::nullopt;
+
+	return value.get<std::string>();
+}
+
+auto type_traits<std::int8_t>::get(const json& value) -> std::optional<std::int8_t>
+{
+	return clampi<std::int8_t>(value);
+}
+
+auto type_traits<std::int16_t>::get(const json& value) -> std::optional<std::int16_t>
+{
+	return clampi<std::int16_t>(value);
+}
+
+auto type_traits<std::int32_t>::get(const json& value) -> std::optional<std::int32_t>
+{
+	return clampi<std::int32_t>(value);
+}
+
+auto type_traits<std::int64_t>::get(const json& value) noexcept -> std::optional<std::int64_t>
+{
+	if (!value.is_number_integer())
+		return std::nullopt;
+
+	return value.get<std::int64_t>();
+}
+
+auto type_traits<std::uint8_t>::get(const json& value) -> std::optional<std::uint8_t>
+{
+	return clampu<std::uint8_t>(value);
+}
+
+auto type_traits<std::uint16_t>::get(const json& value) -> std::optional<std::uint16_t>
+{
+	return clampu<std::uint16_t>(value);
+}
+
+auto type_traits<std::uint32_t>::get(const json& value) -> std::optional<std::uint32_t>
+{
+	return clampu<std::uint32_t>(value);
+}
+
+auto type_traits<std::uint64_t>::get(const json& value) noexcept -> std::optional<std::uint64_t>
+{
+	if (!value.is_number_unsigned())
+		return std::nullopt;
+
+	return value.get<std::uint64_t>();
+}
+
+auto pretty(const json& value, int indent) -> std::string
+{
+	switch (value.type()) {
+	case json::value_t::null:
+		return "null";
+	case json::value_t::string:
+		return value.get<std::string>();
+	case json::value_t::boolean:
+		return value.get<bool>() ? "true" : "false";
+	case json::value_t::number_integer:
+		return std::to_string(value.get<std::int64_t>());
+	case json::value_t::number_unsigned:
+		return std::to_string(value.get<std::uint64_t>());
+	case json::value_t::number_float:
+		return std::to_string(value.get<double>());
+	default:
+		return value.dump(indent);
+	}
+}
+
+auto contains(const nlohmann::json& array, const nlohmann::json& value) noexcept -> bool
+{
+	for (const auto& v : array)
+		if (v == value)
+			return true;
+
+	return false;
+}
+
+} // !irccd::json_util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/json_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,295 @@
+/*
+ * json_util.hpp -- utilities for JSON
+ *
+ * Copyright (c) 2018 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_JSON_UTIL_HPP
+#define IRCCD_JSON_UTIL_HPP
+
+/**
+ * \file json_util.hpp
+ * \brief Utilities for JSON.
+ */
+
+#include <cstdint>
+#include <optional>
+#include <string>
+
+#include <json.hpp>
+
+/**
+ * \brief Utilities for JSON.
+ */
+namespace irccd::json_util {
+
+/**
+ * \brief Describe how to convert a JSON value.
+ *
+ * This traits must be specialized for every type you want to convert from JSON
+ * to its native type.
+ *
+ * You only need to implement the get function with the following signature:
+ *
+ * ```cpp
+ * static std::optional<T> get(const nlohmann::json& value);
+ * ```
+ *
+ * The implementation should not throw an exception but return a null optional
+ * instead.
+ *
+ * This traits is already specialized for the given types:
+ *
+ * - bool
+ * - double
+ * - std::uint(8, 16, 32, 64)_t
+ * - std::string
+ */
+template <typename T>
+struct type_traits;
+
+/**
+ * \brief Specialization for `bool`.
+ */
+template <>
+struct type_traits<bool> {
+	/**
+	 * Convert the JSON value to bool.
+	 *
+	 * \param value the value
+	 * \return the bool or empty if not a boolean type
+	 */
+	static auto get(const nlohmann::json& value) noexcept -> std::optional<bool>;
+};
+
+/**
+ * \brief Specialization for `double`.
+ */
+template <>
+struct type_traits<double> {
+	/**
+	 * Convert the JSON value to bool.
+	 *
+	 * \param value the value
+	 * \return the double or empty if not a double type
+	 */
+	static auto get(const nlohmann::json& value) noexcept -> std::optional<double>;
+};
+
+/**
+ * \brief Specialization for `std::string`.
+ */
+template <>
+struct type_traits<std::string> {
+	/**
+	 * Convert the JSON value to std::string.
+	 *
+	 * \param value the value
+	 * \return the string or empty if not a string type
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::string>;
+};
+
+/**
+ * \brief Specialization for `std::int8_t`.
+ */
+template <>
+struct type_traits<std::int8_t> {
+	/**
+	 * Convert the JSON value to std::int8_t.
+	 *
+	 * \param value the value
+	 * \return the value or empty if value does not fit between the range
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::int8_t>;
+};
+
+/**
+ * \brief Specialization for `std::int16_t`.
+ */
+template <>
+struct type_traits<std::int16_t> {
+	/**
+	 * Convert the JSON value to std::int16_t.
+	 *
+	 * \param value the value
+	 * \return the value or empty if value does not fit between the range
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::int16_t>;
+};
+
+/**
+ * \brief Specialization for `std::int32_t`.
+ */
+template <>
+struct type_traits<std::int32_t> {
+	/**
+	 * Convert the JSON value to std::int32_t.
+	 *
+	 * \param value the value
+	 * \return the value or empty if value does not fit between the range
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::int32_t>;
+};
+
+/**
+ * \brief Specialization for `std::int64_t`.
+ */
+template <>
+struct type_traits<std::int64_t> {
+	/**
+	 * Convert the JSON value to std::int64_t.
+	 *
+	 * \param value the value
+	 * \return the int or empty if not a int type
+	 */
+	static auto get(const nlohmann::json& value) noexcept -> std::optional<std::int64_t>;
+};
+
+/**
+ * \brief Specialization for `std::uint8_t`.
+ */
+template <>
+struct type_traits<std::uint8_t> {
+	/**
+	 * Convert the JSON value to std::uint8_t.
+	 *
+	 * \param value the value
+	 * \return the value or empty if value does not fit between the range
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::uint8_t>;
+};
+
+/**
+ * \brief Specialization for `std::uint16_t`.
+ */
+template <>
+struct type_traits<std::uint16_t> {
+	/**
+	 * Convert the JSON value to std::uint16_t.
+	 *
+	 * \param value the value
+	 * \return the value or empty if value does not fit between the range
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::uint16_t>;
+};
+
+/**
+ * \brief Specialization for `std::int32_t`.
+ */
+template <>
+struct type_traits<std::uint32_t> {
+	/**
+	 * Convert the JSON value to std::uint32_t.
+	 *
+	 * \param value the value
+	 * \return the value or empty if value does not fit between the range
+	 */
+	static auto get(const nlohmann::json& value) -> std::optional<std::uint32_t>;
+};
+
+/**
+ * \brief Specialization for `std::uint64_t`.
+ */
+template <>
+struct type_traits<std::uint64_t> {
+	/**
+	 * Convert the JSON value to std::uint64_t.
+	 *
+	 * \param value the value
+	 * \return the int or empty if not a int type
+	 */
+	static auto get(const nlohmann::json& value) noexcept -> std::optional<std::uint64_t>;
+};
+
+/**
+ * \brief Convenient JSON object parser
+ *
+ * This class helps destructuring insecure JSON input by returning optional
+ * values if they are not present or invalid.
+ */
+class deserializer : public nlohmann::json {
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param obj the JSON object
+	 */
+	deserializer(const nlohmann::json& obj)
+		: nlohmann::json(obj)
+	{
+	}
+
+	/**
+	 * Get a value from the document object.
+	 *
+	 * \param key the property key
+	 * \return the value or std::nullopt if not found or not convertible
+	 */
+	template <typename Type>
+	auto get(const std::string& key) const noexcept -> std::optional<Type>
+	{
+		const auto it = find(key);
+
+		if (it == end())
+			return std::nullopt;
+
+		return type_traits<Type>::get(*it);
+	}
+
+	/**
+	 * Get an optional value from the document object.
+	 *
+	 * If the value is undefined, the default value is returned. Otherwise, if
+	 * the value is not in the given type, std::nullopt is returned.
+	 *
+	 * \param key the property key
+	 * \param def the default value if property is undefined
+	 * \return the value, std::nullopt or def
+	 */
+	template <typename Type, typename DefaultValue>
+	auto optional(const std::string& key, DefaultValue&& def) const noexcept -> std::optional<Type>
+	{
+		const auto it = find(key);
+
+		if (it == end())
+			return std::optional<Type>(std::forward<DefaultValue>(def));
+
+		return type_traits<Type>::get(*it);
+	}
+};
+
+/**
+ * Print the value as human readable.
+ *
+ * \note This only works on flat objects.
+ * \param value the value
+ * \param indent the optional indent for objects/arrays
+ * \return the string
+ */
+auto pretty(const nlohmann::json& value, int indent = 4) -> std::string;
+
+/**
+ * Check if a JSON array contains a specific value in any order.
+ *
+ * \param array the JSON array
+ * \param value the JSON value
+ * \return true if value is present
+ */
+auto contains(const nlohmann::json& array, const nlohmann::json& value) noexcept -> bool;
+
+} // !irccd::json_util
+
+#endif // !IRCCD_JSON_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/options.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,191 @@
+/*
+ * options.cpp -- parse Unix command line options
+ *
+ * Copyright (c) 2015-2018 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 <cassert>
+
+#include "options.hpp"
+
+namespace irccd {
+
+namespace option {
+
+namespace {
+
+using iterator = std::vector<std::string>::iterator;
+using args = std::vector<std::string>;
+
+inline bool is_option(const std::string& arg) noexcept
+{
+    return arg.size() >= 2 && arg[0] == '-';
+}
+
+inline bool is_long_option(const std::string& arg) noexcept
+{
+    assert(is_option(arg));
+
+    return arg.size() >= 3 && arg[1] == '-';
+}
+
+inline bool is_short_simple(const std::string& arg) noexcept
+{
+    assert(is_option(arg) && !is_long_option(arg));
+
+    return arg.size() == 2;
+}
+
+void parse_long_option(result& result, args& args, iterator& it, iterator& end, const options& definition)
+{
+    auto arg = *it++;
+    auto opt = definition.find(arg);
+
+    if (opt == definition.end())
+        throw invalid_option(arg);
+
+    // Need argument?
+    if (opt->second) {
+        if (it == end || is_option(*it))
+            throw missing_value(arg);
+
+        result.insert(std::make_pair(arg, *it++));
+        it = args.erase(args.begin(), it);
+        end = args.end();
+    } else {
+        result.insert(std::make_pair(arg, ""));
+        it = args.erase(args.begin());
+        end = args.end();
+    }
+}
+
+void parse_short_option_simple(result& result, args& args, iterator& it, iterator &end, const options& definition)
+{
+    /*
+     * Here two cases:
+     *
+     * -v (no option)
+     * -c value
+     */
+    auto arg = *it++;
+    auto opt = definition.find(arg);
+
+    if (opt == definition.end())
+        throw invalid_option(arg);
+
+    // Need argument?
+    if (opt->second) {
+        if (it == end || is_option(*it))
+            throw missing_value(arg);
+
+        result.insert(std::make_pair(arg, *it++));
+        it = args.erase(args.begin(), it);
+        end = args.end();
+    } else {
+        result.insert(std::make_pair(arg, ""));
+        it = args.erase(args.begin());
+        end = args.end();
+    }
+}
+
+void parse_short_option_compressed(result& result, args& args, iterator& it, iterator &end, const options& definition)
+{
+    /*
+     * Here multiple scenarios:
+     *
+     * 1. -abc (-a -b -c if all are simple boolean arguments)
+     * 2. -vc foo.conf (-v -c foo.conf if -c is argument dependant)
+     * 3. -vcfoo.conf (-v -c foo.conf also)
+     */
+    auto value = it->substr(1);
+    auto len = value.length();
+    int toremove = 1;
+
+    for (std::size_t i = 0; i < len; ++i) {
+        auto arg = std::string{'-'} + value[i];
+        auto opt = definition.find(arg);
+
+        if (opt == definition.end())
+            throw invalid_option(arg);
+
+        if (opt->second) {
+            if (i == (len - 1)) {
+                // End of string, get the next argument (see 2.).
+                if (++it == end || is_option(*it))
+                    throw missing_value(arg);
+
+                result.insert(std::make_pair(arg, *it));
+                toremove += 1;
+            } else {
+                result.insert(std::make_pair(arg, value.substr(i + 1)));
+                i = len;
+            }
+        } else
+            result.insert(std::make_pair(arg, ""));
+    }
+
+    it = args.erase(args.begin(), args.begin() + toremove);
+    end = args.end();
+}
+
+void parse_short_option(result& result, args& args, iterator& it, iterator &end, const options& definition)
+{
+    if (is_short_simple(*it))
+        parse_short_option_simple(result, args, it, end, definition);
+    else
+        parse_short_option_compressed(result, args, it, end, definition);
+}
+
+} // !namespace
+
+result read(std::vector<std::string>& args, const options& definition)
+{
+    result result;
+
+    auto it = args.begin();
+    auto end = args.end();
+
+    while (it != end) {
+        if (!is_option(*it))
+            break;
+
+        if (is_long_option(*it))
+            parse_long_option(result, args, it, end, definition);
+        else
+            parse_short_option(result, args, it, end, definition);
+    }
+
+    return result;
+}
+
+result read(int& argc, char**& argv, const options& definition)
+{
+    std::vector<std::string> args;
+
+    for (int i = 0; i < argc; ++i)
+        args.push_back(argv[i]);
+
+    auto before = args.size();
+    auto result = read(args, definition);
+
+    argc -= before - args.size();
+    argv += before - args.size();
+
+    return result;
+}
+
+} // !option
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/options.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,163 @@
+/*
+ * options.hpp -- parse Unix command line options
+ *
+ * Copyright (c) 2015-2018 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_OPTIONS_HPP
+#define IRCCD_OPTIONS_HPP
+
+/**
+ * \file options.hpp
+ * \brief Basic Unix options parser.
+ */
+
+#include <exception>
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace irccd {
+
+/**
+ * Namespace for options parsing.
+ */
+namespace option {
+
+/**
+ * \brief This exception is thrown when an invalid option has been found.
+ */
+class invalid_option : public std::exception {
+private:
+    std::string message_;
+    std::string name_;
+
+public:
+    /**
+     * Construct the exception.
+     *
+     * \param name the argument missing
+     */
+    inline invalid_option(std::string name)
+        : name_(std::move(name))
+    {
+        message_ = std::string("invalid option: ") + name_;
+    }
+
+    /**
+     * Get the option name.
+     *
+     * \return the name
+     */
+    inline const std::string& name() const noexcept
+    {
+        return name_;
+    }
+
+    /**
+     * Get the error message.
+     *
+     * \return the error message
+     */
+    const char* what() const noexcept override
+    {
+        return message_.c_str();
+    }
+};
+
+/**
+ * \brief This exception is thrown when an option requires a value and no value
+ * has been given.
+ */
+class missing_value : public std::exception {
+private:
+    std::string message_;
+    std::string name_;
+
+public:
+    /**
+     * Construct the exception.
+     *
+     * \param name the option that requires a value
+     */
+    inline missing_value(std::string name)
+        : name_(std::move(name))
+    {
+        message_ = std::string("missing argument for: ") + name_;
+    }
+
+    /**
+     * Get the option name.
+     *
+     * \return the name
+     */
+    inline const std::string& name() const noexcept
+    {
+        return name_;
+    }
+
+    /**
+     * Get the error message.
+     *
+     * \return the error message
+     */
+    const char* what() const noexcept override
+    {
+        return message_.c_str();
+    }
+};
+
+/**
+ * Packed multimap of options.
+ */
+using result = std::multimap<std::string, std::string>;
+
+/**
+ * Define the allowed options.
+ */
+using options = std::map<std::string, bool>;
+
+/**
+ * Extract the command line options and return a result.
+ *
+ * \param args the arguments
+ * \param definition
+ * \warning the arguments vector is modified in place to remove parsed options
+ * \throw missing_value
+ * \throw invalid_option
+ * \return the result
+ */
+result read(std::vector<std::string>& args, const options& definition);
+
+/**
+ * Overloaded function for usage with main() arguments.
+ *
+ * \param argc the number of arguments
+ * \param argv the argument vector
+ * \param definition
+ * \note don't forget to remove the first argv[0] argument
+ * \warning the argc and argv are modified in place to remove parsed options
+ * \throw missing_value
+ * \throw invalid_option
+ * \return the result
+ */
+result read(int& argc, char**& argv, const options& definition);
+
+} // !option
+
+} // !irccd
+
+#endif // !IRCCD_OPTIONS_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/stream.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,439 @@
+/*
+ * stream.hpp -- abstract stream interface
+ *
+ * Copyright (c) 2013-2018 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_STREAM_HPP
+#define IRCCD_STREAM_HPP
+
+/**
+ * \file stream.hpp
+ * \brief Abstract stream interface.
+ */
+
+#include <irccd/sysconfig.hpp>
+
+#include <cassert>
+#include <cstddef>
+#include <functional>
+#include <ostream>
+#include <string>
+#include <system_error>
+#include <utility>
+
+#include <boost/asio.hpp>
+
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
+
+#include "json.hpp"
+
+namespace irccd {
+
+/**
+ * \brief Abstract stream interface
+ * \ingroup core-streams
+ *
+ * Abstract I/O interface that allows reading/writing from a stream in an
+ * asynchronous manner.
+ *
+ * The derived classes must implement non-blocking recv and send operations.
+ */
+class stream {
+public:
+	/**
+	 * \brief Read completion handler.
+	 */
+	using recv_handler = std::function<void (std::error_code, nlohmann::json)>;
+
+	/**
+	 * \brief Write completion handler.
+	 */
+	using send_handler = std::function<void (std::error_code)>;
+
+	/**
+	 * Default constructor.
+	 */
+	stream() = default;
+
+	/**
+	 * Virtual destructor defaulted.
+	 */
+	virtual ~stream() = default;
+
+	/**
+	 * Start asynchronous read.
+	 *
+	 * \pre another read operation must not be running
+	 * \pre handler != nullptr
+	 * \param handler the handler
+	 */
+	virtual void recv(recv_handler handler) = 0;
+
+	/**
+	 * Start asynchronous write.
+	 *
+	 * \pre json.is_object()
+	 * \pre another write operation must not be running
+	 * \pre handler != nullptr
+	 * \param json the JSON message
+	 * \param handler the handler
+	 */
+	virtual void send(const nlohmann::json& json, send_handler handler) = 0;
+};
+
+// {{{ socket_stream_base
+
+/**
+ * \brief Abstract base interface for Boost.Asio sockets.
+ * \ingroup core-streams
+ *
+ * This class provides convenient functions for underlying sockets.
+ *
+ * \see basic_socket_stream
+ */
+class socket_stream_base : public stream {
+private:
+	boost::asio::streambuf input_{2048};
+	boost::asio::streambuf output_;
+
+#if !defined(NDEBUG)
+	bool is_receiving_{false};
+	bool is_sending_{false};
+#endif
+
+	void handle_recv(boost::system::error_code, std::size_t, recv_handler);
+	void handle_send(boost::system::error_code, std::size_t, send_handler);
+
+protected:
+	/**
+	 * Convenient function for receiving for the underlying socket.
+	 *
+	 * \param sc the socket
+	 * \param handler the handler
+	 */
+	template <typename Socket>
+	void recv(Socket& sc, recv_handler handler);
+
+	/**
+	 * Convenient function for sending for the underlying socket.
+	 *
+	 * \param json the JSON object
+	 * \param sc the socket
+	 * \param handler the handler
+	 */
+	template <typename Socket>
+	void send(const nlohmann::json& json, Socket& sc, send_handler handler);
+};
+
+inline void socket_stream_base::handle_recv(boost::system::error_code code,
+                                            std::size_t xfer,
+                                            recv_handler handler)
+{
+#if !defined(NDEBUG)
+	is_receiving_ = false;
+#endif
+
+	if (code == boost::asio::error::not_found) {
+		handler(make_error_code(std::errc::argument_list_too_long), nullptr);
+		return;
+	}
+	if (code == boost::asio::error::eof || xfer == 0) {
+		handler(make_error_code(std::errc::connection_reset), nullptr);
+		return;
+	}
+	if (code) {
+		handler(std::move(code), nullptr);
+		return;
+	}
+
+	// 1. Convert the buffer safely.
+	std::string buffer;
+
+	try {
+		buffer = std::string(
+			boost::asio::buffers_begin(input_.data()),
+			boost::asio::buffers_begin(input_.data()) + xfer - /* \r\n\r\n */ 4
+		);
+
+		input_.consume(xfer);
+	} catch (const std::bad_alloc&) {
+		handler(make_error_code(std::errc::not_enough_memory), nullptr);
+		return;
+	}
+
+	// 2. Convert to JSON.
+	nlohmann::json doc;
+
+	try {
+		doc = nlohmann::json::parse(buffer);
+	} catch (const std::exception&) {
+		handler(make_error_code(std::errc::invalid_argument), nullptr);
+		return;
+	}
+
+	if (!doc.is_object())
+		handler(make_error_code(std::errc::invalid_argument), nullptr);
+	else
+		handler(std::error_code(), std::move(doc));
+}
+
+inline void socket_stream_base::handle_send(boost::system::error_code code,
+                                            std::size_t xfer,
+                                            send_handler handler)
+{
+#if !defined(NDEBUG)
+	is_sending_ = false;
+#endif
+
+	if (code == boost::asio::error::eof || xfer == 0) {
+		handler(make_error_code(std::errc::connection_reset));
+		return;
+	}
+	else
+		handler(std::move(code));
+}
+
+template <typename Socket>
+inline void socket_stream_base::recv(Socket& sc, recv_handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_receiving_);
+
+	is_receiving_ = true;
+#endif
+
+	assert(handler);
+
+	async_read_until(sc, input_, "\r\n\r\n", [this, handler] (auto code, auto xfer) {
+		handle_recv(code, xfer, handler);
+	});
+}
+
+template <typename Socket>
+inline void socket_stream_base::send(const nlohmann::json& json, Socket& sc, send_handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_sending_);
+
+	is_sending_ = true;
+#endif
+
+	assert(json.is_object());
+	assert(handler);
+
+	std::ostream out(&output_);
+
+	out << json.dump(0);
+	out << "\r\n\r\n";
+	out << std::flush;
+
+	async_write(sc, output_, [this, handler] (auto code, auto xfer) {
+		handle_send(code, xfer, handler);
+	});
+}
+
+// }}}
+
+// {{{ basic_socket_stream
+
+/**
+ * \brief Complete implementation for basic sockets
+ * \ingroup core-streams
+ * \tparam Socket Boost.Asio socket (e.g. boost::asio::ip::tcp::socket)
+ */
+template <typename Socket>
+class basic_socket_stream : public socket_stream_base {
+private:
+	Socket socket_;
+
+public:
+	/**
+	 * Construct a socket stream.
+	 *
+	 * \param service the I/O service
+	 */
+	basic_socket_stream(boost::asio::io_context& service);
+
+	/**
+	 * Get the underlying socket.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() const noexcept -> const Socket&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() noexcept -> Socket&;
+
+	/**
+	 * \copydoc stream::recv
+	 */
+	void recv(recv_handler handler) override;
+
+	/**
+	 * \copydoc stream::send
+	 */
+	void send(const nlohmann::json& json, send_handler handler) override;
+};
+
+template <typename Socket>
+inline basic_socket_stream<Socket>::basic_socket_stream(boost::asio::io_context& ctx)
+	: socket_(ctx)
+{
+}
+
+template <typename Socket>
+inline auto basic_socket_stream<Socket>::get_socket() const noexcept -> const Socket&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline auto basic_socket_stream<Socket>::get_socket() noexcept -> Socket&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline void basic_socket_stream<Socket>::recv(recv_handler handler)
+{
+	socket_stream_base::recv(socket_, handler);
+}
+
+template <typename Socket>
+inline void basic_socket_stream<Socket>::send(const nlohmann::json& json, send_handler handler)
+{
+	socket_stream_base::send(json, socket_, handler);
+}
+
+// }}}
+
+// {{{ ip_stream
+
+/**
+ * \brief Convenient alias for boost::asio::ip::tcp::socket
+ * \ingroup core-streams
+ */
+using ip_stream = basic_socket_stream<boost::asio::ip::tcp::socket>;
+
+// }}}
+
+// {{{ local_stream
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Convenient alias for boost::asio::local::stream_protocol::socket
+ * \ingroup core-streams
+ */
+using local_stream = basic_socket_stream<boost::asio::local::stream_protocol::socket>;
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+// }}}
+
+// {{{ tls_stream
+
+#if defined(IRCCD_HAVE_SSL)
+
+/**
+ * \brief TLS/SSL streams.
+ * \ingroup core-streams
+ * \tparam Socket the Boost.Asio compatible socket.
+ */
+template <typename Socket>
+class tls_stream : public socket_stream_base {
+private:
+	boost::asio::ssl::stream<Socket> socket_;
+	std::shared_ptr<boost::asio::ssl::context> context_;
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param service the I/O service
+	 * \param ctx the shared context
+	 */
+	tls_stream(boost::asio::io_context& service, std::shared_ptr<boost::asio::ssl::context> ctx);
+
+	/**
+	 * Get the SSL socket.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() const noexcept -> const boost::asio::ssl::stream<Socket>&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() noexcept -> boost::asio::ssl::stream<Socket>&;
+
+	/**
+	 * \copydoc stream::recv
+	 */
+	void recv(recv_handler handler) override;
+
+	/**
+	 * \copydoc stream::send
+	 */
+	void send(const nlohmann::json& json, send_handler handler) override;
+};
+
+template <typename Socket>
+inline tls_stream<Socket>::tls_stream(boost::asio::io_context& service, std::shared_ptr<boost::asio::ssl::context> ctx)
+	: socket_(service, *ctx)
+	, context_(std::move(ctx))
+{
+}
+
+template <typename Socket>
+inline auto tls_stream<Socket>::get_socket() const noexcept -> const boost::asio::ssl::stream<Socket>&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline auto tls_stream<Socket>::get_socket() noexcept -> boost::asio::ssl::stream<Socket>&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline void tls_stream<Socket>::recv(recv_handler handler)
+{
+	socket_stream_base::recv(socket_, handler);
+}
+
+template <typename Socket>
+inline void tls_stream<Socket>::send(const nlohmann::json& json, send_handler handler)
+{
+	socket_stream_base::send(json, socket_, handler);
+}
+
+#endif // !IRCCD_HAVE_SSL
+
+// }}}
+
+} // !irccd
+
+#endif // !IRCCD_STREAM_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/string_util.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,454 @@
+/*
+ * string_util.cpp -- string utilities
+ *
+ * Copyright (c) 2013-2018 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 <boost/predef/os.h>
+
+#include "sysconfig.hpp"
+
+#if defined(IRCCD_HAVE_POPEN)
+#	include <array>
+#	include <cerrno>
+#	include <cstring>
+#	include <functional>
+#	include <memory>
+#endif
+
+#include <cassert>
+#include <iomanip>
+#include <regex>
+
+#include "string_util.hpp"
+
+using namespace std::string_literals;
+
+namespace irccd::string_util {
+
+// {{{ subst
+
+namespace {
+
+const std::unordered_map<std::string_view, int> 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  }
+};
+
+const std::unordered_map<std::string_view, char> irc_attributes{
+	{ "bold",       '\x02'  },
+	{ "italic",     '\x09'  },
+	{ "strike",     '\x13'  },
+	{ "reset",      '\x0f'  },
+	{ "underline",  '\x15'  },
+	{ "underline2", '\x1f'  },
+	{ "reverse",    '\x16'  }
+};
+
+const std::unordered_map<std::string_view, unsigned> shell_colors{
+	{ "black",      30  },
+	{ "red",        31  },
+	{ "green",      32  },
+	{ "orange",     33  },
+	{ "blue",       34  },
+	{ "purple",     35  },
+	{ "cyan",       36  },
+	{ "white",      37  },
+	{ "default",    39  },
+};
+
+const std::unordered_map<std::string_view, unsigned> shell_attributes{
+	{ "bold",       1   },
+	{ "dim",        2   },
+	{ "underline",  4   },
+	{ "blink",      5   },
+	{ "reverse",    7   },
+	{ "hidden",     8   }
+};
+
+auto is_reserved(char token) noexcept -> bool
+{
+	return token == '#' || token == '@' || token == '$' || token == '!';
+}
+
+auto subst_date(const std::string& text, const subst& params) -> std::string
+{
+	std::ostringstream oss;
+
+#if defined(IRCCD_HAVE_STD_PUT_TIME)
+	oss << std::put_time(std::localtime(&params.time), text.c_str());
+#else
+	/*
+	 * Quick and dirty hack because old version of GCC does not have this
+	 * function.
+	 */
+	char buffer[4096];
+
+	std::strftime(buffer, sizeof (buffer) - 1, text.c_str(), std::localtime(&params.time));
+
+	oss << buffer;
+#endif
+
+	return oss.str();
+}
+
+auto subst_keywords(const std::string& content, const subst& params) -> std::string
+{
+	const auto value = params.keywords.find(std::string(content));
+
+	if (value != params.keywords.end())
+		return value->second;
+
+	return "";
+}
+
+auto subst_env(const std::string& content) -> std::string
+{
+	const auto value = std::getenv(content.c_str());
+
+	if (value != nullptr)
+		return value;
+
+	return "";
+}
+
+auto subst_irc_attrs(const std::string& content) -> std::string
+{
+	auto list = split(content, ",");
+
+	// @{} means reset.
+	if (list.empty())
+		return std::string(1, irc_attributes.at("reset"));
+
+	std::ostringstream oss;
+
+	// Remove useless spaces.
+	std::transform(list.begin(), list.end(), list.begin(), strip);
+
+	/*
+	 * 0: foreground
+	 * 1: background
+	 * 2-n: attributes
+	 */
+	auto foreground = list[0];
+	if (!foreground.empty() || list.size() >= 2) {
+		// Color sequence.
+		oss << '\x03';
+
+		// Foreground.
+		auto it = irc_colors.find(foreground);
+		if (it != irc_colors.end())
+			oss << it->second;
+
+		// Background.
+		if (list.size() >= 2 && (it = irc_colors.find(list[1])) != irc_colors.end())
+			oss << "," << it->second;
+
+		// Attributes.
+		for (std::size_t i = 2; i < list.size(); ++i) {
+			auto attribute = irc_attributes.find(list[i]);
+
+			if (attribute != irc_attributes.end())
+				oss << attribute->second;
+		}
+	}
+
+	return oss.str();
+}
+
+auto subst_shell_attrs(const std::string& content) -> std::string
+{
+#if !BOOST_OS_WINDOWS
+	auto list = split(content, ",");
+
+	if (list.empty())
+		return "\033[0m";
+	if (list.size() > 3)
+		return "";
+
+	std::vector<std::string> seq;
+
+	/*
+	 * Shell sequence looks like this:
+	 *
+	 * ^[[attributes;foreground;backgroundm
+	 */
+	if (list.size() >= 3) {
+		const auto it = shell_attributes.find(list[2]);
+
+		if (it != shell_attributes.end())
+			seq.push_back(std::to_string(it->second));
+		else
+			return "";
+	}
+	if (list.size() >= 1) {
+		const auto it = shell_colors.find(list[0]);
+
+		if (it != shell_colors.end())
+			seq.push_back(std::to_string(it->second));
+		else
+			return "";
+	}
+	if (list.size() >= 2) {
+		const auto it = shell_colors.find(list[1]);
+
+		if (it != shell_colors.end())
+			seq.push_back(std::to_string(it->second + 10));
+		else
+			return "";
+	}
+
+	std::ostringstream oss;
+
+	oss << "\033[";
+	oss << string_util::join(seq, ';');
+	oss << "m";
+
+	return oss.str();
+#else
+	return "";
+#endif
+}
+
+auto subst_shell(const std::string& command) -> std::string
+{
+#if defined(IRCCD_HAVE_POPEN)
+	std::unique_ptr<FILE, std::function<int (FILE*)>> fp(popen(command.c_str(), "r"), pclose);
+
+	if (fp == nullptr)
+		throw std::runtime_error(std::strerror(errno));
+
+	std::string result;
+	std::array<char, 128> buffer;
+	std::size_t n;
+
+	while ((n = std::fread(buffer.data(), 1, 128, fp.get())) > 0)
+		result.append(buffer.data(), n);
+	if (std::ferror(fp.get()))
+		throw std::runtime_error(std::strerror(errno));
+
+	// Erase final '\n'.
+	auto it = result.find('\n');
+	if (it != std::string::npos)
+		result.erase(it);
+
+	return result;
+#else
+	throw std::runtime_error("shell template not available");
+#endif
+}
+
+auto substitute(std::string::const_iterator& it,
+                std::string::const_iterator& end,
+                char token,
+                const subst& params) -> std::string
+{
+	assert(is_reserved(token));
+
+	std::string content, value;
+
+	if (it == end)
+		return "";
+
+	while (it != end && *it != '}')
+		content += *it++;
+
+	if (it == end || *it != '}')
+		throw std::invalid_argument("unclosed "s + token + " construct"s);
+
+	it++;
+
+	// Create default original value if flag is disabled.
+	value = std::string(1, token) + "{"s + content + "}"s;
+
+	switch (token) {
+	case '#':
+		if ((params.flags & subst_flags::keywords) == subst_flags::keywords)
+			value = subst_keywords(content, params);
+		break;
+	case '$':
+		if ((params.flags & subst_flags::env) == subst_flags::env)
+			value = subst_env(content);
+		break;
+	case '@':
+		if ((params.flags & subst_flags::irc_attrs) == subst_flags::irc_attrs)
+			value = subst_irc_attrs(content);
+		else if ((params.flags & subst_flags::shell_attrs) == subst_flags::shell_attrs)
+			value = subst_shell_attrs(content);
+		break;
+	case '!':
+		if ((params.flags & subst_flags::shell) == subst_flags::shell)
+			value = subst_shell(content);
+		break;
+	default:
+		break;
+	}
+
+	return value;
+}
+
+} // !namespace
+
+auto format(std::string text, const subst& params) -> std::string
+{
+	/*
+	 * Change the date format before anything else to avoid interpolation with
+	 * keywords and user input.
+	 */
+	if ((params.flags & subst_flags::date) == subst_flags::date)
+		text = subst_date(text, params);
+
+	std::ostringstream oss;
+
+	for (auto it = text.cbegin(), end = text.cend(); it != end; ) {
+		auto token = *it;
+
+		// Is the current character a reserved token or not?
+		if (!is_reserved(token)) {
+			oss << *it++;
+			continue;
+		}
+
+		// The token was at the end, just write it and return now.
+		if (++it == end) {
+			oss << token;
+			continue;
+		}
+
+		// The token is declaring a template variable, substitute it.
+		if (*it == '{') {
+			oss << substitute(++it, end, token, params);
+			continue;
+		}
+
+		/*
+		 * If the next token is different from the previous one, just let the
+		 * next iteration parse the string because we can have the following
+		 * constructs.
+		 *
+		 * "@#{var}" -> "@value"
+		 */
+		if (*it != token) {
+			oss << token;
+			continue;
+		}
+
+		/*
+		 * Write the token only if it's not a variable because at this step we
+		 * may have the following constructs.
+		 *
+		 * "##" -> "##"
+		 * "##hello" -> "##hello"
+		 * "##{hello}" -> "#{hello}"
+		 */
+		if (++it == end)
+			oss << token << token;
+		else if (*it == '{')
+			oss << token;
+	}
+
+	return oss.str();
+}
+
+// }}}
+
+// {{{ strip
+
+auto strip(std::string str) noexcept -> std::string
+{
+	const auto test = [] (auto c) noexcept { return !std::isspace(c); };
+
+	str.erase(str.begin(), std::find_if(str.begin(), str.end(), test));
+	str.erase(std::find_if(str.rbegin(), str.rend(), test).base(), str.end());
+
+	return str;
+}
+
+// }}}
+
+// {{{ split
+
+auto split(std::string_view list, const std::string& delimiters, int max) -> std::vector<std::string>
+{
+	std::vector<std::string> result;
+	std::size_t next = -1, current;
+	int count = 1;
+	bool finished = false;
+
+	if (list.empty())
+		return result;
+
+	do {
+		std::string val;
+
+		current = next + 1;
+		next = list.find_first_of(delimiters, current);
+
+		// split max, get until the end.
+		if (max >= 0 && count++ >= max) {
+			val = list.substr(current, std::string::npos);
+			finished = true;
+		} else {
+			val = list.substr(current, next - current);
+			finished = next == std::string::npos;
+		}
+
+		result.push_back(val);
+	} while (!finished);
+
+	return result;
+}
+
+// }}}
+
+// {{{ is_identifier
+
+auto is_identifier(std::string_view name) noexcept -> bool
+{
+	static const std::regex regex("[A-Za-z0-9-_]+");
+
+	return std::regex_match(std::string(name), regex);
+}
+
+// }}}
+
+// {{{ is_boolean
+
+auto is_boolean(std::string value) noexcept -> bool
+{
+	std::transform(value.begin(), value.end(), value.begin(), [] (auto c) noexcept {
+		return toupper(c);
+	});
+
+	return value == "1" || value == "YES" || value == "TRUE" || value == "ON";
+}
+
+// }}}
+
+} // !util::string_util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/string_util.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,402 @@
+/*
+ * string_util.hpp -- string utilities
+ *
+ * Copyright (c) 2013-2018 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_STRING_UTIL_HPP
+#define IRCCD_STRING_UTIL_HPP
+
+/**
+ * \file string_util.hpp
+ * \brief String utilities.
+ */
+
+#include "sysconfig.hpp"
+
+#include <ctime>
+#include <initializer_list>
+#include <limits>
+#include <optional>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <unordered_map>
+#include <vector>
+
+/**
+ * \brief String utilities.
+ */
+namespace irccd::string_util {
+
+// {{{ subst
+
+/**
+ * \brief Disable or enable some features.
+ */
+enum class subst_flags : unsigned {
+	date            = (1 << 0),      //!< date templates
+	keywords        = (1 << 1),      //!< keywords
+	env             = (1 << 2),      //!< environment variables
+	shell           = (1 << 3),      //!< command line command
+	irc_attrs       = (1 << 4),      //!< IRC escape codes
+	shell_attrs     = (1 << 5)       //!< shell attributes
+};
+
+/**
+ * \cond IRCCD_ENUM_HIDDEN_SYMBOLS
+ */
+
+/**
+ * Apply bitwise XOR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator^(subst_flags v1, subst_flags v2) noexcept -> subst_flags
+{
+	return static_cast<subst_flags>(static_cast<unsigned>(v1) ^ static_cast<unsigned>(v2));
+}
+
+/**
+ * Apply bitwise AND.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator&(subst_flags v1, subst_flags v2) noexcept -> subst_flags
+{
+	return static_cast<subst_flags>(static_cast<unsigned>(v1) & static_cast<unsigned>(v2));
+}
+
+/**
+ * Apply bitwise OR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator|(subst_flags v1, subst_flags v2) noexcept -> subst_flags
+{
+	return static_cast<subst_flags>(static_cast<unsigned>(v1) | static_cast<unsigned>(v2));
+}
+
+/**
+ * Apply bitwise NOT.
+ *
+ * \param v the value
+ * \return the complement
+ */
+inline auto operator~(subst_flags v) noexcept -> subst_flags
+{
+	return static_cast<subst_flags>(~static_cast<unsigned>(v));
+}
+
+/**
+ * Assign bitwise OR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator|=(subst_flags& v1, subst_flags v2) noexcept -> subst_flags&
+{
+	return v1 = v1 | v2;
+}
+
+/**
+ * Assign bitwise AND.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator&=(subst_flags& v1, subst_flags v2) noexcept -> subst_flags&
+{
+	return v1 = v1 & v2;
+}
+
+/**
+ * Assign bitwise XOR.
+ *
+ * \param v1 the first value
+ * \param v2 the second value
+ * \return the new value
+ */
+inline auto operator^=(subst_flags& v1, subst_flags v2) noexcept -> subst_flags&
+{
+	return v1 = v1 ^ v2;
+}
+
+/**
+ * \endcond
+ */
+
+/**
+ * \brief Used for format() function.
+ */
+class subst {
+public:
+	/**
+	 * Flags for selecting templates.
+	 */
+	subst_flags flags{
+		subst_flags::date |
+		subst_flags::keywords |
+		subst_flags::env |
+		subst_flags::irc_attrs
+	};
+
+	/**
+	 * Fill that field if you want a date.
+	 */
+	std::time_t time{std::time(nullptr)};
+
+	/**
+	 * Fill that map if you want to replace keywords.
+	 */
+	std::unordered_map<std::string, std::string> keywords;
+};
+
+/**
+ * Format a string and update all templates.
+ *
+ * ## Syntax
+ *
+ * The syntax is <strong>?{}</strong> where <strong>?</strong> is replaced by
+ * one of the token defined below. Braces are mandatory and cannot be ommited.
+ *
+ * To write a literal template construct, prepend the token twice.
+ *
+ * ## Availables templates
+ *
+ * The following templates are available:
+ *
+ * - <strong>\#{name}</strong>: name will be substituted from the keywords in
+ *   params,
+ * - <strong>\${name}</strong>: name will be substituted from the environment
+ *   variable,
+ * - <strong>\@{attributes}</strong>: the attributes will be substituted to IRC
+ *   or shell colors (see below),
+ * - <strong>%</strong>, any format accepted by strftime(3).
+ *
+ * ## Attributes
+ *
+ * The attribute format is composed of three parts, foreground, background and
+ * modifiers, each separated by a comma.
+ *
+ * **Note:** you cannot omit parameters, to specify the background, you must
+ * specify the foreground.
+ *
+ * ## Examples
+ *
+ * ### Valid constructs
+ *
+ * - <strong>\#{target}, welcome</strong>: if target is set to "irccd",
+ *   becomes "irccd, welcome",
+ * - <strong>\@{red}\#{target}</strong>: if target is specified, it is written
+ *   in red,
+ *
+ * ### Invalid or literals constructs
+ *
+ * - <strong>\#\#{target}</strong>: will output "\#{target}",
+ * - <strong>\#\#</strong>: will output "\#\#",
+ * - <strong>\#target</strong>: will output "\#target",
+ * - <strong>\#{target</strong>: will throw std::invalid_argument.
+ *
+ * ### Colors & attributes
+ *
+ * - <strong>\@{red,blue}</strong>: will write text red on blue background,
+ * - <strong>\@{default,yellow}</strong>: will write default color text on
+ *   yellow background,
+ * - <strong>\@{white,black,bold,underline}</strong>: will write white text on
+ *   black in both bold and underline.
+ *
+ * \param text the text to format
+ * \param params the additional options
+ * \return the modified text
+ */
+auto format(std::string text, const subst& params = {}) -> std::string;
+
+// }}}
+
+// {{{ strip
+
+/**
+ * Remove leading and trailing spaces.
+ *
+ * \param str the string
+ * \return the removed white spaces
+ */
+auto strip(std::string str) noexcept -> std::string;
+
+// }}}
+
+// {{{ split
+
+/**
+ * Split a string by delimiters.
+ *
+ * \param list the string to split
+ * \param delimiters a list of delimiters
+ * \param max max number of split
+ * \return a list of string splitted
+ */
+auto split(std::string_view list, const std::string& delimiters, int max = -1) -> std::vector<std::string>;
+
+// }}}
+
+// {{{ join
+
+/**
+ * Join values by a separator and return a string.
+ *
+ * \param first the first iterator
+ * \param last the last iterator
+ * \param delim the optional delimiter
+ * \return the string
+ */
+template <typename InputIt, typename DelimType = char>
+auto join(InputIt first, InputIt last, DelimType delim = ':') -> std::string
+{
+	std::ostringstream oss;
+
+	if (first != last) {
+		oss << *first;
+
+		while (++first != last)
+			oss << delim << *first;
+	}
+
+	return oss.str();
+}
+
+/**
+ * Overloaded function that takes a container.
+ *
+ * \param c the container
+ * \param delim the optional delimiter
+ * \return the string
+ */
+template <typename Container, typename DelimType = char>
+auto join(const Container& c, DelimType delim = ':') -> std::string
+{
+	return join(c.begin(), c.end(), delim);
+}
+
+/**
+ * Convenient overload.
+ *
+ * \param list the initializer list
+ * \param delim the delimiter
+ * \return the string
+ */
+template <typename T, typename DelimType = char>
+auto join(std::initializer_list<T> list, DelimType delim = ':') -> std::string
+{
+	return join(list.begin(), list.end(), delim);
+}
+
+// }}}
+
+// {{{ is_identifier
+
+/**
+ * Check if a string is a valid irccd identifier.
+ *
+ * \param name the identifier name
+ * \return true if is valid
+ */
+auto is_identifier(std::string_view name) noexcept -> bool;
+
+// }}}
+
+// {{{ is_boolean
+
+/**
+ * Check if the value is a boolean, 1, yes and true are accepted.
+ *
+ * \param value the value
+ * \return true if is boolean
+ * \note this function is case-insensitive
+ */
+auto is_boolean(std::string value) noexcept -> bool;
+
+// }}}
+
+// {{{ to_int
+
+/**
+ * Convert the given string into a signed integer.
+ *
+ * \param str the string to convert
+ * \param min the minimum value allowed
+ * \param max the maximum value allowed
+ * \return the value or boost::none if not convertible
+ */
+template <typename T = int>
+auto to_int(const std::string& str,
+            T min = std::numeric_limits<T>::min(),
+            T max = std::numeric_limits<T>::max()) noexcept -> std::optional<T>
+{
+	static_assert(std::is_signed<T>::value, "must be signed");
+
+	char* end;
+	auto v = std::strtoll(str.c_str(), &end, 10);
+
+	if (*end != '\0' || v < min || v > max)
+		return std::nullopt;
+
+	return static_cast<T>(v);
+}
+
+// }}}
+
+// {{{ to_uint
+
+/**
+ * Convert the given string into a unsigned integer.
+ *
+ * \note invalid numbers are valid as well
+ * \param str the string to convert
+ * \param min the minimum value allowed
+ * \param max the maximum value allowed
+ * \return the value or boost::none if not convertible
+ */
+template <typename T = unsigned>
+auto to_uint(const std::string& str,
+             T min = std::numeric_limits<T>::min(),
+             T max = std::numeric_limits<T>::max()) noexcept -> std::optional<T>
+{
+	static_assert(std::is_unsigned<T>::value, "must be unsigned");
+
+	char* end;
+	auto v = std::strtoull(str.c_str(), &end, 10);
+
+	if (*end != '\0' || v < min || v > max)
+		return std::nullopt;
+
+	return static_cast<T>(v);
+}
+
+// }}}
+
+} // !irccd::string_util
+
+#endif // !IRCCD_STRING_UTIL_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/system.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,491 @@
+/*
+ * system.cpp -- platform dependent functions for system inspection
+ *
+ * Copyright (c) 2013-2018 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 <cassert>
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+#include <stdexcept>
+#include <string>
+
+#include <boost/dll.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/predef/os.h>
+
+#include "sysconfig.hpp"
+
+#if BOOST_OS_WINDOWS
+#	include <sys/timeb.h>
+#	include <shlobj.h>
+#else
+#	include <sys/utsname.h>
+#	include <sys/types.h>
+#	include <sys/param.h>
+#	include <sys/time.h>
+#	include <unistd.h>
+#endif
+
+#if BOOST_OS_LINUX
+#	include <sys/sysinfo.h>
+#endif
+
+#if BOOST_OS_MACOS
+#	include <sys/sysctl.h>
+#	include <libproc.h>
+#endif
+
+#include "system.hpp"
+#include "string_util.hpp"
+#include "xdg.hpp"
+
+namespace irccd::sys {
+
+namespace {
+
+// {{{ base_directory
+
+/*
+ * base_directory
+ * ------------------------------------------------------------------
+ *
+ * Get the base program directory.
+ *
+ * If irccd has been compiled with relative paths, the base directory is
+ * evaluated by climbing the `bindir' directory from the executable path.
+ *
+ * Otherwise, use the installation prefix.
+ */
+auto base_directory() -> boost::filesystem::path
+{
+	static const boost::filesystem::path bindir(IRCCD_INSTALL_BINDIR);
+	static const boost::filesystem::path prefix(IRCCD_INSTALL_PREFIX);
+
+	boost::filesystem::path path(".");
+
+	if (bindir.is_relative()) {
+		try {
+			path = boost::dll::program_location();
+			path = path.parent_path();
+		} catch (...) {
+			path = ".";
+		}
+
+		// Compute relative base directory.
+		for (auto len = std::distance(bindir.begin(), bindir.end()); len > 0; len--)
+			path = path.parent_path();
+		if (path.empty())
+			path = ".";
+	} else
+		path = prefix;
+
+	return path;
+}
+
+// }}}
+
+// {{{ system_directory
+
+/*
+ * system_directory
+ * ------------------------------------------------------------------
+ *
+ * Compute the system directory path for the given component.
+ *
+ * Referenced by:
+ *
+ * - cachedir,
+ * - datadir,
+ * - sysconfigdir,
+ * - plugindir.
+ */
+auto system_directory(const std::string& component) -> boost::filesystem::path
+{
+	boost::filesystem::path path(component);
+
+	if (path.is_relative())
+		path = base_directory() / component;
+
+	return path.string();
+}
+
+// }}}
+
+// {{{ user_config_directory
+
+/*
+ * user_config_directory
+ * ------------------------------------------------------------------
+ *
+ * Get user configuration directory.
+ *
+ * Referenced by:
+ *
+ * - config_filenames.
+ *
+ * Requires:
+ *
+ * - Windows:
+ *   - <shlobj.h>
+ */
+auto user_config_directory() -> boost::filesystem::path
+{
+	boost::filesystem::path path;
+
+#if BOOST_OS_WINDOWS
+	char folder[MAX_PATH] = {0};
+
+	if (SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, folder) == S_OK) {
+		path /= folder;
+		path /= "\\irccd\\config";
+	} else
+		path = ".";
+#else
+	try {
+		path = xdg().get_config_home();
+	} catch (...) {
+		path = sys::env("HOME");
+		path /= ".config";
+	}
+
+	path /= "irccd";
+#endif
+
+	return path;
+}
+
+// }}}
+
+// {{{ user_plugin_directory
+
+/*
+ * user_plugin_directory
+ * ------------------------------------------------------------------
+ *
+ * Referenced by:
+ *
+ * - plugin_filenames.
+ *
+ * Requires:
+ *
+ * - Windows:
+ *   - <shlobj.h>
+ *
+ * Like add user_config_directory but for plugins.
+ */
+auto user_plugin_directory() -> boost::filesystem::path
+{
+	boost::filesystem::path path;
+
+#if BOOST_OS_WINDOWS
+	char folder[MAX_PATH] = {0};
+
+	if (SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, folder) == S_OK) {
+		path /= folder;
+		path /= "\\irccd\\share";
+	}
+#else
+	try {
+		path = xdg().get_data_home();
+	} catch (...) {
+		path = sys::env("HOME");
+		path /= ".local/share";
+	}
+
+	path /= "irccd";
+#endif
+
+	return path / "plugins";
+}
+
+// }}}
+
+} // !namespace
+
+// {{{ set_program_name
+
+void set_program_name(std::string name) noexcept
+{
+#if defined(IRCCD_HAVE_SETPROGNAME)
+	static std::string save = name;
+
+	setprogname(save.c_str());
+#else
+	(void)name;
+#endif
+}
+
+// }}}
+
+// {{{ name
+
+auto name() -> std::string
+{
+#if BOOST_OS_LINUX
+	return "Linux";
+#elif BOOST_OS_WINDOWS
+	return "Windows";
+#elif BOOST_OS_BSD_FREE
+	return "FreeBSD";
+#elif BOOST_OS_BSD_DRAGONFLY
+	return "DragonFlyBSD";
+#elif BOOST_OS_BSD_OPEN
+	return "OpenBSD";
+#elif BOOST_OS_BSD_NET
+	return "NetBSD";
+#elif BOOST_OS_MACOS
+	return "macOS";
+#elif BOOST_OS_ANDROID
+	return "Android";
+#elif BOOST_OS_AIX
+	return "Aix";
+#elif BOOST_OS_HAIKU
+	return "Haiku";
+#elif BOOST_OS_IOS
+	return "iOS";
+#elif BOOST_OS_SOLARIS
+	return "Solaris";
+#else
+	return "Unknown";
+#endif
+}
+
+// }}}
+
+// {{{ version
+
+/*
+ * Requires:
+ *
+ * - Windows:
+ *   - <windows.h>
+ * - Others:
+ *   - <sys/utsname.h>
+ */
+auto version() -> std::string
+{
+#if BOOST_OS_WINDOWS
+	const auto version = GetVersion();
+	const auto major = (DWORD)(LOBYTE(LOWORD(version)));
+	const auto minor = (DWORD)(HIBYTE(LOWORD(version)));
+
+	return std::to_string(major) + "." + std::to_string(minor);
+#else
+	struct utsname uts;
+
+	if (::uname(&uts) < 0)
+		throw std::runtime_error(std::strerror(errno));
+
+	return std::string(uts.release);
+#endif
+}
+
+// }}}
+
+// {{{ uptime
+
+/*
+ * Requires:
+ *
+ * - Windows:
+ *   - <windows.h>
+ * - Linux:
+ *   - <sys/sysinfo.h>
+ * - Mac:
+ *   - <sys/types.h>
+ *   - <sys/sysctl.h>
+ * - Others:
+ *   - <ctime>
+ */
+auto uptime() -> std::uint64_t
+{
+#if BOOST_OS_WINDOWS
+	return ::GetTickCount64() / 1000;
+#elif BOOST_OS_LINUX
+	struct sysinfo info;
+
+	if (sysinfo(&info) < 0)
+		throw std::runtime_error(std::strerror(errno));
+
+	return info.uptime;
+#elif BOOST_OS_MACOS
+	struct timeval boottime;
+	size_t length = sizeof (boottime);
+	int mib[2] = { CTL_KERN, KERN_BOOTTIME };
+
+	if (sysctl(mib, 2, &boottime, &length, nullptr, 0) < 0)
+		throw std::runtime_error(std::strerror(errno));
+
+	time_t bsec = boottime.tv_sec, csec = time(nullptr);
+
+	return difftime(csec, bsec);
+#else
+	struct timespec ts;
+
+	if (clock_gettime(CLOCK_UPTIME, &ts) < 0)
+		throw std::runtime_error(std::strerror(errno));
+
+	return ts.tv_sec;
+#endif
+}
+
+// }}}
+
+// {{{ ticks
+
+/*
+ * Requires:
+ *
+ * - Windows:
+ *   - <sys/timeb.h>
+ * - Others:
+ *   - <sys/times.h>
+ */
+auto ticks() -> std::uint64_t
+{
+#if BOOST_OS_WINDOWS
+	_timeb tp;
+
+	_ftime(&tp);
+
+	return tp.time * 1000LL + tp.millitm;
+#else
+	struct timeval tp;
+
+	gettimeofday(&tp, NULL);
+
+	return tp.tv_sec * 1000LL + tp.tv_usec / 1000;
+#endif
+}
+
+// }}}
+
+// {{{ home
+
+/*
+ * Requires:
+ *
+ * - Windows:
+ *   - <shlobj.h>
+ */
+auto home() -> std::string
+{
+#if BOOST_OS_WINDOWS
+	char path[MAX_PATH];
+
+	if (SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, path) != S_OK)
+		return "";
+
+	return std::string(path);
+#else
+	return env("HOME");
+#endif
+}
+
+// }}}
+
+// {{{ env
+
+/*
+ * Requires:
+ *
+ * - <cstdlib>
+ */
+auto env(const std::string& var) -> std::string
+{
+	const auto value = std::getenv(var.c_str());
+
+	if (value == nullptr)
+		return "";
+
+	return value;
+}
+
+// }}}
+
+// {{{ cachedir
+
+auto cachedir() -> boost::filesystem::path
+{
+	return system_directory(IRCCD_INSTALL_LOCALSTATEDIR) / "cache/irccd";
+}
+
+// }}}
+
+// {{{ datadir
+
+auto datadir() -> boost::filesystem::path
+{
+	return system_directory(IRCCD_INSTALL_DATADIR);
+}
+
+// }}}
+
+// {{{ sysconfdir
+
+auto sysconfdir() -> boost::filesystem::path
+{
+	return system_directory(IRCCD_INSTALL_SYSCONFDIR) / "irccd";
+}
+
+// }}}
+
+// {{{ plugindir
+
+auto plugindir() -> boost::filesystem::path
+{
+	return system_directory(IRCCD_INSTALL_LIBDIR) / "irccd";
+}
+
+// }}}
+
+// {{{ config_filenames
+
+auto config_filenames(std::string_view file) -> std::vector<std::string>
+{
+	// TODO: remove this once we can use std::filesystem.
+	const std::string filename(file);
+
+	return {
+		(user_config_directory() / filename).string(),
+		(sysconfdir() / filename).string()
+	};
+}
+
+// }}}
+
+// {{{ plugin_filenames
+
+auto plugin_filenames(const std::string& name,
+                      const std::vector<std::string>& extensions) -> std::vector<std::string>
+{
+	assert(!extensions.empty());
+
+	std::vector<std::string> result;
+
+	for (const auto& ext : extensions)
+		result.push_back((user_plugin_directory() / (name + ext)).string());
+	for (const auto& ext : extensions)
+		result.push_back((plugindir() / (name + ext)).string());
+
+	return result;
+}
+
+// }}}
+
+} // !irccd::sys
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/system.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,165 @@
+/*
+ * system.hpp -- platform dependent functions for system inspection
+ *
+ * Copyright (c) 2013-2018 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_SYSTEM_HPP
+#define IRCCD_SYSTEM_HPP
+
+/**
+ * \file system.hpp
+ * \brief System dependant functions
+ */
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <boost/filesystem.hpp>
+
+#include "sysconfig.hpp"
+
+/**
+ * \brief Namespace for system functions.
+ */
+namespace irccd::sys {
+
+/**
+ * Set the program name, needed for some functions or some systems.
+ *
+ * \param name the program name
+ */
+void set_program_name(std::string name) noexcept;
+
+/**
+ * Get the system name.
+ *
+ * \return the name
+ */
+auto name() -> std::string;
+
+/**
+ * Get the system version.
+ *
+ * \return the version
+ */
+auto version() -> std::string;
+
+/**
+ * Get the number of seconds elapsed since the boottime.
+ *
+ * \return the number of seconds
+ */
+auto uptime() -> std::uint64_t;
+
+/**
+ * Get the milliseconds elapsed since the application
+ * startup.
+ *
+ * \return the milliseconds
+ */
+auto ticks() -> std::uint64_t;
+
+/**
+ * Get an environment variable.
+ *
+ * \param var the environment variable
+ * \return the value or empty string
+ */
+auto env(const std::string& var) -> std::string;
+
+/**
+ * Get home directory usually /home/foo
+ *
+ * \return the home directory
+ */
+auto home() -> std::string;
+
+/**
+ * Get the cache directory as specified as compile time option
+ * IRCCD_INSTALL_LOCALSTATEDIR, if the value is absolute, it is returned as-is.
+ *
+ * If the component is relative, it is evaluated using the binary executable
+ * path.
+ *
+ * \return the evaluated cache directory.
+ * \see datadir
+ * \see configdir
+ */
+auto cachedir() -> boost::filesystem::path;
+
+/**
+ * Like cachedir but for IRCCD_INSTALL_DATADIR.
+ *
+ * \return the evaluated data directory.
+ * \see cachedir
+ * \see datadir
+ */
+auto datadir() -> boost::filesystem::path;
+
+/**
+ * Like cachedir but for IRCCD_INSTALL_SYSCONFDIR.
+ *
+ * \return the evaluated config directory.
+ * \see cachedir
+ * \see datadir
+ * \note use config_filenames for irccd.conf, irccdctl.conf files
+ */
+auto sysconfdir() -> boost::filesystem::path;
+
+/**
+ * Like cachedir but for IRCCD_INSTALL_LIBDIR.
+ *
+ * \return the evaluated system plugin directory.
+ * \see cachedir
+ * \see datadir
+ */
+auto plugindir() -> boost::filesystem::path;
+
+/**
+ * Get user account login or empty if not available.
+ *
+ * \return the user account name
+ */
+auto username() -> std::string;
+
+/**
+ * Construct a list of paths to read configuration files from.
+ *
+ * This function does not test the presence of the files as a condition race
+ * may occur.
+ *
+ * The caller is responsible of opening files for each path.
+ *
+ * \param file the filename to append for convenience
+ * \return the list of paths to check in order
+ */
+auto config_filenames(std::string_view file) -> std::vector<std::string>;
+
+/**
+ * Construct a list of paths for reading plugins.
+ *
+ * \param name the plugin id (without extension)
+ * \param extensions the list of extensions supported
+ * \return the list of filenames to check
+ */
+auto plugin_filenames(const std::string& name,
+                      const std::vector<std::string>& extensions) -> std::vector<std::string>;
+
+} // !irccd::sys
+
+#endif // !IRCCD_SYSTEM_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccd/irccd/xdg.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,192 @@
+/*
+ * xdg.hpp -- XDG directory specifications
+ *
+ * Copyright (c) 2013-2018 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_XDG_HPP
+#define IRCCD_XDG_HPP
+
+/**
+ * \file xdg.hpp
+ * \brief XDG directory specifications.
+ * \author David Demelier <markand@malikana.fr>
+ */
+
+#include <cstdlib>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace irccd {
+
+/**
+ * \brief XDG directory specifications.
+ *
+ * Read and get XDG directories.
+ *
+ * This file should compiles on Windows to facilitate portability but its
+ * functions must not be used.
+ */
+class xdg {
+private:
+	std::string config_home_;
+	std::string data_home_;
+	std::string cache_home_;
+	std::string runtime_dir_;
+	std::vector<std::string> config_dirs_;
+	std::vector<std::string> data_dirs_;
+
+	auto is_absolute(const std::string& path) const noexcept -> bool
+	{
+		return path.length() > 0 && path[0] == '/';
+	}
+
+	auto split(const std::string& arg) const -> std::vector<std::string>
+	{
+		std::stringstream iss(arg);
+		std::string item;
+		std::vector<std::string> elems;
+
+		while (std::getline(iss, item, ':')) {
+			if (is_absolute(item))
+				elems.push_back(item);
+		}
+
+		return elems;
+	}
+
+	auto env_or_home(const std::string& var, const std::string& repl) const -> std::string
+	{
+		auto value = std::getenv(var.c_str());
+
+		if (value == nullptr || !is_absolute(value)) {
+			auto home = std::getenv("HOME");
+
+			if (home == nullptr)
+				throw std::runtime_error("could not get home directory");
+
+			return std::string(home) + "/" + repl;
+		}
+
+		return value;
+	}
+
+	auto list_or_defaults(const std::string& var,
+	                      const std::vector<std::string>& list) const -> std::vector<std::string>
+	{
+		const auto value = std::getenv(var.c_str());
+
+		if (!value)
+			return list;
+
+		// No valid item at all? Use defaults.
+		if (const auto result = split(value); !result.empty())
+			return result;
+
+		return list;
+	}
+
+public:
+	/**
+	 * Open an xdg instance and load directories.
+	 *
+	 * \throw std::runtime_error on failures
+	 */
+	xdg()
+		: config_home_(env_or_home("XDG_CONFIG_HOME", ".config"))
+		, data_home_(env_or_home("XDG_DATA_HOME", ".local/share"))
+		, cache_home_(env_or_home("XDG_CACHE_HOME", ".cache"))
+		, config_dirs_(list_or_defaults("XDG_CONFIG_DIRS", { "/etc/xdg" }))
+		, data_dirs_(list_or_defaults("XDG_DATA_DIRS", { "/usr/local/share", "/usr/share" }))
+	{
+
+		/*
+		 * Runtime directory is a special case and does not have a replacement,
+		 * the application should manage this by itself.
+		 */
+		if (const auto runtime = std::getenv("XDG_RUNTIME_DIR"); runtime && is_absolute(runtime))
+			runtime_dir_ = runtime;
+	}
+
+	/**
+	 * Get the config directory. ${XDG_CONFIG_HOME} or ${HOME}/.config
+	 *
+	 * \return the config directory
+	 */
+	auto get_config_home() const noexcept -> const std::string&
+	{
+		return config_home_;
+	}
+
+	/**
+	 * Get the data directory. ${XDG_DATA_HOME} or ${HOME}/.local/share
+	 *
+	 * \return the data directory
+	 */
+	auto get_data_home() const noexcept -> const std::string&
+	{
+		return data_home_;
+	}
+
+	/**
+	 * Get the cache directory. ${XDG_CACHE_HOME} or ${HOME}/.cache
+	 *
+	 * \return the cache directory
+	 */
+	auto get_cache_home() const noexcept -> const std::string&
+	{
+		return cache_home_;
+	}
+
+	/**
+	 * Get the runtime directory.
+	 *
+	 * There is no replacement for XDG_RUNTIME_DIR, if it is not set, an empty
+	 * value is returned and the user is responsible of using something else.
+	 *
+	 * \return the runtime directory
+	 */
+	auto get_runtime_dir() const noexcept -> const std::string&
+	{
+		return runtime_dir_;
+	}
+
+	/**
+	 * Get the standard config directories. ${XDG_CONFIG_DIRS} or { "/etc/xdg" }
+	 *
+	 * \return the list of config directories
+	 */
+	auto get_config_dirs() const noexcept -> const std::vector<std::string>&
+	{
+		return config_dirs_;
+	}
+
+	/**
+	 * Get the data directories. ${XDG_DATA_DIRS} or { "/usr/local/share",
+	 * "/usr/share" }
+	 *
+	 * \return the list of data directories
+	 */
+	auto get_data_dirs() const noexcept -> const std::vector<std::string>&
+	{
+		return data_dirs_;
+	}
+};
+
+} // !irccd
+
+#endif // !IRCCD_XDG_HPP
--- a/plugins/links/links.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/plugins/links/links.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -16,7 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/string_util.hpp>
@@ -30,9 +30,12 @@
 using std::string_view;
 using std::unique_ptr;
 
-namespace irccd {
+using irccd::string_util::to_uint;
 
-using string_util::to_uint;
+using irccd::daemon::bot;
+using irccd::daemon::message_event;
+
+namespace irccd {
 
 auto links_plugin::get_name() const noexcept -> string_view
 {
@@ -72,9 +75,9 @@
 		format_info = it->second;
 }
 
-void links_plugin::handle_message(irccd& irccd, const message_event& ev)
+void links_plugin::handle_message(bot& bot, const message_event& ev)
 {
-	requester::run(irccd.get_service(), ev.server, ev.origin, ev.channel, ev.message);
+	requester::run(bot.get_service(), ev.server, ev.origin, ev.channel, ev.message);
 }
 
 auto links_plugin::abi() -> version
--- a/plugins/links/links.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/plugins/links/links.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -31,7 +31,7 @@
 /**
  * \brief Links plugin.
  */
-class links_plugin : public plugin {
+class links_plugin : public daemon::plugin {
 public:
 	// options.
 	static inline unsigned conf_timeout{30U};
@@ -83,7 +83,7 @@
 	/**
 	 * \copydoc plugin::handle_message
 	 */
-	void handle_message(irccd&, const message_event&) override;
+	void handle_message(daemon::bot&, const daemon::message_event&) override;
 
 	/**
 	 * Export ABI.
@@ -98,7 +98,7 @@
 	 * \param id the plugin id
 	 * \return the compiled version
 	 */
-	static auto init(std::string) -> std::unique_ptr<plugin>;
+	static auto init(std::string) -> std::unique_ptr<daemon::plugin>;
 };
 
 } // !irccd
--- a/plugins/links/requester.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/plugins/links/requester.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -59,17 +59,20 @@
 
 using boost::posix_time::seconds;
 
-namespace irccd {
+using irccd::string_util::subst;
+using irccd::string_util::format;
 
-using string_util::subst;
-using string_util::format;
+using irccd::daemon::irc::user;
+using irccd::daemon::server;
+
+namespace irccd {
 
 void requester::notify(const string& title)
 {
 	subst subst;
 
 	subst.keywords.emplace("channel", channel_);
-	subst.keywords.emplace("nickname", irc::user::parse(origin_).nick);
+	subst.keywords.emplace("nickname", user::parse(origin_).nick);
 	subst.keywords.emplace("origin", origin_);
 	subst.keywords.emplace("server", server_->get_id());
 	subst.keywords.emplace("title", title);
--- a/plugins/links/requester.hpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/plugins/links/requester.hpp	Fri Nov 16 12:25:00 2018 +0100
@@ -43,8 +43,12 @@
 
 namespace irccd {
 
+namespace daemon {
+
 class server;
 
+} // !daemon
+
 /**
  * \brief Convenient HTTP get requester.
  */
@@ -59,7 +63,7 @@
 	> socket_;
 
 	std::size_t level_{0U};
-	std::shared_ptr<server> server_;
+	std::shared_ptr<daemon::server> server_;
 	std::string channel_;
 	std::string origin_;
 
@@ -92,7 +96,7 @@
 	void start();
 
 	requester(boost::asio::io_context&,
-	          std::shared_ptr<server>,
+	          std::shared_ptr<daemon::server>,
 	          std::string,
 	          std::string,
 	          uri,
@@ -109,7 +113,7 @@
 	 * \param message the message text
 	 */
 	static void run(boost::asio::io_context& ctx,
-	                std::shared_ptr<server> sv,
+	                std::shared_ptr<daemon::server> sv,
 	                std::string origin,
 	                std::string channel,
 	                std::string message);
--- a/tests/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -18,11 +18,16 @@
 
 project(tests)
 
-add_subdirectory(src/libirccd-core)
+# Libraries.
 add_subdirectory(src/libirccd)
+add_subdirectory(src/libirccd-daemon)
+
+if (IRCCD_HAVE_JS)
+	add_subdirectory(src/libirccd-js)
+endif ()
+
+# Tools.
 add_subdirectory(src/irccdctl)
 
-if (IRCCD_HAVE_JS)
-	add_subdirectory(src/plugins)
-	add_subdirectory(src/libirccd-js)
-endif ()
+# Plugins.
+add_subdirectory(src/plugins)
--- a/tests/src/irccdctl/cli-plugin-config/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-config/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-plugin-config
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-plugin-config/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-config/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -41,8 +41,8 @@
 			{ "v2", "456" }
 		});
 
-		irccd_.plugins().add(std::move(conf1));
-		irccd_.plugins().add(std::move(conf2));
+		bot_.plugins().add(std::move(conf1));
+		bot_.plugins().add(std::move(conf2));
 	}
 };
 
--- a/tests/src/irccdctl/cli-plugin-info/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-info/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-plugin-info
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-plugin-info/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-info/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -40,7 +40,7 @@
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-	irccd_.plugins().add(std::make_unique<mock_plugin>("test"));
+	bot_.plugins().add(std::make_unique<mock_plugin>("test"));
 	start();
 
 	const auto [code, out, err] = exec({ "plugin-info", "test" });
--- a/tests/src/irccdctl/cli-plugin-list/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-list/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-plugin-list
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-plugin-list/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-list/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -40,8 +40,8 @@
 
 BOOST_AUTO_TEST_CASE(output)
 {
-	irccd_.plugins().add(std::make_unique<mock_plugin>("p1"));
-	irccd_.plugins().add(std::make_unique<mock_plugin>("p2"));
+	bot_.plugins().add(std::make_unique<mock_plugin>("p1"));
+	bot_.plugins().add(std::make_unique<mock_plugin>("p2"));
 	start();
 
 	const auto [code, out, err] = exec({ "plugin-list" });
--- a/tests/src/irccdctl/cli-plugin-load/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-load/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-plugin-load
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-plugin-load/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-load/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,7 +22,11 @@
 #include <irccd/test/cli_fixture.hpp>
 #include <irccd/test/mock_plugin.hpp>
 
-using namespace irccd::test;
+using irccd::daemon::plugin;
+using irccd::daemon::plugin_loader;
+
+using irccd::test::mock_plugin;
+using irccd::test::cli_fixture;
 
 namespace irccd {
 
@@ -60,9 +64,9 @@
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-	irccd_.plugins().add(std::make_unique<mock_plugin>("p1"));
-	irccd_.plugins().add(std::make_unique<mock_plugin>("p2"));
-	irccd_.plugins().add_loader(std::make_unique<custom_plugin_loader>());
+	bot_.plugins().add(std::make_unique<mock_plugin>("p1"));
+	bot_.plugins().add(std::make_unique<mock_plugin>("p2"));
+	bot_.plugins().add_loader(std::make_unique<custom_plugin_loader>());
 	start();
 
 	// Load a plugin first.
--- a/tests/src/irccdctl/cli-plugin-reload/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-reload/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-plugin-reload
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-plugin-reload/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-reload/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,7 +22,11 @@
 #include <irccd/test/cli_fixture.hpp>
 #include <irccd/test/mock.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+using irccd::test::mock;
+
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
 
 namespace irccd {
 
@@ -40,7 +44,7 @@
 		return "reload";
 	}
 
-	void handle_reload(irccd&) override
+	void handle_reload(bot&) override
 	{
 		push("handle_reload");
 	}
@@ -60,7 +64,7 @@
 {
 	const auto plugin = std::make_shared<reloadable_plugin>();
 
-	irccd_.plugins().add(plugin);
+	bot_.plugins().add(plugin);
 	start();
 
 	const auto [code, out, err] = exec({ "plugin-reload", "test" });
--- a/tests/src/irccdctl/cli-plugin-unload/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-unload/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-plugin-unload
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-plugin-unload/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-plugin-unload/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,7 +22,11 @@
 #include <irccd/test/cli_fixture.hpp>
 #include <irccd/test/mock.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+using irccd::test::mock;
+
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
 
 namespace irccd {
 
@@ -40,7 +44,7 @@
 		return "unload";
 	}
 
-	void handle_unload(irccd&) override
+	void handle_unload(bot&) override
 	{
 		push("handle_unload");
 	}
@@ -60,7 +64,7 @@
 {
 	const auto plugin = std::make_shared<unloadable_plugin>();
 
-	irccd_.plugins().add(plugin);
+	bot_.plugins().add(plugin);
 	start();
 
 	const auto [code, out, err] = exec({ "plugin-unload", "test" });
@@ -69,7 +73,7 @@
 	BOOST_TEST(out.size() == 0U);
 	BOOST_TEST(err.size() == 0U);
 	BOOST_TEST(plugin->find("handle_unload").size() == 1U);
-	BOOST_TEST(!irccd_.plugins().has("p"));
+	BOOST_TEST(!bot_.plugins().has("p"));
 }
 
 BOOST_AUTO_TEST_SUITE_END()
--- a/tests/src/irccdctl/cli-rule-add/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-add/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-rule-add
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-rule-edit/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-edit/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-rule-edit
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-rule-edit/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-edit/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,9 @@
 
 #include <irccd/test/cli_fixture.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+
+using irccd::daemon::rule;
 
 namespace irccd {
 
@@ -32,7 +34,7 @@
 	rule_edit_fixture()
 		: cli_fixture(IRCCDCTL_EXECUTABLE)
 	{
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s1", "s2" },
 			{ "c1", "c2" },
 			{ "o1", "o2" },
--- a/tests/src/irccdctl/cli-rule-info/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-info/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-rule-info
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-rule-info/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-info/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,9 @@
 
 #include <irccd/test/cli_fixture.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+
+using irccd::daemon::rule;
 
 namespace irccd {
 
@@ -39,7 +41,7 @@
 
 BOOST_AUTO_TEST_CASE(info)
 {
-	irccd_.rules().add({
+	bot_.rules().add({
 		{ "s1", "s2" },
 		{ "c1", "c2" },
 		{ "o1", "o2" },
--- a/tests/src/irccdctl/cli-rule-list/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-list/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-rule-list
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-rule-list/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-list/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,9 @@
 
 #include <irccd/test/cli_fixture.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+
+using irccd::daemon::rule;
 
 namespace irccd {
 
@@ -39,7 +41,7 @@
 
 BOOST_AUTO_TEST_CASE(simple)
 {
-	irccd_.rules().add({
+	bot_.rules().add({
 		{ "s1", "s2" },
 		{ "c1", "c2" },
 		{ "o1", "o2" },
--- a/tests/src/irccdctl/cli-rule-move/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-move/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-rule-move
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-rule-move/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-move/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,9 @@
 
 #include <irccd/test/cli_fixture.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+
+using irccd::daemon::rule;
 
 namespace irccd {
 
@@ -32,7 +34,7 @@
 	rule_move_fixture()
 		: cli_fixture(IRCCDCTL_EXECUTABLE)
 	{
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s1" },
 			{ "c1" },
 			{ "o1" },
@@ -40,7 +42,7 @@
 			{ "onTopic" },
 			rule::action_type::accept
 		});
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s2" },
 			{ "c2" },
 			{ "o2" },
@@ -48,7 +50,7 @@
 			{ "onCommand" },
 			rule::action_type::drop
 		});
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s3" },
 			{ "c3" },
 			{ "o3" },
--- a/tests/src/irccdctl/cli-rule-remove/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-remove/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-rule-remove
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-rule-remove/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-rule-remove/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,9 @@
 
 #include <irccd/test/cli_fixture.hpp>
 
-using namespace irccd::test;
+using irccd::test::cli_fixture;
+
+using irccd::daemon::rule;
 
 namespace irccd {
 
@@ -32,7 +34,7 @@
 	rule_remove_fixture()
 		: cli_fixture(IRCCDCTL_EXECUTABLE)
 	{
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s1" },
 			{ "c1" },
 			{ "o1" },
@@ -40,7 +42,7 @@
 			{ "onTopic" },
 			rule::action_type::accept
 		});
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s2" },
 			{ "c2" },
 			{ "o2" },
@@ -48,7 +50,7 @@
 			{ "onCommand" },
 			rule::action_type::drop
 		});
-		irccd_.rules().add({
+		bot_.rules().add({
 			{ "s3" },
 			{ "c3" },
 			{ "o3" },
--- a/tests/src/irccdctl/cli-server-disconnect/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-disconnect/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-disconnect
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-disconnect/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-disconnect/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -39,11 +39,11 @@
 
 BOOST_AUTO_TEST_CASE(one)
 {
-	const auto s1 = std::make_shared<mock_server>(irccd_.get_service(), "s1", "localhost");
-	const auto s2 = std::make_shared<mock_server>(irccd_.get_service(), "s2", "localhost");
+	const auto s1 = std::make_shared<mock_server>(bot_.get_service(), "s1", "localhost");
+	const auto s2 = std::make_shared<mock_server>(bot_.get_service(), "s2", "localhost");
 
-	irccd_.servers().add(s1);
-	irccd_.servers().add(s2);
+	bot_.servers().add(s1);
+	bot_.servers().add(s2);
 	s1->clear();
 	s2->clear();
 	start();
@@ -60,11 +60,11 @@
 
 BOOST_AUTO_TEST_CASE(all)
 {
-	const auto s1 = std::make_shared<mock_server>(irccd_.get_service(), "s1", "localhost");
-	const auto s2 = std::make_shared<mock_server>(irccd_.get_service(), "s2", "localhost");
+	const auto s1 = std::make_shared<mock_server>(bot_.get_service(), "s1", "localhost");
+	const auto s2 = std::make_shared<mock_server>(bot_.get_service(), "s2", "localhost");
 
-	irccd_.servers().add(s1);
-	irccd_.servers().add(s2);
+	bot_.servers().add(s1);
+	bot_.servers().add(s2);
 	s1->clear();
 	s2->clear();
 	start();
--- a/tests/src/irccdctl/cli-server-info/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-info/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-info
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-invite/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-invite/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-invite
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-join/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-join/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-join
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-kick/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-kick/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-kick
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-list/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-list/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-list
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-list/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-list/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -39,8 +39,8 @@
 
 BOOST_AUTO_TEST_CASE(output)
 {
-	irccd_.servers().add(std::make_unique<mock_server>(irccd_.get_service(), "s1", "localhost"));
-	irccd_.servers().add(std::make_unique<mock_server>(irccd_.get_service(), "s2", "localhost"));
+	bot_.servers().add(std::make_unique<mock_server>(bot_.get_service(), "s1", "localhost"));
+	bot_.servers().add(std::make_unique<mock_server>(bot_.get_service(), "s2", "localhost"));
 	start();
 
 	const auto [code, out, err] = exec({ "server-list" });
--- a/tests/src/irccdctl/cli-server-me/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-me/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-me
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-message/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-message/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-message
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-mode/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-mode/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-mode
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-nick/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-nick/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-nick
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-notice/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-notice/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-notice
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-part/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-part/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-part
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-reconnect/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-reconnect/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-reconnect
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/irccdctl/cli-server-reconnect/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-reconnect/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -39,11 +39,11 @@
 
 BOOST_AUTO_TEST_CASE(one)
 {
-	const auto s1 = std::make_shared<mock_server>(irccd_.get_service(), "s1", "localhost");
-	const auto s2 = std::make_shared<mock_server>(irccd_.get_service(), "s2", "localhost");
+	const auto s1 = std::make_shared<mock_server>(bot_.get_service(), "s1", "localhost");
+	const auto s2 = std::make_shared<mock_server>(bot_.get_service(), "s2", "localhost");
 
-	irccd_.servers().add(s1);
-	irccd_.servers().add(s2);
+	bot_.servers().add(s1);
+	bot_.servers().add(s2);
 	s1->clear();
 	s2->clear();
 	start();
@@ -63,11 +63,11 @@
 
 BOOST_AUTO_TEST_CASE(all)
 {
-	const auto s1 = std::make_shared<mock_server>(irccd_.get_service(), "s1", "localhost");
-	const auto s2 = std::make_shared<mock_server>(irccd_.get_service(), "s2", "localhost");
+	const auto s1 = std::make_shared<mock_server>(bot_.get_service(), "s1", "localhost");
+	const auto s2 = std::make_shared<mock_server>(bot_.get_service(), "s2", "localhost");
 
-	irccd_.servers().add(s1);
-	irccd_.servers().add(s2);
+	bot_.servers().add(s1);
+	bot_.servers().add(s2);
 	s1->clear();
 	s2->clear();
 	start();
--- a/tests/src/irccdctl/cli-server-topic/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/irccdctl/cli-server-topic/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -19,7 +19,7 @@
 irccd_define_test(
 	NAME cli-server-topic
 	SOURCES main.cpp
-	LIBRARIES libirccd-core
+	LIBRARIES libirccd
 	DEPENDS irccd irccdctl
 	FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>"
 )
--- a/tests/src/libirccd-core/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-add_subdirectory(fs-util)
-add_subdirectory(stream)
-add_subdirectory(string-util)
--- a/tests/src/libirccd-core/fs-util/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME fs-util
-	SOURCES main.cpp
-	LIBRARIES libirccd-core
-)
--- a/tests/src/libirccd-core/fs-util/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/*
- * main.cpp -- test fs_util functions
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "fs_util"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/fs_util.hpp>
-#include <irccd/system.hpp>
-
-namespace irccd {
-
-namespace {
-
-/*
- * fs_util::find function (name)
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(fs_find_name)
-
-BOOST_AUTO_TEST_CASE(not_recursive)
-{
-	auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-1.txt", false);
-	auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-2.txt", false);
-
-	BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
-	BOOST_TEST(file2.empty());
-}
-
-BOOST_AUTO_TEST_CASE(recursive)
-{
-	auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-1.txt", true);
-	auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-2.txt", true);
-
-	BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
-	BOOST_TEST(file2.find("file-2.txt") != std::string::npos);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * fs_util::find function (regex)
- * ------------------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(fs_find_regex)
-
-BOOST_AUTO_TEST_CASE(not_recursive)
-{
-	const std::regex regex("file-[12]\\.txt");
-	const auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", regex, false);
-
-	BOOST_TEST(file.find("file-1.txt") != std::string::npos);
-}
-
-BOOST_AUTO_TEST_CASE(recursive)
-{
-	const std::regex regex("file-[12]\\.txt");
-	const auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root/level-1", regex, true);
-
-	BOOST_TEST(file.find("file-2.txt") != std::string::npos);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd-core/stream/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME stream
-	SOURCES main.cpp
-	LIBRARIES libirccd-core
-)
--- a/tests/src/libirccd-core/stream/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,262 +0,0 @@
-/*
- * main.cpp -- test network classes
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "stream"
-#include <boost/test/unit_test.hpp>
-#include <boost/mpl/list.hpp>
-#include <boost/predef/os.h>
-
-#include <irccd/sysconfig.hpp>
-
-#include <irccd/acceptor.hpp>
-#include <irccd/connector.hpp>
-#include <irccd/stream.hpp>
-
-using boost::asio::io_service;
-using boost::asio::ip::tcp;
-
-#if defined(IRCCD_HAVE_SSL)
-using boost::asio::ssl::context;
-#endif
-
-namespace irccd {
-
-namespace {
-
-class stream_fixture {
-public:
-	io_service service_;
-
-	std::unique_ptr<acceptor> acceptor_;
-	std::unique_ptr<connector> connector_;
-
-	std::shared_ptr<stream> stream1_;
-	std::shared_ptr<stream> stream2_;
-
-	virtual auto create_acceptor() -> std::unique_ptr<acceptor> = 0;
-
-	virtual auto create_connector() -> std::unique_ptr<connector> = 0;
-
-	void init()
-	{
-		acceptor_ = create_acceptor();
-		connector_ = create_connector();
-
-		acceptor_->accept([this] (auto code, auto stream) {
-			if (code)
-				throw std::system_error(code);
-
-			stream1_ = std::move(stream);
-		});
-		connector_->connect([this] (auto code, auto stream) {
-			if (code)
-				throw std::system_error(code);
-
-			stream2_ = std::move(stream);
-		});
-
-		service_.run();
-		service_.reset();
-	}
-};
-
-class ip_stream_fixture : public stream_fixture {
-private:
-	tcp::endpoint endpoint_;
-
-protected:
-	/**
-	 * \copydoc io_fixture::create_acceptor
-	 */
-	auto create_acceptor() -> std::unique_ptr<acceptor> override
-	{
-		tcp::endpoint endpoint(tcp::v4(), 0U);
-		tcp::acceptor acceptor(service_, std::move(endpoint));
-
-		endpoint_ = acceptor.local_endpoint();
-
-		return std::make_unique<ip_acceptor>(service_, std::move(acceptor));
-	}
-
-	/**
-	 * \copydoc io_fixture::create_connector
-	 */
-	auto create_connector() -> std::unique_ptr<connector> override
-	{
-		const auto hostname = "127.0.0.1";
-		const auto port = std::to_string(endpoint_.port());
-
-		return std::make_unique<ip_connector>(service_, hostname, port, true, false);
-	}
-};
-
-#if defined(IRCCD_HAVE_SSL)
-
-class tls_ip_stream_fixture : public stream_fixture {
-private:
-	tcp::endpoint endpoint_;
-
-protected:
-	/**
-	 * \copydoc io_fixture::create_acceptor
-	 */
-	auto create_acceptor() -> std::unique_ptr<acceptor> override
-	{
-		context context(context::tlsv12);
-
-		context.use_certificate_file(TESTS_SOURCE_DIR "/data/test.crt", context::pem);
-		context.use_private_key_file(TESTS_SOURCE_DIR "/data/test.key", context::pem);
-
-		tcp::endpoint endpoint(tcp::v4(), 0U);
-		tcp::acceptor acceptor(service_, std::move(endpoint));
-
-		endpoint_ = acceptor.local_endpoint();
-
-		return std::make_unique<tls_acceptor<ip_acceptor>>(std::move(context), service_, std::move(acceptor));
-	}
-
-	/**
-	 * \copydoc io_fixture::create_connector
-	 */
-	auto create_connector() -> std::unique_ptr<connector> override
-	{
-		context context(context::tlsv12);
-
-		const auto hostname = "127.0.0.1";
-		const auto port = std::to_string(endpoint_.port());
-
-		return std::make_unique<tls_connector<ip_connector>>(std::move(context),
-			service_, hostname, port, true, false);
-	}
-};
-
-#endif // !IRCCD_HAVE_SSL
-
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-
-class local_stream_fixture : public stream_fixture {
-private:
-	const std::string path_{CMAKE_CURRENT_BINARY_DIR "/stream-test.sock"};
-
-public:
-
-	/**
-	 * \copydoc io_fixture::create_acceptor
-	 */
-	auto create_acceptor() -> std::unique_ptr<acceptor> override
-	{
-		return std::make_unique<local_acceptor>(service_, path_);
-	}
-
-	/**
-	 * \copydoc io_fixture::create_connector
-	 */
-	auto create_connector() -> std::unique_ptr<connector> override
-	{
-		return std::make_unique<local_connector>(service_, path_);
-	}
-};
-
-#if defined(IRCCD_HAVE_SSL)
-
-class tls_local_stream_fixture : public stream_fixture {
-private:
-	const std::string path_{CMAKE_CURRENT_BINARY_DIR "/stream-test.sock"};
-
-public:
-
-	/**
-	 * \copydoc io_fixture::create_acceptor
-	 */
-	auto create_acceptor() -> std::unique_ptr<acceptor> override
-	{
-		context context(context::tlsv12);
-
-		context.use_certificate_file(TESTS_SOURCE_DIR "/data/test.crt", context::pem);
-		context.use_private_key_file(TESTS_SOURCE_DIR "/data/test.key", context::pem);
-
-		return std::make_unique<tls_acceptor<local_acceptor>>(std::move(context), service_, path_);
-	}
-
-	/**
-	 * \copydoc io_fixture::create_connector
-	 */
-	auto create_connector() -> std::unique_ptr<connector> override
-	{
-		return std::make_unique<tls_connector<local_connector>>(context(context::tlsv12), service_, path_);
-	}
-};
-
-#endif // !IRCCD_HAVE_SSL
-
-#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
-
-/**
- * List of fixtures to tests.
- */
-using list = boost::mpl::list<
-	ip_stream_fixture
-#if defined(IRCCD_HAVE_SSL)
-	, tls_ip_stream_fixture
-#endif
-#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
-	, local_stream_fixture
-#	if defined(IRCCD_HAVE_SSL)
-	, tls_local_stream_fixture
-#	endif
-#endif
->;
-
-BOOST_AUTO_TEST_CASE_TEMPLATE(invalid_argument, Test, list)
-{
-	Test fixture;
-
-	const nlohmann::json message{
-		{ "abc", 123 },
-		{ "def", 456 }
-	};
-
-	fixture.init();
-	fixture.stream1_->recv([] (auto code, auto message) {
-		BOOST_TEST(!code);
-		BOOST_TEST(message.is_object());
-		BOOST_TEST(message["abc"].template get<int>() == 123);
-		BOOST_TEST(message["def"].template get<int>() == 456);
-	});
-	fixture.stream2_->send(message, [] (auto code) {
-		BOOST_TEST(!code);
-	});
-	fixture.service_.run();
-}
-
-BOOST_AUTO_TEST_CASE_TEMPLATE(connection_reset, Test, list)
-{
-	Test fixture;
-
-	fixture.init();
-	fixture.stream1_->recv([] (auto code, auto message) {
-		BOOST_TEST(code.value() == static_cast<int>(std::errc::connection_reset));
-		BOOST_TEST(message.is_null());
-	});
-	fixture.stream2_ = nullptr;
-	fixture.service_.run();
-}
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd-core/string-util/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME string-util
-	SOURCES main.cpp
-	LIBRARIES libirccd-core
-)
--- a/tests/src/libirccd-core/string-util/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,384 +0,0 @@
-/*
- * main.cpp -- test string_util functions
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "string_util"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/string_util.hpp>
-#include <irccd/system.hpp>
-
-namespace irccd {
-
-namespace {
-
-/*
- * string_util::format function
- * --------------------------------------------------------
- */
-BOOST_AUTO_TEST_SUITE(format)
-
-BOOST_AUTO_TEST_CASE(nothing)
-{
-	std::string expected = "hello world!";
-	std::string result = string_util::format("hello world!");
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(escape)
-{
-	string_util::subst params;
-
-	params.keywords.emplace("target", "hello");
-
-	BOOST_TEST(string_util::format("$@#") == "$@#");
-	BOOST_TEST(string_util::format(" $ @ # ") == " $ @ # ");
-	BOOST_TEST(string_util::format("#") == "#");
-	BOOST_TEST(string_util::format(" # ") == " # ");
-	BOOST_TEST(string_util::format("#@") == "#@");
-	BOOST_TEST(string_util::format("##") == "##");
-	BOOST_TEST(string_util::format("#!") == "#!");
-	BOOST_TEST(string_util::format("##{target}") == "#{target}");
-	BOOST_TEST(string_util::format("@#{target}", params) == "@hello");
-	BOOST_TEST(string_util::format("#{target}#", params) == "hello#");
-	BOOST_REQUIRE_THROW(string_util::format("#{failure"), std::exception);
-}
-
-BOOST_AUTO_TEST_CASE(disable_date)
-{
-	string_util::subst params;
-
-	params.flags &= ~(string_util::subst_flags::date);
-
-	BOOST_TEST(string_util::format("%H:%M", params) == "%H:%M");
-}
-
-BOOST_AUTO_TEST_CASE(disable_keywords)
-{
-	string_util::subst params;
-
-	params.keywords.emplace("target", "hello");
-	params.flags &= ~(string_util::subst_flags::keywords);
-
-	BOOST_TEST(string_util::format("#{target}", params) == "#{target}");
-}
-
-BOOST_AUTO_TEST_CASE(disable_env)
-{
-	string_util::subst params;
-
-	params.flags &= ~(string_util::subst_flags::env);
-
-	BOOST_TEST(string_util::format("${HOME}", params) == "${HOME}");
-}
-
-BOOST_AUTO_TEST_CASE(keyword_simple)
-{
-	string_util::subst params;
-
-	params.keywords.insert({"target", "irccd"});
-
-	std::string expected = "hello irccd!";
-	std::string result = string_util::format("hello #{target}!", params);
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(keyword_multiple)
-{
-	string_util::subst params;
-
-	params.keywords.insert({"target", "irccd"});
-	params.keywords.insert({"source", "nightmare"});
-
-	std::string expected = "hello irccd from nightmare!";
-	std::string result = string_util::format("hello #{target} from #{source}!", params);
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(keyword_adj_twice)
-{
-	string_util::subst params;
-
-	params.keywords.insert({"target", "irccd"});
-
-	std::string expected = "hello irccdirccd!";
-	std::string result = string_util::format("hello #{target}#{target}!", params);
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(keyword_missing)
-{
-	std::string expected = "hello !";
-	std::string result = string_util::format("hello #{target}!");
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(env_simple)
-{
-	std::string home = sys::env("HOME");
-
-	if (!home.empty()) {
-		std::string expected = "my home is " + home;
-		std::string result = string_util::format("my home is ${HOME}");
-
-		BOOST_TEST(expected == result);
-	}
-}
-
-BOOST_AUTO_TEST_CASE(env_missing)
-{
-	std::string expected = "value is ";
-	std::string result = string_util::format("value is ${HOPE_THIS_VAR_NOT_EXIST}");
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::split function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(split)
-
-using list = std::vector<std::string>;
-
-BOOST_AUTO_TEST_CASE(simple)
-{
-	list expected { "a", "b" };
-	list result = string_util::split("a;b", ";");
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(cut)
-{
-	list expected { "msg", "#staff", "foo bar baz" };
-	list result = string_util::split("msg;#staff;foo bar baz", ";", 3);
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::strip function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(strip)
-
-BOOST_AUTO_TEST_CASE(left)
-{
-	std::string value = "   123";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "123");
-}
-
-BOOST_AUTO_TEST_CASE(right)
-{
-	std::string value = "123   ";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "123");
-}
-
-BOOST_AUTO_TEST_CASE(both)
-{
-	std::string value = "   123   ";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "123");
-}
-
-BOOST_AUTO_TEST_CASE(none)
-{
-	std::string value = "without";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "without");
-}
-
-BOOST_AUTO_TEST_CASE(between_empty)
-{
-	std::string value = "one list";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "one list");
-}
-
-BOOST_AUTO_TEST_CASE(between_left)
-{
-	std::string value = "  space at left";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "space at left");
-}
-
-BOOST_AUTO_TEST_CASE(between_right)
-{
-	std::string value = "space at right  ";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "space at right");
-}
-
-BOOST_AUTO_TEST_CASE(between_both)
-{
-	std::string value = "  space at both  ";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "space at both");
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-	std::string value = "    ";
-	std::string result = string_util::strip(value);
-
-	BOOST_TEST(result == "");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::join function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(join)
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-	std::string expected = "";
-	std::string result = string_util::join<int>({});
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(one)
-{
-	std::string expected = "1";
-	std::string result = string_util::join({1});
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(two)
-{
-	std::string expected = "1:2";
-	std::string result = string_util::join({1, 2});
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(delimiter_string)
-{
-	std::string expected = "1;;2;;3";
-	std::string result = string_util::join({1, 2, 3}, ";;");
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_CASE(delimiter_char)
-{
-	std::string expected = "1@2@3@4";
-	std::string result = string_util::join({1, 2, 3, 4}, '@');
-
-	BOOST_TEST(expected == result);
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::is_identifier function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(is_identifier_valid)
-
-BOOST_AUTO_TEST_CASE(correct)
-{
-	BOOST_TEST(string_util::is_identifier("localhost"));
-	BOOST_TEST(string_util::is_identifier("localhost2"));
-	BOOST_TEST(string_util::is_identifier("localhost2-4_"));
-}
-
-BOOST_AUTO_TEST_CASE(incorrect)
-{
-	BOOST_TEST(!string_util::is_identifier(""));
-	BOOST_TEST(!string_util::is_identifier("localhost with spaces"));
-	BOOST_TEST(!string_util::is_identifier("localhost*"));
-	BOOST_TEST(!string_util::is_identifier("&&"));
-	BOOST_TEST(!string_util::is_identifier("@'"));
-	BOOST_TEST(!string_util::is_identifier("##"));
-	BOOST_TEST(!string_util::is_identifier("===++"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-/*
- * string_util::is_boolean function
- * --------------------------------------------------------
- */
-
-BOOST_AUTO_TEST_SUITE(is_boolean)
-
-BOOST_AUTO_TEST_CASE(correct)
-{
-	// true
-	BOOST_TEST(string_util::is_boolean("true"));
-	BOOST_TEST(string_util::is_boolean("True"));
-	BOOST_TEST(string_util::is_boolean("TRUE"));
-	BOOST_TEST(string_util::is_boolean("TruE"));
-
-	// yes
-	BOOST_TEST(string_util::is_boolean("yes"));
-	BOOST_TEST(string_util::is_boolean("Yes"));
-	BOOST_TEST(string_util::is_boolean("YES"));
-	BOOST_TEST(string_util::is_boolean("YeS"));
-
-	// on
-	BOOST_TEST(string_util::is_boolean("on"));
-	BOOST_TEST(string_util::is_boolean("On"));
-	BOOST_TEST(string_util::is_boolean("oN"));
-	BOOST_TEST(string_util::is_boolean("ON"));
-
-	// 1
-	BOOST_TEST(string_util::is_boolean("1"));
-}
-
-BOOST_AUTO_TEST_CASE(incorrect)
-{
-	BOOST_TEST(!string_util::is_boolean("false"));
-	BOOST_TEST(!string_util::is_boolean("lol"));
-	BOOST_TEST(!string_util::is_boolean(""));
-	BOOST_TEST(!string_util::is_boolean("0"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,52 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+add_subdirectory(command-plugin-config)
+add_subdirectory(command-plugin-info)
+add_subdirectory(command-plugin-list)
+add_subdirectory(command-plugin-load)
+add_subdirectory(command-plugin-reload)
+add_subdirectory(command-plugin-unload)
+add_subdirectory(command-rule-add)
+add_subdirectory(command-rule-edit)
+add_subdirectory(command-rule-info)
+add_subdirectory(command-rule-list)
+add_subdirectory(command-rule-move)
+add_subdirectory(command-rule-remove)
+add_subdirectory(command-server-connect)
+add_subdirectory(command-server-disconnect)
+add_subdirectory(command-server-info)
+add_subdirectory(command-server-invite)
+add_subdirectory(command-server-join)
+add_subdirectory(command-server-kick)
+add_subdirectory(command-server-list)
+add_subdirectory(command-server-me)
+add_subdirectory(command-server-message)
+add_subdirectory(command-server-mode)
+add_subdirectory(command-server-nick)
+add_subdirectory(command-server-notice)
+add_subdirectory(command-server-part)
+add_subdirectory(command-server-reconnect)
+add_subdirectory(command-server-topic)
+
+add_subdirectory(dynlib-plugin)
+add_subdirectory(irc)
+add_subdirectory(logger)
+add_subdirectory(rules)
+add_subdirectory(rule-util)
+add_subdirectory(server-util)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-config/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-plugin-config
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-config/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,125 @@
+/*
+ * main.cpp -- test plugin-config remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-config"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_plugin;
+
+using irccd::daemon::plugin_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(plugin_config_test_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(set)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-config" },
+		{ "plugin",     "test"          },
+		{ "variable",   "verbosy"       },
+		{ "value",      "falsy"         }
+	});
+
+	const auto config = bot_.plugins().require("test")->get_options();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(!config.empty());
+	BOOST_TEST(config.at("verbosy") == "falsy");
+}
+
+BOOST_AUTO_TEST_CASE(get)
+{
+	auto plugin = std::make_unique<mock_plugin>("test");
+
+	plugin->set_options({
+		{ "x1", "10" },
+		{ "x2", "20" }
+	});
+	bot_.plugins().clear();
+	bot_.plugins().add(std::move(plugin));
+
+	const auto [json, code] = request({
+		{ "command",    "plugin-config" },
+		{ "plugin",     "test"          },
+		{ "variable",   "x1"            }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
+	BOOST_TEST(json["variables"].count("x2") == 0U);
+}
+
+BOOST_AUTO_TEST_CASE(getall)
+{
+	auto plugin = std::make_unique<mock_plugin>("test");
+
+	plugin->set_options({
+		{ "x1", "10" },
+		{ "x2", "20" }
+	});
+	bot_.plugins().clear();
+	bot_.plugins().add(std::move(plugin));
+
+	const auto [json, code] = request({
+		{ "command",    "plugin-config" },
+		{ "plugin",     "test"          }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
+	BOOST_TEST(json["variables"]["x2"].get<std::string>() == "20");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+	const auto [json, code] = request({
+		{ "command", "plugin-config" }
+	});
+
+	BOOST_TEST(code == plugin_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-config" },
+		{ "plugin",     "unknown"       }
+	});
+
+	BOOST_TEST(code == plugin_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-info/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-plugin-info
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-info/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,80 @@
+/*
+ * main.cpp -- test plugin-info remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-info"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_plugin;
+
+using irccd::daemon::plugin_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(plugin_info_test_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-info"   },
+		{ "plugin",     "test"          },
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json["author"].get<std::string>() == "David Demelier <markand@malikania.fr>");
+	BOOST_TEST(json["license"].get<std::string>() == "ISC");
+	BOOST_TEST(json["summary"].get<std::string>() == "mock plugin");
+	BOOST_TEST(json["version"].get<std::string>() == "1.0");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+	const auto [json, code] = request({
+		{ "command", "plugin-info" }
+	});
+
+	BOOST_TEST(code == plugin_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-info"   },
+		{ "plugin",     "unknown"       }
+	});
+
+	BOOST_TEST(code == plugin_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-list/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-plugin-list
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-list/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,59 @@
+/*
+ * main.cpp -- test plugin-list remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-list"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_plugin;
+
+namespace irccd {
+
+namespace {
+
+class plugin_list_fixture : public command_fixture {
+public:
+	plugin_list_fixture()
+	{
+		bot_.plugins().clear();
+		bot_.plugins().add(std::make_unique<mock_plugin>("t1"));
+		bot_.plugins().add(std::make_unique<mock_plugin>("t2"));
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(plugin_list_fixture_suite, plugin_list_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command", "plugin-list" }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json["list"][0].get<std::string>() == "t1");
+	BOOST_TEST(json["list"][1].get<std::string>() == "t2");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-load/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-plugin-load
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-load/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,175 @@
+/*
+ * main.cpp -- test plugin-load remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-load"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_plugin;
+
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
+using irccd::daemon::plugin_error;
+using irccd::daemon::plugin_loader;
+
+namespace irccd {
+
+namespace {
+
+class broken : public plugin {
+public:
+	broken()
+		: plugin("broken")
+	{
+	}
+
+	auto get_name() const noexcept -> std::string_view override
+	{
+		return "broken";
+	}
+
+	void handle_load(bot&) override
+	{
+		throw std::runtime_error("broken");
+	}
+};
+
+class broken_loader : public plugin_loader {
+public:
+	broken_loader()
+		: plugin_loader({}, { ".none" })
+	{
+	}
+
+	auto open(std::string_view, std::string_view) -> std::shared_ptr<plugin> override
+	{
+		return nullptr;
+	}
+
+	auto find(std::string_view id) noexcept -> std::shared_ptr<plugin> override
+	{
+		if (id == "broken")
+			return std::make_unique<broken>();
+
+		return nullptr;
+	}
+};
+
+class sample_loader : public plugin_loader {
+public:
+	sample_loader()
+		: plugin_loader({}, { ".none" })
+	{
+	}
+
+	auto open(std::string_view, std::string_view) -> std::shared_ptr<plugin> override
+	{
+		return nullptr;
+	}
+
+	auto find(std::string_view id) noexcept -> std::shared_ptr<plugin> override
+	{
+		if (id == "test")
+			return std::make_unique<mock_plugin>("test");
+
+		return nullptr;
+	}
+};
+
+class plugin_load_fixture : public command_fixture {
+public:
+	plugin_load_fixture()
+	{
+		bot_.plugins().add_loader(std::make_unique<sample_loader>());
+		bot_.plugins().add_loader(std::make_unique<broken_loader>());
+		bot_.plugins().clear();
+		bot_.plugins().add(std::make_unique<mock_plugin>("already"));
+	}
+};
+
+} // !namespace
+
+BOOST_FIXTURE_TEST_SUITE(plugin_load_fixture_suite, plugin_load_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-load"   },
+		{ "plugin",     "test"          }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(bot_.plugins().has("test"));
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+	const auto [json, code] = request({
+		{ "command", "plugin-load" }
+	});
+
+	BOOST_TEST(code == plugin_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-load"   },
+		{ "plugin",     "unknown"       }
+	});
+
+	BOOST_TEST(code == plugin_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(already_exists)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-load"   },
+		{ "plugin",     "already"       }
+	});
+
+	BOOST_TEST(code == plugin_error::already_exists);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::already_exists);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(exec_error)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-load"   },
+		{ "plugin",     "broken"        }
+	});
+
+	BOOST_TEST(code == plugin_error::exec_error);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::exec_error);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-reload/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+    NAME command-plugin-reload
+    SOURCES main.cpp
+    LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-reload/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,123 @@
+/*
+ * main.cpp -- test plugin-reload remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-reload"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_plugin;
+
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
+using irccd::daemon::plugin_error;
+using irccd::daemon::plugin_loader;
+
+namespace irccd {
+
+namespace {
+
+class broken_plugin : public plugin {
+public:
+	broken_plugin()
+		: plugin("broken")
+	{
+	}
+
+	auto get_name() const noexcept -> std::string_view override
+	{
+		return "broken";
+	}
+
+	void handle_reload(bot&) override
+	{
+		throw std::runtime_error("broken");
+	}
+};
+
+class plugin_reload_fixture : public command_fixture {
+protected:
+	std::shared_ptr<mock_plugin> plugin_;
+
+	plugin_reload_fixture()
+		: plugin_(std::make_shared<mock_plugin>("test"))
+	{
+		bot_.plugins().clear();
+		bot_.plugins().add(plugin_);
+		bot_.plugins().add(std::make_unique<broken_plugin>());
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(plugin_reload_fixture_suite, plugin_reload_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-reload" },
+		{ "plugin",     "test"          }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(plugin_->find("handle_reload").size() == 1U);
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+	const auto [json, code] = request({
+		{ "command", "plugin-reload" }
+	});
+
+	BOOST_TEST(code == plugin_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-reload" },
+		{ "plugin",     "unknown"       }
+	});
+
+	BOOST_TEST(code == plugin_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(exec_error)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-reload" },
+		{ "plugin",     "broken"        }
+	});
+
+	BOOST_TEST(code == plugin_error::exec_error);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::exec_error);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-unload/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-plugin-unload
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-plugin-unload/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,124 @@
+/*
+ * main.cpp -- test plugin-unload remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-unload"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_plugin;
+
+using irccd::daemon::bot;
+using irccd::daemon::plugin;
+using irccd::daemon::plugin_error;
+using irccd::daemon::plugin_loader;
+
+namespace irccd {
+
+namespace {
+
+class broken_plugin : public plugin {
+public:
+	broken_plugin()
+		: plugin("broken")
+	{
+	}
+
+	auto get_name() const noexcept -> std::string_view override
+	{
+		return "broken";
+	}
+
+	void handle_unload(bot&) override
+	{
+		throw std::runtime_error("broken");
+	}
+};
+
+class plugin_unload_fixture : public command_fixture {
+protected:
+	std::shared_ptr<mock_plugin> plugin_;
+
+	plugin_unload_fixture()
+		: plugin_(std::make_shared<mock_plugin>("test"))
+	{
+		bot_.plugins().clear();
+		bot_.plugins().add(plugin_);
+		bot_.plugins().add(std::make_unique<broken_plugin>());
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(plugin_unload_fixture_suite, plugin_unload_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-unload" },
+		{ "plugin",     "test"          }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(plugin_->find("handle_unload").size() == 1U);
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+	const auto [json, code] = request({
+		{ "command", "plugin-unload" }
+	});
+
+	BOOST_TEST(code == plugin_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-unload" },
+		{ "plugin",     "unknown"       }
+	});
+
+	BOOST_TEST(code == plugin_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+}
+
+BOOST_AUTO_TEST_CASE(exec_error)
+{
+	const auto [json, code] = request({
+		{ "command",    "plugin-unload" },
+		{ "plugin",     "broken"        }
+	});
+
+	BOOST_TEST(code == plugin_error::exec_error);
+	BOOST_TEST(json["error"].get<int>() == plugin_error::exec_error);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
+	BOOST_TEST(!bot_.plugins().has("broken"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-add/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-rule-add
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-add/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,149 @@
+/*
+ * main.cpp -- test rule-add remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-add"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::rule_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(rule_add_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	request({
+		{ "command",    "rule-add"          },
+		{ "servers",    { "s1", "s2" }      },
+		{ "channels",   { "c1", "c2" }      },
+		{ "plugins",    { "p1", "p2" }      },
+		{ "events",     { "onMessage" }     },
+		{ "action",     "accept"            },
+		{ "index",      0                   }
+	});
+
+	const auto [json, code] = request({
+		{ "command", "rule-list" }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+
+	auto servers = json["list"][0]["servers"];
+	auto channels = json["list"][0]["channels"];
+	auto plugins = json["list"][0]["plugins"];
+	auto events = json["list"][0]["events"];
+
+	BOOST_TEST(json_util::contains(servers, "s1"));
+	BOOST_TEST(json_util::contains(servers, "s2"));
+	BOOST_TEST(json_util::contains(channels, "c1"));
+	BOOST_TEST(json_util::contains(channels, "c2"));
+	BOOST_TEST(json_util::contains(plugins, "p1"));
+	BOOST_TEST(json_util::contains(plugins, "p2"));
+	BOOST_TEST(json_util::contains(events, "onMessage"));
+	BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
+}
+
+BOOST_AUTO_TEST_CASE(append)
+{
+	request({
+		{ "command",    "rule-add"          },
+		{ "servers",    { "s1" }            },
+		{ "channels",   { "c1" }            },
+		{ "plugins",    { "p1" }            },
+		{ "events",     { "onMessage" }     },
+		{ "action",     "accept"            },
+		{ "index",      0                   }
+	});
+
+	request({
+		{ "command",    "rule-add"          },
+		{ "servers",    { "s2" }            },
+		{ "channels",   { "c2" }            },
+		{ "plugins",    { "p2" }            },
+		{ "events",     { "onMessage" }     },
+		{ "action",     "drop"              },
+		{ "index",      1                   }
+	});
+
+	const auto [json, code] = request({
+		{ "command", "rule-list" }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json["list"].size() == 2U);
+
+	// Rule 0.
+	{
+		auto servers = json["list"][0]["servers"];
+		auto channels = json["list"][0]["channels"];
+		auto plugins = json["list"][0]["plugins"];
+		auto events = json["list"][0]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 1.
+	{
+		auto servers = json["list"][1]["servers"];
+		auto channels = json["list"][1]["channels"];
+		auto plugins = json["list"][1]["plugins"];
+		auto events = json["list"][1]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s2"));
+		BOOST_TEST(json_util::contains(channels, "c2"));
+		BOOST_TEST(json_util::contains(plugins, "p2"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "drop");
+	}
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_action)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-add"  },
+		{ "action",     "unknown"   }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_action);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_action);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-edit/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-rule-edit
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-edit/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,406 @@
+/*
+ * main.cpp -- test rule-edit remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-edit"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::rule;
+using irccd::daemon::rule_error;
+
+namespace irccd {
+
+namespace {
+
+class rule_edit_fixture : public command_fixture {
+public:
+	rule_edit_fixture()
+	{
+		bot_.rules().add(rule{
+			{ "s1", "s2" },
+			{ "c1", "c2" },
+			{ "o1", "o2" },
+			{ "p1", "p2" },
+			{ "onMessage", "onCommand" },
+			rule::action_type::drop
+		});
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(rule_edit_fixture_suite, rule_edit_fixture)
+
+BOOST_AUTO_TEST_CASE(add_server)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "add-servers",    { "new-s3" }    },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["servers"], "new-s3"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_channel)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "add-channels",   { "new-c3" }    },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["channels"], "new-c3"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_plugin)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "add-plugins",    { "new-p3" }    },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "new-p3"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_event)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "add-events",     { "onQuery" }   },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json_util::contains(json["events"], "onQuery"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(add_event_and_server)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "add-servers",    { "new-s3" }    },
+		{ "add-events",     { "onQuery" }   },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["servers"], "new-s3"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json_util::contains(json["events"], "onQuery"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(change_action)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "action",         "accept"        },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "accept");
+}
+
+BOOST_AUTO_TEST_CASE(remove_server)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "remove-servers", { "s2" }        },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(!json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_channel)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "remove-channels", { "c2" }       },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(!json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_plugin)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "remove-plugins", { "p2" }        },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(!json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_event)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "remove-events",  { "onCommand" } },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(!json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_CASE(remove_event_and_server)
+{
+	request({
+		{ "command",        "rule-edit"     },
+		{ "remove-servers", { "s2" }        },
+		{ "remove-events",  { "onCommand" } },
+		{ "index",          0               }
+	});
+
+	const auto [json, code] = request({
+		{ "command",        "rule-info"     },
+		{ "index",          0               }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json_util::contains(json["servers"], "s1"));
+	BOOST_TEST(!json_util::contains(json["servers"], "s2"));
+	BOOST_TEST(json_util::contains(json["channels"], "c1"));
+	BOOST_TEST(json_util::contains(json["channels"], "c2"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
+	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
+	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
+	BOOST_TEST(!json_util::contains(json["events"], "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-edit" },
+		{ "index",      -100        },
+		{ "action",     "drop"      }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-edit" },
+		{ "index",      100         },
+		{ "action",     "drop"      }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-edit" },
+		{ "index",      "notaint"   },
+		{ "action",     "drop"      }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_action)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-edit" },
+		{ "index",      0           },
+		{ "action",     "unknown"   }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_action);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_action);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-info/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-rule-info
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-info/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,128 @@
+/*
+ * main.cpp -- test rule-info remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-info"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::rule;
+using irccd::daemon::rule_error;
+
+namespace irccd {
+
+namespace {
+
+class rule_info_fixture : public command_fixture {
+public:
+	rule_info_fixture()
+	{
+		bot_.rules().add(rule{
+			{ "s1", "s2" },
+			{ "c1", "c2" },
+			{ "o1", "o2" },
+			{ "p1", "p2" },
+			{ "onMessage", "onCommand" },
+			rule::action_type::drop
+		});
+		bot_.rules().add(rule{
+			{ "s1", },
+			{ "c1", },
+			{ "o1", },
+			{ "p1", },
+			{ "onMessage", },
+			rule::action_type::accept
+		});
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(rule_info_fixture_suite, rule_info_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-info" },
+		{ "index",      0           }
+	});
+
+	auto servers = json["servers"];
+	auto channels = json["channels"];
+	auto plugins = json["plugins"];
+	auto events = json["events"];
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json_util::contains(servers, "s1"));
+	BOOST_TEST(json_util::contains(servers, "s2"));
+	BOOST_TEST(json_util::contains(channels, "c1"));
+	BOOST_TEST(json_util::contains(channels, "c2"));
+	BOOST_TEST(json_util::contains(plugins, "p1"));
+	BOOST_TEST(json_util::contains(plugins, "p2"));
+	BOOST_TEST(json_util::contains(events, "onMessage"));
+	BOOST_TEST(json_util::contains(events, "onCommand"));
+	BOOST_TEST(json["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-info" },
+		{ "index",      -100        }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-info" },
+		{ "index",      100         }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-info" },
+		{ "index",      "notaint"   }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-list/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-rule-list
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-list/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,119 @@
+/*
+ * main.cpp -- test rule-list remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-list"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::rule;
+using irccd::daemon::rule_error;
+
+namespace irccd {
+
+namespace {
+
+class rule_list_fixture : public command_fixture {
+public:
+	rule_list_fixture()
+	{
+		bot_.rules().add(rule{
+			{ "s1", "s2" },
+			{ "c1", "c2" },
+			{ "o1", "o2" },
+			{ "p1", "p2" },
+			{ "onMessage", "onCommand" },
+			rule::action_type::drop
+		});
+		bot_.rules().add(rule{
+			{ "s1", },
+			{ "c1", },
+			{ "o1", },
+			{ "p1", },
+			{ "onMessage", },
+			rule::action_type::accept
+		});
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(rule_list_fixture_suite, rule_list_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json["list"].is_array());
+	BOOST_TEST(json["list"].size() == 2U);
+
+	// Rule 0.
+	{
+		auto servers = json["list"][0]["servers"];
+		auto channels = json["list"][0]["channels"];
+		auto plugins = json["list"][0]["plugins"];
+		auto events = json["list"][0]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(servers, "s2"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(channels, "c2"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(plugins, "p2"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json_util::contains(events, "onCommand"));
+		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "drop");
+	}
+
+	// Rule 1.
+	{
+		auto servers = json["list"][1]["servers"];
+		auto channels = json["list"][1]["channels"];
+		auto plugins = json["list"][1]["plugins"];
+		auto events = json["list"][1]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
+	}
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+	bot_.rules().remove(0);
+	bot_.rules().remove(0);
+
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json["list"].is_array());
+	BOOST_TEST(json["list"].empty());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-move/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-rule-move
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-move/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,365 @@
+/*
+ * main.cpp -- test rule-move remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-move"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::rule;
+using irccd::daemon::rule_error;
+
+namespace irccd {
+
+namespace {
+
+class rule_move_fixture : public command_fixture {
+public:
+	rule_move_fixture()
+	{
+		bot_.rules().add(rule{
+			{ "s0" },
+			{ "c0" },
+			{ "o0" },
+			{ "p0" },
+			{ "onMessage" },
+			rule::action_type::drop
+		});
+		bot_.rules().add(rule{
+			{ "s1", },
+			{ "c1", },
+			{ "o1", },
+			{ "p1", },
+			{ "onMessage", },
+			rule::action_type::accept
+		});
+		bot_.rules().add(rule{
+			{ "s2", },
+			{ "c2", },
+			{ "o2", },
+			{ "p2", },
+			{ "onMessage", },
+			rule::action_type::accept
+		});
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(rule_move_fixture_suite, rule_move_fixture)
+
+BOOST_AUTO_TEST_CASE(backward)
+{
+	request({
+		{ "command",    "rule-move" },
+		{ "from",       2           },
+		{ "to",         0           }
+	});
+
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+
+	// Rule 2.
+	{
+		auto servers = json["list"][0]["servers"];
+		auto channels = json["list"][0]["channels"];
+		auto plugins = json["list"][0]["plugins"];
+		auto events = json["list"][0]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s2"));
+		BOOST_TEST(json_util::contains(channels, "c2"));
+		BOOST_TEST(json_util::contains(plugins, "p2"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 0.
+	{
+		auto servers = json["list"][1]["servers"];
+		auto channels = json["list"][1]["channels"];
+		auto plugins = json["list"][1]["plugins"];
+		auto events = json["list"][1]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s0"));
+		BOOST_TEST(json_util::contains(channels, "c0"));
+		BOOST_TEST(json_util::contains(plugins, "p0"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "drop");
+	}
+
+	// Rule 1.
+	{
+		auto servers = json["list"][2]["servers"];
+		auto channels = json["list"][2]["channels"];
+		auto plugins = json["list"][2]["plugins"];
+		auto events = json["list"][2]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "accept");
+	}
+}
+
+BOOST_AUTO_TEST_CASE(upward)
+{
+	request({
+		{ "command",    "rule-move" },
+		{ "from",       0           },
+		{ "to",         2           }
+	});
+
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+
+	// Rule 1.
+	{
+		auto servers = json["list"][0]["servers"];
+		auto channels = json["list"][0]["channels"];
+		auto plugins = json["list"][0]["plugins"];
+		auto events = json["list"][0]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 2.
+	{
+		auto servers = json["list"][1]["servers"];
+		auto channels = json["list"][1]["channels"];
+		auto plugins = json["list"][1]["plugins"];
+		auto events = json["list"][1]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s2"));
+		BOOST_TEST(json_util::contains(channels, "c2"));
+		BOOST_TEST(json_util::contains(plugins, "p2"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 0.
+	{
+		auto servers = json["list"][2]["servers"];
+		auto channels = json["list"][2]["channels"];
+		auto plugins = json["list"][2]["plugins"];
+		auto events = json["list"][2]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s0"));
+		BOOST_TEST(json_util::contains(channels, "c0"));
+		BOOST_TEST(json_util::contains(plugins, "p0"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "drop");
+	}
+}
+
+BOOST_AUTO_TEST_CASE(same)
+{
+	request({
+		{ "command",    "rule-move" },
+		{ "from",       1           },
+		{ "to",         1           }
+	});
+
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+
+	// Rule 0.
+	{
+		auto servers = json["list"][0]["servers"];
+		auto channels = json["list"][0]["channels"];
+		auto plugins = json["list"][0]["plugins"];
+		auto events = json["list"][0]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s0"));
+		BOOST_TEST(json_util::contains(channels, "c0"));
+		BOOST_TEST(json_util::contains(plugins, "p0"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "drop");
+	}
+
+	// Rule 1.
+	{
+		auto servers = json["list"][1]["servers"];
+		auto channels = json["list"][1]["channels"];
+		auto plugins = json["list"][1]["plugins"];
+		auto events = json["list"][1]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 2.
+	{
+		auto servers = json["list"][2]["servers"];
+		auto channels = json["list"][2]["channels"];
+		auto plugins = json["list"][2]["plugins"];
+		auto events = json["list"][2]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s2"));
+		BOOST_TEST(json_util::contains(channels, "c2"));
+		BOOST_TEST(json_util::contains(plugins, "p2"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "accept");
+	}
+}
+
+BOOST_AUTO_TEST_CASE(beyond)
+{
+	request({
+		{ "command",    "rule-move" },
+		{ "from",       0           },
+		{ "to",         123         }
+	});
+
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+
+	// Rule 1.
+	{
+		auto servers = json["list"][0]["servers"];
+		auto channels = json["list"][0]["channels"];
+		auto plugins = json["list"][0]["plugins"];
+		auto events = json["list"][0]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s1"));
+		BOOST_TEST(json_util::contains(channels, "c1"));
+		BOOST_TEST(json_util::contains(plugins, "p1"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 2.
+	{
+		auto servers = json["list"][1]["servers"];
+		auto channels = json["list"][1]["channels"];
+		auto plugins = json["list"][1]["plugins"];
+		auto events = json["list"][1]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s2"));
+		BOOST_TEST(json_util::contains(channels, "c2"));
+		BOOST_TEST(json_util::contains(plugins, "p2"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
+	}
+
+	// Rule 0.
+	{
+		auto servers = json["list"][2]["servers"];
+		auto channels = json["list"][2]["channels"];
+		auto plugins = json["list"][2]["plugins"];
+		auto events = json["list"][2]["events"];
+
+		BOOST_TEST(json_util::contains(servers, "s0"));
+		BOOST_TEST(json_util::contains(channels, "c0"));
+		BOOST_TEST(json_util::contains(plugins, "p0"));
+		BOOST_TEST(json_util::contains(events, "onMessage"));
+		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "drop");
+	}
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1_from)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-move" },
+		{ "from",       -100        },
+		{ "to",         0           }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_1_to)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-move" },
+		{ "from",       0           },
+		{ "to",         -100        }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2_from)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-move" },
+		{ "from",       100         },
+		{ "to",         0           }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3_from)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-move" },
+		{ "from",       "notaint"   },
+		{ "to",         0           }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3_to)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-move" },
+		{ "from",       0           },
+		{ "to",         "notaint"   }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-remove/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-rule-remove
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-rule-remove/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,133 @@
+/*
+ * main.cpp -- test rule-remove remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-remove"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/json_util.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::rule;
+using irccd::daemon::rule_error;
+
+namespace irccd {
+
+namespace {
+
+class rule_remove_fixture : public command_fixture {
+public:
+	rule_remove_fixture()
+	{
+		bot_.rules().add(rule{
+			{ "s1", "s2" },
+			{ "c1", "c2" },
+			{ "o1", "o2" },
+			{ "p1", "p2" },
+			{ "onMessage", "onCommand" },
+			rule::action_type::drop
+		});
+		bot_.rules().add(rule{
+			{ "s1", },
+			{ "c1", },
+			{ "o1", },
+			{ "p1", },
+			{ "onMessage", },
+			rule::action_type::accept
+		});
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(rule_remove_fixture_suite, rule_remove_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	request({
+		{ "command",    "rule-remove"   },
+		{ "index",      1               }
+	});
+
+	const auto [json, code] = request({{ "command", "rule-list" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json["list"].is_array());
+	BOOST_TEST(json["list"].size() == 1U);
+
+	auto servers = json["list"][0]["servers"];
+	auto channels = json["list"][0]["channels"];
+	auto plugins = json["list"][0]["plugins"];
+	auto events = json["list"][0]["events"];
+
+	BOOST_TEST(json_util::contains(servers, "s1"));
+	BOOST_TEST(json_util::contains(servers, "s2"));
+	BOOST_TEST(json_util::contains(channels, "c1"));
+	BOOST_TEST(json_util::contains(channels, "c2"));
+	BOOST_TEST(json_util::contains(plugins, "p1"));
+	BOOST_TEST(json_util::contains(plugins, "p2"));
+	BOOST_TEST(json_util::contains(events, "onMessage"));
+	BOOST_TEST(json_util::contains(events, "onCommand"));
+	BOOST_TEST(json["list"][0]["action"].get<std::string>() == "drop");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_index_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-remove"   },
+		{ "index",      -100            }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-remove"   },
+		{ "index",      100             }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_index_3)
+{
+	const auto [json, code] = request({
+		{ "command",    "rule-remove"   },
+		{ "index",      "notaint"       }
+	});
+
+	BOOST_TEST(code == rule_error::invalid_index);
+	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-connect/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-connect
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-connect/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,262 @@
+/*
+ * main.cpp -- test server-connect remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-connect"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_server;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_connect_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(minimal)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"    },
+		{ "name",       "local"             },
+		{ "hostname",   "irc.example.org"   }
+	});
+
+	const auto s = bot_.servers().get("local");
+
+	BOOST_TEST(!code);
+	BOOST_TEST(s);
+	BOOST_TEST(s->get_id() == "local");
+	BOOST_TEST(s->get_hostname() == "irc.example.org");
+	BOOST_TEST(s->get_port() == 6667U);
+}
+
+#if defined(IRCCD_HAVE_SSL)
+
+BOOST_AUTO_TEST_CASE(full)
+{
+	const auto [json, code] = request({
+		{ "command",            "server-connect"        },
+		{ "name",               "local2"                },
+		{ "hostname",           "irc.example2.org"      },
+		{ "password",           "nonono"                },
+		{ "nickname",           "francis"               },
+		{ "realname",           "the_francis"           },
+		{ "username",           "frc"                   },
+		{ "ipv4",               false                   },
+		{ "ipv6",               true                    },
+		{ "ctcpVersion",        "ultra bot"             },
+		{ "commandChar",        "::"                    },
+		{ "port",               18000                   },
+		{ "ssl",                true                    },
+		{ "sslVerify",          true                    },
+		{ "autoRejoin",         true                    },
+		{ "joinInvite",         true                    }
+	});
+
+	const auto s = bot_.servers().get("local2");
+
+	BOOST_TEST(!code);
+	BOOST_TEST(s);
+	BOOST_TEST(s->get_id() == "local2");
+	BOOST_TEST(s->get_hostname() == "irc.example2.org");
+	BOOST_TEST(s->get_port() == 18000U);
+	BOOST_TEST(s->get_password() == "nonono");
+	BOOST_TEST(s->get_nickname() == "francis");
+	BOOST_TEST(s->get_realname() == "the_francis");
+	BOOST_TEST(s->get_username() == "frc");
+	BOOST_TEST(s->get_command_char() == "::");
+	BOOST_TEST(s->get_ctcp_version() == "ultra bot");
+	BOOST_TEST(!static_cast<bool>(s->get_options() & server::options::ipv4));
+	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::ipv6));
+	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::ssl));
+	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::auto_rejoin));
+	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::join_invite));
+}
+
+#endif // !IRCCD_HAVE_SSL
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(already_exists)
+{
+	bot_.servers().add(std::make_unique<mock_server>(ctx_, "local"));
+
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "local"                 },
+		{ "hostname",   "127.0.0.1"             }
+	});
+
+	BOOST_TEST(code == server_error::already_exists);
+	BOOST_TEST(json["error"].get<int>() == server_error::already_exists);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_hostname_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+	});
+
+	BOOST_TEST(code == server_error::invalid_hostname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_hostname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_hostname_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   123456                  }
+	});
+
+	BOOST_TEST(code == server_error::invalid_hostname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_hostname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       ""                      },
+		{ "hostname",   "127.0.0.1"             }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       123456                  },
+		{ "hostname",   "127.0.0.1"             }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_port_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   "127.0.0.1"             },
+		{ "port",       "notaint"               }
+	});
+
+	BOOST_TEST(code == server_error::invalid_port);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_port);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_port_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   "127.0.0.1"             },
+		{ "port",       -123                    }
+	});
+
+	BOOST_TEST(code == server_error::invalid_port);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_port);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_port_3)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   "127.0.0.1"             },
+		{ "port",       1000000                 }
+	});
+
+	BOOST_TEST(code == server_error::invalid_port);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_port);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+#if !defined(IRCCD_HAVE_SSL)
+
+BOOST_AUTO_TEST_CASE(ssl_disabled)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   "127.0.0.1"             },
+		{ "ssl",        true                    }
+	});
+
+	BOOST_TEST(code == server_error::ssl_disabled);
+	BOOST_TEST(json["error"].get<int>() == server_error::ssl_disabled);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+#endif
+
+BOOST_AUTO_TEST_CASE(invalid_family_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   "127.0.0.1"             },
+		{ "port",       6667                    },
+		{ "ipv4",       "invalid"               }
+	});
+
+	BOOST_TEST(code == server_error::invalid_family);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_family);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_family_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-connect"        },
+		{ "name",       "new"                   },
+		{ "hostname",   "127.0.0.1"             },
+		{ "port",       6667                    },
+		{ "ipv6",       1234                    }
+	});
+
+	BOOST_TEST(code == server_error::invalid_family);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_family);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-disconnect/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-disconnect
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-disconnect/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,108 @@
+/*
+ * main.cpp -- test server-disconnect remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-disconnect"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_server;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+class server_disconnect_fixture : public command_fixture {
+protected:
+	std::shared_ptr<mock_server> s1_;
+	std::shared_ptr<mock_server> s2_;
+
+	server_disconnect_fixture()
+		: s1_(new mock_server(ctx_, "s1", "localhost"))
+		, s2_(new mock_server(ctx_, "s2", "localhost"))
+	{
+		bot_.servers().add(s1_);
+		bot_.servers().add(s2_);
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(server_disconnect_fixture_suite, server_disconnect_fixture)
+
+BOOST_AUTO_TEST_CASE(one)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-disconnect"     },
+		{ "server",     "s1"                    }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json["command"].get<std::string>() == "server-disconnect");
+	BOOST_TEST(s1_->find("disconnect").size() == 1U);
+	BOOST_TEST(!bot_.servers().has("s1"));
+	BOOST_TEST(bot_.servers().has("s2"));
+}
+
+BOOST_AUTO_TEST_CASE(all)
+{
+	const auto [json, code] = request({{ "command", "server-disconnect" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json["command"].get<std::string>() == "server-disconnect");
+	BOOST_TEST(s1_->find("disconnect").size() == 1U);
+	BOOST_TEST(s2_->find("disconnect").size() == 1U);
+	BOOST_TEST(!bot_.servers().has("s1"));
+	BOOST_TEST(!bot_.servers().has("s2"));
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-disconnect"     },
+		{ "server",     123456                  }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-disconnect"     },
+		{ "server",     "unknown"               }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-info/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-info
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-info/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,111 @@
+/*
+ * main.cpp -- test server-info remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-info"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_server;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_info_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	auto server = std::make_unique<mock_server>(ctx_, "test", "example.org");
+
+	server->set_port(8765);
+	server->set_password("none");
+	server->set_nickname("pascal");
+	server->set_username("psc");
+	server->set_realname("Pascal le grand frere");
+	server->set_ctcp_version("yeah");
+	server->set_command_char("@");
+	server->set_ping_timeout(20000);
+
+	bot_.servers().clear();
+	bot_.servers().add(std::move(server));
+
+	const auto [json, code] = request({
+		{ "command",    "server-info"   },
+		{ "server",     "test"          },
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json["hostname"].get<std::string>() == "example.org");
+	BOOST_TEST(json["name"].get<std::string>() == "test");
+	BOOST_TEST(json["nickname"].get<std::string>() == "pascal");
+	BOOST_TEST(json["port"].get<int>() == 8765);
+	BOOST_TEST(json["realname"].get<std::string>() == "Pascal le grand frere");
+	BOOST_TEST(json["username"].get<std::string>() == "psc");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-info"   },
+		{ "server",     123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-info"   },
+		{ "server",     ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-info"   },
+		{ "server",     "unknown"       }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-invite/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-invite
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-invite/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,157 @@
+/*
+ * main.cpp -- test server-invite remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-invite"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_invite_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     "test"          },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	const auto cmd = server_->find("invite").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "francis");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "#music");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     123456          },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     ""              },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     "test"          },
+		{ "target",     ""              },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_nickname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     "test"          },
+		{ "target",     123456          },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_nickname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     "test"          },
+		{ "target",     "jean"          },
+		{ "channel",    ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     "test"          },
+		{ "target",     "jean"          },
+		{ "channel",    123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-invite" },
+		{ "server",     "unknown"       },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-join/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-join
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-join/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,153 @@
+/*
+ * main.cpp -- test server-join remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-join"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_join_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     "test"          },
+		{ "channel",    "#music"        },
+		{ "password",   "plop"          }
+	});
+
+	const auto cmd = server_->find("join").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#music");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "plop");
+}
+
+BOOST_AUTO_TEST_CASE(nopassword)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     "test"          },
+		{ "channel",    "#music"        }
+	});
+
+	const auto cmd = server_->find("join").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#music");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     123456          },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     ""              },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     "test"          },
+		{ "channel",    ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     "test"          },
+		{ "channel",    123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_password)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     "test"          },
+		{ "channel",    "#staff"        },
+		{ "password",   123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_password);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_password);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-join"   },
+		{ "server",     "unknown"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-kick/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-kick
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-kick/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,191 @@
+/*
+ * main.cpp -- test server-kick remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-kick"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_kick_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     "francis"       },
+		{ "channel",    "#staff"        },
+		{ "reason",     "too noisy"     }
+	});
+
+	const auto cmd = server_->find("kick").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "#staff");
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "francis");
+	BOOST_TEST(std::any_cast<std::string>(cmd[2]) == "too noisy");
+}
+
+BOOST_AUTO_TEST_CASE(noreason)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     "francis"       },
+		{ "channel",    "#staff"        }
+	});
+
+	const auto cmd = server_->find("kick").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "#staff");
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "francis");
+	BOOST_TEST(std::any_cast<std::string>(cmd[2]) == "");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     123456          },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     ""              },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     ""              },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_nickname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     123456          },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_nickname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     "jean"          },
+		{ "channel",    ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     "jean"          },
+		{ "channel",    123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_message)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "test"          },
+		{ "target",     "jean"          },
+		{ "channel",    "#staff"        },
+		{ "reason",     123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_message);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_message);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-kick"   },
+		{ "server",     "unknown"       },
+		{ "target",     "francis"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-list/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-list
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-list/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,61 @@
+/*
+ * main.cpp -- test server-list remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-list"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_server;
+
+namespace irccd {
+
+namespace {
+
+class server_list_fixture : public command_fixture {
+protected:
+	server_list_fixture()
+	{
+		bot_.servers().clear();
+		bot_.servers().add(std::make_unique<mock_server>(ctx_, "s1", "localhost"));
+		bot_.servers().add(std::make_unique<mock_server>(ctx_, "s2", "localhost"));
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(server_list_fixture_suite, server_list_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command", "server-list" }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(json["list"].is_array());
+	BOOST_TEST(json["list"].size() == 2U);
+	BOOST_TEST(json["list"][0].get<std::string>() == "s1");
+	BOOST_TEST(json["list"][1].get<std::string>() == "s2");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-me/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-me
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-me/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,129 @@
+/*
+ * main.cpp -- test server-me remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-me"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_me_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-me"     },
+		{ "server",     "test"          },
+		{ "target",     "jean"          },
+		{ "message",    "hello!"        }
+	});
+
+	const auto cmd = server_->find("me").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "hello!");
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "jean");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-me"     },
+		{ "server",     123456          },
+		{ "target",     "#music"        },
+		{ "message",    "hello!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-me"     },
+		{ "server",     ""              },
+		{ "target",     "#music"        },
+		{ "message",    "hello!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-me"     },
+		{ "server",     "test"          },
+		{ "target",     ""              },
+		{ "message",    "hello!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-me"     },
+		{ "server",     "test"          },
+		{ "target",     123456          },
+		{ "message",    "hello!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-me"     },
+		{ "server",     "unknown"       },
+		{ "target",     "#music"        },
+		{ "message",    "hello!"        }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-message/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-message
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-message/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,129 @@
+/*
+ * main.cpp -- test server-message remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-message"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_message_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-message"        },
+		{ "server",     "test"                  },
+		{ "target",     "#staff"                },
+		{ "message",    "plop!"                 }
+	});
+
+	const auto cmd = server_->find("message").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "plop!");
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-message"        },
+		{ "server",     123456                  },
+		{ "target",     "#music"                },
+		{ "message",    "plop!"                 }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-message"        },
+		{ "server",     ""                      },
+		{ "target",     "#music"                },
+		{ "message",    "plop!"                 }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-message"        },
+		{ "server",     "test"                  },
+		{ "target",     ""                      },
+		{ "message",    "plop!"                 }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-message"        },
+		{ "server",     "test"                  },
+		{ "target",     123456                  },
+		{ "message",    "plop!"                 }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-message"        },
+		{ "server",     "unknown"               },
+		{ "target",     "#music"                },
+		{ "message",    "plop!"                 }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-mode/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-mode
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-mode/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,156 @@
+/*
+ * main.cpp -- test server-mode remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-mode"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_mode_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     "test"          },
+		{ "channel",    "#irccd"        },
+		{ "mode",       "+t"            }
+	});
+
+	const auto cmd = server_->find("mode").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#irccd");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "+t");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     123456          },
+		{ "channel",    "#music"        },
+		{ "mode",       "+i"            }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     ""              },
+		{ "channel",    "#music"        },
+		{ "mode",       "+i"            }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     "test"          },
+		{ "channel",    ""              },
+		{ "mode",       "+i"            }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     "test"          },
+		{ "channel",    123456          },
+		{ "mode",       "+i"            }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_mode_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     "test"          },
+		{ "channel",    "#music"        },
+		{ "mode",       ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_mode);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_mode);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_mode_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     "test"          },
+		{ "channel",    "#music"        },
+		{ "mode",       123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_mode);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_mode);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-mode"   },
+		{ "server",     "unknown"       },
+		{ "channel",    "#music"        },
+		{ "mode",       "+i"            }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-nick/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-nick
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-nick/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,120 @@
+/*
+ * main.cpp -- test server-nick remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-nick"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_nick_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-nick"   },
+		{ "server",     "test"          },
+		{ "nickname",   "chris"         }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(json.is_object());
+	BOOST_TEST(server_->get_nickname() == "chris");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-nick"   },
+		{ "server",     123456          },
+		{ "nickname",   "chris"         }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-nick"   },
+		{ "server",     ""              },
+		{ "nickname",   "chris"         }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-nick"   },
+		{ "server",     "test"          },
+		{ "nickname",   ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_nickname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_nickname_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-nick"   },
+		{ "server",     "test"          },
+		{ "nickname",   123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_nickname);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-nick"   },
+		{ "server",     "unknown"       },
+		{ "nickname",   "chris"         }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-notice/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-notice
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-notice/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,129 @@
+/*
+ * main.cpp -- test server-notice remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-notice"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_notice_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-notice" },
+		{ "server",     "test"          },
+		{ "target",     "#staff"        },
+		{ "message",    "quiet!"        }
+	});
+
+	const auto cmd = server_->find("notice").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "quiet!");
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-notice" },
+		{ "server",     123456          },
+		{ "target",     "#music"        },
+		{ "message",    "quiet!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-notice" },
+		{ "server",     ""              },
+		{ "target",     "#music"        },
+		{ "message",    "quiet!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-notice" },
+		{ "server",     "test"          },
+		{ "target",     ""              },
+		{ "message",    "quiet!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-notice" },
+		{ "server",     "test"          },
+		{ "target",     123456          },
+		{ "message",    "quiet!"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-notice" },
+		{ "server",     "unknown"       },
+		{ "target",     "#music"        },
+		{ "message",    "quiet!"        }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-part/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-part
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-part/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,139 @@
+/*
+ * main.cpp -- test server-part remote command
+ *
+ * Copyright (c) 2013-2018 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 part and this permission part 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 BOOST_TEST_MODULE "server-part"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_part_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     "test"          },
+		{ "channel",    "#staff"        },
+		{ "reason",     "too noisy"     }
+	});
+
+	const auto cmd = server_->find("part").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "too noisy");
+}
+
+BOOST_AUTO_TEST_CASE(noreason)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     "test"          },
+		{ "channel",    "#staff"        }
+	});
+
+	const auto cmd = server_->find("part").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     123456          },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     ""              },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     "test"          },
+		{ "channel",    ""              }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     "test"          },
+		{ "channel",    123456          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-part"   },
+		{ "server",     "unknown"       },
+		{ "channel",    "#music"        }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-reconnect/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-reconnect
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-reconnect/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,121 @@
+/*
+ * main.cpp -- test server-reconnect remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-reconnect"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+using irccd::test::mock_server;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+class server_reconnect_fixture : public command_fixture {
+protected:
+	std::shared_ptr<mock_server> s1_;
+	std::shared_ptr<mock_server> s2_;
+
+	server_reconnect_fixture()
+		: s1_(new mock_server(ctx_, "s1", "localhost"))
+		, s2_(new mock_server(ctx_, "s2", "localhost"))
+	{
+		bot_.servers().clear();
+		bot_.servers().add(s1_);
+		bot_.servers().add(s2_);
+		s1_->clear();
+		s2_->clear();
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(server_reconnect_fixture_suite, server_reconnect_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [_, code] = request({
+		{ "command",    "server-reconnect"      },
+		{ "server",     "s1"                    }
+	});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(s1_->find("disconnect").size() == 1U);
+	BOOST_TEST(s1_->find("connect").size() == 1U);
+	BOOST_TEST(s2_->empty());
+}
+
+BOOST_AUTO_TEST_CASE(all)
+{
+	const auto [_, code] = request({{ "command", "server-reconnect" }});
+
+	BOOST_TEST(!code);
+	BOOST_TEST(s1_->find("disconnect").size() == 1U);
+	BOOST_TEST(s1_->find("connect").size() == 1U);
+	BOOST_TEST(s2_->find("disconnect").size() == 1U);
+	BOOST_TEST(s2_->find("connect").size() == 1U);
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-reconnect"      },
+		{ "server",     123456                  }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-reconnect"      },
+		{ "server",     ""                      }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-reconnect"      },
+		{ "server",     "unknown"               }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-topic/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME command-server-topic
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-ctl
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/command-server-topic/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,129 @@
+/*
+ * main.cpp -- test server-topic remote command
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-topic"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/test/command_fixture.hpp>
+
+using irccd::test::command_fixture;
+
+using irccd::daemon::server;
+using irccd::daemon::server_error;
+
+namespace irccd {
+
+namespace {
+
+BOOST_FIXTURE_TEST_SUITE(server_topic_fixture_suite, command_fixture)
+
+BOOST_AUTO_TEST_CASE(basic)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-topic"  },
+		{ "server",     "test"          },
+		{ "channel",    "#staff"        },
+		{ "topic",      "new version"   }
+	});
+
+	const auto cmd = server_->find("topic").back();
+
+	BOOST_TEST(!code);
+	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
+	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "new version");
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-topic"  },
+		{ "server",     123456          },
+		{ "channel",    "#music"        },
+		{ "topic",      "plop"          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_identifier_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-topic"  },
+		{ "server",     ""              },
+		{ "channel",    "#music"        },
+		{ "topic",      "plop"          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_identifier);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_1)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-topic"  },
+		{ "server",     "test"          },
+		{ "channel",    ""              },
+		{ "topic",      "plop"          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(invalid_channel_2)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-topic"  },
+		{ "server",     "test"          },
+		{ "channel",    123456          },
+		{ "topic",      "plop"          }
+	});
+
+	BOOST_TEST(code == server_error::invalid_channel);
+	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_CASE(not_found)
+{
+	const auto [json, code] = request({
+		{ "command",    "server-topic"  },
+		{ "server",     "unknown"       },
+		{ "channel",    "#music"        },
+		{ "topic",      "plop"          }
+	});
+
+	BOOST_TEST(code == server_error::not_found);
+	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
+	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/dynlib-plugin/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,44 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+find_package(Boost REQUIRED QUIET)
+
+add_library(test-plugin MODULE test_plugin.cpp)
+target_link_libraries(test-plugin libirccd-daemon Boost::boost)
+set_target_properties(
+	test-plugin
+	PROPERTIES
+		PREFIX ""
+		RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+foreach (c ${CMAKE_CONFIGURATION_TYPES})
+	string(TOUPPER ${c} c)
+	set_target_properties(
+		test-plugin
+		PROPERTIES
+			RUNTIME_OUTPUT_DIRECTORY_${c} ${CMAKE_CURRENT_BINARY_DIR}
+	)
+endforeach ()
+
+irccd_define_test(
+	NAME dynlib-plugin
+	SOURCES main.cpp
+	LIBRARIES libirccd libirccd-test
+	DEPENDS test-plugin
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/dynlib-plugin/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,196 @@
+/*
+ * main.cpp -- test dynlib_plugin
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "dynlib_plugin"
+#include <boost/test/unit_test.hpp>
+
+/*
+ * For this test, we update internal plugin configuration each time a function
+ * is called and check if it has been called correctly using get_options.
+ */
+
+#include <irccd/daemon/bot.hpp>
+#include <irccd/daemon/dynlib_plugin.hpp>
+#include <irccd/daemon/server.hpp>
+
+using irccd::daemon::bot;
+using irccd::daemon::dynlib_plugin_loader;
+using irccd::daemon::plugin;
+
+namespace irccd {
+
+namespace {
+
+class fixture {
+protected:
+	boost::asio::io_service service_;
+	std::shared_ptr<plugin> plugin_;
+	bot bot_{service_};
+
+	fixture()
+	{
+		plugin_ = dynlib_plugin_loader({CMAKE_CURRENT_BINARY_DIR}).find("test-plugin");
+
+		if (!plugin_)
+			throw std::runtime_error("test plugin not found");
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(dynlib_plugin_suite, fixture)
+
+BOOST_AUTO_TEST_CASE(handle_command)
+{
+	plugin_->handle_command(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["command"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_connect)
+{
+	plugin_->handle_connect(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["connect"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_invite)
+{
+	plugin_->handle_invite(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["invite"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_join)
+{
+	plugin_->handle_join(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["join"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_kick)
+{
+	plugin_->handle_kick(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["kick"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_load)
+{
+	plugin_->handle_load(bot_);
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["load"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_message)
+{
+	plugin_->handle_message(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["message"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_me)
+{
+	plugin_->handle_me(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["me"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_mode)
+{
+	plugin_->handle_mode(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["mode"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_names)
+{
+	plugin_->handle_names(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["names"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_nick)
+{
+	plugin_->handle_nick(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["nick"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_notice)
+{
+	plugin_->handle_notice(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["notice"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_part)
+{
+	plugin_->handle_part(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["part"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_reload)
+{
+	plugin_->handle_reload(bot_);
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["reload"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_topic)
+{
+	plugin_->handle_topic(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["topic"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_unload)
+{
+	plugin_->handle_unload(bot_);
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["unload"] == "true");
+}
+
+BOOST_AUTO_TEST_CASE(handle_whois)
+{
+	plugin_->handle_whois(bot_, {});
+
+	BOOST_TEST(plugin_->get_options().size() == 1U);
+	BOOST_TEST(plugin_->get_options()["whois"] == "true");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/dynlib-plugin/test_plugin.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,160 @@
+/*
+ * test_plugin.cpp -- basic exported plugin test
+ *
+ * Copyright (c) 2013-2018 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 <irccd/daemon/dynlib_plugin.hpp>
+
+using irccd::daemon::bot;
+using irccd::daemon::connect_event;
+using irccd::daemon::disconnect_event;
+using irccd::daemon::invite_event;
+using irccd::daemon::join_event;
+using irccd::daemon::kick_event;
+using irccd::daemon::me_event;
+using irccd::daemon::message_event;
+using irccd::daemon::mode_event;
+using irccd::daemon::names_event;
+using irccd::daemon::nick_event;
+using irccd::daemon::notice_event;
+using irccd::daemon::part_event;
+using irccd::daemon::plugin;
+using irccd::daemon::plugin_error;
+using irccd::daemon::topic_event;
+using irccd::daemon::whois_event;
+
+namespace irccd {
+
+class test_plugin : public plugin {
+private:
+	map config_;
+
+public:
+	test_plugin()
+		: plugin("test")
+	{
+	}
+
+	auto get_options() const -> map override
+	{
+		return config_;
+	}
+
+	auto get_name() const noexcept -> std::string_view override
+	{
+		return "test";
+	}
+
+	void handle_command(bot&, const message_event&) override
+	{
+		config_["command"] = "true";
+	}
+
+	void handle_connect(bot&, const connect_event&) override
+	{
+		config_["connect"] = "true";
+	}
+
+	void handle_invite(bot&, const invite_event&) override
+	{
+		config_["invite"] = "true";
+	}
+
+	void handle_join(bot&, const join_event&) override
+	{
+		config_["join"] = "true";
+	}
+
+	void handle_kick(bot&, const kick_event&) override
+	{
+		config_["kick"] = "true";
+	}
+
+	void handle_load(bot&) override
+	{
+		config_["load"] = "true";
+	}
+
+	void handle_message(bot&, const message_event&) override
+	{
+		config_["message"] = "true";
+	}
+
+	void handle_me(bot&, const me_event&) override
+	{
+		config_["me"] = "true";
+	}
+
+	void handle_mode(bot&, const mode_event&) override
+	{
+		config_["mode"] = "true";
+	}
+
+	void handle_names(bot&, const names_event&) override
+	{
+		config_["names"] = "true";
+	}
+
+	void handle_nick(bot&, const nick_event&) override
+	{
+		config_["nick"] = "true";
+	}
+
+	void handle_notice(bot&, const notice_event&) override
+	{
+		config_["notice"] = "true";
+	}
+
+	void handle_part(bot&, const part_event&) override
+	{
+		config_["part"] = "true";
+	}
+
+	void handle_reload(bot&) override
+	{
+		config_["reload"] = "true";
+	}
+
+	void handle_topic(bot&, const topic_event&) override
+	{
+		config_["topic"] = "true";
+	}
+
+	void handle_unload(bot&) override
+	{
+		config_["unload"] = "true";
+	}
+
+	void handle_whois(bot&, const whois_event&) override
+	{
+		config_["whois"] = "true";
+	}
+
+	static auto abi() -> version
+	{
+		return version();
+	}
+
+	static auto init(std::string) -> std::unique_ptr<plugin>
+	{
+		return std::make_unique<test_plugin>();
+	}
+};
+
+BOOST_DLL_ALIAS(test_plugin::abi, irccd_abi_test_plugin)
+BOOST_DLL_ALIAS(test_plugin::init, irccd_init_test_plugin)
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/irc/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME irc
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/irc/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,84 @@
+/*
+ * main.cpp -- test irc functions
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "irc"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/irc.hpp>
+
+using irccd::daemon::irc::message;
+using irccd::daemon::irc::user;
+
+namespace irccd {
+
+namespace {
+
+BOOST_AUTO_TEST_SUITE(message_parse)
+
+BOOST_AUTO_TEST_CASE(no_prefix)
+{
+	const auto m = message::parse("PRIVMSG jean :bonjour à toi");
+
+	BOOST_TEST(m.prefix.empty());
+	BOOST_TEST(m.command == "PRIVMSG");
+	BOOST_TEST(m.args.size() == 2U);
+	BOOST_TEST(m.args[0] == "jean");
+	BOOST_TEST(m.args[1] == "bonjour à toi");
+}
+
+BOOST_AUTO_TEST_CASE(prefix)
+{
+	const auto m = message::parse(":127.0.0.1 PRIVMSG jean :bonjour à toi");
+
+	BOOST_TEST(m.prefix == "127.0.0.1");
+	BOOST_TEST(m.command == "PRIVMSG");
+	BOOST_TEST(m.args.size() == 2U);
+	BOOST_TEST(m.args[0] == "jean");
+	BOOST_TEST(m.args[1] == "bonjour à toi");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE(user_parse)
+
+BOOST_AUTO_TEST_CASE(basics)
+{
+	const auto user = user::parse("jean!~jean@127.0.0.1");
+
+	BOOST_TEST(user.nick == "jean");
+	BOOST_TEST(user.host == "~jean@127.0.0.1");
+
+	const auto usersimple = user::parse("jean");
+
+	BOOST_TEST(usersimple.nick == "jean");
+	BOOST_TEST(usersimple.host.empty());
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+	const auto user = user::parse("");
+
+	BOOST_TEST(user.nick.empty());
+	BOOST_TEST(user.host.empty());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/logger/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME logger
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/logger/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,133 @@
+/*
+ * main.cpp -- test logger functions
+ *
+ * Copyright (c) 2013-2018 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 <algorithm>
+
+#define BOOST_TEST_MODULE "Logger"
+#include <boost/test/unit_test.hpp>
+#include <boost/format.hpp>
+
+#include <irccd/daemon/logger.hpp>
+
+using boost::format;
+using boost::str;
+
+using irccd::daemon::logger::sink;
+using irccd::daemon::logger::filter;
+
+namespace irccd {
+
+namespace {
+
+class sample_sink : public sink {
+public:
+	std::string line_debug;
+	std::string line_info;
+	std::string line_warning;
+
+	void write_debug(const std::string& line) override
+	{
+		line_debug = line;
+	}
+
+	void write_info(const std::string& line) override
+	{
+		line_info = line;
+	}
+
+	void write_warning(const std::string& line) override
+	{
+		line_warning = line;
+	}
+};
+
+class sample_filter : public filter {
+public:
+	auto pre_debug(std::string_view category,
+	               std::string_view component,
+	               std::string_view message) const -> std::string override
+	{
+		return str(format("DEBUG %s:%s:%s") % category % component % message);
+	}
+
+	auto pre_info(std::string_view category,
+	              std::string_view component,
+	              std::string_view message) const -> std::string override
+	{
+		return str(format("INFO %s:%s:%s") % category % component % message);
+	}
+
+	auto pre_warning(std::string_view category,
+	                 std::string_view component,
+	                 std::string_view message) const -> std::string override
+	{
+		return str(format("WARN %s:%s:%s") % category % component % message);
+	}
+};
+
+class logger_test {
+public:
+	sample_sink log_;
+
+	logger_test()
+	{
+		log_.set_filter(std::make_unique<sample_filter>());
+		log_.set_verbose(true);
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(logger_test_suite, logger_test)
+
+#if !defined(NDEBUG)
+
+BOOST_AUTO_TEST_CASE(debug)
+{
+	log_.debug("test", "debug") << "success" << std::endl;
+
+	BOOST_TEST(log_.line_debug == "DEBUG test:debug:success");
+}
+
+#endif
+
+BOOST_AUTO_TEST_CASE(info)
+{
+	log_.info("test", "info") << "success" << std::endl;
+
+	BOOST_TEST(log_.line_info == "INFO test:info:success");
+}
+
+BOOST_AUTO_TEST_CASE(info_quiet)
+{
+	log_.set_verbose(false);
+	log_.info("test", "info") << "success" << std::endl;
+
+	BOOST_REQUIRE(log_.line_info.empty());
+}
+
+BOOST_AUTO_TEST_CASE(warning)
+{
+	log_.warning("test", "warning") << "success" << std::endl;
+
+	BOOST_TEST(log_.line_warning == "WARN test:warning:success");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/rule-util/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME rule-util
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/rule-util/error-invalid-action.conf	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,2 @@
+[rule]
+action = unknown
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/rule-util/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,115 @@
+/*
+ * main.cpp -- test rule_util functions
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule_util"
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+
+#include <irccd/ini.hpp>
+
+#include <irccd/daemon/rule.hpp>
+#include <irccd/daemon/rule_util.hpp>
+
+using irccd::daemon::rule;
+using irccd::daemon::rule_error;
+using irccd::daemon::rule_util::from_config;
+
+namespace irccd {
+
+namespace {
+
+auto open(const std::string& config) -> ini::document
+{
+	boost::filesystem::path path;
+
+	path /= CMAKE_CURRENT_SOURCE_DIR;
+	path /= config;
+
+	return ini::read_file(path.string());
+}
+
+BOOST_AUTO_TEST_SUITE(load_from_config)
+
+BOOST_AUTO_TEST_SUITE(valid)
+
+BOOST_AUTO_TEST_CASE(servers)
+{
+	const auto rule = from_config(open("simple.conf")[0]);
+
+	BOOST_TEST(rule.servers.size() == 1U);
+	BOOST_TEST(rule.servers.count("s1"));
+	BOOST_TEST(rule.channels.empty());
+	BOOST_TEST(rule.plugins.empty());
+	BOOST_TEST(rule.events.empty());
+}
+
+BOOST_AUTO_TEST_CASE(channels)
+{
+	const auto rule = from_config(open("simple.conf")[1]);
+
+	BOOST_TEST(rule.servers.empty());
+	BOOST_TEST(rule.channels.size() == 1U);
+	BOOST_TEST(rule.channels.count("#c1"));
+	BOOST_TEST(rule.plugins.empty());
+	BOOST_TEST(rule.events.empty());
+}
+
+BOOST_AUTO_TEST_CASE(plugins)
+{
+	const auto rule = from_config(open("simple.conf")[2]);
+
+	BOOST_TEST(rule.servers.empty());
+	BOOST_TEST(rule.channels.empty());
+	BOOST_TEST(rule.plugins.size() == 1U);
+	BOOST_TEST(rule.plugins.count("hangman"));
+	BOOST_TEST(rule.events.empty());
+}
+
+BOOST_AUTO_TEST_CASE(events)
+{
+	const auto rule = from_config(open("simple.conf")[3]);
+
+	BOOST_TEST(rule.servers.empty());
+	BOOST_TEST(rule.channels.empty());
+	BOOST_TEST(rule.plugins.empty());
+	BOOST_TEST(rule.events.size() == 1U);
+	BOOST_TEST(rule.events.count("onCommand"));
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_action)
+{
+	BOOST_REQUIRE_THROW(from_config(open("error-invalid-action.conf")[0]), rule_error);
+
+	try {
+		from_config(open("error-invalid-action.conf")[0]);
+	} catch (const rule_error& ex) {
+		BOOST_TEST(ex.code() == rule_error::invalid_action);
+	}
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/rule-util/simple.conf	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,22 @@
+[rule]
+servers = "s1"
+action = accept
+
+[rule]
+channels = "#c1"
+action = drop
+
+[rule]
+plugins = "hangman"
+action = accept
+
+[rule]
+events = "onCommand"
+action = drop
+
+[rule]
+servers = "s1"
+channels = "#c1"
+plugins = "hangman"
+events = "onCommand"
+action = accept
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/rules/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME rules
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/rules/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,278 @@
+/*
+ * main.cpp -- test irccd rules
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "Rules"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/daemon/bot.hpp>
+#include <irccd/daemon/logger.hpp>
+#include <irccd/daemon/rule_service.hpp>
+
+using boost::asio::io_context;
+
+using irccd::daemon::bot;
+using irccd::daemon::logger::silent_sink;
+using irccd::daemon::rule;
+using irccd::daemon::rule_service;
+
+namespace irccd {
+
+namespace {
+
+/*
+ * Simulate the following rules configuration:
+ *
+ * #
+ * # On all servers, each channel #staff can't use the onCommand event,
+ * # everything else is allowed.
+ * #
+ * [rule]       #1
+ * servers      = ""
+ * channels     = "#staff"
+ * events       = "onCommand"
+ * action       = drop
+ *
+ * #
+ * # However, the same onCommand on #staff is allowed on server "unsafe"
+ * #
+ * [rule]       #2
+ * servers      = "unsafe"
+ * channels     = "#staff"
+ * events       = "onCommand"
+ * action       = accept
+ *
+ * #
+ * # Plugin game is only allowed on server "malikania" and "localhost",
+ * # channel "#games" and events "onMessage, onCommand".
+ * #
+ * # The first rule #3-1 disable the plugin game for every server, it is
+ * # reenabled again with the #3-2.
+ * #
+ * [rule]       #3-1
+ * plugins      = "game"
+ * action       = drop
+ *
+ * [rule]       #3-2
+ * servers      = "malikania localhost"
+ * channels     = "#games"
+ * plugins      = "game"
+ * events       = "onMessage onCommand"
+ * action       = accept
+ */
+class rules_test {
+protected:
+	io_context service_;
+	bot bot_{service_};
+	rule_service rules_{bot_};
+
+	rules_test()
+	{
+		bot_.set_log(std::make_unique<silent_sink>());
+
+		// #1
+		{
+			rules_.add({
+				rule::set{                }, // Servers
+				rule::set{ "#staff"       }, // Channels
+				rule::set{                }, // Origins
+				rule::set{                }, // Plugins
+				rule::set{ "onCommand"    }, // Events
+				rule::action_type::drop
+			});
+		}
+
+		// #2
+		{
+			rules_.add({
+				rule::set{ "unsafe"       },
+				rule::set{ "#staff"       },
+				rule::set{                },
+				rule::set{                },
+				rule::set{ "onCommand"    },
+				rule::action_type::accept
+			});
+		}
+
+		// #3-1
+		{
+			rules_.add({
+				rule::set{},
+				rule::set{},
+				rule::set{},
+				rule::set{"game"},
+				rule::set{},
+				rule::action_type::drop
+			});
+		}
+
+		// #3-2
+		{
+			rules_.add({
+				rule::set{ "malikania", "localhost"   },
+				rule::set{ "#games"                   },
+				rule::set{                            },
+				rule::set{ "game"                     },
+				rule::set{ "onCommand", "onMessage"   },
+				rule::action_type::accept
+			});
+		}
+	}
+};
+
+BOOST_FIXTURE_TEST_SUITE(rules_test_suite, rules_test)
+
+BOOST_AUTO_TEST_CASE(basic_match1)
+{
+	rule m;
+
+	/*
+	 * [rule]
+	 */
+	BOOST_TEST(m.match("freenode", "#test", "a", "", ""));
+	BOOST_TEST(m.match("", "", "", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(basic_match2)
+{
+	rule m{rule::set{"freenode"}};
+
+	/*
+	 * [rule]
+	 * servers	= "freenode"
+	 */
+
+	BOOST_TEST(m.match("freenode", "#test", "a", "", ""));
+	BOOST_TEST(!m.match("malikania", "#test", "a", "", ""));
+	BOOST_TEST(m.match("freenode", "", "jean", "", "onMessage"));
+}
+
+BOOST_AUTO_TEST_CASE(basic_match3)
+{
+	rule m{rule::set{"freenode"}, rule::set{"#staff"}};
+
+	/*
+	 * [rule]
+	 * servers	= "freenode"
+	 * channels	= "#staff"
+	 */
+
+	BOOST_TEST(m.match("freenode", "#staff", "a", "", ""));
+	BOOST_TEST(!m.match("freenode", "#test", "a", "", ""));
+	BOOST_TEST(!m.match("malikania", "#staff", "a", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(basic_match4)
+{
+	rule m{rule::set{"malikania"}, rule::set{"#staff"}, rule::set{"a"}};
+
+	/*
+	 * [rule]
+	 * servers	= "malikania"
+	 * channels	= "#staff"
+	 * plugins	= "a"
+	 */
+
+	BOOST_TEST(m.match("malikania", "#staff", "a", "",""));
+	BOOST_TEST(!m.match("malikania", "#staff", "b", "", ""));
+	BOOST_TEST(!m.match("freenode", "#staff", "a", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(complex_match1)
+{
+	rule m{rule::set{"malikania", "freenode"}};
+
+	/*
+	 * [rule]
+	 * servers	= "malikania freenode"
+	 */
+
+	BOOST_TEST(m.match("malikania", "", "", "", ""));
+	BOOST_TEST(m.match("freenode", "", "", "", ""));
+	BOOST_TEST(!m.match("no", "", "", "", ""));
+}
+
+BOOST_AUTO_TEST_CASE(origin_match)
+{
+	rule m{
+		rule::set{"malikania"},
+		rule::set{},
+		rule::set{"markand"},
+		rule::set{},
+		rule::set{},
+		rule::action_type::accept
+	};
+
+	/*
+	 * [rule]
+	 * servers = "malikania"
+	 * origins = "markand"
+	 */
+	BOOST_TEST(m.match("malikania", "#staff", "markand", "system", "onCommand"));
+	BOOST_TEST(!m.match("malikania", "#staff", "", "system", "onNames"));
+	BOOST_TEST(!m.match("malikania", "#staff", "jean", "system", "onMessage"));
+}
+
+BOOST_AUTO_TEST_CASE(basic_solve)
+{
+	/* Allowed */
+	BOOST_TEST(rules_.solve("malikania", "#staff", "", "a", "onMessage"));
+
+	/* Allowed */
+	BOOST_TEST(rules_.solve("freenode", "#staff", "", "b", "onTopic"));
+
+	/* Not allowed */
+	BOOST_TEST(!rules_.solve("malikania", "#staff", "", "", "onCommand"));
+
+	/* Not allowed */
+	BOOST_TEST(!rules_.solve("freenode", "#staff", "", "c", "onCommand"));
+
+	/* Allowed */
+	BOOST_TEST(rules_.solve("unsafe", "#staff", "", "c", "onCommand"));
+}
+
+BOOST_AUTO_TEST_CASE(games_solve)
+{
+	/* Allowed */
+	BOOST_TEST(rules_.solve("malikania", "#games", "", "game", "onMessage"));
+
+	/* Allowed */
+	BOOST_TEST(rules_.solve("localhost", "#games", "", "game", "onMessage"));
+
+	/* Allowed */
+	BOOST_TEST(rules_.solve("malikania", "#games", "", "game", "onCommand"));
+
+	/* Not allowed */
+	BOOST_TEST(!rules_.solve("malikania", "#games", "", "game", "onQuery"));
+
+	/* Not allowed */
+	BOOST_TEST(!rules_.solve("freenode", "#no", "", "game", "onMessage"));
+
+	/* Not allowed */
+	BOOST_TEST(!rules_.solve("malikania", "#test", "", "game", "onMessage"));
+}
+
+BOOST_AUTO_TEST_CASE(fix_645)
+{
+	BOOST_TEST(!rules_.solve("MALIKANIA", "#STAFF", "", "SYSTEM", "onCommand"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/server-util/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME server-util
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/server-util/full.conf	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,13 @@
+[server]
+name = "localhost"
+hostname = "irc.localhost"
+channels = ( "#staff", "#test" )
+port = 3344
+password = "secret"
+reconnect-timeout = 60
+nickname = "superbot"
+username = "sp"
+realname = "SuperBot 2000 NT"
+join-invite = true
+auto-rejoin = true
+auto-reconnect = true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/server-util/full.json	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,9 @@
+{
+	"name": "localhost",
+	"hostname": "irc.localhost",
+	"port": 3344,
+	"password": "secret",
+	"nickname": "superbot",
+	"username": "sp",
+	"realname": "SuperBot 2000 NT"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/server-util/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,203 @@
+/*
+ * main.cpp -- test server_util functions
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server_util"
+#include <boost/test/unit_test.hpp>
+#include <boost/filesystem.hpp>
+
+#include <irccd/ini.hpp>
+
+#include <irccd/daemon/server.hpp>
+#include <irccd/daemon/server_util.hpp>
+
+using nlohmann::json;
+
+using irccd::daemon::server;
+using irccd::daemon::server_util::from_config;
+using irccd::daemon::server_util::from_json;
+using irccd::daemon::server_util::message_type;
+
+namespace irccd {
+
+namespace server_util {
+
+auto operator<<(std::ostream& out, message_type::kind kind) -> std::ostream&
+{
+	if (kind == message_type::is_command)
+		out << "command";
+	else
+		out << "message";
+
+	return out;
+}
+
+} // !server_util
+
+namespace {
+
+class fixture {
+protected:
+	boost::asio::io_service ctx_;
+};
+
+auto open_config(const std::string& config) -> ini::document
+{
+	boost::filesystem::path path;
+
+	path /= CMAKE_CURRENT_SOURCE_DIR;
+	path /= config;
+
+	return ini::read_file(path.string());
+}
+
+auto open_json(const std::string& file) -> json
+{
+	boost::filesystem::path path;
+
+	path /= CMAKE_CURRENT_SOURCE_DIR;
+	path /= file;
+
+	std::ifstream input(path.string());
+
+	if (!input)
+		throw std::runtime_error(std::strerror(errno));
+
+	return json::parse(std::string(std::istreambuf_iterator<char>(input.rdbuf()), {}));
+}
+
+BOOST_FIXTURE_TEST_SUITE(load_from_config, fixture)
+
+BOOST_AUTO_TEST_SUITE(valid)
+
+BOOST_AUTO_TEST_CASE(full)
+{
+	const auto sv = from_config(ctx_, open_config("full.conf")[0]);
+
+	BOOST_TEST(sv->get_id() == "localhost");
+	BOOST_TEST(sv->get_hostname() == "irc.localhost");
+	BOOST_TEST(sv->get_port() == 3344U);
+	BOOST_TEST(sv->get_password() == "secret");
+	BOOST_TEST(sv->get_nickname() == "superbot");
+	BOOST_TEST(sv->get_username() == "sp");
+	BOOST_TEST(sv->get_realname() == "SuperBot 2000 NT");
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::join_invite));
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_rejoin));
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_reconnect));
+}
+
+#if defined(IRCCD_HAVE_SSL)
+
+BOOST_AUTO_TEST_CASE(ssl)
+{
+	const auto sv = from_config(ctx_, open_config("ssl.conf")[0]);
+
+	BOOST_TEST(sv->get_id() == "localhost");
+	BOOST_TEST(sv->get_hostname() == "irc.localhost");
+	BOOST_TEST(sv->get_port() == 6697U);
+	BOOST_TEST(sv->get_password() == "secret");
+	BOOST_TEST(sv->get_nickname() == "secure");
+	BOOST_TEST(sv->get_username() == "sc");
+	BOOST_TEST(sv->get_realname() == "SuperBot 2000 NT SSL");
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::ssl));
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::join_invite));
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_rejoin));
+	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_reconnect));
+}
+
+#endif // !IRCCD_HAVE_SSL
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_FIXTURE_TEST_SUITE(load_from_json, fixture)
+
+BOOST_AUTO_TEST_SUITE(valid)
+
+BOOST_AUTO_TEST_CASE(full)
+{
+	const auto sv = from_json(ctx_, open_json("full.json"));
+
+	BOOST_TEST(sv->get_id() == "localhost");
+	BOOST_TEST(sv->get_hostname() == "irc.localhost");
+	BOOST_TEST(sv->get_port() == 3344U);
+	BOOST_TEST(sv->get_password() == "secret");
+	BOOST_TEST(sv->get_nickname() == "superbot");
+	BOOST_TEST(sv->get_username() == "sp");
+	BOOST_TEST(sv->get_realname() == "SuperBot 2000 NT");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE_END()
+
+BOOST_AUTO_TEST_SUITE(message)
+
+BOOST_AUTO_TEST_CASE(valid_short)
+{
+	const auto m = message_type::parse("!hello", "!", "hello");
+
+	BOOST_TEST(m.type == message_type::is_command);
+	BOOST_TEST(m.message == "");
+}
+
+BOOST_AUTO_TEST_CASE(valid_arguments)
+{
+	const auto m = message_type::parse("!hello world", "!", "hello");
+
+	BOOST_TEST(m.type == message_type::is_command);
+	BOOST_TEST(m.message == "world");
+}
+
+BOOST_AUTO_TEST_CASE(cchar_with_message_short)
+{
+	const auto m = message_type::parse("!hello", "!", "hangman");
+
+	BOOST_TEST(m.type == message_type::is_message);
+	BOOST_TEST(m.message == "!hello");
+}
+
+BOOST_AUTO_TEST_CASE(cchar_with_message_arguments)
+{
+	const auto m = message_type::parse("!hello world", "!", "hangman");
+
+	BOOST_TEST(m.type == message_type::is_message);
+	BOOST_TEST(m.message == "!hello world");
+}
+
+BOOST_AUTO_TEST_CASE(command_with_different_cchar_short)
+{
+	const auto m = message_type::parse("!hello", ">", "hello");
+
+	BOOST_TEST(m.type == message_type::is_message);
+	BOOST_TEST(m.message == "!hello");
+}
+
+BOOST_AUTO_TEST_CASE(command_with_different_cchar_arguments)
+{
+	const auto m = message_type::parse("!hello", ">", "hello");
+
+	BOOST_TEST(m.type == message_type::is_message);
+	BOOST_TEST(m.message == "!hello");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd-daemon/server-util/ssl.conf	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,14 @@
+[server]
+name = "localhost"
+hostname = "irc.localhost"
+port = 6697
+ssl = true
+ssl-verify = false
+password = "secret"
+reconnect-timeout = 60
+nickname = "secure"
+username = "sc"
+realname = "SuperBot 2000 NT SSL"
+join-invite = true
+auto-rejoin = true
+auto-reconnect = true
--- a/tests/src/libirccd-js/js-api-logger/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/libirccd-js/js-api-logger/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -23,8 +23,11 @@
 
 #include <irccd/test/js_fixture.hpp>
 
-using namespace irccd::js;
-using namespace irccd::test;
+using irccd::daemon::logger::sink;
+
+using irccd::js::duk::get_stack;
+
+using irccd::test::js_fixture;
 
 namespace irccd {
 
@@ -36,7 +39,7 @@
 	std::string line_warning;
 	std::string line_debug;
 
-	class sample_sink : public logger::sink {
+	class sample_sink : public sink {
 	private:
 		logger_fixture& test_;
 
@@ -64,8 +67,8 @@
 
 	logger_fixture()
 	{
-		irccd_.set_log(std::make_unique<sample_sink>(*this));
-		irccd_.get_log().set_verbose(true);
+		bot_.set_log(std::make_unique<sample_sink>(*this));
+		bot_.get_log().set_verbose(true);
 	}
 };
 
@@ -74,7 +77,7 @@
 BOOST_AUTO_TEST_CASE(info)
 {
 	if (duk_peval_string(plugin_->get_context(), "Irccd.Logger.info(\"hello!\");") != 0)
-		throw duk::get_stack(plugin_->get_context(), -1);
+		throw get_stack(plugin_->get_context(), -1);
 
 	BOOST_TEST("plugin test: hello!" == line_info);
 }
@@ -82,7 +85,7 @@
 BOOST_AUTO_TEST_CASE(warning)
 {
 	if (duk_peval_string(plugin_->get_context(), "Irccd.Logger.warning(\"FAIL!\");") != 0)
-		throw duk::get_stack(plugin_->get_context(), -1);
+		throw get_stack(plugin_->get_context(), -1);
 
 	BOOST_TEST("plugin test: FAIL!" == line_warning);
 }
@@ -92,7 +95,7 @@
 BOOST_AUTO_TEST_CASE(debug)
 {
 	if (duk_peval_string(plugin_->get_context(), "Irccd.Logger.debug(\"starting\");") != 0)
-		throw duk::get_stack(plugin_->get_context(), -1);
+		throw get_stack(plugin_->get_context(), -1);
 
 	BOOST_TEST("plugin test: starting" == line_debug);
 }
--- a/tests/src/libirccd-js/js-api-timer/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/libirccd-js/js-api-timer/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -44,7 +44,7 @@
 		duk_pop_n(plugin_->get_context(), 2);
 
 		plugin_->open();
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
--- a/tests/src/libirccd-js/js-plugin/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/libirccd-js/js-plugin/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -26,7 +26,12 @@
 
 #include <irccd/test/irccd_fixture.hpp>
 
-using namespace irccd::test;
+using irccd::daemon::plugin;
+
+using irccd::test::irccd_fixture;
+
+using irccd::js::js_plugin;
+using irccd::js::js_api;
 
 namespace irccd {
 
@@ -34,14 +39,14 @@
 
 class js_plugin_fixture : public irccd_fixture {
 protected:
-	std::shared_ptr<js::js_plugin> plugin_;
+	std::shared_ptr<js_plugin> plugin_;
 
 	void load(const std::string& path)
 	{
-		plugin_ = std::make_unique<js::js_plugin>("test", path);
+		plugin_ = std::make_unique<js_plugin>("test", path);
 
-		for (const auto& f : js::js_api::registry)
-			f()->load(irccd_, plugin_);
+		for (const auto& f : js_api::registry)
+			f()->load(bot_, plugin_);
 
 		plugin_->open();
 	}
@@ -57,7 +62,7 @@
 		{ "path",	   "none"  },
 		{ "verbose",	"false" }
 	});
-	plugin_->handle_load(irccd_);
+	plugin_->handle_load(bot_);
 
 	BOOST_TEST(plugin_->get_options().at("path") == "none");
 	BOOST_TEST(plugin_->get_options().at("verbose") == "false");
@@ -72,7 +77,7 @@
 		{ "path",	   "none"  },
 		{ "verbose",	"false" }
 	});
-	plugin_->handle_load(irccd_);
+	plugin_->handle_load(bot_);
 
 	BOOST_TEST(plugin_->get_options().at("path") == "none");
 	BOOST_TEST(plugin_->get_options().at("verbose") == "false");
@@ -83,7 +88,7 @@
 {
 	load(CMAKE_CURRENT_SOURCE_DIR "/config-fill.js");
 
-	plugin_->handle_load(irccd_);
+	plugin_->handle_load(bot_);
 	plugin_->set_options({
 		{ "path",	   "none"  },
 		{ "verbose",	"false" }
@@ -102,20 +107,20 @@
 
 	js_plugin_loader_fixture()
 	{
-		irccd_.set_config(config(CMAKE_CURRENT_SOURCE_DIR "/irccd.conf"));
+		bot_.set_config(config(CMAKE_CURRENT_SOURCE_DIR "/irccd.conf"));
 
-		auto loader = std::make_unique<js::js_plugin_loader>(irccd_);
+		auto loader = std::make_unique<js::js_plugin_loader>(bot_);
 
 		for (const auto& f : js::js_api::registry)
 			loader->get_modules().push_back(f());
 
-		irccd_.plugins().add_loader(std::move(loader));
+		bot_.plugins().add_loader(std::move(loader));
 	}
 
 	void load(std::string name, std::string path)
 	{
-		irccd_.plugins().load(name, path);
-		plugin_ = irccd_.plugins().require(name);
+		bot_.plugins().load(name, path);
+		plugin_ = bot_.plugins().require(name);
 	}
 };
 
--- a/tests/src/libirccd/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/libirccd/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -16,37 +16,6 @@
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #
 
-add_subdirectory(command-plugin-config)
-add_subdirectory(command-plugin-info)
-add_subdirectory(command-plugin-list)
-add_subdirectory(command-plugin-load)
-add_subdirectory(command-plugin-reload)
-add_subdirectory(command-plugin-unload)
-add_subdirectory(command-rule-add)
-add_subdirectory(command-rule-edit)
-add_subdirectory(command-rule-info)
-add_subdirectory(command-rule-list)
-add_subdirectory(command-rule-move)
-add_subdirectory(command-rule-remove)
-add_subdirectory(command-server-connect)
-add_subdirectory(command-server-disconnect)
-add_subdirectory(command-server-info)
-add_subdirectory(command-server-invite)
-add_subdirectory(command-server-join)
-add_subdirectory(command-server-kick)
-add_subdirectory(command-server-list)
-add_subdirectory(command-server-me)
-add_subdirectory(command-server-message)
-add_subdirectory(command-server-mode)
-add_subdirectory(command-server-nick)
-add_subdirectory(command-server-notice)
-add_subdirectory(command-server-part)
-add_subdirectory(command-server-reconnect)
-add_subdirectory(command-server-topic)
-
-add_subdirectory(dynlib-plugin)
-add_subdirectory(irc)
-add_subdirectory(logger)
-add_subdirectory(rules)
-add_subdirectory(rule-util)
-add_subdirectory(server-util)
+add_subdirectory(fs-util)
+add_subdirectory(stream)
+add_subdirectory(string-util)
--- a/tests/src/libirccd/command-plugin-config/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-plugin-config
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-plugin-config/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/*
- * main.cpp -- test plugin-config remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-config"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(plugin_config_test_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(set)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-config" },
-		{ "plugin",     "test"          },
-		{ "variable",   "verbosy"       },
-		{ "value",      "falsy"         }
-	});
-
-	const auto config = irccd_.plugins().require("test")->get_options();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(!config.empty());
-	BOOST_TEST(config.at("verbosy") == "falsy");
-}
-
-BOOST_AUTO_TEST_CASE(get)
-{
-	auto plugin = std::make_unique<mock_plugin>("test");
-
-	plugin->set_options({
-		{ "x1", "10" },
-		{ "x2", "20" }
-	});
-	irccd_.plugins().clear();
-	irccd_.plugins().add(std::move(plugin));
-
-	const auto [json, code] = request({
-		{ "command",    "plugin-config" },
-		{ "plugin",     "test"          },
-		{ "variable",   "x1"            }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
-	BOOST_TEST(json["variables"].count("x2") == 0U);
-}
-
-BOOST_AUTO_TEST_CASE(getall)
-{
-	auto plugin = std::make_unique<mock_plugin>("test");
-
-	plugin->set_options({
-		{ "x1", "10" },
-		{ "x2", "20" }
-	});
-	irccd_.plugins().clear();
-	irccd_.plugins().add(std::move(plugin));
-
-	const auto [json, code] = request({
-		{ "command",    "plugin-config" },
-		{ "plugin",     "test"          }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json["variables"]["x1"].get<std::string>() == "10");
-	BOOST_TEST(json["variables"]["x2"].get<std::string>() == "20");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-	const auto [json, code] = request({
-		{ "command", "plugin-config" }
-	});
-
-	BOOST_TEST(code == plugin_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-config" },
-		{ "plugin",     "unknown"       }
-	});
-
-	BOOST_TEST(code == plugin_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-plugin-info/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-plugin-info
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-plugin-info/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-/*
- * main.cpp -- test plugin-info remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-info"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(plugin_info_test_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-info"   },
-		{ "plugin",     "test"          },
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json["author"].get<std::string>() == "David Demelier <markand@malikania.fr>");
-	BOOST_TEST(json["license"].get<std::string>() == "ISC");
-	BOOST_TEST(json["summary"].get<std::string>() == "mock plugin");
-	BOOST_TEST(json["version"].get<std::string>() == "1.0");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-	const auto [json, code] = request({
-		{ "command", "plugin-info" }
-	});
-
-	BOOST_TEST(code == plugin_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-info"   },
-		{ "plugin",     "unknown"       }
-	});
-
-	BOOST_TEST(code == plugin_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-plugin-list/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-plugin-list
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-plugin-list/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/*
- * main.cpp -- test plugin-list remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-list"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class plugin_list_fixture : public command_fixture {
-public:
-	plugin_list_fixture()
-	{
-		irccd_.plugins().clear();
-		irccd_.plugins().add(std::make_unique<mock_plugin>("t1"));
-		irccd_.plugins().add(std::make_unique<mock_plugin>("t2"));
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(plugin_list_fixture_suite, plugin_list_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command", "plugin-list" }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json["list"][0].get<std::string>() == "t1");
-	BOOST_TEST(json["list"][1].get<std::string>() == "t2");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-plugin-load/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-plugin-load
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-plugin-load/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-/*
- * main.cpp -- test plugin-load remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-load"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class broken : public plugin {
-public:
-	broken()
-		: plugin("broken")
-	{
-	}
-
-	auto get_name() const noexcept -> std::string_view override
-	{
-		return "broken";
-	}
-
-	void handle_load(irccd&) override
-	{
-		throw std::runtime_error("broken");
-	}
-};
-
-class broken_loader : public plugin_loader {
-public:
-	broken_loader()
-		: plugin_loader({}, { ".none" })
-	{
-	}
-
-	auto open(std::string_view, std::string_view) -> std::shared_ptr<plugin> override
-	{
-		return nullptr;
-	}
-
-	auto find(std::string_view id) noexcept -> std::shared_ptr<plugin> override
-	{
-		if (id == "broken")
-			return std::make_unique<broken>();
-
-		return nullptr;
-	}
-};
-
-class sample_loader : public plugin_loader {
-public:
-	sample_loader()
-		: plugin_loader({}, { ".none" })
-	{
-	}
-
-	auto open(std::string_view, std::string_view) -> std::shared_ptr<plugin> override
-	{
-		return nullptr;
-	}
-
-	auto find(std::string_view id) noexcept -> std::shared_ptr<plugin> override
-	{
-		if (id == "test")
-			return std::make_unique<mock_plugin>("test");
-
-		return nullptr;
-	}
-};
-
-class plugin_load_fixture : public command_fixture {
-public:
-	plugin_load_fixture()
-	{
-		irccd_.plugins().add_loader(std::make_unique<sample_loader>());
-		irccd_.plugins().add_loader(std::make_unique<broken_loader>());
-		irccd_.plugins().clear();
-		irccd_.plugins().add(std::make_unique<mock_plugin>("already"));
-	}
-};
-
-} // !namespace
-
-BOOST_FIXTURE_TEST_SUITE(plugin_load_fixture_suite, plugin_load_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-load"   },
-		{ "plugin",     "test"          }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(irccd_.plugins().has("test"));
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-	const auto [json, code] = request({
-		{ "command", "plugin-load" }
-	});
-
-	BOOST_TEST(code == plugin_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-load"   },
-		{ "plugin",     "unknown"       }
-	});
-
-	BOOST_TEST(code == plugin_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(already_exists)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-load"   },
-		{ "plugin",     "already"       }
-	});
-
-	BOOST_TEST(code == plugin_error::already_exists);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::already_exists);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(exec_error)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-load"   },
-		{ "plugin",     "broken"        }
-	});
-
-	BOOST_TEST(code == plugin_error::exec_error);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::exec_error);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !irccd
--- a/tests/src/libirccd/command-plugin-reload/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-    NAME command-plugin-reload
-    SOURCES main.cpp
-    LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-plugin-reload/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * main.cpp -- test plugin-reload remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-reload"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class broken_plugin : public plugin {
-public:
-	broken_plugin()
-		: plugin("broken")
-	{
-	}
-
-	auto get_name() const noexcept -> std::string_view override
-	{
-		return "broken";
-	}
-
-	void handle_reload(irccd&) override
-	{
-		throw std::runtime_error("broken");
-	}
-};
-
-class plugin_reload_fixture : public command_fixture {
-protected:
-	std::shared_ptr<mock_plugin> plugin_;
-
-	plugin_reload_fixture()
-		: plugin_(std::make_shared<mock_plugin>("test"))
-	{
-		irccd_.plugins().clear();
-		irccd_.plugins().add(plugin_);
-		irccd_.plugins().add(std::make_unique<broken_plugin>());
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(plugin_reload_fixture_suite, plugin_reload_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-reload" },
-		{ "plugin",     "test"          }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(plugin_->find("handle_reload").size() == 1U);
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-	const auto [json, code] = request({
-		{ "command", "plugin-reload" }
-	});
-
-	BOOST_TEST(code == plugin_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-reload" },
-		{ "plugin",     "unknown"       }
-	});
-
-	BOOST_TEST(code == plugin_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(exec_error)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-reload" },
-		{ "plugin",     "broken"        }
-	});
-
-	BOOST_TEST(code == plugin_error::exec_error);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::exec_error);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-plugin-unload/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-plugin-unload
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-plugin-unload/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-/*
- * main.cpp -- test plugin-unload remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "plugin-unload"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class broken_plugin : public plugin {
-public:
-	broken_plugin()
-		: plugin("broken")
-	{
-	}
-
-	auto get_name() const noexcept -> std::string_view override
-	{
-		return "broken";
-	}
-
-	void handle_unload(irccd&) override
-	{
-		throw std::runtime_error("broken");
-	}
-};
-
-class plugin_unload_fixture : public command_fixture {
-protected:
-	std::shared_ptr<mock_plugin> plugin_;
-
-	plugin_unload_fixture()
-		: plugin_(std::make_shared<mock_plugin>("test"))
-	{
-		irccd_.plugins().clear();
-		irccd_.plugins().add(plugin_);
-		irccd_.plugins().add(std::make_unique<broken_plugin>());
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(plugin_unload_fixture_suite, plugin_unload_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-unload" },
-		{ "plugin",     "test"          }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(plugin_->find("handle_unload").size() == 1U);
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-	const auto [json, code] = request({
-		{ "command", "plugin-unload" }
-	});
-
-	BOOST_TEST(code == plugin_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-unload" },
-		{ "plugin",     "unknown"       }
-	});
-
-	BOOST_TEST(code == plugin_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-}
-
-BOOST_AUTO_TEST_CASE(exec_error)
-{
-	const auto [json, code] = request({
-		{ "command",    "plugin-unload" },
-		{ "plugin",     "broken"        }
-	});
-
-	BOOST_TEST(code == plugin_error::exec_error);
-	BOOST_TEST(json["error"].get<int>() == plugin_error::exec_error);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "plugin");
-	BOOST_TEST(!irccd_.plugins().has("broken"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-rule-add/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-rule-add
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-rule-add/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/*
- * main.cpp -- test rule-add remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-add"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(rule_add_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	request({
-		{ "command",    "rule-add"          },
-		{ "servers",    { "s1", "s2" }      },
-		{ "channels",   { "c1", "c2" }      },
-		{ "plugins",    { "p1", "p2" }      },
-		{ "events",     { "onMessage" }     },
-		{ "action",     "accept"            },
-		{ "index",      0                   }
-	});
-
-	const auto [json, code] = request({
-		{ "command", "rule-list" }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-
-	auto servers = json["list"][0]["servers"];
-	auto channels = json["list"][0]["channels"];
-	auto plugins = json["list"][0]["plugins"];
-	auto events = json["list"][0]["events"];
-
-	BOOST_TEST(json_util::contains(servers, "s1"));
-	BOOST_TEST(json_util::contains(servers, "s2"));
-	BOOST_TEST(json_util::contains(channels, "c1"));
-	BOOST_TEST(json_util::contains(channels, "c2"));
-	BOOST_TEST(json_util::contains(plugins, "p1"));
-	BOOST_TEST(json_util::contains(plugins, "p2"));
-	BOOST_TEST(json_util::contains(events, "onMessage"));
-	BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
-}
-
-BOOST_AUTO_TEST_CASE(append)
-{
-	request({
-		{ "command",    "rule-add"          },
-		{ "servers",    { "s1" }            },
-		{ "channels",   { "c1" }            },
-		{ "plugins",    { "p1" }            },
-		{ "events",     { "onMessage" }     },
-		{ "action",     "accept"            },
-		{ "index",      0                   }
-	});
-
-	request({
-		{ "command",    "rule-add"          },
-		{ "servers",    { "s2" }            },
-		{ "channels",   { "c2" }            },
-		{ "plugins",    { "p2" }            },
-		{ "events",     { "onMessage" }     },
-		{ "action",     "drop"              },
-		{ "index",      1                   }
-	});
-
-	const auto [json, code] = request({
-		{ "command", "rule-list" }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json["list"].size() == 2U);
-
-	// Rule 0.
-	{
-		auto servers = json["list"][0]["servers"];
-		auto channels = json["list"][0]["channels"];
-		auto plugins = json["list"][0]["plugins"];
-		auto events = json["list"][0]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 1.
-	{
-		auto servers = json["list"][1]["servers"];
-		auto channels = json["list"][1]["channels"];
-		auto plugins = json["list"][1]["plugins"];
-		auto events = json["list"][1]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s2"));
-		BOOST_TEST(json_util::contains(channels, "c2"));
-		BOOST_TEST(json_util::contains(plugins, "p2"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "drop");
-	}
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_action)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-add"  },
-		{ "action",     "unknown"   }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_action);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_action);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-rule-edit/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-rule-edit
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-rule-edit/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,403 +0,0 @@
-/*
- * main.cpp -- test rule-edit remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-edit"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class rule_edit_fixture : public command_fixture {
-public:
-	rule_edit_fixture()
-	{
-		irccd_.rules().add(rule{
-			{ "s1", "s2" },
-			{ "c1", "c2" },
-			{ "o1", "o2" },
-			{ "p1", "p2" },
-			{ "onMessage", "onCommand" },
-			rule::action_type::drop
-		});
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(rule_edit_fixture_suite, rule_edit_fixture)
-
-BOOST_AUTO_TEST_CASE(add_server)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "add-servers",    { "new-s3" }    },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["servers"], "new-s3"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_channel)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "add-channels",   { "new-c3" }    },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["channels"], "new-c3"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_plugin)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "add-plugins",    { "new-p3" }    },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "new-p3"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_event)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "add-events",     { "onQuery" }   },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json_util::contains(json["events"], "onQuery"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(add_event_and_server)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "add-servers",    { "new-s3" }    },
-		{ "add-events",     { "onQuery" }   },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["servers"], "new-s3"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json_util::contains(json["events"], "onQuery"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(change_action)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "action",         "accept"        },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "accept");
-}
-
-BOOST_AUTO_TEST_CASE(remove_server)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "remove-servers", { "s2" }        },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(!json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_channel)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "remove-channels", { "c2" }       },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(!json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_plugin)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "remove-plugins", { "p2" }        },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(!json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_event)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "remove-events",  { "onCommand" } },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(!json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_CASE(remove_event_and_server)
-{
-	request({
-		{ "command",        "rule-edit"     },
-		{ "remove-servers", { "s2" }        },
-		{ "remove-events",  { "onCommand" } },
-		{ "index",          0               }
-	});
-
-	const auto [json, code] = request({
-		{ "command",        "rule-info"     },
-		{ "index",          0               }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json_util::contains(json["servers"], "s1"));
-	BOOST_TEST(!json_util::contains(json["servers"], "s2"));
-	BOOST_TEST(json_util::contains(json["channels"], "c1"));
-	BOOST_TEST(json_util::contains(json["channels"], "c2"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p1"));
-	BOOST_TEST(json_util::contains(json["plugins"], "p2"));
-	BOOST_TEST(json_util::contains(json["events"], "onMessage"));
-	BOOST_TEST(!json_util::contains(json["events"], "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-edit" },
-		{ "index",      -100        },
-		{ "action",     "drop"      }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-edit" },
-		{ "index",      100         },
-		{ "action",     "drop"      }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-edit" },
-		{ "index",      "notaint"   },
-		{ "action",     "drop"      }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_action)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-edit" },
-		{ "index",      0           },
-		{ "action",     "unknown"   }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_action);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_action);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-rule-info/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-rule-info
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-rule-info/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +0,0 @@
-/*
- * main.cpp -- test rule-info remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-info"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class rule_info_fixture : public command_fixture {
-public:
-	rule_info_fixture()
-	{
-		irccd_.rules().add(rule{
-			{ "s1", "s2" },
-			{ "c1", "c2" },
-			{ "o1", "o2" },
-			{ "p1", "p2" },
-			{ "onMessage", "onCommand" },
-			rule::action_type::drop
-		});
-		irccd_.rules().add(rule{
-			{ "s1", },
-			{ "c1", },
-			{ "o1", },
-			{ "p1", },
-			{ "onMessage", },
-			rule::action_type::accept
-		});
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(rule_info_fixture_suite, rule_info_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-info" },
-		{ "index",      0           }
-	});
-
-	auto servers = json["servers"];
-	auto channels = json["channels"];
-	auto plugins = json["plugins"];
-	auto events = json["events"];
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json_util::contains(servers, "s1"));
-	BOOST_TEST(json_util::contains(servers, "s2"));
-	BOOST_TEST(json_util::contains(channels, "c1"));
-	BOOST_TEST(json_util::contains(channels, "c2"));
-	BOOST_TEST(json_util::contains(plugins, "p1"));
-	BOOST_TEST(json_util::contains(plugins, "p2"));
-	BOOST_TEST(json_util::contains(events, "onMessage"));
-	BOOST_TEST(json_util::contains(events, "onCommand"));
-	BOOST_TEST(json["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-info" },
-		{ "index",      -100        }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-info" },
-		{ "index",      100         }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-info" },
-		{ "index",      "notaint"   }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-rule-list/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-rule-list
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-rule-list/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/*
- * main.cpp -- test rule-list remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-list"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class rule_list_fixture : public command_fixture {
-public:
-	rule_list_fixture()
-	{
-		irccd_.rules().add(rule{
-			{ "s1", "s2" },
-			{ "c1", "c2" },
-			{ "o1", "o2" },
-			{ "p1", "p2" },
-			{ "onMessage", "onCommand" },
-			rule::action_type::drop
-		});
-		irccd_.rules().add(rule{
-			{ "s1", },
-			{ "c1", },
-			{ "o1", },
-			{ "p1", },
-			{ "onMessage", },
-			rule::action_type::accept
-		});
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(rule_list_fixture_suite, rule_list_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json["list"].is_array());
-	BOOST_TEST(json["list"].size() == 2U);
-
-	// Rule 0.
-	{
-		auto servers = json["list"][0]["servers"];
-		auto channels = json["list"][0]["channels"];
-		auto plugins = json["list"][0]["plugins"];
-		auto events = json["list"][0]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(servers, "s2"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(channels, "c2"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(plugins, "p2"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json_util::contains(events, "onCommand"));
-		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "drop");
-	}
-
-	// Rule 1.
-	{
-		auto servers = json["list"][1]["servers"];
-		auto channels = json["list"][1]["channels"];
-		auto plugins = json["list"][1]["plugins"];
-		auto events = json["list"][1]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
-	}
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-	irccd_.rules().remove(0);
-	irccd_.rules().remove(0);
-
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json["list"].is_array());
-	BOOST_TEST(json["list"].empty());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-rule-move/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-rule-move
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-rule-move/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,362 +0,0 @@
-/*
- * main.cpp -- test rule-move remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-move"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class rule_move_fixture : public command_fixture {
-public:
-	rule_move_fixture()
-	{
-		irccd_.rules().add(rule{
-			{ "s0" },
-			{ "c0" },
-			{ "o0" },
-			{ "p0" },
-			{ "onMessage" },
-			rule::action_type::drop
-		});
-		irccd_.rules().add(rule{
-			{ "s1", },
-			{ "c1", },
-			{ "o1", },
-			{ "p1", },
-			{ "onMessage", },
-			rule::action_type::accept
-		});
-		irccd_.rules().add(rule{
-			{ "s2", },
-			{ "c2", },
-			{ "o2", },
-			{ "p2", },
-			{ "onMessage", },
-			rule::action_type::accept
-		});
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(rule_move_fixture_suite, rule_move_fixture)
-
-BOOST_AUTO_TEST_CASE(backward)
-{
-	request({
-		{ "command",    "rule-move" },
-		{ "from",       2           },
-		{ "to",         0           }
-	});
-
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-
-	// Rule 2.
-	{
-		auto servers = json["list"][0]["servers"];
-		auto channels = json["list"][0]["channels"];
-		auto plugins = json["list"][0]["plugins"];
-		auto events = json["list"][0]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s2"));
-		BOOST_TEST(json_util::contains(channels, "c2"));
-		BOOST_TEST(json_util::contains(plugins, "p2"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 0.
-	{
-		auto servers = json["list"][1]["servers"];
-		auto channels = json["list"][1]["channels"];
-		auto plugins = json["list"][1]["plugins"];
-		auto events = json["list"][1]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s0"));
-		BOOST_TEST(json_util::contains(channels, "c0"));
-		BOOST_TEST(json_util::contains(plugins, "p0"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "drop");
-	}
-
-	// Rule 1.
-	{
-		auto servers = json["list"][2]["servers"];
-		auto channels = json["list"][2]["channels"];
-		auto plugins = json["list"][2]["plugins"];
-		auto events = json["list"][2]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "accept");
-	}
-}
-
-BOOST_AUTO_TEST_CASE(upward)
-{
-	request({
-		{ "command",    "rule-move" },
-		{ "from",       0           },
-		{ "to",         2           }
-	});
-
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-
-	// Rule 1.
-	{
-		auto servers = json["list"][0]["servers"];
-		auto channels = json["list"][0]["channels"];
-		auto plugins = json["list"][0]["plugins"];
-		auto events = json["list"][0]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 2.
-	{
-		auto servers = json["list"][1]["servers"];
-		auto channels = json["list"][1]["channels"];
-		auto plugins = json["list"][1]["plugins"];
-		auto events = json["list"][1]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s2"));
-		BOOST_TEST(json_util::contains(channels, "c2"));
-		BOOST_TEST(json_util::contains(plugins, "p2"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 0.
-	{
-		auto servers = json["list"][2]["servers"];
-		auto channels = json["list"][2]["channels"];
-		auto plugins = json["list"][2]["plugins"];
-		auto events = json["list"][2]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s0"));
-		BOOST_TEST(json_util::contains(channels, "c0"));
-		BOOST_TEST(json_util::contains(plugins, "p0"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "drop");
-	}
-}
-
-BOOST_AUTO_TEST_CASE(same)
-{
-	request({
-		{ "command",    "rule-move" },
-		{ "from",       1           },
-		{ "to",         1           }
-	});
-
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-
-	// Rule 0.
-	{
-		auto servers = json["list"][0]["servers"];
-		auto channels = json["list"][0]["channels"];
-		auto plugins = json["list"][0]["plugins"];
-		auto events = json["list"][0]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s0"));
-		BOOST_TEST(json_util::contains(channels, "c0"));
-		BOOST_TEST(json_util::contains(plugins, "p0"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "drop");
-	}
-
-	// Rule 1.
-	{
-		auto servers = json["list"][1]["servers"];
-		auto channels = json["list"][1]["channels"];
-		auto plugins = json["list"][1]["plugins"];
-		auto events = json["list"][1]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 2.
-	{
-		auto servers = json["list"][2]["servers"];
-		auto channels = json["list"][2]["channels"];
-		auto plugins = json["list"][2]["plugins"];
-		auto events = json["list"][2]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s2"));
-		BOOST_TEST(json_util::contains(channels, "c2"));
-		BOOST_TEST(json_util::contains(plugins, "p2"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "accept");
-	}
-}
-
-BOOST_AUTO_TEST_CASE(beyond)
-{
-	request({
-		{ "command",    "rule-move" },
-		{ "from",       0           },
-		{ "to",         123         }
-	});
-
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-
-	// Rule 1.
-	{
-		auto servers = json["list"][0]["servers"];
-		auto channels = json["list"][0]["channels"];
-		auto plugins = json["list"][0]["plugins"];
-		auto events = json["list"][0]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s1"));
-		BOOST_TEST(json_util::contains(channels, "c1"));
-		BOOST_TEST(json_util::contains(plugins, "p1"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][0]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 2.
-	{
-		auto servers = json["list"][1]["servers"];
-		auto channels = json["list"][1]["channels"];
-		auto plugins = json["list"][1]["plugins"];
-		auto events = json["list"][1]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s2"));
-		BOOST_TEST(json_util::contains(channels, "c2"));
-		BOOST_TEST(json_util::contains(plugins, "p2"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][1]["action"].get<std::string>() == "accept");
-	}
-
-	// Rule 0.
-	{
-		auto servers = json["list"][2]["servers"];
-		auto channels = json["list"][2]["channels"];
-		auto plugins = json["list"][2]["plugins"];
-		auto events = json["list"][2]["events"];
-
-		BOOST_TEST(json_util::contains(servers, "s0"));
-		BOOST_TEST(json_util::contains(channels, "c0"));
-		BOOST_TEST(json_util::contains(plugins, "p0"));
-		BOOST_TEST(json_util::contains(events, "onMessage"));
-		BOOST_TEST(json["list"][2]["action"].get<std::string>() == "drop");
-	}
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1_from)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-move" },
-		{ "from",       -100        },
-		{ "to",         0           }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_1_to)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-move" },
-		{ "from",       0           },
-		{ "to",         -100        }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2_from)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-move" },
-		{ "from",       100         },
-		{ "to",         0           }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3_from)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-move" },
-		{ "from",       "notaint"   },
-		{ "to",         0           }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3_to)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-move" },
-		{ "from",       0           },
-		{ "to",         "notaint"   }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-rule-remove/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-rule-remove
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-rule-remove/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/*
- * main.cpp -- test rule-remove remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule-remove"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/json_util.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class rule_remove_fixture : public command_fixture {
-public:
-	rule_remove_fixture()
-	{
-		irccd_.rules().add(rule{
-			{ "s1", "s2" },
-			{ "c1", "c2" },
-			{ "o1", "o2" },
-			{ "p1", "p2" },
-			{ "onMessage", "onCommand" },
-			rule::action_type::drop
-		});
-		irccd_.rules().add(rule{
-			{ "s1", },
-			{ "c1", },
-			{ "o1", },
-			{ "p1", },
-			{ "onMessage", },
-			rule::action_type::accept
-		});
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(rule_remove_fixture_suite, rule_remove_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	request({
-		{ "command",    "rule-remove"   },
-		{ "index",      1               }
-	});
-
-	const auto [json, code] = request({{ "command", "rule-list" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json["list"].is_array());
-	BOOST_TEST(json["list"].size() == 1U);
-
-	auto servers = json["list"][0]["servers"];
-	auto channels = json["list"][0]["channels"];
-	auto plugins = json["list"][0]["plugins"];
-	auto events = json["list"][0]["events"];
-
-	BOOST_TEST(json_util::contains(servers, "s1"));
-	BOOST_TEST(json_util::contains(servers, "s2"));
-	BOOST_TEST(json_util::contains(channels, "c1"));
-	BOOST_TEST(json_util::contains(channels, "c2"));
-	BOOST_TEST(json_util::contains(plugins, "p1"));
-	BOOST_TEST(json_util::contains(plugins, "p2"));
-	BOOST_TEST(json_util::contains(events, "onMessage"));
-	BOOST_TEST(json_util::contains(events, "onCommand"));
-	BOOST_TEST(json["list"][0]["action"].get<std::string>() == "drop");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_index_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-remove"   },
-		{ "index",      -100            }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-remove"   },
-		{ "index",      100             }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_index_3)
-{
-	const auto [json, code] = request({
-		{ "command",    "rule-remove"   },
-		{ "index",      "notaint"       }
-	});
-
-	BOOST_TEST(code == rule_error::invalid_index);
-	BOOST_TEST(json["error"].get<int>() == rule_error::invalid_index);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "rule");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-connect/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-connect
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-connect/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-/*
- * main.cpp -- test server-connect remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-connect"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_connect_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(minimal)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"    },
-		{ "name",       "local"             },
-		{ "hostname",   "irc.example.org"   }
-	});
-
-	const auto s = irccd_.servers().get("local");
-
-	BOOST_TEST(!code);
-	BOOST_TEST(s);
-	BOOST_TEST(s->get_id() == "local");
-	BOOST_TEST(s->get_hostname() == "irc.example.org");
-	BOOST_TEST(s->get_port() == 6667U);
-}
-
-#if defined(IRCCD_HAVE_SSL)
-
-BOOST_AUTO_TEST_CASE(full)
-{
-	const auto [json, code] = request({
-		{ "command",            "server-connect"        },
-		{ "name",               "local2"                },
-		{ "hostname",           "irc.example2.org"      },
-		{ "password",           "nonono"                },
-		{ "nickname",           "francis"               },
-		{ "realname",           "the_francis"           },
-		{ "username",           "frc"                   },
-		{ "ipv4",               false                   },
-		{ "ipv6",               true                    },
-		{ "ctcpVersion",        "ultra bot"             },
-		{ "commandChar",        "::"                    },
-		{ "port",               18000                   },
-		{ "ssl",                true                    },
-		{ "sslVerify",          true                    },
-		{ "autoRejoin",         true                    },
-		{ "joinInvite",         true                    }
-	});
-
-	const auto s = irccd_.servers().get("local2");
-
-	BOOST_TEST(!code);
-	BOOST_TEST(s);
-	BOOST_TEST(s->get_id() == "local2");
-	BOOST_TEST(s->get_hostname() == "irc.example2.org");
-	BOOST_TEST(s->get_port() == 18000U);
-	BOOST_TEST(s->get_password() == "nonono");
-	BOOST_TEST(s->get_nickname() == "francis");
-	BOOST_TEST(s->get_realname() == "the_francis");
-	BOOST_TEST(s->get_username() == "frc");
-	BOOST_TEST(s->get_command_char() == "::");
-	BOOST_TEST(s->get_ctcp_version() == "ultra bot");
-	BOOST_TEST(!static_cast<bool>(s->get_options() & server::options::ipv4));
-	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::ipv6));
-	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::ssl));
-	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::auto_rejoin));
-	BOOST_TEST(static_cast<bool>(s->get_options() & server::options::join_invite));
-}
-
-#endif // !IRCCD_HAVE_SSL
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(already_exists)
-{
-	irccd_.servers().add(std::make_unique<mock_server>(ctx_, "local"));
-
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "local"                 },
-		{ "hostname",   "127.0.0.1"             }
-	});
-
-	BOOST_TEST(code == server_error::already_exists);
-	BOOST_TEST(json["error"].get<int>() == server_error::already_exists);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_hostname_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-	});
-
-	BOOST_TEST(code == server_error::invalid_hostname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_hostname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_hostname_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   123456                  }
-	});
-
-	BOOST_TEST(code == server_error::invalid_hostname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_hostname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       ""                      },
-		{ "hostname",   "127.0.0.1"             }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       123456                  },
-		{ "hostname",   "127.0.0.1"             }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_port_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   "127.0.0.1"             },
-		{ "port",       "notaint"               }
-	});
-
-	BOOST_TEST(code == server_error::invalid_port);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_port);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_port_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   "127.0.0.1"             },
-		{ "port",       -123                    }
-	});
-
-	BOOST_TEST(code == server_error::invalid_port);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_port);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_port_3)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   "127.0.0.1"             },
-		{ "port",       1000000                 }
-	});
-
-	BOOST_TEST(code == server_error::invalid_port);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_port);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-#if !defined(IRCCD_HAVE_SSL)
-
-BOOST_AUTO_TEST_CASE(ssl_disabled)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   "127.0.0.1"             },
-		{ "ssl",        true                    }
-	});
-
-	BOOST_TEST(code == server_error::ssl_disabled);
-	BOOST_TEST(json["error"].get<int>() == server_error::ssl_disabled);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-#endif
-
-BOOST_AUTO_TEST_CASE(invalid_family_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   "127.0.0.1"             },
-		{ "port",       6667                    },
-		{ "ipv4",       "invalid"               }
-	});
-
-	BOOST_TEST(code == server_error::invalid_family);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_family);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_family_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-connect"        },
-		{ "name",       "new"                   },
-		{ "hostname",   "127.0.0.1"             },
-		{ "port",       6667                    },
-		{ "ipv6",       1234                    }
-	});
-
-	BOOST_TEST(code == server_error::invalid_family);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_family);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-disconnect/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-disconnect
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-disconnect/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/*
- * main.cpp -- test server-disconnect remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-disconnect"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class server_disconnect_fixture : public command_fixture {
-protected:
-	std::shared_ptr<mock_server> s1_;
-	std::shared_ptr<mock_server> s2_;
-
-	server_disconnect_fixture()
-		: s1_(new mock_server(ctx_, "s1", "localhost"))
-		, s2_(new mock_server(ctx_, "s2", "localhost"))
-	{
-		irccd_.servers().add(s1_);
-		irccd_.servers().add(s2_);
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(server_disconnect_fixture_suite, server_disconnect_fixture)
-
-BOOST_AUTO_TEST_CASE(one)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-disconnect"     },
-		{ "server",     "s1"                    }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json["command"].get<std::string>() == "server-disconnect");
-	BOOST_TEST(s1_->find("disconnect").size() == 1U);
-	BOOST_TEST(!irccd_.servers().has("s1"));
-	BOOST_TEST(irccd_.servers().has("s2"));
-}
-
-BOOST_AUTO_TEST_CASE(all)
-{
-	const auto [json, code] = request({{ "command", "server-disconnect" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json["command"].get<std::string>() == "server-disconnect");
-	BOOST_TEST(s1_->find("disconnect").size() == 1U);
-	BOOST_TEST(s2_->find("disconnect").size() == 1U);
-	BOOST_TEST(!irccd_.servers().has("s1"));
-	BOOST_TEST(!irccd_.servers().has("s2"));
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-disconnect"     },
-		{ "server",     123456                  }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-disconnect"     },
-		{ "server",     "unknown"               }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-info/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-info
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-info/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-/*
- * main.cpp -- test server-info remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-info"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_info_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	auto server = std::make_unique<mock_server>(ctx_, "test", "example.org");
-
-	server->set_port(8765);
-	server->set_password("none");
-	server->set_nickname("pascal");
-	server->set_username("psc");
-	server->set_realname("Pascal le grand frere");
-	server->set_ctcp_version("yeah");
-	server->set_command_char("@");
-	server->set_ping_timeout(20000);
-
-	irccd_.servers().clear();
-	irccd_.servers().add(std::move(server));
-
-	const auto [json, code] = request({
-		{ "command",    "server-info"   },
-		{ "server",     "test"          },
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json["hostname"].get<std::string>() == "example.org");
-	BOOST_TEST(json["name"].get<std::string>() == "test");
-	BOOST_TEST(json["nickname"].get<std::string>() == "pascal");
-	BOOST_TEST(json["port"].get<int>() == 8765);
-	BOOST_TEST(json["realname"].get<std::string>() == "Pascal le grand frere");
-	BOOST_TEST(json["username"].get<std::string>() == "psc");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-info"   },
-		{ "server",     123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-info"   },
-		{ "server",     ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-info"   },
-		{ "server",     "unknown"       }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-invite/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-invite
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-invite/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-/*
- * main.cpp -- test server-invite remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-invite"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_invite_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     "test"          },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	const auto cmd = server_->find("invite").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "francis");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "#music");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     123456          },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     ""              },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     "test"          },
-		{ "target",     ""              },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_nickname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     "test"          },
-		{ "target",     123456          },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_nickname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     "test"          },
-		{ "target",     "jean"          },
-		{ "channel",    ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     "test"          },
-		{ "target",     "jean"          },
-		{ "channel",    123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-invite" },
-		{ "server",     "unknown"       },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-join/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-join
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-join/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/*
- * main.cpp -- test server-join remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-join"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_join_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     "test"          },
-		{ "channel",    "#music"        },
-		{ "password",   "plop"          }
-	});
-
-	const auto cmd = server_->find("join").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#music");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "plop");
-}
-
-BOOST_AUTO_TEST_CASE(nopassword)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     "test"          },
-		{ "channel",    "#music"        }
-	});
-
-	const auto cmd = server_->find("join").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#music");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     123456          },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     ""              },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     "test"          },
-		{ "channel",    ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     "test"          },
-		{ "channel",    123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_password)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     "test"          },
-		{ "channel",    "#staff"        },
-		{ "password",   123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_password);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_password);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-join"   },
-		{ "server",     "unknown"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-kick/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-kick
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-kick/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,188 +0,0 @@
-/*
- * main.cpp -- test server-kick remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-kick"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_kick_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     "francis"       },
-		{ "channel",    "#staff"        },
-		{ "reason",     "too noisy"     }
-	});
-
-	const auto cmd = server_->find("kick").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "#staff");
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "francis");
-	BOOST_TEST(std::any_cast<std::string>(cmd[2]) == "too noisy");
-}
-
-BOOST_AUTO_TEST_CASE(noreason)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     "francis"       },
-		{ "channel",    "#staff"        }
-	});
-
-	const auto cmd = server_->find("kick").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "#staff");
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "francis");
-	BOOST_TEST(std::any_cast<std::string>(cmd[2]) == "");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     123456          },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     ""              },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     ""              },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_nickname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     123456          },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_nickname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     "jean"          },
-		{ "channel",    ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     "jean"          },
-		{ "channel",    123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_message)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "test"          },
-		{ "target",     "jean"          },
-		{ "channel",    "#staff"        },
-		{ "reason",     123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_message);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_message);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-kick"   },
-		{ "server",     "unknown"       },
-		{ "target",     "francis"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-list/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-list
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-list/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/*
- * main.cpp -- test server-list remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-list"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class server_list_fixture : public command_fixture {
-protected:
-	server_list_fixture()
-	{
-		irccd_.servers().clear();
-		irccd_.servers().add(std::make_unique<mock_server>(ctx_, "s1", "localhost"));
-		irccd_.servers().add(std::make_unique<mock_server>(ctx_, "s2", "localhost"));
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(server_list_fixture_suite, server_list_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command", "server-list" }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(json["list"].is_array());
-	BOOST_TEST(json["list"].size() == 2U);
-	BOOST_TEST(json["list"][0].get<std::string>() == "s1");
-	BOOST_TEST(json["list"][1].get<std::string>() == "s2");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-me/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-me
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-me/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/*
- * main.cpp -- test server-me remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-me"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_me_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-me"     },
-		{ "server",     "test"          },
-		{ "target",     "jean"          },
-		{ "message",    "hello!"        }
-	});
-
-	const auto cmd = server_->find("me").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "hello!");
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "jean");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-me"     },
-		{ "server",     123456          },
-		{ "target",     "#music"        },
-		{ "message",    "hello!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-me"     },
-		{ "server",     ""              },
-		{ "target",     "#music"        },
-		{ "message",    "hello!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-me"     },
-		{ "server",     "test"          },
-		{ "target",     ""              },
-		{ "message",    "hello!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-me"     },
-		{ "server",     "test"          },
-		{ "target",     123456          },
-		{ "message",    "hello!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-me"     },
-		{ "server",     "unknown"       },
-		{ "target",     "#music"        },
-		{ "message",    "hello!"        }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-message/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-message
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-message/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/*
- * main.cpp -- test server-message remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-message"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_message_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-message"        },
-		{ "server",     "test"                  },
-		{ "target",     "#staff"                },
-		{ "message",    "plop!"                 }
-	});
-
-	const auto cmd = server_->find("message").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "plop!");
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-message"        },
-		{ "server",     123456                  },
-		{ "target",     "#music"                },
-		{ "message",    "plop!"                 }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-message"        },
-		{ "server",     ""                      },
-		{ "target",     "#music"                },
-		{ "message",    "plop!"                 }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-message"        },
-		{ "server",     "test"                  },
-		{ "target",     ""                      },
-		{ "message",    "plop!"                 }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-message"        },
-		{ "server",     "test"                  },
-		{ "target",     123456                  },
-		{ "message",    "plop!"                 }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-message"        },
-		{ "server",     "unknown"               },
-		{ "target",     "#music"                },
-		{ "message",    "plop!"                 }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-mode/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-mode
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-mode/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-/*
- * main.cpp -- test server-mode remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-mode"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_mode_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     "test"          },
-		{ "channel",    "#irccd"        },
-		{ "mode",       "+t"            }
-	});
-
-	const auto cmd = server_->find("mode").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#irccd");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "+t");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     123456          },
-		{ "channel",    "#music"        },
-		{ "mode",       "+i"            }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     ""              },
-		{ "channel",    "#music"        },
-		{ "mode",       "+i"            }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     "test"          },
-		{ "channel",    ""              },
-		{ "mode",       "+i"            }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     "test"          },
-		{ "channel",    123456          },
-		{ "mode",       "+i"            }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_mode_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     "test"          },
-		{ "channel",    "#music"        },
-		{ "mode",       ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_mode);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_mode);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_mode_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     "test"          },
-		{ "channel",    "#music"        },
-		{ "mode",       123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_mode);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_mode);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-mode"   },
-		{ "server",     "unknown"       },
-		{ "channel",    "#music"        },
-		{ "mode",       "+i"            }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-nick/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-nick
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-nick/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * main.cpp -- test server-nick remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-nick"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_nick_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-nick"   },
-		{ "server",     "test"          },
-		{ "nickname",   "chris"         }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(json.is_object());
-	BOOST_TEST(server_->get_nickname() == "chris");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-nick"   },
-		{ "server",     123456          },
-		{ "nickname",   "chris"         }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-nick"   },
-		{ "server",     ""              },
-		{ "nickname",   "chris"         }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-nick"   },
-		{ "server",     "test"          },
-		{ "nickname",   ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_nickname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_nickname_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-nick"   },
-		{ "server",     "test"          },
-		{ "nickname",   123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_nickname);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_nickname);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-nick"   },
-		{ "server",     "unknown"       },
-		{ "nickname",   "chris"         }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-notice/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-notice
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-notice/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/*
- * main.cpp -- test server-notice remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-notice"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_notice_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-notice" },
-		{ "server",     "test"          },
-		{ "target",     "#staff"        },
-		{ "message",    "quiet!"        }
-	});
-
-	const auto cmd = server_->find("notice").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "quiet!");
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-notice" },
-		{ "server",     123456          },
-		{ "target",     "#music"        },
-		{ "message",    "quiet!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-notice" },
-		{ "server",     ""              },
-		{ "target",     "#music"        },
-		{ "message",    "quiet!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-notice" },
-		{ "server",     "test"          },
-		{ "target",     ""              },
-		{ "message",    "quiet!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-notice" },
-		{ "server",     "test"          },
-		{ "target",     123456          },
-		{ "message",    "quiet!"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-notice" },
-		{ "server",     "unknown"       },
-		{ "target",     "#music"        },
-		{ "message",    "quiet!"        }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-part/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-part
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-part/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/*
- * main.cpp -- test server-part remote command
- *
- * Copyright (c) 2013-2018 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 part and this permission part 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 BOOST_TEST_MODULE "server-part"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_part_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     "test"          },
-		{ "channel",    "#staff"        },
-		{ "reason",     "too noisy"     }
-	});
-
-	const auto cmd = server_->find("part").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "too noisy");
-}
-
-BOOST_AUTO_TEST_CASE(noreason)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     "test"          },
-		{ "channel",    "#staff"        }
-	});
-
-	const auto cmd = server_->find("part").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     123456          },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     ""              },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     "test"          },
-		{ "channel",    ""              }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     "test"          },
-		{ "channel",    123456          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-part"   },
-		{ "server",     "unknown"       },
-		{ "channel",    "#music"        }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-reconnect/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-reconnect
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-reconnect/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * main.cpp -- test server-reconnect remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-reconnect"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-class server_reconnect_fixture : public command_fixture {
-protected:
-	std::shared_ptr<mock_server> s1_;
-	std::shared_ptr<mock_server> s2_;
-
-	server_reconnect_fixture()
-		: s1_(new mock_server(ctx_, "s1", "localhost"))
-		, s2_(new mock_server(ctx_, "s2", "localhost"))
-	{
-		irccd_.servers().clear();
-		irccd_.servers().add(s1_);
-		irccd_.servers().add(s2_);
-		s1_->clear();
-		s2_->clear();
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(server_reconnect_fixture_suite, server_reconnect_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [_, code] = request({
-		{ "command",    "server-reconnect"      },
-		{ "server",     "s1"                    }
-	});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(s1_->find("disconnect").size() == 1U);
-	BOOST_TEST(s1_->find("connect").size() == 1U);
-	BOOST_TEST(s2_->empty());
-}
-
-BOOST_AUTO_TEST_CASE(all)
-{
-	const auto [_, code] = request({{ "command", "server-reconnect" }});
-
-	BOOST_TEST(!code);
-	BOOST_TEST(s1_->find("disconnect").size() == 1U);
-	BOOST_TEST(s1_->find("connect").size() == 1U);
-	BOOST_TEST(s2_->find("disconnect").size() == 1U);
-	BOOST_TEST(s2_->find("connect").size() == 1U);
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-reconnect"      },
-		{ "server",     123456                  }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-reconnect"      },
-		{ "server",     ""                      }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-reconnect"      },
-		{ "server",     "unknown"               }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/command-server-topic/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME command-server-topic
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-ctl
-)
--- a/tests/src/libirccd/command-server-topic/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/*
- * main.cpp -- test server-topic remote command
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server-topic"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/test/command_fixture.hpp>
-
-using namespace irccd::test;
-
-namespace irccd {
-
-namespace {
-
-BOOST_FIXTURE_TEST_SUITE(server_topic_fixture_suite, command_fixture)
-
-BOOST_AUTO_TEST_CASE(basic)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-topic"  },
-		{ "server",     "test"          },
-		{ "channel",    "#staff"        },
-		{ "topic",      "new version"   }
-	});
-
-	const auto cmd = server_->find("topic").back();
-
-	BOOST_TEST(!code);
-	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
-	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "new version");
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-topic"  },
-		{ "server",     123456          },
-		{ "channel",    "#music"        },
-		{ "topic",      "plop"          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_identifier_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-topic"  },
-		{ "server",     ""              },
-		{ "channel",    "#music"        },
-		{ "topic",      "plop"          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_identifier);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_identifier);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_1)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-topic"  },
-		{ "server",     "test"          },
-		{ "channel",    ""              },
-		{ "topic",      "plop"          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(invalid_channel_2)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-topic"  },
-		{ "server",     "test"          },
-		{ "channel",    123456          },
-		{ "topic",      "plop"          }
-	});
-
-	BOOST_TEST(code == server_error::invalid_channel);
-	BOOST_TEST(json["error"].get<int>() == server_error::invalid_channel);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_CASE(not_found)
-{
-	const auto [json, code] = request({
-		{ "command",    "server-topic"  },
-		{ "server",     "unknown"       },
-		{ "channel",    "#music"        },
-		{ "topic",      "plop"          }
-	});
-
-	BOOST_TEST(code == server_error::not_found);
-	BOOST_TEST(json["error"].get<int>() == server_error::not_found);
-	BOOST_TEST(json["errorCategory"].get<std::string>() == "server");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/dynlib-plugin/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-find_package(Boost REQUIRED QUIET)
-
-add_library(test-plugin MODULE test_plugin.cpp)
-target_link_libraries(test-plugin libirccd Boost::boost)
-set_target_properties(
-	test-plugin
-	PROPERTIES
-		PREFIX ""
-		RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
-)
-
-foreach (c ${CMAKE_CONFIGURATION_TYPES})
-	string(TOUPPER ${c} c)
-	set_target_properties(
-		test-plugin
-		PROPERTIES
-			RUNTIME_OUTPUT_DIRECTORY_${c} ${CMAKE_CURRENT_BINARY_DIR}
-	)
-endforeach ()
-
-irccd_define_test(
-	NAME dynlib-plugin
-	SOURCES main.cpp
-	LIBRARIES libirccd libirccd-test
-	DEPENDS test-plugin
-)
--- a/tests/src/libirccd/dynlib-plugin/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,192 +0,0 @@
-/*
- * main.cpp -- test dynlib_plugin
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "dynlib_plugin"
-#include <boost/test/unit_test.hpp>
-
-/*
- * For this test, we update internal plugin configuration each time a function
- * is called and check if it has been called correctly using get_options.
- */
-
-#include <irccd/daemon/dynlib_plugin.hpp>
-#include <irccd/daemon/server.hpp>
-#include <irccd/daemon/irccd.hpp>
-
-namespace irccd {
-
-namespace {
-
-class fixture {
-protected:
-	boost::asio::io_service service_;
-	std::shared_ptr<plugin> plugin_;
-	irccd irccd_{service_};
-
-	fixture()
-	{
-		plugin_ = dynlib_plugin_loader({CMAKE_CURRENT_BINARY_DIR}).find("test-plugin");
-
-		if (!plugin_)
-			throw std::runtime_error("test plugin not found");
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(dynlib_plugin_suite, fixture)
-
-BOOST_AUTO_TEST_CASE(handle_command)
-{
-	plugin_->handle_command(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["command"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_connect)
-{
-	plugin_->handle_connect(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["connect"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_invite)
-{
-	plugin_->handle_invite(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["invite"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_join)
-{
-	plugin_->handle_join(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["join"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_kick)
-{
-	plugin_->handle_kick(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["kick"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_load)
-{
-	plugin_->handle_load(irccd_);
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["load"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_message)
-{
-	plugin_->handle_message(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["message"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_me)
-{
-	plugin_->handle_me(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["me"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_mode)
-{
-	plugin_->handle_mode(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["mode"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_names)
-{
-	plugin_->handle_names(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["names"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_nick)
-{
-	plugin_->handle_nick(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["nick"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_notice)
-{
-	plugin_->handle_notice(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["notice"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_part)
-{
-	plugin_->handle_part(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["part"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_reload)
-{
-	plugin_->handle_reload(irccd_);
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["reload"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_topic)
-{
-	plugin_->handle_topic(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["topic"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_unload)
-{
-	plugin_->handle_unload(irccd_);
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["unload"] == "true");
-}
-
-BOOST_AUTO_TEST_CASE(handle_whois)
-{
-	plugin_->handle_whois(irccd_, {});
-
-	BOOST_TEST(plugin_->get_options().size() == 1U);
-	BOOST_TEST(plugin_->get_options()["whois"] == "true");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/dynlib-plugin/test_plugin.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-/*
- * test_plugin.cpp -- basic exported plugin test
- *
- * Copyright (c) 2013-2018 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 <irccd/daemon/dynlib_plugin.hpp>
-
-namespace irccd {
-
-class test_plugin : public plugin {
-private:
-	map config_;
-
-public:
-	test_plugin()
-		: plugin("test")
-	{
-	}
-
-	auto get_options() const -> map override
-	{
-		return config_;
-	}
-
-	auto get_name() const noexcept -> std::string_view override
-	{
-		return "test";
-	}
-
-	void handle_command(irccd&, const message_event&) override
-	{
-		config_["command"] = "true";
-	}
-
-	void handle_connect(irccd&, const connect_event&) override
-	{
-		config_["connect"] = "true";
-	}
-
-	void handle_invite(irccd&, const invite_event&) override
-	{
-		config_["invite"] = "true";
-	}
-
-	void handle_join(irccd&, const join_event&) override
-	{
-		config_["join"] = "true";
-	}
-
-	void handle_kick(irccd&, const kick_event&) override
-	{
-		config_["kick"] = "true";
-	}
-
-	void handle_load(irccd&) override
-	{
-		config_["load"] = "true";
-	}
-
-	void handle_message(irccd&, const message_event&) override
-	{
-		config_["message"] = "true";
-	}
-
-	void handle_me(irccd&, const me_event&) override
-	{
-		config_["me"] = "true";
-	}
-
-	void handle_mode(irccd&, const mode_event&) override
-	{
-		config_["mode"] = "true";
-	}
-
-	void handle_names(irccd&, const names_event&) override
-	{
-		config_["names"] = "true";
-	}
-
-	void handle_nick(irccd&, const nick_event&) override
-	{
-		config_["nick"] = "true";
-	}
-
-	void handle_notice(irccd&, const notice_event&) override
-	{
-		config_["notice"] = "true";
-	}
-
-	void handle_part(irccd&, const part_event&) override
-	{
-		config_["part"] = "true";
-	}
-
-	void handle_reload(irccd&) override
-	{
-		config_["reload"] = "true";
-	}
-
-	void handle_topic(irccd&, const topic_event&) override
-	{
-		config_["topic"] = "true";
-	}
-
-	void handle_unload(irccd&) override
-	{
-		config_["unload"] = "true";
-	}
-
-	void handle_whois(irccd&, const whois_event&) override
-	{
-		config_["whois"] = "true";
-	}
-
-	static auto abi() -> version
-	{
-		return version();
-	}
-
-	static auto init(std::string) -> std::unique_ptr<plugin>
-	{
-		return std::make_unique<test_plugin>();
-	}
-};
-
-BOOST_DLL_ALIAS(test_plugin::abi, irccd_abi_test_plugin)
-BOOST_DLL_ALIAS(test_plugin::init, irccd_init_test_plugin)
-
-} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/fs-util/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME fs-util
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/fs-util/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,83 @@
+/*
+ * main.cpp -- test fs_util functions
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "fs_util"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/fs_util.hpp>
+#include <irccd/system.hpp>
+
+namespace irccd {
+
+namespace {
+
+/*
+ * fs_util::find function (name)
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(fs_find_name)
+
+BOOST_AUTO_TEST_CASE(not_recursive)
+{
+	auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-1.txt", false);
+	auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-2.txt", false);
+
+	BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
+	BOOST_TEST(file2.empty());
+}
+
+BOOST_AUTO_TEST_CASE(recursive)
+{
+	auto file1 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-1.txt", true);
+	auto file2 = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", "file-2.txt", true);
+
+	BOOST_TEST(file1.find("file-1.txt") != std::string::npos);
+	BOOST_TEST(file2.find("file-2.txt") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * fs_util::find function (regex)
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(fs_find_regex)
+
+BOOST_AUTO_TEST_CASE(not_recursive)
+{
+	const std::regex regex("file-[12]\\.txt");
+	const auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root", regex, false);
+
+	BOOST_TEST(file.find("file-1.txt") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(recursive)
+{
+	const std::regex regex("file-[12]\\.txt");
+	const auto file = fs_util::find(CMAKE_SOURCE_DIR "/tests/data/root/level-1", regex, true);
+
+	BOOST_TEST(file.find("file-2.txt") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- a/tests/src/libirccd/irc/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME irc
-	SOURCES main.cpp
-	LIBRARIES libirccd
-)
--- a/tests/src/libirccd/irc/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/*
- * main.cpp -- test irc functions
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "irc"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irc.hpp>
-
-namespace irccd {
-
-namespace {
-
-BOOST_AUTO_TEST_SUITE(message_parse)
-
-BOOST_AUTO_TEST_CASE(no_prefix)
-{
-	const auto m = irc::message::parse("PRIVMSG jean :bonjour à toi");
-
-	BOOST_TEST(m.prefix.empty());
-	BOOST_TEST(m.command == "PRIVMSG");
-	BOOST_TEST(m.args.size() == 2U);
-	BOOST_TEST(m.args[0] == "jean");
-	BOOST_TEST(m.args[1] == "bonjour à toi");
-}
-
-BOOST_AUTO_TEST_CASE(prefix)
-{
-	const auto m = irc::message::parse(":127.0.0.1 PRIVMSG jean :bonjour à toi");
-
-	BOOST_TEST(m.prefix == "127.0.0.1");
-	BOOST_TEST(m.command == "PRIVMSG");
-	BOOST_TEST(m.args.size() == 2U);
-	BOOST_TEST(m.args[0] == "jean");
-	BOOST_TEST(m.args[1] == "bonjour à toi");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE(user_parse)
-
-BOOST_AUTO_TEST_CASE(basics)
-{
-	const auto user = irc::user::parse("jean!~jean@127.0.0.1");
-
-	BOOST_TEST(user.nick == "jean");
-	BOOST_TEST(user.host == "~jean@127.0.0.1");
-
-	const auto usersimple = irc::user::parse("jean");
-
-	BOOST_TEST(usersimple.nick == "jean");
-	BOOST_TEST(usersimple.host.empty());
-}
-
-BOOST_AUTO_TEST_CASE(empty)
-{
-	const auto user = irc::user::parse("");
-
-	BOOST_TEST(user.nick.empty());
-	BOOST_TEST(user.host.empty());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/logger/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME logger
-	SOURCES main.cpp
-	LIBRARIES libirccd
-)
--- a/tests/src/libirccd/logger/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/*
- * main.cpp -- test logger functions
- *
- * Copyright (c) 2013-2018 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 <algorithm>
-
-#define BOOST_TEST_MODULE "Logger"
-#include <boost/test/unit_test.hpp>
-#include <boost/format.hpp>
-
-#include <irccd/daemon/logger.hpp>
-
-using boost::format;
-using boost::str;
-
-namespace irccd {
-
-namespace {
-
-class sample_sink : public logger::sink {
-public:
-	std::string line_debug;
-	std::string line_info;
-	std::string line_warning;
-
-	void write_debug(const std::string& line) override
-	{
-		line_debug = line;
-	}
-
-	void write_info(const std::string& line) override
-	{
-		line_info = line;
-	}
-
-	void write_warning(const std::string& line) override
-	{
-		line_warning = line;
-	}
-};
-
-class sample_filter : public logger::filter {
-public:
-	auto pre_debug(std::string_view category,
-	               std::string_view component,
-	               std::string_view message) const -> std::string override
-	{
-		return str(format("DEBUG %s:%s:%s") % category % component % message);
-	}
-
-	auto pre_info(std::string_view category,
-	              std::string_view component,
-	              std::string_view message) const -> std::string override
-	{
-		return str(format("INFO %s:%s:%s") % category % component % message);
-	}
-
-	auto pre_warning(std::string_view category,
-	                 std::string_view component,
-	                 std::string_view message) const -> std::string override
-	{
-		return str(format("WARN %s:%s:%s") % category % component % message);
-	}
-};
-
-class logger_test {
-public:
-	sample_sink log_;
-
-	logger_test()
-	{
-		log_.set_filter(std::make_unique<sample_filter>());
-		log_.set_verbose(true);
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(logger_test_suite, logger_test)
-
-#if !defined(NDEBUG)
-
-BOOST_AUTO_TEST_CASE(debug)
-{
-	log_.debug("test", "debug") << "success" << std::endl;
-
-	BOOST_TEST(log_.line_debug == "DEBUG test:debug:success");
-}
-
-#endif
-
-BOOST_AUTO_TEST_CASE(info)
-{
-	log_.info("test", "info") << "success" << std::endl;
-
-	BOOST_TEST(log_.line_info == "INFO test:info:success");
-}
-
-BOOST_AUTO_TEST_CASE(info_quiet)
-{
-	log_.set_verbose(false);
-	log_.info("test", "info") << "success" << std::endl;
-
-	BOOST_REQUIRE(log_.line_info.empty());
-}
-
-BOOST_AUTO_TEST_CASE(warning)
-{
-	log_.warning("test", "warning") << "success" << std::endl;
-
-	BOOST_TEST(log_.line_warning == "WARN test:warning:success");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/rule-util/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME rule-util
-	SOURCES main.cpp
-	LIBRARIES libirccd
-)
--- a/tests/src/libirccd/rule-util/error-invalid-action.conf	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-[rule]
-action = unknown
--- a/tests/src/libirccd/rule-util/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/*
- * main.cpp -- test rule_util functions
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "rule_util"
-#include <boost/test/unit_test.hpp>
-#include <boost/filesystem.hpp>
-
-#include <irccd/ini.hpp>
-
-#include <irccd/daemon/rule.hpp>
-#include <irccd/daemon/rule_util.hpp>
-
-namespace irccd {
-
-namespace {
-
-auto open(const std::string& config) -> ini::document
-{
-	boost::filesystem::path path;
-
-	path /= CMAKE_CURRENT_SOURCE_DIR;
-	path /= config;
-
-	return ini::read_file(path.string());
-}
-
-BOOST_AUTO_TEST_SUITE(from_config)
-
-BOOST_AUTO_TEST_SUITE(valid)
-
-BOOST_AUTO_TEST_CASE(servers)
-{
-	const auto rule = rule_util::from_config(open("simple.conf")[0]);
-
-	BOOST_TEST(rule.servers.size() == 1U);
-	BOOST_TEST(rule.servers.count("s1"));
-	BOOST_TEST(rule.channels.empty());
-	BOOST_TEST(rule.plugins.empty());
-	BOOST_TEST(rule.events.empty());
-}
-
-BOOST_AUTO_TEST_CASE(channels)
-{
-	const auto rule = rule_util::from_config(open("simple.conf")[1]);
-
-	BOOST_TEST(rule.servers.empty());
-	BOOST_TEST(rule.channels.size() == 1U);
-	BOOST_TEST(rule.channels.count("#c1"));
-	BOOST_TEST(rule.plugins.empty());
-	BOOST_TEST(rule.events.empty());
-}
-
-BOOST_AUTO_TEST_CASE(plugins)
-{
-	const auto rule = rule_util::from_config(open("simple.conf")[2]);
-
-	BOOST_TEST(rule.servers.empty());
-	BOOST_TEST(rule.channels.empty());
-	BOOST_TEST(rule.plugins.size() == 1U);
-	BOOST_TEST(rule.plugins.count("hangman"));
-	BOOST_TEST(rule.events.empty());
-}
-
-BOOST_AUTO_TEST_CASE(events)
-{
-	const auto rule = rule_util::from_config(open("simple.conf")[3]);
-
-	BOOST_TEST(rule.servers.empty());
-	BOOST_TEST(rule.channels.empty());
-	BOOST_TEST(rule.plugins.empty());
-	BOOST_TEST(rule.events.size() == 1U);
-	BOOST_TEST(rule.events.count("onCommand"));
-}
-
-BOOST_AUTO_TEST_SUITE(errors)
-
-BOOST_AUTO_TEST_CASE(invalid_action)
-{
-	BOOST_REQUIRE_THROW(rule_util::from_config(open("error-invalid-action.conf")[0]), rule_error);
-
-	try {
-		rule_util::from_config(open("error-invalid-action.conf")[0]);
-	} catch (const rule_error& ex) {
-		BOOST_TEST(ex.code() == rule_error::invalid_action);
-	}
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/rule-util/simple.conf	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-[rule]
-servers = "s1"
-action = accept
-
-[rule]
-channels = "#c1"
-action = drop
-
-[rule]
-plugins = "hangman"
-action = accept
-
-[rule]
-events = "onCommand"
-action = drop
-
-[rule]
-servers = "s1"
-channels = "#c1"
-plugins = "hangman"
-events = "onCommand"
-action = accept
--- a/tests/src/libirccd/rules/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME rules
-	SOURCES main.cpp
-	LIBRARIES libirccd
-)
--- a/tests/src/libirccd/rules/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,271 +0,0 @@
-/*
- * main.cpp -- test irccd rules
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "Rules"
-#include <boost/test/unit_test.hpp>
-
-#include <irccd/daemon/irccd.hpp>
-#include <irccd/daemon/logger.hpp>
-#include <irccd/daemon/rule_service.hpp>
-
-namespace irccd {
-
-namespace {
-
-/*
- * Simulate the following rules configuration:
- *
- * #
- * # On all servers, each channel #staff can't use the onCommand event,
- * # everything else is allowed.
- * #
- * [rule]       #1
- * servers      = ""
- * channels     = "#staff"
- * events       = "onCommand"
- * action       = drop
- *
- * #
- * # However, the same onCommand on #staff is allowed on server "unsafe"
- * #
- * [rule]       #2
- * servers      = "unsafe"
- * channels     = "#staff"
- * events       = "onCommand"
- * action       = accept
- *
- * #
- * # Plugin game is only allowed on server "malikania" and "localhost",
- * # channel "#games" and events "onMessage, onCommand".
- * #
- * # The first rule #3-1 disable the plugin game for every server, it is
- * # reenabled again with the #3-2.
- * #
- * [rule]       #3-1
- * plugins      = "game"
- * action       = drop
- *
- * [rule]       #3-2
- * servers      = "malikania localhost"
- * channels     = "#games"
- * plugins      = "game"
- * events       = "onMessage onCommand"
- * action       = accept
- */
-class rules_test {
-protected:
-	boost::asio::io_service service_;
-	irccd daemon_{service_};
-	rule_service rules_{daemon_};
-
-	rules_test()
-	{
-		daemon_.set_log(std::make_unique<logger::silent_sink>());
-
-		// #1
-		{
-			rules_.add({
-				rule::set{                }, // Servers
-				rule::set{ "#staff"       }, // Channels
-				rule::set{                }, // Origins
-				rule::set{                }, // Plugins
-				rule::set{ "onCommand"    }, // Events
-				rule::action_type::drop
-			});
-		}
-
-		// #2
-		{
-			rules_.add({
-				rule::set{ "unsafe"       },
-				rule::set{ "#staff"       },
-				rule::set{                },
-				rule::set{                },
-				rule::set{ "onCommand"    },
-				rule::action_type::accept
-			});
-		}
-
-		// #3-1
-		{
-			rules_.add({
-				rule::set{},
-				rule::set{},
-				rule::set{},
-				rule::set{"game"},
-				rule::set{},
-				rule::action_type::drop
-			});
-		}
-
-		// #3-2
-		{
-			rules_.add({
-				rule::set{ "malikania", "localhost"   },
-				rule::set{ "#games"                   },
-				rule::set{                            },
-				rule::set{ "game"                     },
-				rule::set{ "onCommand", "onMessage"   },
-				rule::action_type::accept
-			});
-		}
-	}
-};
-
-BOOST_FIXTURE_TEST_SUITE(rules_test_suite, rules_test)
-
-BOOST_AUTO_TEST_CASE(basic_match1)
-{
-	rule m;
-
-	/*
-	 * [rule]
-	 */
-	BOOST_TEST(m.match("freenode", "#test", "a", "", ""));
-	BOOST_TEST(m.match("", "", "", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(basic_match2)
-{
-	rule m{rule::set{"freenode"}};
-
-	/*
-	 * [rule]
-	 * servers	= "freenode"
-	 */
-
-	BOOST_TEST(m.match("freenode", "#test", "a", "", ""));
-	BOOST_TEST(!m.match("malikania", "#test", "a", "", ""));
-	BOOST_TEST(m.match("freenode", "", "jean", "", "onMessage"));
-}
-
-BOOST_AUTO_TEST_CASE(basic_match3)
-{
-	rule m{rule::set{"freenode"}, rule::set{"#staff"}};
-
-	/*
-	 * [rule]
-	 * servers	= "freenode"
-	 * channels	= "#staff"
-	 */
-
-	BOOST_TEST(m.match("freenode", "#staff", "a", "", ""));
-	BOOST_TEST(!m.match("freenode", "#test", "a", "", ""));
-	BOOST_TEST(!m.match("malikania", "#staff", "a", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(basic_match4)
-{
-	rule m{rule::set{"malikania"}, rule::set{"#staff"}, rule::set{"a"}};
-
-	/*
-	 * [rule]
-	 * servers	= "malikania"
-	 * channels	= "#staff"
-	 * plugins	= "a"
-	 */
-
-	BOOST_TEST(m.match("malikania", "#staff", "a", "",""));
-	BOOST_TEST(!m.match("malikania", "#staff", "b", "", ""));
-	BOOST_TEST(!m.match("freenode", "#staff", "a", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(complex_match1)
-{
-	rule m{rule::set{"malikania", "freenode"}};
-
-	/*
-	 * [rule]
-	 * servers	= "malikania freenode"
-	 */
-
-	BOOST_TEST(m.match("malikania", "", "", "", ""));
-	BOOST_TEST(m.match("freenode", "", "", "", ""));
-	BOOST_TEST(!m.match("no", "", "", "", ""));
-}
-
-BOOST_AUTO_TEST_CASE(origin_match)
-{
-	rule m{
-		rule::set{"malikania"},
-		rule::set{},
-		rule::set{"markand"},
-		rule::set{},
-		rule::set{},
-		rule::action_type::accept
-	};
-
-	/*
-	 * [rule]
-	 * servers = "malikania"
-	 * origins = "markand"
-	 */
-	BOOST_TEST(m.match("malikania", "#staff", "markand", "system", "onCommand"));
-	BOOST_TEST(!m.match("malikania", "#staff", "", "system", "onNames"));
-	BOOST_TEST(!m.match("malikania", "#staff", "jean", "system", "onMessage"));
-}
-
-BOOST_AUTO_TEST_CASE(basic_solve)
-{
-	/* Allowed */
-	BOOST_TEST(rules_.solve("malikania", "#staff", "", "a", "onMessage"));
-
-	/* Allowed */
-	BOOST_TEST(rules_.solve("freenode", "#staff", "", "b", "onTopic"));
-
-	/* Not allowed */
-	BOOST_TEST(!rules_.solve("malikania", "#staff", "", "", "onCommand"));
-
-	/* Not allowed */
-	BOOST_TEST(!rules_.solve("freenode", "#staff", "", "c", "onCommand"));
-
-	/* Allowed */
-	BOOST_TEST(rules_.solve("unsafe", "#staff", "", "c", "onCommand"));
-}
-
-BOOST_AUTO_TEST_CASE(games_solve)
-{
-	/* Allowed */
-	BOOST_TEST(rules_.solve("malikania", "#games", "", "game", "onMessage"));
-
-	/* Allowed */
-	BOOST_TEST(rules_.solve("localhost", "#games", "", "game", "onMessage"));
-
-	/* Allowed */
-	BOOST_TEST(rules_.solve("malikania", "#games", "", "game", "onCommand"));
-
-	/* Not allowed */
-	BOOST_TEST(!rules_.solve("malikania", "#games", "", "game", "onQuery"));
-
-	/* Not allowed */
-	BOOST_TEST(!rules_.solve("freenode", "#no", "", "game", "onMessage"));
-
-	/* Not allowed */
-	BOOST_TEST(!rules_.solve("malikania", "#test", "", "game", "onMessage"));
-}
-
-BOOST_AUTO_TEST_CASE(fix_645)
-{
-	BOOST_TEST(!rules_.solve("MALIKANIA", "#STAFF", "", "SYSTEM", "onCommand"));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/server-util/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-#
-# CMakeLists.txt -- CMake build system for irccd
-#
-# Copyright (c) 2013-2018 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.
-#
-
-irccd_define_test(
-	NAME server-util
-	SOURCES main.cpp
-	LIBRARIES libirccd
-)
--- a/tests/src/libirccd/server-util/full.conf	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-[server]
-name = "localhost"
-hostname = "irc.localhost"
-channels = ( "#staff", "#test" )
-port = 3344
-password = "secret"
-reconnect-timeout = 60
-nickname = "superbot"
-username = "sp"
-realname = "SuperBot 2000 NT"
-join-invite = true
-auto-rejoin = true
-auto-reconnect = true
--- a/tests/src/libirccd/server-util/full.json	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-{
-	"name": "localhost",
-	"hostname": "irc.localhost",
-	"port": 3344,
-	"password": "secret",
-	"nickname": "superbot",
-	"username": "sp",
-	"realname": "SuperBot 2000 NT"
-}
--- a/tests/src/libirccd/server-util/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +0,0 @@
-/*
- * main.cpp -- test server_util functions
- *
- * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "server_util"
-#include <boost/test/unit_test.hpp>
-#include <boost/filesystem.hpp>
-
-#include <irccd/ini.hpp>
-
-#include <irccd/daemon/server.hpp>
-#include <irccd/daemon/server_util.hpp>
-
-using nlohmann::json;
-
-using irccd::server_util::from_config;
-using irccd::server_util::from_json;
-using irccd::server_util::message_type;
-
-namespace irccd {
-
-namespace server_util {
-
-auto operator<<(std::ostream& out, message_type::kind kind) -> std::ostream&
-{
-	if (kind == message_type::is_command)
-		out << "command";
-	else
-		out << "message";
-
-	return out;
-}
-
-} // !server_util
-
-namespace {
-
-class fixture {
-protected:
-	boost::asio::io_service ctx_;
-};
-
-auto open_config(const std::string& config) -> ini::document
-{
-	boost::filesystem::path path;
-
-	path /= CMAKE_CURRENT_SOURCE_DIR;
-	path /= config;
-
-	return ini::read_file(path.string());
-}
-
-auto open_json(const std::string& file) -> json
-{
-	boost::filesystem::path path;
-
-	path /= CMAKE_CURRENT_SOURCE_DIR;
-	path /= file;
-
-	std::ifstream input(path.string());
-
-	if (!input)
-		throw std::runtime_error(std::strerror(errno));
-
-	return json::parse(std::string(std::istreambuf_iterator<char>(input.rdbuf()), {}));
-}
-
-BOOST_FIXTURE_TEST_SUITE(from_config, fixture)
-
-BOOST_AUTO_TEST_SUITE(valid)
-
-BOOST_AUTO_TEST_CASE(full)
-{
-	const auto sv = server_util::from_config(ctx_, open_config("full.conf")[0]);
-
-	BOOST_TEST(sv->get_id() == "localhost");
-	BOOST_TEST(sv->get_hostname() == "irc.localhost");
-	BOOST_TEST(sv->get_port() == 3344U);
-	BOOST_TEST(sv->get_password() == "secret");
-	BOOST_TEST(sv->get_nickname() == "superbot");
-	BOOST_TEST(sv->get_username() == "sp");
-	BOOST_TEST(sv->get_realname() == "SuperBot 2000 NT");
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::join_invite));
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_rejoin));
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_reconnect));
-}
-
-#if defined(IRCCD_HAVE_SSL)
-
-BOOST_AUTO_TEST_CASE(ssl)
-{
-	const auto sv = server_util::from_config(ctx_, open_config("ssl.conf")[0]);
-
-	BOOST_TEST(sv->get_id() == "localhost");
-	BOOST_TEST(sv->get_hostname() == "irc.localhost");
-	BOOST_TEST(sv->get_port() == 6697U);
-	BOOST_TEST(sv->get_password() == "secret");
-	BOOST_TEST(sv->get_nickname() == "secure");
-	BOOST_TEST(sv->get_username() == "sc");
-	BOOST_TEST(sv->get_realname() == "SuperBot 2000 NT SSL");
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::ssl));
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::join_invite));
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_rejoin));
-	BOOST_TEST(static_cast<bool>(sv->get_options() & server::options::auto_reconnect));
-}
-
-#endif // !IRCCD_HAVE_SSL
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_FIXTURE_TEST_SUITE(from_json, fixture)
-
-BOOST_AUTO_TEST_SUITE(valid)
-
-BOOST_AUTO_TEST_CASE(full)
-{
-	const auto sv = server_util::from_json(ctx_, open_json("full.json"));
-
-	BOOST_TEST(sv->get_id() == "localhost");
-	BOOST_TEST(sv->get_hostname() == "irc.localhost");
-	BOOST_TEST(sv->get_port() == 3344U);
-	BOOST_TEST(sv->get_password() == "secret");
-	BOOST_TEST(sv->get_nickname() == "superbot");
-	BOOST_TEST(sv->get_username() == "sp");
-	BOOST_TEST(sv->get_realname() == "SuperBot 2000 NT");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE_END()
-
-BOOST_AUTO_TEST_SUITE(message)
-
-BOOST_AUTO_TEST_CASE(valid_short)
-{
-	const auto m = message_type::parse("!hello", "!", "hello");
-
-	BOOST_TEST(m.type == message_type::is_command);
-	BOOST_TEST(m.message == "");
-}
-
-BOOST_AUTO_TEST_CASE(valid_arguments)
-{
-	const auto m = message_type::parse("!hello world", "!", "hello");
-
-	BOOST_TEST(m.type == message_type::is_command);
-	BOOST_TEST(m.message == "world");
-}
-
-BOOST_AUTO_TEST_CASE(cchar_with_message_short)
-{
-	const auto m = message_type::parse("!hello", "!", "hangman");
-
-	BOOST_TEST(m.type == message_type::is_message);
-	BOOST_TEST(m.message == "!hello");
-}
-
-BOOST_AUTO_TEST_CASE(cchar_with_message_arguments)
-{
-	const auto m = message_type::parse("!hello world", "!", "hangman");
-
-	BOOST_TEST(m.type == message_type::is_message);
-	BOOST_TEST(m.message == "!hello world");
-}
-
-BOOST_AUTO_TEST_CASE(command_with_different_cchar_short)
-{
-	const auto m = message_type::parse("!hello", ">", "hello");
-
-	BOOST_TEST(m.type == message_type::is_message);
-	BOOST_TEST(m.message == "!hello");
-}
-
-BOOST_AUTO_TEST_CASE(command_with_different_cchar_arguments)
-{
-	const auto m = message_type::parse("!hello", ">", "hello");
-
-	BOOST_TEST(m.type == message_type::is_message);
-	BOOST_TEST(m.message == "!hello");
-}
-
-BOOST_AUTO_TEST_SUITE_END()
-
-} // !namespace
-
-} // !irccd
--- a/tests/src/libirccd/server-util/ssl.conf	Thu Nov 15 13:19:17 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-[server]
-name = "localhost"
-hostname = "irc.localhost"
-port = 6697
-ssl = true
-ssl-verify = false
-password = "secret"
-reconnect-timeout = 60
-nickname = "secure"
-username = "sc"
-realname = "SuperBot 2000 NT SSL"
-join-invite = true
-auto-rejoin = true
-auto-reconnect = true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/stream/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME stream
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/stream/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,262 @@
+/*
+ * main.cpp -- test network classes
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "stream"
+#include <boost/test/unit_test.hpp>
+#include <boost/mpl/list.hpp>
+#include <boost/predef/os.h>
+
+#include <irccd/sysconfig.hpp>
+
+#include <irccd/acceptor.hpp>
+#include <irccd/connector.hpp>
+#include <irccd/stream.hpp>
+
+using boost::asio::io_service;
+using boost::asio::ip::tcp;
+
+#if defined(IRCCD_HAVE_SSL)
+using boost::asio::ssl::context;
+#endif
+
+namespace irccd {
+
+namespace {
+
+class stream_fixture {
+public:
+	io_service service_;
+
+	std::unique_ptr<acceptor> acceptor_;
+	std::unique_ptr<connector> connector_;
+
+	std::shared_ptr<stream> stream1_;
+	std::shared_ptr<stream> stream2_;
+
+	virtual auto create_acceptor() -> std::unique_ptr<acceptor> = 0;
+
+	virtual auto create_connector() -> std::unique_ptr<connector> = 0;
+
+	void init()
+	{
+		acceptor_ = create_acceptor();
+		connector_ = create_connector();
+
+		acceptor_->accept([this] (auto code, auto stream) {
+			if (code)
+				throw std::system_error(code);
+
+			stream1_ = std::move(stream);
+		});
+		connector_->connect([this] (auto code, auto stream) {
+			if (code)
+				throw std::system_error(code);
+
+			stream2_ = std::move(stream);
+		});
+
+		service_.run();
+		service_.reset();
+	}
+};
+
+class ip_stream_fixture : public stream_fixture {
+private:
+	tcp::endpoint endpoint_;
+
+protected:
+	/**
+	 * \copydoc io_fixture::create_acceptor
+	 */
+	auto create_acceptor() -> std::unique_ptr<acceptor> override
+	{
+		tcp::endpoint endpoint(tcp::v4(), 0U);
+		tcp::acceptor acceptor(service_, std::move(endpoint));
+
+		endpoint_ = acceptor.local_endpoint();
+
+		return std::make_unique<ip_acceptor>(service_, std::move(acceptor));
+	}
+
+	/**
+	 * \copydoc io_fixture::create_connector
+	 */
+	auto create_connector() -> std::unique_ptr<connector> override
+	{
+		const auto hostname = "127.0.0.1";
+		const auto port = std::to_string(endpoint_.port());
+
+		return std::make_unique<ip_connector>(service_, hostname, port, true, false);
+	}
+};
+
+#if defined(IRCCD_HAVE_SSL)
+
+class tls_ip_stream_fixture : public stream_fixture {
+private:
+	tcp::endpoint endpoint_;
+
+protected:
+	/**
+	 * \copydoc io_fixture::create_acceptor
+	 */
+	auto create_acceptor() -> std::unique_ptr<acceptor> override
+	{
+		context context(context::tlsv12);
+
+		context.use_certificate_file(TESTS_SOURCE_DIR "/data/test.crt", context::pem);
+		context.use_private_key_file(TESTS_SOURCE_DIR "/data/test.key", context::pem);
+
+		tcp::endpoint endpoint(tcp::v4(), 0U);
+		tcp::acceptor acceptor(service_, std::move(endpoint));
+
+		endpoint_ = acceptor.local_endpoint();
+
+		return std::make_unique<tls_acceptor<ip_acceptor>>(std::move(context), service_, std::move(acceptor));
+	}
+
+	/**
+	 * \copydoc io_fixture::create_connector
+	 */
+	auto create_connector() -> std::unique_ptr<connector> override
+	{
+		context context(context::tlsv12);
+
+		const auto hostname = "127.0.0.1";
+		const auto port = std::to_string(endpoint_.port());
+
+		return std::make_unique<tls_connector<ip_connector>>(std::move(context),
+			service_, hostname, port, true, false);
+	}
+};
+
+#endif // !IRCCD_HAVE_SSL
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+class local_stream_fixture : public stream_fixture {
+private:
+	const std::string path_{CMAKE_CURRENT_BINARY_DIR "/stream-test.sock"};
+
+public:
+
+	/**
+	 * \copydoc io_fixture::create_acceptor
+	 */
+	auto create_acceptor() -> std::unique_ptr<acceptor> override
+	{
+		return std::make_unique<local_acceptor>(service_, path_);
+	}
+
+	/**
+	 * \copydoc io_fixture::create_connector
+	 */
+	auto create_connector() -> std::unique_ptr<connector> override
+	{
+		return std::make_unique<local_connector>(service_, path_);
+	}
+};
+
+#if defined(IRCCD_HAVE_SSL)
+
+class tls_local_stream_fixture : public stream_fixture {
+private:
+	const std::string path_{CMAKE_CURRENT_BINARY_DIR "/stream-test.sock"};
+
+public:
+
+	/**
+	 * \copydoc io_fixture::create_acceptor
+	 */
+	auto create_acceptor() -> std::unique_ptr<acceptor> override
+	{
+		context context(context::tlsv12);
+
+		context.use_certificate_file(TESTS_SOURCE_DIR "/data/test.crt", context::pem);
+		context.use_private_key_file(TESTS_SOURCE_DIR "/data/test.key", context::pem);
+
+		return std::make_unique<tls_acceptor<local_acceptor>>(std::move(context), service_, path_);
+	}
+
+	/**
+	 * \copydoc io_fixture::create_connector
+	 */
+	auto create_connector() -> std::unique_ptr<connector> override
+	{
+		return std::make_unique<tls_connector<local_connector>>(context(context::tlsv12), service_, path_);
+	}
+};
+
+#endif // !IRCCD_HAVE_SSL
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+/**
+ * List of fixtures to tests.
+ */
+using list = boost::mpl::list<
+	ip_stream_fixture
+#if defined(IRCCD_HAVE_SSL)
+	, tls_ip_stream_fixture
+#endif
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+	, local_stream_fixture
+#	if defined(IRCCD_HAVE_SSL)
+	, tls_local_stream_fixture
+#	endif
+#endif
+>;
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(invalid_argument, Test, list)
+{
+	Test fixture;
+
+	const nlohmann::json message{
+		{ "abc", 123 },
+		{ "def", 456 }
+	};
+
+	fixture.init();
+	fixture.stream1_->recv([] (auto code, auto message) {
+		BOOST_TEST(!code);
+		BOOST_TEST(message.is_object());
+		BOOST_TEST(message["abc"].template get<int>() == 123);
+		BOOST_TEST(message["def"].template get<int>() == 456);
+	});
+	fixture.stream2_->send(message, [] (auto code) {
+		BOOST_TEST(!code);
+	});
+	fixture.service_.run();
+}
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(connection_reset, Test, list)
+{
+	Test fixture;
+
+	fixture.init();
+	fixture.stream1_->recv([] (auto code, auto message) {
+		BOOST_TEST(code.value() == static_cast<int>(std::errc::connection_reset));
+		BOOST_TEST(message.is_null());
+	});
+	fixture.stream2_ = nullptr;
+	fixture.service_.run();
+}
+
+} // !namespace
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/string-util/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2018 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.
+#
+
+irccd_define_test(
+	NAME string-util
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/src/libirccd/string-util/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -0,0 +1,384 @@
+/*
+ * main.cpp -- test string_util functions
+ *
+ * Copyright (c) 2013-2018 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 BOOST_TEST_MODULE "string_util"
+#include <boost/test/unit_test.hpp>
+
+#include <irccd/string_util.hpp>
+#include <irccd/system.hpp>
+
+namespace irccd {
+
+namespace {
+
+/*
+ * string_util::format function
+ * --------------------------------------------------------
+ */
+BOOST_AUTO_TEST_SUITE(format)
+
+BOOST_AUTO_TEST_CASE(nothing)
+{
+	std::string expected = "hello world!";
+	std::string result = string_util::format("hello world!");
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(escape)
+{
+	string_util::subst params;
+
+	params.keywords.emplace("target", "hello");
+
+	BOOST_TEST(string_util::format("$@#") == "$@#");
+	BOOST_TEST(string_util::format(" $ @ # ") == " $ @ # ");
+	BOOST_TEST(string_util::format("#") == "#");
+	BOOST_TEST(string_util::format(" # ") == " # ");
+	BOOST_TEST(string_util::format("#@") == "#@");
+	BOOST_TEST(string_util::format("##") == "##");
+	BOOST_TEST(string_util::format("#!") == "#!");
+	BOOST_TEST(string_util::format("##{target}") == "#{target}");
+	BOOST_TEST(string_util::format("@#{target}", params) == "@hello");
+	BOOST_TEST(string_util::format("#{target}#", params) == "hello#");
+	BOOST_REQUIRE_THROW(string_util::format("#{failure"), std::exception);
+}
+
+BOOST_AUTO_TEST_CASE(disable_date)
+{
+	string_util::subst params;
+
+	params.flags &= ~(string_util::subst_flags::date);
+
+	BOOST_TEST(string_util::format("%H:%M", params) == "%H:%M");
+}
+
+BOOST_AUTO_TEST_CASE(disable_keywords)
+{
+	string_util::subst params;
+
+	params.keywords.emplace("target", "hello");
+	params.flags &= ~(string_util::subst_flags::keywords);
+
+	BOOST_TEST(string_util::format("#{target}", params) == "#{target}");
+}
+
+BOOST_AUTO_TEST_CASE(disable_env)
+{
+	string_util::subst params;
+
+	params.flags &= ~(string_util::subst_flags::env);
+
+	BOOST_TEST(string_util::format("${HOME}", params) == "${HOME}");
+}
+
+BOOST_AUTO_TEST_CASE(keyword_simple)
+{
+	string_util::subst params;
+
+	params.keywords.insert({"target", "irccd"});
+
+	std::string expected = "hello irccd!";
+	std::string result = string_util::format("hello #{target}!", params);
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(keyword_multiple)
+{
+	string_util::subst params;
+
+	params.keywords.insert({"target", "irccd"});
+	params.keywords.insert({"source", "nightmare"});
+
+	std::string expected = "hello irccd from nightmare!";
+	std::string result = string_util::format("hello #{target} from #{source}!", params);
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(keyword_adj_twice)
+{
+	string_util::subst params;
+
+	params.keywords.insert({"target", "irccd"});
+
+	std::string expected = "hello irccdirccd!";
+	std::string result = string_util::format("hello #{target}#{target}!", params);
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(keyword_missing)
+{
+	std::string expected = "hello !";
+	std::string result = string_util::format("hello #{target}!");
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(env_simple)
+{
+	std::string home = sys::env("HOME");
+
+	if (!home.empty()) {
+		std::string expected = "my home is " + home;
+		std::string result = string_util::format("my home is ${HOME}");
+
+		BOOST_TEST(expected == result);
+	}
+}
+
+BOOST_AUTO_TEST_CASE(env_missing)
+{
+	std::string expected = "value is ";
+	std::string result = string_util::format("value is ${HOPE_THIS_VAR_NOT_EXIST}");
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::split function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(split)
+
+using list = std::vector<std::string>;
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+	list expected { "a", "b" };
+	list result = string_util::split("a;b", ";");
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(cut)
+{
+	list expected { "msg", "#staff", "foo bar baz" };
+	list result = string_util::split("msg;#staff;foo bar baz", ";", 3);
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::strip function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(strip)
+
+BOOST_AUTO_TEST_CASE(left)
+{
+	std::string value = "   123";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "123");
+}
+
+BOOST_AUTO_TEST_CASE(right)
+{
+	std::string value = "123   ";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "123");
+}
+
+BOOST_AUTO_TEST_CASE(both)
+{
+	std::string value = "   123   ";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "123");
+}
+
+BOOST_AUTO_TEST_CASE(none)
+{
+	std::string value = "without";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "without");
+}
+
+BOOST_AUTO_TEST_CASE(between_empty)
+{
+	std::string value = "one list";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "one list");
+}
+
+BOOST_AUTO_TEST_CASE(between_left)
+{
+	std::string value = "  space at left";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "space at left");
+}
+
+BOOST_AUTO_TEST_CASE(between_right)
+{
+	std::string value = "space at right  ";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "space at right");
+}
+
+BOOST_AUTO_TEST_CASE(between_both)
+{
+	std::string value = "  space at both  ";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "space at both");
+}
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+	std::string value = "    ";
+	std::string result = string_util::strip(value);
+
+	BOOST_TEST(result == "");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::join function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(join)
+
+BOOST_AUTO_TEST_CASE(empty)
+{
+	std::string expected = "";
+	std::string result = string_util::join<int>({});
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(one)
+{
+	std::string expected = "1";
+	std::string result = string_util::join({1});
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(two)
+{
+	std::string expected = "1:2";
+	std::string result = string_util::join({1, 2});
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(delimiter_string)
+{
+	std::string expected = "1;;2;;3";
+	std::string result = string_util::join({1, 2, 3}, ";;");
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_CASE(delimiter_char)
+{
+	std::string expected = "1@2@3@4";
+	std::string result = string_util::join({1, 2, 3, 4}, '@');
+
+	BOOST_TEST(expected == result);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::is_identifier function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(is_identifier_valid)
+
+BOOST_AUTO_TEST_CASE(correct)
+{
+	BOOST_TEST(string_util::is_identifier("localhost"));
+	BOOST_TEST(string_util::is_identifier("localhost2"));
+	BOOST_TEST(string_util::is_identifier("localhost2-4_"));
+}
+
+BOOST_AUTO_TEST_CASE(incorrect)
+{
+	BOOST_TEST(!string_util::is_identifier(""));
+	BOOST_TEST(!string_util::is_identifier("localhost with spaces"));
+	BOOST_TEST(!string_util::is_identifier("localhost*"));
+	BOOST_TEST(!string_util::is_identifier("&&"));
+	BOOST_TEST(!string_util::is_identifier("@'"));
+	BOOST_TEST(!string_util::is_identifier("##"));
+	BOOST_TEST(!string_util::is_identifier("===++"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * string_util::is_boolean function
+ * --------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(is_boolean)
+
+BOOST_AUTO_TEST_CASE(correct)
+{
+	// true
+	BOOST_TEST(string_util::is_boolean("true"));
+	BOOST_TEST(string_util::is_boolean("True"));
+	BOOST_TEST(string_util::is_boolean("TRUE"));
+	BOOST_TEST(string_util::is_boolean("TruE"));
+
+	// yes
+	BOOST_TEST(string_util::is_boolean("yes"));
+	BOOST_TEST(string_util::is_boolean("Yes"));
+	BOOST_TEST(string_util::is_boolean("YES"));
+	BOOST_TEST(string_util::is_boolean("YeS"));
+
+	// on
+	BOOST_TEST(string_util::is_boolean("on"));
+	BOOST_TEST(string_util::is_boolean("On"));
+	BOOST_TEST(string_util::is_boolean("oN"));
+	BOOST_TEST(string_util::is_boolean("ON"));
+
+	// 1
+	BOOST_TEST(string_util::is_boolean("1"));
+}
+
+BOOST_AUTO_TEST_CASE(incorrect)
+{
+	BOOST_TEST(!string_util::is_boolean("false"));
+	BOOST_TEST(!string_util::is_boolean("lol"));
+	BOOST_TEST(!string_util::is_boolean(""));
+	BOOST_TEST(!string_util::is_boolean("0"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // !namespace
+
+} // !irccd
--- a/tests/src/plugins/CMakeLists.txt	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/CMakeLists.txt	Fri Nov 16 12:25:00 2018 +0100
@@ -16,11 +16,13 @@
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #
 
-add_subdirectory(ask)
-add_subdirectory(auth)
-add_subdirectory(hangman)
-add_subdirectory(history)
-add_subdirectory(joke)
-add_subdirectory(logger)
-add_subdirectory(plugin)
-add_subdirectory(tictactoe)
+if (IRCCD_HAVE_JS)
+	add_subdirectory(ask)
+	add_subdirectory(auth)
+	add_subdirectory(hangman)
+	add_subdirectory(history)
+	add_subdirectory(joke)
+	add_subdirectory(logger)
+	add_subdirectory(plugin)
+	add_subdirectory(tictactoe)
+endif ()
--- a/tests/src/plugins/ask/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/ask/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -19,12 +19,14 @@
 #define BOOST_TEST_MODULE "Ask plugin"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -36,7 +38,7 @@
 		plugin_->set_options({
 			{ "file", CMAKE_CURRENT_SOURCE_DIR "/answers.conf" }
 		});
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -52,7 +54,7 @@
 	 * both answers in that amount of tries.
 	 */
 	for (int i = 0; i < 1000; ++i) {
-		plugin_->handle_command(irccd_, {server_, "tester", "#dummy", ""});
+		plugin_->handle_command(bot_, {server_, "tester", "#dummy", ""});
 
 		const auto cmd = server_->find("message").back();
 
@@ -76,4 +78,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/auth/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/auth/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -19,12 +19,15 @@
 #define BOOST_TEST_MODULE "Auth plugin"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::test::js_plugin_fixture;
+using irccd::test::mock_server;
+
+namespace irccd {
 
 namespace {
 
@@ -51,7 +54,7 @@
 			{ "quakenet.password",  "hello"         },
 			{ "quakenet.username",  "mario"         }
 		});
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -59,7 +62,7 @@
 
 BOOST_AUTO_TEST_CASE(nickserv1)
 {
-	plugin_->handle_connect(irccd_, { nickserv1_ });
+	plugin_->handle_connect(bot_, { nickserv1_ });
 
 	const auto cmd = nickserv1_->find("message").front();
 
@@ -69,7 +72,7 @@
 
 BOOST_AUTO_TEST_CASE(nickserv2)
 {
-	plugin_->handle_connect(irccd_, { nickserv2_ });
+	plugin_->handle_connect(bot_, { nickserv2_ });
 
 	const auto cmd = nickserv2_->find("message").front();
 
@@ -79,7 +82,7 @@
 
 BOOST_AUTO_TEST_CASE(quakenet)
 {
-	plugin_->handle_connect(irccd_, { quakenet_ });
+	plugin_->handle_connect(bot_, { quakenet_ });
 
 	const auto cmd = quakenet_->find("message").front();
 
@@ -91,4 +94,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/hangman/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/hangman/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,12 +22,16 @@
 #define BOOST_TEST_MODULE "Hangman plugin"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::daemon::plugin;
+
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -56,7 +60,7 @@
 			config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/words.conf");
 
 		plugin_->set_options(config);
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -66,20 +70,20 @@
 {
 	load({{ "collaborative", "false" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
 
 	auto cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "start=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:_ _ _");
 
-	plugin_->handle_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+	plugin_->handle_message(bot_, {server_, "jean!jean@localhost", "#hangman", "s"});
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
 
-	plugin_->handle_message(irccd_, {server_, "jean!jean@localhost", "#hangman", "s"});
+	plugin_->handle_message(bot_, {server_, "jean!jean@localhost", "#hangman", "s"});
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
@@ -90,17 +94,17 @@
 {
 	load({{ "collaborative", "false" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "a" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "b" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "c" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "d" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "e" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "f" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "g" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "h" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "i" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "j" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "a" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "b" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "c" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "d" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "e" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "f" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "g" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "h" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "i" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "j" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -112,8 +116,8 @@
 {
 	load({{ "collaborative", "false" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "s" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "s" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -125,7 +129,7 @@
 {
 	load();
 
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#hangman", ""});
 
 	const auto cmd = server_->find("message").back();
 
@@ -137,10 +141,10 @@
 {
 	load({{ "collaborative", "false" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "s" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "k" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "y" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "s" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "k" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "y" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -152,8 +156,8 @@
 {
 	load({{ "collaborative", "false" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "sky" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "sky" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -165,8 +169,8 @@
 {
 	load();
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "x" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "x" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -178,8 +182,8 @@
 {
 	load();
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "cheese" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "cheese" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -192,15 +196,15 @@
 	// Disable collaborative mode.
 	load({{ "collaborative", "false" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "s" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "s" });
 
 	auto cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "k" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "k" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
@@ -212,21 +216,21 @@
 	// Enable collaborative mode.
 	load({{ "collaborative", "true" }});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "s" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "s" });
 
 	auto cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "k" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "k" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "wrong-player=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:k");
 
-	plugin_->handle_message(irccd_, { server_, "francis!francis@localhost", "#hangman", "k" });
+	plugin_->handle_message(bot_, { server_, "francis!francis@localhost", "#hangman", "k" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
@@ -237,21 +241,21 @@
 {
 	load();
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#HANGMAN", "s" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#HANGMAN", "s" });
 
 	auto cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "found=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:s _ _");
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#HaNGMaN", "k" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#HaNGMaN", "k" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "wrong-player=hangman:!hangman:test:#hangman:jean!jean@localhost:jean:k");
 
-	plugin_->handle_message(irccd_, { server_, "francis!francis@localhost", "#hAngmAn", "k" });
+	plugin_->handle_message(bot_, { server_, "francis!francis@localhost", "#hAngmAn", "k" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#hangman");
@@ -263,26 +267,26 @@
 	load();
 
 	// Query mode is never collaborative.
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "irccd", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "irccd", "" });
 
 	auto cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "jean!jean@localhost");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "start=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:_ _ _");
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "irccd", "s" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "irccd", "s" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "jean!jean@localhost");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "found=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:s _ _");
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "irccd", "k" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "irccd", "k" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "jean!jean@localhost");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "found=hangman:!hangman:test:jean!jean@localhost:jean!jean@localhost:jean:s k _");
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "irccd", "sky" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "irccd", "sky" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "jean!jean@localhost");
@@ -293,9 +297,9 @@
 {
 	load();
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#hangman", "y" });
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#hangman", "y" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#hangman", "" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -330,13 +334,13 @@
 	unsigned last, current;
 
 	// 1. Initial game + finish.
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#hangman", ""});
 	last = std::any_cast<std::string>(server_->find("message").back()[1]).length();
 	found.insert(last);
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#hangman", words[last]});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#hangman", words[last]});
 
 	// 2. Current must not be the last one.
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#hangman", ""});
 	current = std::any_cast<std::string>(server_->find("message").back()[1]).length();
 
 	BOOST_TEST(last != current);
@@ -344,10 +348,10 @@
 
 	found.insert(current);
 	last = current;
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#hangman", words[current]});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#hangman", words[current]});
 
 	// 3. Last word must be the one that is kept into the map.
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#hangman", ""});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#hangman", ""});
 	current = std::any_cast<std::string>(server_->find("message").back()[1]).length();
 
 	BOOST_TEST(last != current);
@@ -358,4 +362,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/history/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/history/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,12 +21,16 @@
 #define BOOST_TEST_MODULE "History plugin"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::daemon::plugin;
+
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -50,7 +54,7 @@
 			config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/words.conf");
 
 		plugin_->set_options(config);
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -60,7 +64,7 @@
 {
 	load({{"file", CMAKE_CURRENT_SOURCE_DIR "/error.json"}});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#history", "seen francis" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#history", "seen francis" });
 
 	const auto cmd = server_->find("message").front();
 
@@ -75,8 +79,8 @@
 	remove(CMAKE_CURRENT_BINARY_DIR "/seen.json");
 	load({{ "file", CMAKE_CURRENT_BINARY_DIR "/seen.json" }});
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#history", "hello" });
-	plugin_->handle_command(irccd_, { server_, "destructor!dst@localhost", "#history", "seen jean" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#history", "hello" });
+	plugin_->handle_command(bot_, { server_, "destructor!dst@localhost", "#history", "seen jean" });
 
 	auto cmd = server_->find("message").front();
 
@@ -91,8 +95,8 @@
 	remove(CMAKE_CURRENT_BINARY_DIR "/said.json");
 	load({{ "file", CMAKE_CURRENT_BINARY_DIR "/said.json" }});
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#history", "hello" });
-	plugin_->handle_command(irccd_, { server_, "destructor!dst@localhost", "#history", "said jean" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#history", "hello" });
+	plugin_->handle_command(bot_, { server_, "destructor!dst@localhost", "#history", "said jean" });
 
 	const auto cmd = server_->find("message").front();
 
@@ -105,8 +109,8 @@
 	remove(CMAKE_CURRENT_BINARY_DIR "/unknown.json");
 	load({{ "file", CMAKE_CURRENT_BINARY_DIR "/unknown.json" }});
 
-	plugin_->handle_message(irccd_, { server_, "jean!jean@localhost", "#history", "hello" });
-	plugin_->handle_command(irccd_, { server_, "destructor!dst@localhost", "#history", "seen nobody" });
+	plugin_->handle_message(bot_, { server_, "jean!jean@localhost", "#history", "hello" });
+	plugin_->handle_command(bot_, { server_, "destructor!dst@localhost", "#history", "seen nobody" });
 
 	const auto cmd = server_->find("message").front();
 
@@ -121,10 +125,10 @@
 	remove(CMAKE_CURRENT_BINARY_DIR "/issue-642.json");
 	load({{"file", CMAKE_CURRENT_BINARY_DIR "/issue-642.json"}});
 
-	plugin_->handle_message(irccd_, { server_, "JeaN!JeaN@localhost", "#history", "hello" });
+	plugin_->handle_message(bot_, { server_, "JeaN!JeaN@localhost", "#history", "hello" });
 
 	// Full caps.
-	plugin_->handle_command(irccd_, { server_, "destructor!dst@localhost", "#HISTORY", "said JEAN" });
+	plugin_->handle_command(bot_, { server_, "destructor!dst@localhost", "#HISTORY", "said JEAN" });
 
 	auto cmd = server_->find("message").front();
 
@@ -132,7 +136,7 @@
 	BOOST_TEST(std::regex_match(std::any_cast<std::string>(cmd[1]), rule));
 
 	// Random caps.
-	plugin_->handle_command(irccd_, { server_, "destructor!dst@localhost", "#HiSToRy", "said JeaN" });
+	plugin_->handle_command(bot_, { server_, "destructor!dst@localhost", "#HiSToRy", "said JeaN" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#history");
@@ -143,4 +147,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/joke/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/joke/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,7 +21,11 @@
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::daemon::plugin;
+
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -42,7 +46,7 @@
 			config.emplace("file", CMAKE_CURRENT_SOURCE_DIR "/jokes.json");
 
 		plugin_->set_options(config);
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -68,7 +72,7 @@
 	load();
 
 	const auto call = [&] () {
-		plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#joke", "" });
+		plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#joke", "" });
 
 		const auto cmd = server_->find("message").back();
 		const auto msg = std::any_cast<std::string>(cmd[1]);
@@ -107,7 +111,7 @@
 	};
 
 	const auto call = [&] () {
-		plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#joke", "" });
+		plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#joke", "" });
 
 		const auto cmd = server_->find("message").back();
 
@@ -136,7 +140,7 @@
 	};
 
 	const auto call = [&] () {
-		plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#joke", "" });
+		plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#joke", "" });
 
 		const auto cmd = server_->find("message").back();
 
@@ -161,7 +165,7 @@
 {
 	load({{"file", "doesnotexist.json"}});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#joke", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#joke", "" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -173,7 +177,7 @@
 {
 	load({{"file", CMAKE_CURRENT_SOURCE_DIR "/error-not-array.json"}});
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#joke", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#joke", "" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -185,7 +189,7 @@
 {
 	load({{"file", CMAKE_CURRENT_SOURCE_DIR "/error-empty.json"}});
 
-	plugin_->handle_command(irccd_, {server_, "jean!jean@localhost", "#joke", ""});
+	plugin_->handle_command(bot_, {server_, "jean!jean@localhost", "#joke", ""});
 
 	const auto cmd = server_->find("message").back();
 
@@ -199,4 +203,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/logger/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/logger/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,12 +22,16 @@
 #define BOOST_TEST_MODULE "Logger plugin"
 #include <boost/test/unit_test.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::daemon::plugin;
+
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -65,7 +69,7 @@
 			config.emplace("path", CMAKE_CURRENT_BINARY_DIR "/log.txt");
 
 		plugin_->set_options(config);
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -75,7 +79,7 @@
 {
 	load();
 
-	plugin_->handle_join(irccd_, {server_, "jean!jean@localhost", "#staff"});
+	plugin_->handle_join(bot_, {server_, "jean!jean@localhost", "#staff"});
 
 	BOOST_REQUIRE_EQUAL("join=test:#staff:jean!jean@localhost:jean\n", last());
 }
@@ -84,7 +88,7 @@
 {
 	load();
 
-	plugin_->handle_kick(irccd_, {server_, "jean!jean@localhost", "#staff", "badboy", "please do not flood"});
+	plugin_->handle_kick(bot_, {server_, "jean!jean@localhost", "#staff", "badboy", "please do not flood"});
 
 	BOOST_REQUIRE_EQUAL("kick=test:#staff:jean!jean@localhost:jean:badboy:please do not flood\n", last());
 }
@@ -93,7 +97,7 @@
 {
 	load();
 
-	plugin_->handle_me(irccd_, {server_, "jean!jean@localhost", "#staff", "is drinking water"});
+	plugin_->handle_me(bot_, {server_, "jean!jean@localhost", "#staff", "is drinking water"});
 
 	BOOST_REQUIRE_EQUAL("me=test:#staff:jean!jean@localhost:jean:is drinking water\n", last());
 }
@@ -102,7 +106,7 @@
 {
 	load();
 
-	plugin_->handle_message(irccd_, {server_, "jean!jean@localhost", "#staff", "hello guys"});
+	plugin_->handle_message(bot_, {server_, "jean!jean@localhost", "#staff", "hello guys"});
 
 	BOOST_REQUIRE_EQUAL("message=test:#staff:jean!jean@localhost:jean:hello guys\n", last());
 }
@@ -111,7 +115,7 @@
 {
 	load();
 
-	plugin_->handle_mode(irccd_, {server_, "jean!jean@localhost", "chris", "+i", "l", "u", "m"});
+	plugin_->handle_mode(bot_, {server_, "jean!jean@localhost", "chris", "+i", "l", "u", "m"});
 
 	BOOST_REQUIRE_EQUAL("mode=test:jean!jean@localhost:chris:+i:l:u:m\n", last());
 }
@@ -120,7 +124,7 @@
 {
 	load();
 
-	plugin_->handle_notice(irccd_, {server_, "jean!jean@localhost", "chris", "tu veux voir mon chat ?"});
+	plugin_->handle_notice(bot_, {server_, "jean!jean@localhost", "chris", "tu veux voir mon chat ?"});
 
 	BOOST_REQUIRE_EQUAL("notice=test:jean!jean@localhost:chris:tu veux voir mon chat ?\n", last());
 }
@@ -129,7 +133,7 @@
 {
 	load();
 
-	plugin_->handle_part(irccd_, {server_, "jean!jean@localhost", "#staff", "too noisy here"});
+	plugin_->handle_part(bot_, {server_, "jean!jean@localhost", "#staff", "too noisy here"});
 
 	BOOST_REQUIRE_EQUAL("part=test:#staff:jean!jean@localhost:jean:too noisy here\n", last());
 }
@@ -138,7 +142,7 @@
 {
 	load();
 
-	plugin_->handle_topic(irccd_, {server_, "jean!jean@localhost", "#staff", "oh yeah yeaaaaaaaah"});
+	plugin_->handle_topic(bot_, {server_, "jean!jean@localhost", "#staff", "oh yeah yeaaaaaaaah"});
 
 	BOOST_REQUIRE_EQUAL("topic=test:#staff:jean!jean@localhost:jean:oh yeah yeaaaaaaaah\n", last());
 }
@@ -147,7 +151,7 @@
 {
 	load();
 
-	plugin_->handle_message(irccd_, {server_, "jean!jean@localhost", "#STAFF", "hello guys"});
+	plugin_->handle_message(bot_, {server_, "jean!jean@localhost", "#STAFF", "hello guys"});
 
 	BOOST_REQUIRE_EQUAL("message=test:#staff:jean!jean@localhost:jean:hello guys\n", last());
 }
@@ -156,4 +160,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/plugin/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/plugin/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -22,7 +22,7 @@
 
 #include <irccd/string_util.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 #include <irccd/daemon/server.hpp>
 
@@ -31,7 +31,11 @@
 using boost::format;
 using boost::str;
 
-namespace irccd::test {
+using irccd::daemon::plugin;
+
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -70,7 +74,7 @@
 	test_fixture()
 		: js_plugin_fixture(PLUGIN_PATH)
 	{
-		irccd_.plugins().add(std::make_shared<fake_plugin>("fake"));
+		bot_.plugins().add(std::make_shared<fake_plugin>("fake"));
 
 		plugin_->set_formats({
 			{ "usage", "usage=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" },
@@ -78,7 +82,7 @@
 			{ "not-found", "not-found=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}:#{name}" },
 			{ "too-long", "too-long=#{plugin}:#{command}:#{server}:#{channel}:#{origin}:#{nickname}" }
 		});
-		plugin_->handle_load(irccd_);
+		plugin_->handle_load(bot_);
 	}
 };
 
@@ -86,20 +90,20 @@
 
 BOOST_AUTO_TEST_CASE(format_usage)
 {
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#staff", "" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#staff", "" });
 
 	auto cmd = server_->find("message").front();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#staff", "fail" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#staff", "fail" });
 	cmd = server_->find("message").front();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "usage=plugin:!plugin:test:#staff:jean!jean@localhost:jean");
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#staff", "info" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#staff", "info" });
 	cmd = server_->find("message").front();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#staff");
@@ -108,7 +112,7 @@
 
 BOOST_AUTO_TEST_CASE(format_info)
 {
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#staff", "info fake" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#staff", "info fake" });
 
 	const auto cmd = server_->find("message").front();
 
@@ -118,7 +122,7 @@
 
 BOOST_AUTO_TEST_CASE(format_not_found)
 {
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#staff", "info doesnotexistsihope" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#staff", "info doesnotexistsihope" });
 
 	const auto cmd = server_->find("message").front();
 
@@ -129,9 +133,9 @@
 BOOST_AUTO_TEST_CASE(format_too_long)
 {
 	for (int i = 0; i < 100; ++i)
-		irccd_.plugins().add(std::make_shared<fake_plugin>(str(format("plugin-n-%1%") % i)));
+		bot_.plugins().add(std::make_shared<fake_plugin>(str(format("plugin-n-%1%") % i)));
 
-	plugin_->handle_command(irccd_, { server_, "jean!jean@localhost", "#staff", "list" });
+	plugin_->handle_command(bot_, { server_, "jean!jean@localhost", "#staff", "list" });
 
 	const auto cmd = server_->find("message").front();
 
@@ -143,4 +147,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd
--- a/tests/src/plugins/tictactoe/main.cpp	Thu Nov 15 13:19:17 2018 +0100
+++ b/tests/src/plugins/tictactoe/main.cpp	Fri Nov 16 12:25:00 2018 +0100
@@ -21,13 +21,15 @@
 
 #include <irccd/string_util.hpp>
 
-#include <irccd/daemon/irccd.hpp>
+#include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/plugin_service.hpp>
 #include <irccd/daemon/server.hpp>
 
 #include <irccd/test/js_plugin_fixture.hpp>
 
-namespace irccd::test {
+using irccd::test::js_plugin_fixture;
+
+namespace irccd {
 
 namespace {
 
@@ -67,8 +69,8 @@
 
 	auto start()
 	{
-		plugin_->handle_command(irccd_, { server_, "a!a@localhost", "#tictactoe", "b" });
-		plugin_->handle_names(irccd_, { server_, "#tictactoe", { "a", "b" }});
+		plugin_->handle_command(bot_, { server_, "a!a@localhost", "#tictactoe", "b" });
+		plugin_->handle_names(bot_, { server_, "#tictactoe", { "a", "b" }});
 
 		return next_players();
 	}
@@ -86,7 +88,7 @@
 
 		for (const auto& p : points) {
 			server_->clear();
-			plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", p });
+			plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", p });
 			players = next_players();
 		}
 	}
@@ -100,7 +102,7 @@
 
 	const auto players = next_players();
 
-	plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "a 3" });
+	plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "a 3" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -128,7 +130,7 @@
 
 	const auto players = next_players();
 
-	plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "b 1" });
+	plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "b 1" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -148,8 +150,8 @@
 {
 	auto players = start();
 
-	plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "a 1" });
-	plugin_->handle_message(irccd_, { server_, players.second, "#tictactoe", "a 1" });
+	plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "a 1" });
+	plugin_->handle_message(bot_, { server_, players.second, "#tictactoe", "a 1" });
 
 	const auto cmd = server_->find("message").back();
 
@@ -168,7 +170,7 @@
 BOOST_AUTO_TEST_CASE(invalid)
 {
 	// empty name (no names)
-	plugin_->handle_command(irccd_, { server_, "jean", "#tictactoe", "" });
+	plugin_->handle_command(bot_, { server_, "jean", "#tictactoe", "" });
 
 	auto cmd = server_->find("message").back();
 
@@ -176,22 +178,22 @@
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "invalid=#tictactoe:!tictactoe:jean:jean:tictactoe:test");
 
 	// bot name (no names)
-	plugin_->handle_command(irccd_, { server_, "jean", "#tictactoe", "irccd" });
+	plugin_->handle_command(bot_, { server_, "jean", "#tictactoe", "irccd" });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#tictactoe");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "invalid=#tictactoe:!tictactoe:jean:jean:tictactoe:test");
 
 	// target is origin (no names)
-	plugin_->handle_command(irccd_, { server_, server_->get_nickname(), "#tictactoe", server_->get_nickname() });
+	plugin_->handle_command(bot_, { server_, server_->get_nickname(), "#tictactoe", server_->get_nickname() });
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#tictactoe");
 	BOOST_TEST(std::any_cast<std::string>(cmd[1]) == "invalid=#tictactoe:!tictactoe:irccd:irccd:tictactoe:test");
 
 	// not existing (names)
-	plugin_->handle_command(irccd_, { server_, server_->get_nickname(), "#tictactoe", server_->get_nickname() });
-	plugin_->handle_names(irccd_, { server_, "#tictactoe", { "a", "b", "c" }});
+	plugin_->handle_command(bot_, { server_, server_->get_nickname(), "#tictactoe", server_->get_nickname() });
+	plugin_->handle_names(bot_, { server_, "#tictactoe", { "a", "b", "c" }});
 	cmd = server_->find("message").back();
 
 	BOOST_TEST(std::any_cast<std::string>(cmd[0]) == "#tictactoe");
@@ -219,7 +221,7 @@
 		else
 			b = true;
 
-		plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "a 3" });
+		plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "a 3" });
 	}
 }
 
@@ -227,9 +229,9 @@
 {
 	const auto players = start();
 
-	plugin_->handle_disconnect(irccd_, { server_ });
+	plugin_->handle_disconnect(bot_, { server_ });
 	server_->clear();
-	plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "a 1" });
+	plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "a 1" });
 
 	BOOST_TEST(server_->empty());
 }
@@ -239,8 +241,8 @@
 	const auto players = start();
 
 	server_->clear();
-	plugin_->handle_kick(irccd_, { server_, "kefka", "#tictactoe", players.first, "" });
-	plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "a 1" });
+	plugin_->handle_kick(bot_, { server_, "kefka", "#tictactoe", players.first, "" });
+	plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "a 1" });
 
 	BOOST_TEST(server_->empty());
 }
@@ -250,8 +252,8 @@
 	const auto players = start();
 
 	server_->clear();
-	plugin_->handle_part(irccd_, { server_, players.first, "#tictactoe", "" });
-	plugin_->handle_message(irccd_, { server_, players.first, "#tictactoe", "a 1" });
+	plugin_->handle_part(bot_, { server_, players.first, "#tictactoe", "" });
+	plugin_->handle_message(bot_, { server_, players.first, "#tictactoe", "a 1" });
 
 	BOOST_TEST(server_->empty());
 }
@@ -260,4 +262,4 @@
 
 } // !namespace
 
-} // !irccd::test
+} // !irccd