diff irccdctl/cli.cpp @ 527:a88796ed040a

Irccdctl: switch to Boost.Asio, closes #697
author David Demelier <markand@malikania.fr>
date Thu, 16 Nov 2017 22:45:12 +0100
parents e03521cf207b
children 9daccaeedcce
line wrap: on
line diff
--- a/irccdctl/cli.cpp	Tue Nov 14 20:25:30 2017 +0100
+++ b/irccdctl/cli.cpp	Thu Nov 16 22:45:12 2017 +0100
@@ -16,150 +16,107 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <cassert>
-#include <iostream>
-#include <sstream>
+#include <boost/system/system_error.hpp>
 
-#include <boost/timer/timer.hpp>
+#include <irccd/errors.hpp>
+#include <irccd/json_util.hpp>
+#include <irccd/options.hpp>
+#include <irccd/string_util.hpp>
 
-#include <json.hpp>
+#include <irccd/ctl/controller.hpp>
 
 #include "cli.hpp"
-#include "irccdctl.hpp"
-#include "json_util.hpp"
-#include "logger.hpp"
-#include "options.hpp"
-#include "net_util.hpp"
-#include "string_util.hpp"
-
-using namespace std::string_literals;
 
 namespace irccd {
 
+namespace ctl {
+
+void cli::recv_response(ctl::controller& ctl, nlohmann::json req, handler_t handler)
+{
+    ctl.recv([&ctl, req, handler, this] (auto code, auto message) {
+        if (code)
+            throw boost::system::system_error(code);
+
+        auto c = json_util::to_string(message["command"]);
+
+        if (c != req["command"].get<std::string>()) {
+            recv_response(ctl, std::move(req), std::move(handler));
+            return;
+        }
+
+        if (message["error"].is_number_integer())
+            throw boost::system::system_error(static_cast<network_error>(message["error"].template get<int>()));
+        if (message["error"].is_string())
+            throw std::runtime_error(message["error"].template get<std::string>());
+
+        if (handler)
+            handler(std::move(message));
+    });
+}
+
+void cli::request(ctl::controller& ctl, nlohmann::json req, handler_t handler)
+{
+    ctl.send(req, [&ctl, req, handler, this] (auto code, auto) {
+        if (code)
+            throw boost::system::system_error(code);
+
+        recv_response(ctl, std::move(req), std::move(handler));
+    });
+}
+
 /*
- * Cli.
+ * plugin_info_cli.
  * ------------------------------------------------------------------
  */
 
-void Cli::check(const nlohmann::json &response)
-{
-    if (!json_util::get_bool(response, "status", false)) {
-        auto error = json_util::get_string(response, "error");
-
-        if (error.empty())
-            throw std::runtime_error("command failed with an unknown error");
-
-        throw std::runtime_error(error);
-    }
-}
-
-nlohmann::json Cli::request(Irccdctl &irccdctl, nlohmann::json args)
+void plugin_config_cli::set(ctl::controller& ctl, const std::vector<std::string>&args)
 {
-    auto msg = nlohmann::json();
-
-    if (!args.is_object())
-        args = nlohmann::json::object();
-
-    args.push_back({"command", m_name});
-    irccdctl.client().request(args);
-
-    auto id = irccdctl.client().onMessage.connect([&] (auto input) {
-        msg = std::move(input);
-    });
-
-    try {
-        boost::timer::cpu_timer timer;
-
-        while (irccdctl.client().isConnected() && !msg.is_object() && timer.elapsed().wall / 1000000LL < 3000)
-            net_util::poll(3000 - timer.elapsed().wall / 1000000LL, irccdctl);
-    } catch (const std::exception &) {
-        irccdctl.client().onMessage.disconnect(id);
-        throw;
-    }
-
-    irccdctl.client().onMessage.disconnect(id);
-
-    if (!msg.is_object())
-        throw std::runtime_error("no response received");
-    if (json_util::get_string(msg, "command") != m_name)
-        throw std::runtime_error("unexpected command result received");
-
-    check(msg);
-
-    return msg;
-}
-
-void Cli::call(Irccdctl &irccdctl, nlohmann::json args)
-{
-    check(request(irccdctl, args));
-}
-
-namespace cli {
-
-/*
- * PluginConfigCli.
- * ------------------------------------------------------------------
- */
-
-void PluginConfigCli::set(Irccdctl &irccdctl, const std::vector<std::string> &args)
-{
-    check(request(irccdctl, nlohmann::json::object({
+    request(ctl, {
         { "plugin", args[0] },
         { "variable", args[1] },
         { "value", args[2] }
-    })));
+    });
 }
 
-void PluginConfigCli::get(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_config_cli::get(ctl::controller& ctl, const std::vector<std::string>& args)
 {
-    auto result = request(irccdctl, nlohmann::json::object({
+    auto json = nlohmann::json::object({
         { "plugin", args[0] },
         { "variable", args[1] }
-    }));
+    });
 
-    check(result);
-
-    if (result["variables"].is_object())
-        std::cout << json_util::pretty(result["variables"][args[1]]) << std::endl;
+    request(ctl, std::move(json), [args] (auto result) {
+        if (result["variables"].is_object())
+            std::cout << json_util::pretty(result["variables"][args[1]]) << std::endl;
+    });
 }
 
-void PluginConfigCli::getall(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_config_cli::getall(ctl::controller& ctl, const std::vector<std::string> &args)
 {
-    auto result = request(irccdctl, nlohmann::json::object({{ "plugin", args[0] }}));
+    request(ctl, {{ "plugin", args[0] }}, [] (auto result) {
+        auto variables = result["variables"];
 
-    check(result);
-
-    auto variables = result["variables"];
-
-    for (auto v = variables.begin(); v != variables.end(); ++v)
-        std::cout << std::setw(16) << std::left << v.key() << " : " << json_util::pretty(v.value()) << std::endl;
+        for (auto v = variables.begin(); v != variables.end(); ++v)
+            std::cout << std::setw(16) << std::left << v.key() << " : " << json_util::pretty(v.value()) << std::endl;
+    });
 }
 
-PluginConfigCli::PluginConfigCli()
-    : Cli("plugin-config",
-          "configure a plugin",
-          "plugin-config plugin [variable] [value]",
-          "Get or set a plugin configuration variable.\n\n"
-          "If both variable and value are provided, sets the plugin configuration "
-          "to the\nrespective variable name and value.\n\n"
-          "If only variable is specified, shows its current value. Otherwise, list "
-          "all\nvariables and their values.\n\n"
-          "Examples:\n"
-          "\tirccdctl plugin-config ask")
+std::string plugin_config_cli::name() const
 {
+    return "plugin-config";
 }
 
-void PluginConfigCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_config_cli::exec(ctl::controller& ctl, const std::vector<std::string> &args)
 {
     switch (args.size()) {
     case 3:
-        set(irccdctl, args);
+        set(ctl, args);
         break;
     case 2:
-        get(irccdctl, args);
+        get(ctl, args);
         break;
     case 1:
-        getall(irccdctl, args);
+        getall(ctl, args);
         break;
     default:
         throw std::invalid_argument("plugin-config requires at least 1 argument");
@@ -167,188 +124,150 @@
 }
 
 /*
- * PluginInfoCli.
+ * plugin_info_cli.
  * ------------------------------------------------------------------
  */
 
-PluginInfoCli::PluginInfoCli()
-    : Cli("plugin-info",
-          "get plugin information",
-          "plugin-info plugin",
-          "Get plugin information.\n\n"
-          "Example:\n"
-          "\tirccdctl plugin-info ask"
-    )
+std::string plugin_info_cli::name() const
 {
+    return "plugin-info";
 }
 
-void PluginInfoCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_info_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("plugin-info requires 1 argument");
 
-    auto result = request(irccdctl, {{ "plugin", args[0] }});
-
-    std::cout << std::boolalpha;
-    std::cout << "Author         : " << json_util::get_string(result, "author") << std::endl;
-    std::cout << "License        : " << json_util::get_string(result, "license") << std::endl;
-    std::cout << "Summary        : " << json_util::get_string(result, "summary") << std::endl;
-    std::cout << "Version        : " << json_util::get_string(result, "version") << std::endl;
+    request(ctl, {{ "plugin", args[0] }}, [] (auto result) {
+        std::cout << std::boolalpha;
+        std::cout << "Author         : " << json_util::get_string(result, "author") << std::endl;
+        std::cout << "License        : " << json_util::get_string(result, "license") << std::endl;
+        std::cout << "Summary        : " << json_util::get_string(result, "summary") << std::endl;
+        std::cout << "Version        : " << json_util::get_string(result, "version") << std::endl;
+    });
 }
 
 /*
- * PluginListCli.
+ * plugin_list_cli.
  * ------------------------------------------------------------------
  */
 
-PluginListCli::PluginListCli()
-    : Cli("plugin-list",
-          "list loaded plugins",
-          "plugin-list",
-          "Get the list of all loaded plugins.\n\n"
-          "Example:\n"
-          "\tirccdctl plugin-list")
+std::string plugin_list_cli::name() const
 {
+    return "plugin-list";
 }
 
-void PluginListCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &)
+void plugin_list_cli::exec(ctl::controller& ctl, const std::vector<std::string>&)
 {
-    auto result = request(irccdctl);
-
-    for (const auto &value : result["list"])
-        if (value.is_string())
-            std::cout << value.get<std::string>() << std::endl;
+    request(ctl, {{ "command", "plugin-list" }}, [] (auto result) {
+        for (const auto &value : result["list"])
+            if (value.is_string())
+                std::cout << value.template get<std::string>() << std::endl;
+    });
 }
 
 /*
- * PluginLoadCli.
+ * plugin_load_cli.
  * ------------------------------------------------------------------
  */
 
-PluginLoadCli::PluginLoadCli()
-    : Cli("plugin-load",
-          "load a plugin",
-          "plugin-load logger",
-          "Load a plugin into the irccd instance.\n\n"
-          "Example:\n"
-          "\tirccdctl plugin-load logger")
+std::string plugin_load_cli::name() const
 {
+    return "plugin-load";
 }
 
-void PluginLoadCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_load_cli::exec(ctl::controller& ctl, const std::vector<std::string> &args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("plugin-load requires 1 argument");
 
-    check(request(irccdctl, {{"plugin", args[0]}}));
+    request(ctl, {{ "plugin", args[0] }});
 }
 
 /*
- * PluginReloadCli.
+ * plugin_reload_cli.
  * ------------------------------------------------------------------
  */
 
-PluginReloadCli::PluginReloadCli()
-    : Cli("plugin-reload",
-          "reload a plugin",
-          "plugin-reload plugin",
-          "Reload a plugin by calling the appropriate onReload event, the plugin is not\n"
-          "unloaded and must be already loaded.\n\n"
-          "Example:\n"
-          "\tirccdctl plugin-reload logger")
+std::string plugin_reload_cli::name() const
 {
+    return "plugin-reload";
 }
 
-void PluginReloadCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_reload_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("plugin-reload requires 1 argument");
 
-    check(request(irccdctl, {{ "plugin", args[0] }}));
+    request(ctl, {{ "plugin", args[0] }});
 }
 
 /*
- * PluginUnloadCli.
+ * plugin_unload_cli.
  * ------------------------------------------------------------------
  */
 
-PluginUnloadCli::PluginUnloadCli()
-    : Cli("plugin-unload",
-          "unload a plugin",
-          "plugin-unload plugin",
-          "Unload a loaded plugin from the irccd instance.\n\n"
-          "Example:\n"
-          "\tirccdctl plugin-unload logger")
+std::string plugin_unload_cli::name() const
 {
+    return "plugin-unload";
 }
 
-void PluginUnloadCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void plugin_unload_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("plugin-unload requires 1 argument");
 
-    check(request(irccdctl, {{ "plugin", args[0] }}));
+    request(ctl, {{ "plugin", args[0] }});
 }
 
 /*
- * ServerChannelCli.
+ * server_channel_cli.
  * ------------------------------------------------------------------
  */
 
-ServerChannelMode::ServerChannelMode()
-    : Cli("server-cmode",
-          "change channel mode",
-          "server-cmode server channel mode",
-          "Change the mode of the specified channel.\n\n"
-          "Example:\n"
-          "\tirccdctl server-cmode freenode #staff +t")
+std::string server_channel_mode_cli::name() const
 {
+    return "server-cmode";
 }
 
-void ServerChannelMode::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_channel_mode_cli::exec(ctl::controller& ctl, const std::vector<std::string> &args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-cmode requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "command",    "server-cmode"  },
         { "server",     args[0]         },
         { "channel",    args[1]         },
         { "mode",       args[2]         }
-    }));
+    });
 }
 
 /*
- * ServerChannelNoticeCli.
+ * server_channel_notice_cli.
  * ------------------------------------------------------------------
  */
 
-ServerChannelNoticeCli::ServerChannelNoticeCli()
-    : Cli("server-cnotice",
-          "send a channel notice",
-          "server-cnotice server channel message",
-          "Send a notice to a public channel. This is a notice that everyone on the channel\n"
-          "will receive.\n\n"
-          "Example:\n"
-          "\tirccdctl server-cnotice freenode #staff \"Don't flood!\"")
+std::string server_channel_notice_cli::name() const
 {
+    return "server-cnotice";
 }
 
-void ServerChannelNoticeCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_channel_notice_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-cnotice requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "command",    "server-cnotice"    },
         { "server",     args[0]             },
         { "channel",    args[1]             },
         { "message",    args[2]             }
-    }));
+    });
 }
 
 /*
- * ServerConnectCli.
+ * server_connect_cli.
  * ------------------------------------------------------------------
  */
 
@@ -376,25 +295,12 @@
 
 } // !namespace
 
-ServerConnectCli::ServerConnectCli()
-    : Cli("server-connect",
-          "add a server",
-          "server-connect [options] id host [port]",
-          "Connect to a new IRC server.\n\n"
-          "Available options:\n"
-          "  -c, --command\t\tspecify the command char\n"
-          "  -n, --nickname\tspecify a nickname\n"
-          "  -r, --realname\tspecify a real name\n"
-          "  -S, --ssl-verify\tverify SSL\n"
-          "  -s, --ssl\t\tconnect using SSL\n"
-          "  -u, --username\tspecify a user name\n\n"
-          "Example:\n"
-          "\tirccdctl server-connect -n jean example irc.example.org\n"
-          "\tirccdctl server-connect --ssl example irc.example.org 6697")
+std::string server_connect_cli::name() const
 {
+    return "server-connect";
 }
 
-void ServerConnectCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_connect_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     std::vector<std::string> copy(args);
 
@@ -427,26 +333,20 @@
     if ((it = result.find("-u")) != result.end() || (it = result.find("--username")) != result.end())
         object["username"] = it->second;
 
-    check(request(irccdctl, object));
+    request(ctl, object);
 }
 
 /*
- * ServerDisconnectCli.
+ * server_disconnect_cli.
  * ------------------------------------------------------------------
  */
 
-ServerDisconnectCli::ServerDisconnectCli()
-    : Cli("server-disconnect",
-          "disconnect server",
-          "server-disconnect [server]",
-          "Disconnect from a server.\n\n"
-          "If server is not specified, irccd disconnects all servers.\n\n"
-          "Example:\n"
-          "\tirccdctl server-disconnect localhost")
+std::string server_disconnect_cli::name() const
 {
+    return "server-disconnect";
 }
 
-void ServerDisconnectCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_disconnect_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     auto object = nlohmann::json::object({
         { "command", "server-disconnect" }
@@ -455,101 +355,85 @@
     if (args.size() > 0)
         object["server"] = args[0];
 
-    check(request(irccdctl, object));
+    request(ctl, object);
 }
 
 /*
- * ServerInfoCli.
+ * server_info_cli.
  * ------------------------------------------------------------------
  */
 
-ServerInfoCli::ServerInfoCli()
-    : Cli("server-info",
-          "get server information",
-          "server-info server",
-          "Get information about a server.\n\n"
-          "Example:\n"
-          "\tirccdctl server-info freenode")
+std::string server_info_cli::name() const
 {
+    return "server-info";
 }
 
-void ServerInfoCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_info_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("server-info requires 1 argument");
 
-    auto result = request(irccdctl, {
+    auto json = nlohmann::json::object({
         { "command",    "server-info"   },
         { "server",     args[0]         }
     });
 
-    check(result);
+    request(ctl, std::move(json), [] (auto result) {
+        std::cout << std::boolalpha;
+        std::cout << "Name           : " << json_util::pretty(result["name"]) << std::endl;
+        std::cout << "Host           : " << json_util::pretty(result["host"]) << std::endl;
+        std::cout << "Port           : " << json_util::pretty(result["port"]) << std::endl;
+        std::cout << "Ipv6           : " << json_util::pretty(result["ipv6"]) << std::endl;
+        std::cout << "SSL            : " << json_util::pretty(result["ssl"]) << std::endl;
+        std::cout << "SSL verified   : " << json_util::pretty(result["sslVerify"]) << std::endl;
+        std::cout << "Channels       : ";
 
-    std::cout << std::boolalpha;
-    std::cout << "Name           : " << json_util::pretty(result["name"]) << std::endl;
-    std::cout << "Host           : " << json_util::pretty(result["host"]) << std::endl;
-    std::cout << "Port           : " << json_util::pretty(result["port"]) << std::endl;
-    std::cout << "Ipv6           : " << json_util::pretty(result["ipv6"]) << std::endl;
-    std::cout << "SSL            : " << json_util::pretty(result["ssl"]) << std::endl;
-    std::cout << "SSL verified   : " << json_util::pretty(result["sslVerify"]) << std::endl;
-    std::cout << "Channels       : ";
+        for (const auto &v : result["channels"])
+            if (v.is_string())
+                std::cout << v.template get<std::string>() << " ";
 
-    for (const auto &v : result["channels"])
-        if (v.is_string())
-            std::cout << v.get<std::string>() << " ";
+        std::cout << std::endl;
 
-    std::cout << std::endl;
-
-    std::cout << "Nickname       : " << json_util::pretty(result["nickname"]) << std::endl;
-    std::cout << "User name      : " << json_util::pretty(result["username"]) << std::endl;
-    std::cout << "Real name      : " << json_util::pretty(result["realname"]) << std::endl;
+        std::cout << "Nickname       : " << json_util::pretty(result["nickname"]) << std::endl;
+        std::cout << "User name      : " << json_util::pretty(result["username"]) << std::endl;
+        std::cout << "Real name      : " << json_util::pretty(result["realname"]) << std::endl;
+    });
 }
 
 /*
- * ServerInviteCli.
+ * server_invite_cli.
  * ------------------------------------------------------------------
  */
 
-ServerInviteCli::ServerInviteCli()
-    : Cli("server-invite",
-          "invite someone",
-          "server-invite server nickname channel",
-          "Invite the specified target on the channel.\n\n"
-          "Example:\n"
-          "\tirccdctl server-invite freenode xorg62 #staff")
+std::string server_invite_cli::name() const
 {
+    return "server-invite";
 }
 
-void ServerInviteCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_invite_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-invite requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "command",    "server-invite" },
         { "server",     args[0]         },
         { "target",     args[1]         },
         { "channel",    args[2]         }
-    }));
+    });
 }
 
 /*
- * ServerJoinCli.
+ * server_join_cli.
  * ------------------------------------------------------------------
  */
 
-ServerJoinCli::ServerJoinCli()
-    : Cli("server-join",
-          "join a channel",
-          "server-join server channel [password]",
-          "Join the specified channel, the password is optional.\n\n"
-          "Example:\n"
-          "\tirccdctl server-join freenode #test\n"
-          "\tirccdctl server-join freenode #private-club secret")
+std::string server_join_cli::name() const
 {
+    return "server-join";
 }
 
-void ServerJoinCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_join_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 2)
         throw std::invalid_argument("server-join requires at least 2 arguments");
@@ -562,25 +446,20 @@
     if (args.size() == 3)
         object["password"] = args[2];
 
-    check(request(irccdctl, object));
+    request(ctl, object);
 }
 
 /*
- * ServerKickCli.
+ * server_kick_cli.
  * ------------------------------------------------------------------
  */
 
-ServerKickCli::ServerKickCli()
-    : Cli("server-kick",
-          "kick someone from a channel",
-          "server-kick server target channel [reason]",
-          "Kick the specified target from the channel, the reason is optional.\n\n"
-          "Example:\n"
-          "\tirccdctl server-kick freenode jean #staff \"Stop flooding\"")
+std::string server_kick_cli::name() const
 {
+    return "server-kick";
 }
 
-void ServerKickCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_kick_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-kick requires at least 3 arguments ");
@@ -594,187 +473,147 @@
     if (args.size() == 4)
         object["reason"] = args[3];
 
-    check(request(irccdctl, object));
+    request(ctl, object);
 }
 
 /*
- * ServerListCli.
+ * server_list_cli.
  * ------------------------------------------------------------------
  */
 
-ServerListCli::ServerListCli()
-    : Cli("server-list",
-          "get list of servers",
-          "server-list",
-          "Get the list of all connected servers.\n\n"
-          "Example:\n"
-          "\tirccdctl server-list")
+std::string server_list_cli::name() const
 {
+    return "server-list";
 }
 
-void ServerListCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &)
+void server_list_cli::exec(ctl::controller& ctl, const std::vector<std::string>&)
 {
-    auto response = request(irccdctl);
-
-    check(response);
-
-    for (const auto &n : response["list"])
-        if (n.is_string())
-            std::cout << n.get<std::string>() << std::endl;
+    request(ctl, {{ "command", "server-list" }}, [] (auto result) {
+        for (const auto &n : result["list"])
+            if (n.is_string())
+                std::cout << n.template get<std::string>() << std::endl;
+    });
 }
 
 /*
- * ServerMeCli.
+ * server_me_cli.
  * ------------------------------------------------------------------
  */
 
-ServerMeCli::ServerMeCli()
-    : Cli("server-me",
-          "send an action emote",
-          "server-me server target message",
-          "Send an action emote.\n\n"
-          "Example:\n"
-          "\tirccdctl server-me freenode #staff \"going back soon\"")
+std::string server_me_cli::name() const
 {
+    return "server-me";
 }
 
-void ServerMeCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_me_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::runtime_error("server-me requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "server",     args[0]     },
         { "target",     args[1]     },
         { "message",    args[2]     }
-    }));
+    });
 }
 
 /*
- * ServerMessageCli.
+ * server_message_cli.
  * ------------------------------------------------------------------
  */
 
-ServerMessageCli::ServerMessageCli()
-    : Cli("server-message",
-          "send a message",
-          "server-message server target message",
-          "Send a message to the specified target or channel.\n\n"
-          "Example:\n"
-          "\tirccdctl server-message freenode #staff \"Hello from irccd\"")
+std::string server_message_cli::name() const
 {
+    return "server-message";
 }
 
-void ServerMessageCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_message_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-message requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "server",     args[0] },
         { "target",     args[1] },
         { "message",    args[2] }
-    }));
+    });
 }
 
 /*
- * ServerModeCli.
+ * server_mode_cli.
  * ------------------------------------------------------------------
  */
 
-ServerModeCli::ServerModeCli()
-    : Cli("server-mode",
-          "the the user mode",
-          "server-mode server mode",
-          "Set the irccd's user mode.\n\n"
-          "Example:\n"
-          "\tirccdctl server-mode +i")
+std::string server_mode_cli::name() const
 {
+    return "server-mode";
 }
 
-void ServerModeCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_mode_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 2)
         throw std::invalid_argument("server-mode requires 2 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "server", args[0] },
         { "mode",   args[1] }
-    }));
+    });
 }
 
 /*
- * ServerNickCli.
+ * server_nick_cli.
  * ------------------------------------------------------------------
  */
 
-ServerNickCli::ServerNickCli()
-    : Cli("server-nick",
-          "change your nickname",
-          "server-nick server nickname",
-          "Change irccd's nickname.\n\n"
-          "Example:\n"
-          "\tirccdctl server-nick freenode david")
+std::string server_nick_cli::name() const
 {
+    return "server-nick";
 }
 
-void ServerNickCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_nick_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 2)
         throw std::invalid_argument("server-nick requires 2 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "server",     args[0] },
         { "nickname",   args[1] }
-    }));
+    });
 }
 
 /*
- * ServerNoticeCli.
+ * server_notice_cli.
  * ------------------------------------------------------------------
  */
 
-ServerNoticeCli::ServerNoticeCli()
-    : Cli("server-notice",
-          "send a private notice",
-          "server-notice server target message",
-          "Send a private notice to the specified target.\n\n"
-          "Example:\n"
-          "\tirccdctl server-notice freenode jean \"I know you are here.\"")
+std::string server_notice_cli::name() const
 {
+    return "server-notice";
 }
 
-void ServerNoticeCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_notice_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-notice requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "server",     args[0] },
         { "target",     args[1] },
         { "message",    args[2] }
-    }));
+    });
 }
 
 /*
- * ServerPartCli.
+ * server_part_cli.
  * ------------------------------------------------------------------
  */
 
-ServerPartCli::ServerPartCli()
-    : Cli("server-part",
-          "leave a channel",
-          "server-part server channel [reason]",
-          "Leave the specified channel, the reason is optional.\n\n"
-          "Not all IRC servers support giving a reason to leave a channel, do not specify\n"
-          "it if this is a concern.\n\n"
-          "Example:\n"
-          "\tirccdctl server-part freenode #staff\n"
-          "\tirccdctl server-part freenode #botwar \"too noisy\"")
+std::string server_part_cli::name() const
 {
+    return "server-part";
 }
 
-void ServerPartCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_part_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 2)
         throw std::invalid_argument("server-part requires at least 2 arguments");
@@ -787,27 +626,20 @@
     if (args.size() >= 3)
         object["reason"] = args[2];
 
-    check(request(irccdctl, object));
+    request(ctl, object);
 }
 
 /*
- * ServerReconnectCli.
+ * server_reconnect_cli.
  * ------------------------------------------------------------------
  */
 
-ServerReconnectCli::ServerReconnectCli()
-    : Cli("server-reconnect",
-          "force reconnection of a server",
-          "server-reconnect [server]",
-          "Force reconnection of one or all servers.\n\n"
-          "If server is not specified, all servers will try to reconnect.\n\n"
-          "Example:\n"
-          "\tirccdctl server-reconnect\n"
-          "\tirccdctl server-reconnect wanadoo")
+std::string server_reconnect_cli::name() const
 {
+    return "server-reconnect";
 }
 
-void ServerReconnectCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_reconnect_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     auto object = nlohmann::json::object({
         { "command", "server-reconnect" }
@@ -816,60 +648,42 @@
     if (args.size() >= 1)
         object["server"] = args[0];
 
-    check(request(irccdctl, object));
+    request(ctl, object);
 }
 
 /*
- * ServerTopicCli.
+ * server_topic_cli.
  * ------------------------------------------------------------------
  */
 
-ServerTopicCli::ServerTopicCli()
-    : Cli("server-topic",
-          "change channel topic",
-          "server-topic server channel topic",
-          "Change the topic of the specified channel.\n\n"
-          "Example:\n"
-          "\tirccdctl server-topic freenode #wmfs \"This is the best channel\"")
+std::string server_topic_cli::name() const
 {
+    return "server-topic";
 }
 
-void ServerTopicCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void server_topic_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 3)
         throw std::invalid_argument("server-topic requires 3 arguments");
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "server",     args[0] },
         { "channel",    args[1] },
         { "topic",      args[2] }
-    }));
+    });
 }
 
 /*
- * RuleAddCli.
+ * rule_add_cli.
  * ------------------------------------------------------------------
  */
 
-RuleAddCli::RuleAddCli()
-    : Cli("rule-add",
-          "add a new rule",
-          "rule-add [options] accept|drop",
-          "Add a new rule to irccd.\n\n"
-          "If no index is specified, the rule is added to the end.\n\n"
-          "Available options:\n"
-          "  -c, --add-channel\t\tmatch a channel\n"
-          "  -e, --add-event\t\tmatch an event\n"
-          "  -i, --index\t\t\trule position\n"
-          "  -p, --add-plugin\t\tmatch a plugin\n"
-          "  -s, --add-server\t\tmatch a server\n\n"
-          "Example:\n"
-          "\tirccdctl rule-add -p hangman drop\n"
-          "\tirccdctl rule-add -s localhost -c #games -p hangman accept")
+std::string rule_add_cli::name() const
 {
+    return "rule-add";
 }
 
-void RuleAddCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void rule_add_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     static const option::options options{
         { "-c",             true },
@@ -918,41 +732,24 @@
 
     // And action.
     if (copy[0] != "accept" && copy[0] != "drop")
-        throw std::runtime_error("invalid action '"s + copy[0] + "'");
+        throw std::runtime_error(string_util::sprintf("invalid action '%s'", copy[0]));
 
     json["action"] = copy[0];
 
-    check(request(irccdctl, json));
+    request(ctl, json);
 }
 
 /*
- * RuleEditCli.
+ * rule_edit_cli.
  * ------------------------------------------------------------------
  */
 
-RuleEditCli::RuleEditCli()
-    : Cli("rule-edit",
-          "edit an existing rule",
-          "rule-edit [options] index",
-          "Edit an existing rule in irccd.\n\n"
-          "All options can be specified multiple times.\n\n"
-          "Available options:\n"
-          "  -a, --action\t\t\tset action\n"
-          "  -c, --add-channel\t\tmatch a channel\n"
-          "  -C, --remove-channel\t\tremove a channel\n"
-          "  -e, --add-event\t\tmatch an event\n"
-          "  -E, --remove-event\t\tremove an event\n"
-          "  -p, --add-plugin\t\tmatch a plugin\n"
-          "  -P, --add-plugin\t\tremove a plugin\n"
-          "  -s, --add-server\t\tmatch a server\n"
-          "  -S, --remove-server\t\tremove a server\n\n"
-          "Example:\n"
-          "\tirccdctl rule-edit -p hangman 0\n"
-          "\tirccdctl rule-edit -S localhost -c #games -p hangman 1")
+std::string rule_edit_cli::name() const
 {
+    return "rule-edit";
 }
 
-void RuleEditCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void rule_edit_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     static const option::options options{
         { "-a",                 true },
@@ -1018,17 +815,17 @@
     // Index.
     json["index"] = string_util::to_number<unsigned>(copy[0]);
 
-    check(request(irccdctl, json));
+    request(ctl, json);
 }
 
 /*
- * RuleListCli.
+ * rule_list_cli.
  * ------------------------------------------------------------------
  */
 
 namespace {
 
-void showRule(const nlohmann::json& json, int index)
+void show_rule(const nlohmann::json& json, int index)
 {
     assert(json.is_object());
 
@@ -1062,48 +859,34 @@
 
 } // !namespace
 
-RuleListCli::RuleListCli()
-    : Cli("rule-list",
-          "list all rules",
-          "rule-list",
-          "List all rules.\n\n"
-          "Example:\n"
-          "\tirccdctl rule-list")
+std::string rule_list_cli::name() const
 {
+    return "rule-list";
 }
 
-void RuleListCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &)
+void rule_list_cli::exec(ctl::controller& ctl, const std::vector<std::string>&)
 {
-    auto response = request(irccdctl);
-    auto pos = 0;
-
-    check(response);
+    request(ctl, {{ "command", "rule-list" }}, [] (auto result) {
+        auto pos = 0;
 
-    for (const auto &obj : response["list"]) {
-        if (!obj.is_object())
-            continue;
-
-        showRule(obj, pos++);
-    }
+        for (const auto& obj : result["list"]) {
+            if (obj.is_object())
+                show_rule(obj, pos++);
+        }
+    });
 }
 
 /*
- * RuleInfoCli.
+ * rule_info_cli.
  * ------------------------------------------------------------------
  */
 
-RuleInfoCli::RuleInfoCli()
-    : Cli("rule-info",
-          "show a rule",
-          "rule-info index",
-          "Show a rule.\n\n"
-          "Example:\n"
-          "\tirccdctl rule-info 0\n"
-          "\tirccdctl rule-info 1")
+std::string rule_info_cli::name() const
 {
+    return "rule-info";
 }
 
-void RuleInfoCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void rule_info_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("rule-info requires 1 argument");
@@ -1116,32 +899,27 @@
         throw std::invalid_argument("invalid number '" + args[0] + "'");
     }
 
-    auto result = request(irccdctl, {
+    auto json = nlohmann::json::object({
         { "command",    "rule-info" },
         { "index",      index       }
     });
 
-    check(result);
-    showRule(result, 0);
+    request(ctl, std::move(json), [] (auto result) {
+        show_rule(result, 0);
+    });
 }
 
 /*
- * RuleRemoveCli.
+ * rule_remove_cli.
  * ------------------------------------------------------------------
  */
 
-RuleRemoveCli::RuleRemoveCli()
-    : Cli("rule-remove",
-          "remove a rule",
-          "rule-remove index",
-          "Remove an existing rule.\n\n"
-          "Example:\n"
-          "\tirccdctl rule-remove 0\n"
-          "\tirccdctl rule-remove 1")
+std::string rule_remove_cli::name() const
 {
+    return "rule-remove";
 }
 
-void RuleRemoveCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void rule_remove_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 1)
         throw std::invalid_argument("rule-remove requires 1 argument");
@@ -1154,34 +932,23 @@
         throw std::invalid_argument("invalid number '" + args[0] + "'");
     }
 
-    auto result = request(irccdctl, {
+    request(ctl, {
         { "command",    "rule-remove"   },
         { "index",      index           }
     });
-
-    check(result);
 }
 
 /*
- * RuleMoveCli.
+ * rule_move_cli.
  * ------------------------------------------------------------------
  */
 
-RuleMoveCli::RuleMoveCli()
-    : Cli("rule-move",
-          "move a rule to a new position",
-          "rule-move source destination",
-          "Move a rule from the given source at the specified destination index.\n\n"
-          "The rule will replace the existing one at the given destination moving\ndown every "
-          "other rules. If destination is greater or equal the number of rules,\nthe rule "
-          "is moved to the end.\n\n"
-          "Example:\n"
-          "\tirccdctl rule-move 0 5\n"
-          "\tirccdctl rule-move 4 3")
+std::string rule_move_cli::name() const
 {
+    return "rule-move";
 }
 
-void RuleMoveCli::exec(Irccdctl &irccdctl, const std::vector<std::string> &args)
+void rule_move_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
     if (args.size() < 2)
         throw std::invalid_argument("rule-move requires 2 arguments");
@@ -1196,11 +963,11 @@
         throw std::invalid_argument("invalid number");
     }
 
-    check(request(irccdctl, {
+    request(ctl, {
         { "command",    "rule-move" },
         { "from",       from        },
         { "to",         to          }
-    }));
+    });
 }
 
 /*
@@ -1361,7 +1128,7 @@
     std::cout << "realname:    " << json_util::pretty(v, "realname") << "\n";
 }
 
-const std::unordered_map<std::string, std::function<void (const nlohmann::json &)>> events{
+const std::unordered_map<std::string, std::function<void (const nlohmann::json&)>> events{
     { "onChannelMode",      onChannelMode   },
     { "onChannelNotice",    onChannelNotice },
     { "onConnect",          onConnect       },
@@ -1380,60 +1147,42 @@
     { "onWhois",            onWhois         }
 };
 
+void get_event(ctl::controller& ctl, std::string fmt)
+{
+    ctl.recv([&ctl, fmt] (auto code, auto message) {
+        if (code)
+            throw boost::system::system_error(code);
+
+        auto it = events.find(json_util::to_string(message["event"]));
+
+        if (it != events.end()) {
+            if (fmt == "json")
+                std::cout << message.dump(4) << std::endl;
+            else {
+                it->second(message);
+                std::cout << std::endl;
+            }
+        }
+
+        get_event(ctl, std::move(fmt));
+    });
+}
+
 } // !namespace
 
-WatchCli::WatchCli()
-    : Cli("watch",
-          "watch irccd events",
-          "watch [-f|--format json|native]",
-          "Start watching irccd events.\n\n"
-          "You can use different output formats, native is human readable format, json is\n"
-          "pretty formatted json.\n\n"
-          "Example:\n"
-          "\tirccdctl watch\n"
-          "\tirccdctl watch -f json")
+std::string watch_cli::name() const
 {
+    return "watch";
 }
 
-void WatchCli::exec(Irccdctl& client, const std::vector<std::string>& args)
+void watch_cli::exec(ctl::controller& ctl, const std::vector<std::string>& args)
 {
-    std::string fmt = format(args);
+    auto fmt = format(args);
 
     if (fmt != "native" && fmt != "json")
         throw std::invalid_argument("invalid format given: " + fmt);
 
-    auto id = client.client().onEvent.connect([&] (auto ev) {
-        try {
-            auto name = ev.find("event");
-
-            if (name == ev.end() || !name->is_string())
-                return;
-
-            auto it = events.find(*name);
-
-            // Silently ignore to avoid breaking user output.
-            if (it == events.end())
-                return;
-
-            if (fmt == "json")
-                std::cout << ev.dump(4) << std::endl;
-            else {
-                it->second(ev);
-                std::cout << std::endl;
-            }
-        } catch (...) {
-        }
-    });
-
-    try {
-        while (client.client().isConnected()) {
-            net_util::poll(500, client);
-        }
-    } catch (const std::exception &ex) {
-        log::warning() << ex.what() << std::endl;
-    }
-
-    client.client().onEvent.disconnect(id);
+    get_event(ctl, fmt);
 }
 
 } // !cli