changeset 826:f85faf0f5d70

irccd: rename command to transport_command
author David Demelier <markand@malikania.fr>
date Tue, 08 Jan 2019 21:47:59 +0100
parents e9da936309df
children 2ecff01d4277
files irccd/main.cpp libirccd-daemon/CMakeLists.txt libirccd-daemon/irccd/daemon/command.cpp libirccd-daemon/irccd/daemon/command.hpp libirccd-daemon/irccd/daemon/transport_command.cpp libirccd-daemon/irccd/daemon/transport_command.hpp libirccd-daemon/irccd/daemon/transport_service.cpp libirccd-daemon/irccd/daemon/transport_service.hpp libirccd-test/irccd/test/cli_fixture.cpp libirccd-test/irccd/test/command_fixture.cpp
diffstat 10 files changed, 1681 insertions(+), 1690 deletions(-) [+]
line wrap: on
line diff
--- a/irccd/main.cpp	Tue Jan 08 20:41:20 2019 +0100
+++ b/irccd/main.cpp	Tue Jan 08 21:47:59 2019 +0100
@@ -27,11 +27,11 @@
 #include <irccd/options.hpp>
 #include <irccd/system.hpp>
 
-#include <irccd/daemon/command.hpp>
 #include <irccd/daemon/dynlib_plugin.hpp>
 #include <irccd/daemon/bot.hpp>
 #include <irccd/daemon/logger.hpp>
 #include <irccd/daemon/plugin_service.hpp>
+#include <irccd/daemon/transport_command.hpp>
 #include <irccd/daemon/transport_service.hpp>
 
 #if defined(IRCCD_HAVE_JS)
@@ -178,7 +178,7 @@
 	init(argc, argv);
 
 	// 1. Load commands.
-	for (const auto& f : command::registry())
+	for (const auto& f : transport_command::registry())
 		instance->transports().get_commands().push_back(f());
 
 	// 2. Load plugin loaders.
--- a/libirccd-daemon/CMakeLists.txt	Tue Jan 08 20:41:20 2019 +0100
+++ b/libirccd-daemon/CMakeLists.txt	Tue Jan 08 21:47:59 2019 +0100
@@ -23,8 +23,6 @@
 	${libirccd-daemon_SOURCE_DIR}/irccd/daemon.hpp
 	${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
@@ -49,6 +47,8 @@
 	${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_command.cpp
+	${libirccd-daemon_SOURCE_DIR}/irccd/daemon/transport_command.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
--- a/libirccd-daemon/irccd/daemon/command.cpp	Tue Jan 08 20:41:20 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,863 +0,0 @@
-/*
- * command.cpp -- remote command
- *
- * Copyright (c) 2013-2019 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER 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 <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->get<std::string>()] = value->get<std::string>();
-	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
-
-auto command::registry() noexcept -> const std::vector<constructor>&
-{
-	static const std::vector<command::constructor> list{
-		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>()
-	};
-
-	return list;
-};
-
-// {{{ 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
--- a/libirccd-daemon/irccd/daemon/command.hpp	Tue Jan 08 20:41:20 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,794 +0,0 @@
-/*
- * command.hpp -- remote command
- *
- * Copyright (c) 2013-2019 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER 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<std::unique_ptr<command> ()>;
-
-	/**
-	 * \brief Registry of all commands.
-	 */
-	static auto registry() noexcept -> const std::vector<constructor>&;
-
-	/**
-	 * 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/transport_command.cpp	Tue Jan 08 21:47:59 2019 +0100
@@ -0,0 +1,863 @@
+/*
+ * command.cpp -- remote command
+ *
+ * Copyright (c) 2013-2019 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER 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 <irccd/string_util.hpp>
+
+#include "bot.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"
+#include "transport_command.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->get<std::string>()] = value->get<std::string>();
+	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 -> transport_command::constructor
+{
+	return [] () noexcept {
+		return std::make_unique<T>();
+	};
+}
+
+} // !namespace
+
+auto transport_command::registry() noexcept -> const std::vector<constructor>&
+{
+	static const std::vector<transport_command::constructor> list{
+		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>()
+	};
+
+	return list;
+};
+
+// {{{ 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/transport_command.hpp	Tue Jan 08 21:47:59 2019 +0100
@@ -0,0 +1,794 @@
+/*
+ * transport_command.hpp -- remote command
+ *
+ * Copyright (c) 2013-2019 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER 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_COMMAND_HPP
+#define IRCCD_DAEMON_TRANSPORT_COMMAND_HPP
+
+/**
+ * \file transport_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;
+
+// {{{ transport_command
+
+/**
+ * \brief Server side remote command
+ * \ingroup transports
+ */
+class transport_command {
+public:
+	/**
+	 * \brief Convenient alias.
+	 */
+	using document = json_util::deserializer;
+
+	/**
+	 * \brief Command constructor factory.
+	 */
+	using constructor = std::function<std::unique_ptr<transport_command> ()>;
+
+	/**
+	 * \brief Registry of all commands.
+	 */
+	static auto registry() noexcept -> const std::vector<constructor>&;
+
+	/**
+	 * Default destructor virtual.
+	 */
+	virtual ~transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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 transport_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
--- a/libirccd-daemon/irccd/daemon/transport_service.cpp	Tue Jan 08 20:41:20 2019 +0100
+++ b/libirccd-daemon/irccd/daemon/transport_service.cpp	Tue Jan 08 21:47:59 2019 +0100
@@ -23,9 +23,9 @@
 #include <irccd/json_util.hpp>
 
 #include "bot.hpp"
-#include "command.hpp"
 #include "logger.hpp"
 #include "transport_client.hpp"
+#include "transport_command.hpp"
 #include "transport_server.hpp"
 #include "transport_service.hpp"
 #include "transport_util.hpp"
--- a/libirccd-daemon/irccd/daemon/transport_service.hpp	Tue Jan 08 20:41:20 2019 +0100
+++ b/libirccd-daemon/irccd/daemon/transport_service.hpp	Tue Jan 08 21:47:59 2019 +0100
@@ -38,8 +38,8 @@
 namespace daemon {
 
 class bot;
-class command;
 class transport_client;
+class transport_command;
 class transport_server;
 
 /**
@@ -52,7 +52,7 @@
 	/**
 	 * \brief the list of transport commands.
 	 */
-	using commands = std::vector<std::unique_ptr<command>>;
+	using commands = std::vector<std::unique_ptr<transport_command>>;
 
 	/**
 	 * \brief The list of transport acceptors.
--- a/libirccd-test/irccd/test/cli_fixture.cpp	Tue Jan 08 20:41:20 2019 +0100
+++ b/libirccd-test/irccd/test/cli_fixture.cpp	Tue Jan 08 21:47:59 2019 +0100
@@ -24,7 +24,7 @@
 #include <irccd/string_util.hpp>
 #include <irccd/acceptor.hpp>
 
-#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/transport_command.hpp>
 #include <irccd/daemon/transport_service.hpp>
 #include <irccd/daemon/transport_server.hpp>
 
@@ -33,10 +33,6 @@
 
 namespace proc = boost::process;
 
-using irccd::daemon::bot;
-using irccd::daemon::command;
-using irccd::daemon::transport_server;
-
 namespace irccd::test {
 
 namespace {
@@ -64,11 +60,11 @@
 
 	auto acceptor = std::make_unique<ip_acceptor>(bot_.get_service(), std::move(raw_acceptor));
 
-	for (const auto& f : command::registry())
+	for (const auto& f : daemon::transport_command::registry())
 		bot_.transports().get_commands().push_back(f());
 
 	bot_.servers().add(server_);
-	bot_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
+	bot_.transports().add(std::make_unique<daemon::transport_server>(std::move(acceptor)));
 	bot_.plugins().add_loader(std::make_unique<test_plugin_loader>());
 	server_->clear();
 }
--- a/libirccd-test/irccd/test/command_fixture.cpp	Tue Jan 08 20:41:20 2019 +0100
+++ b/libirccd-test/irccd/test/command_fixture.cpp	Tue Jan 08 21:47:59 2019 +0100
@@ -19,23 +19,18 @@
 #include <irccd/acceptor.hpp>
 #include <irccd/connector.hpp>
 
-#include <irccd/daemon/command.hpp>
+#include <irccd/daemon/transport_command.hpp>
 #include <irccd/daemon/transport_server.hpp>
 #include <irccd/daemon/transport_service.hpp>
 
 #include "command_fixture.hpp"
 
-using boost::asio::ip::tcp;
-using boost::asio::deadline_timer;
-
-using boost::posix_time::seconds;
-
-using irccd::daemon::command;
-using irccd::daemon::transport_server;
+namespace asio = boost::asio;
+namespace posix_time = boost::posix_time;
 
 namespace irccd::test {
 
-auto command_fixture::recv(deadline_timer& timer) -> result
+auto command_fixture::recv(asio::deadline_timer& timer) -> result
 {
 	result r;
 
@@ -57,11 +52,11 @@
 auto command_fixture::wait_command(const std::string& cmd) -> result
 {
 	result r;
-	deadline_timer timer(bot_.get_service());
+	asio::deadline_timer timer(bot_.get_service());
 
-	timer.expires_from_now(seconds(30));
+	timer.expires_from_now(posix_time::seconds(30));
 	timer.async_wait([] (auto code) {
-		if (code != boost::asio::error::operation_aborted)
+		if (code != asio::error::operation_aborted)
 			throw std::runtime_error("operation timed out");
 	});
 
@@ -96,27 +91,27 @@
 	: server_(new mock_server(ctx_, "test", "localhost"))
 	, plugin_(new mock_plugin("test"))
 {
-	tcp::endpoint ep(tcp::v4(), 0U);
-	tcp::acceptor raw_acceptor(bot_.get_service(), std::move(ep));
+	asio::ip::tcp::endpoint ep(asio::ip::tcp::v4(), 0U);
+	asio::ip::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>(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())
+	for (const auto& f : daemon::transport_command::registry())
 		bot_.transports().get_commands().push_back(f());
 
 	// 2. Create controller and transport server.
 	ctl_ = std::make_unique<ctl::controller>(std::move(connector));
-	bot_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
+	bot_.transports().add(std::make_unique<daemon::transport_server>(std::move(acceptor)));
 
 	// 3. Wait for controller to connect.
-	boost::asio::deadline_timer timer(ctx_);
+	asio::deadline_timer timer(ctx_);
 
-	timer.expires_from_now(boost::posix_time::seconds(10));
+	timer.expires_from_now(posix_time::seconds(10));
 	timer.async_wait([] (auto code) {
-		if (code && code != boost::asio::error::operation_aborted)
+		if (code && code != asio::error::operation_aborted)
 			throw std::system_error(make_error_code(std::errc::timed_out));
 	});