Mercurial > irccd
changeset 895:f0d6bc79aa32
irccd: implement hooks, closes #2342 @2h
The hook mechanism is an alternative approach to plugins which allow the user to
write lightweight scripts in any language.
line wrap: on
line diff
--- a/CHANGES.md Sun Sep 01 17:23:37 2019 +0200 +++ b/CHANGES.md Thu Sep 05 13:39:32 2019 +0200 @@ -1,6 +1,14 @@ IRC Client Daemon CHANGES ========================= +irccd current +---------------------- + +irccd: + +- Added a new hook system. Hooks consist of an alternative approach to plugins + to extend irccd in any language (#2342). + irccd 3.0.1 2019-09-01 ----------------------
--- a/doc/examples/CMakeLists.txt Sun Sep 01 17:23:37 2019 +0200 +++ b/doc/examples/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -36,6 +36,11 @@ DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} ) +install( + FILES ${examples_SOURCE_DIR}/sample-hook.sh + DESTINATION ${CMAKE_INSTALL_DOCDIR}/examples +) + irccd_set_global(CPACK_COMPONENT_EXAMPLES_HIDDEN On) irccd_set_global(CPACK_COMPONENT_EXAMPLES_DESCRIPTION "Install examples of configuration files") irccd_set_global(CPACK_COMPONENT_EXAMPLES_GROUP "Documentation")
--- a/doc/examples/irccd.conf.sample Sun Sep 01 17:23:37 2019 +0200 +++ b/doc/examples/irccd.conf.sample Thu Sep 05 13:39:32 2019 +0200 @@ -147,3 +147,10 @@ # # [templates.hangman] # win = "you win!" + +# Section hooks: +# This sections lets you define hooks that are executed each time an IRC +# event arrives. +# +# [hooks] +# mail = "/path/to/mail.py"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/examples/sample-hook.sh Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,130 @@ +#!/bin/sh +# +# sample-hook.sh -- a sample hook in shell script +# +# 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. +# + +onConnect() +{ + echo "event: connect" + echo "server: $2" +} + +onDisconnect() +{ + echo "event: disconnect" + echo "server: $2" +} + +onInvite() +{ + echo "event: invite" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "target: $5" +} + +onJoin() +{ + echo "event: join" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" +} + +onKick() +{ + echo "event: kick" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "target: $5" + echo "reason: $6" +} + +onMessage() +{ + echo "event: message" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "message: $5" +} + +onMe() +{ + echo "event: me" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "message: $5" +} + +onMode() +{ + echo "event: mode" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "mode: $5" + echo "limit: $6" + echo "user: $7" + echo "mask: $8" +} + +onNick() +{ + echo "event: nick" + echo "server: $2" + echo "origin: $3" + echo "nick: $4" +} + +onNotice() +{ + echo "event: notice" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "message: $5" +} + +onPart() +{ + echo "event: part" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "reason: $5" +} + +onTopic() +{ + echo "event: topic" + echo "server: $2" + echo "origin: $3" + echo "channel: $4" + echo "topic: $5" +} + +# +# Call the appropriate function with the same name as the event and pass +# arguments. +# +# Please keep quotes between $@ as some arguments are quoted (like messages). +# +$1 "$@"
--- a/irccdctl/cli.cpp Sun Sep 01 17:23:37 2019 +0200 +++ b/irccdctl/cli.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -224,6 +224,9 @@ // {{{ cli const std::vector<cli::constructor> cli::registry{ + bind<hook_add_cli>(), + bind<hook_list_cli>(), + bind<hook_remove_cli>(), bind<plugin_config_cli>(), bind<plugin_info_cli>(), bind<plugin_list_cli>(), @@ -281,6 +284,74 @@ // }}} +// {{{ hook_add_cli + +auto hook_add_cli::get_name() const noexcept -> std::string_view +{ + return "hook-add"; +} + +void hook_add_cli::exec(ctl::controller& ctl, const std::vector<std::string>& argv) +{ + if (argv.size() < 2U) + throw std::invalid_argument("hook-add requires 2 arguments"); + + request(ctl, nlohmann::json::object({ + { "command", "hook-add" }, + { "id", argv[0] }, + { "path", argv[1] } + })); +} + +// }}} + +// {{{ hook_list_cli + +auto hook_list_cli::get_name() const noexcept -> std::string_view +{ + return "hook-list"; +} + +void hook_list_cli::exec(ctl::controller& ctl, const std::vector<std::string>&) +{ + request(ctl, {{ "command", "hook-list" }}, [] (auto result) { + for (const auto& obj : result["list"]) { + if (!obj.is_object()) + continue; + + const deserializer document(obj); + + std::cout << std::setw(16) << std::left; + std::cout << document.get<std::string>("id").value_or("(unknown)"); + std::cout << " "; + std::cout << document.get<std::string>("path").value_or("(unknown)"); + std::cout << std::endl; + } + }); +} + +// }}} + +// {{{ hook_remove_cli + +auto hook_remove_cli::get_name() const noexcept -> std::string_view +{ + return "hook-remove"; +} + +void hook_remove_cli::exec(ctl::controller& ctl, const std::vector<std::string>& argv) +{ + if (argv.size() < 1U) + throw std::invalid_argument("hook-remove requires 1 argument"); + + request(ctl, nlohmann::json::object({ + { "command", "hook-remove" }, + { "id", argv[0] }, + })); +} + +// }}} + // {{{ plugin_config_cli void plugin_config_cli::set(ctl::controller& ctl, const std::vector<std::string>&args)
--- a/irccdctl/cli.hpp Sun Sep 01 17:23:37 2019 +0200 +++ b/irccdctl/cli.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -105,6 +105,66 @@ // }}} +// {{{ hook_add_cli + +/** + * \brief Implementation of irccdctl hook-add. + */ +class hook_add_cli : public cli { +public: + /** + * \copydoc cli::get_name + */ + auto get_name() const noexcept -> std::string_view override; + + /** + * \copydoc cli::exec + */ + void exec(ctl::controller& irccdctl, const std::vector<std::string>& args) override; +}; + +// }}} + +// {{{ hook_list_cli + +/** + * \brief Implementation of irccdctl hook-list. + */ +class hook_list_cli : public cli { +public: + /** + * \copydoc cli::get_name + */ + auto get_name() const noexcept -> std::string_view override; + + /** + * \copydoc cli::exec + */ + void exec(ctl::controller& irccdctl, const std::vector<std::string>& args) override; +}; + +// }}} + +// {{{ hook_remove_cli + +/** + * \brief Implementation of irccdctl hook-remove. + */ +class hook_remove_cli : public cli { +public: + /** + * \copydoc cli::get_name + */ + auto get_name() const noexcept -> std::string_view override; + + /** + * \copydoc cli::exec + */ + void exec(ctl::controller& irccdctl, const std::vector<std::string>& args) override; +}; + +// }}} + // {{{ plugin_config_cli /**
--- a/irccdctl/main.cpp Sun Sep 01 17:23:37 2019 +0200 +++ b/irccdctl/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -72,7 +72,10 @@ [[noreturn]] void usage() { - std::cerr << "usage: irccdctl plugin-config id [variable] [value]\n"; + std::cerr << "usage: irccdctl hook-add id path\n"; + std::cerr << " irccdctl hook-list\n"; + std::cerr << " irccdctl hook-remove id\n"; + std::cerr << " irccdctl plugin-config id [variable] [value]\n"; std::cerr << " irccdctl plugin-info id\n"; std::cerr << " irccdctl plugin-list\n"; std::cerr << " irccdctl plugin-load name\n";
--- a/libirccd-ctl/irccd/ctl/controller.cpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-ctl/irccd/ctl/controller.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -22,15 +22,17 @@ #include <irccd/json_util.hpp> #include <irccd/daemon/bot.hpp> -#include <irccd/daemon/server.hpp> +#include <irccd/daemon/hook.hpp> #include <irccd/daemon/plugin.hpp> #include <irccd/daemon/rule.hpp> +#include <irccd/daemon/server.hpp> #include "controller.hpp" using irccd::json_util::deserializer; using irccd::daemon::bot_error; +using irccd::daemon::hook_error; using irccd::daemon::plugin_error; using irccd::daemon::rule_error; using irccd::daemon::server_error; @@ -138,6 +140,8 @@ code = make_error_code(static_cast<plugin_error::error>(*e)); else if (*c == "rule") code = make_error_code(static_cast<rule_error::error>(*e)); + else if (*c == "hook") + code = make_error_code(static_cast<hook_error::error>(*e)); } handler(std::move(code), std::move(msg));
--- a/libirccd-daemon/CMakeLists.txt Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -25,6 +25,10 @@ ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/bot.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/hook.cpp + ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/hook.hpp + ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/hook_service.cpp + ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/hook_service.hpp ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/irc.cpp ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/irc.hpp ${libirccd-daemon_SOURCE_DIR}/irccd/daemon/logger.cpp
--- a/libirccd-daemon/irccd/daemon.hpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/irccd/daemon.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -28,6 +28,8 @@ #include "daemon/bot.hpp" #include "daemon/dynlib_plugin.hpp" +#include "daemon/hook.hpp" +#include "daemon/hook_service.hpp" #include "daemon/irc.hpp" #include "daemon/logger.hpp" #include "daemon/plugin.hpp"
--- a/libirccd-daemon/irccd/daemon/bot.cpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/irccd/daemon/bot.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -26,6 +26,7 @@ #include <irccd/system.hpp> #include "bot.hpp" +#include "hook_service.hpp" #include "logger.hpp" #include "plugin_service.hpp" #include "rule_service.hpp" @@ -185,10 +186,25 @@ sink_->set_filter(*filter_); } +void bot::load_hooks() +{ + const auto sc = config_.get("hooks"); + + for (const auto& opt : sc) { + if (!string_util::is_identifier(opt.get_key())) + throw hook_error(hook_error::invalid_identifier, opt.get_key(), ""); + if (opt.get_value().empty()) + throw hook_error(hook_error::invalid_path, opt.get_key(), ""); + + hook_service_->add(hook(opt.get_key(), opt.get_value())); + } +} + bot::bot(boost::asio::io_service& service, std::string config) : config_(std::move(config)) , service_(service) , sink_(std::make_unique<logger::console_sink>()) + , hook_service_(std::make_unique<hook_service>(*this)) , server_service_(std::make_unique<server_service>(*this)) , tpt_service_(std::make_unique<transport_service>(*this)) , rule_service_(std::make_unique<rule_service>(*this)) @@ -228,6 +244,11 @@ return *sink_; } +auto bot::get_hooks() noexcept -> hook_service& +{ + return *hook_service_; +} + auto bot::get_servers() noexcept -> server_service& { return *server_service_; @@ -267,6 +288,7 @@ // [logs] and [templates] sections. load_logs(); load_templates(); + load_hooks(); if (!loaded_) sink_->info("irccd", "") << "loading configuration from " << config_.get_path() << std::endl;
--- a/libirccd-daemon/irccd/daemon/bot.hpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/irccd/daemon/bot.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -47,6 +47,7 @@ } // !logger +class hook_service; class plugin_service; class rule_service; class server_service; @@ -71,6 +72,7 @@ std::unique_ptr<logger::filter> filter_; // Services. + std::unique_ptr<hook_service> hook_service_; std::unique_ptr<server_service> server_service_; std::unique_ptr<transport_service> tpt_service_; std::unique_ptr<rule_service> rule_service_; @@ -88,6 +90,7 @@ void load_logs_syslog(); void load_logs(); void load_templates(); + void load_hooks(); public: /** @@ -158,6 +161,13 @@ void set_log(std::unique_ptr<logger::sink> sink) noexcept; /** + * Access the hook service. + * + * \return the service + */ + auto get_hooks() noexcept -> hook_service&; + + /** * Access the server service. * * \return the service
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libirccd-daemon/irccd/daemon/hook.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,292 @@ +/* + * hook.cpp -- irccd hooks + * + * 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 <cassert> +#include <initializer_list> +#include <string_view> +#include <sstream> + +#include <boost/process.hpp> + +#include <irccd/string_util.hpp> + +#include "bot.hpp" +#include "hook.hpp" +#include "logger.hpp" +#include "server.hpp" + +using boost::process::args; +using boost::process::child; +using boost::process::exe; +using boost::process::ipstream; +using boost::process::std_out; + +using std::getline; +using std::initializer_list; +using std::ostringstream; +using std::string; +using std::string_view; + +using irccd::string_util::is_identifier; + +namespace irccd::daemon { + +namespace { + +void exec(bot& bot, hook& hook, initializer_list<string> arguments) +{ + try { + ipstream is; + child c(exe = hook.get_path(), args = arguments, std_out > is); + + // Log everything that is output by the hook. + for (string line; c.running() && getline(is, line); ) + bot.get_log().info(hook) << line << std::endl; + + c.wait(); + } catch (const std::exception& ex) { + throw hook_error(hook_error::exec_error, hook.get_id(), ex.what()); + } +} + +} // !namespace + +hook::hook(std::string id, std::string path) noexcept + : id_(std::move(id)) + , path_(std::move(path)) +{ + assert(is_identifier(id_)); + assert(!path_.empty()); +} + +auto hook::get_id() const noexcept -> const std::string& +{ + return id_; +} + +auto hook::get_path() const noexcept -> const std::string& +{ + return path_; +} + +void hook::handle_connect(bot& bot, const connect_event& event) +{ + exec(bot, *this, {"onConnect", event.server->get_id()}); +} + +void hook::handle_disconnect(bot& bot, const disconnect_event& event) +{ + exec(bot, *this, {"onDisconnect", event.server->get_id()}); +} + +void hook::handle_invite(bot& bot, const invite_event& event) +{ + exec(bot, *this, { + "onInvite", + event.server->get_id(), + event.origin, + event.channel, + event.nickname + }); +} + +void hook::handle_join(bot& bot, const join_event& event) +{ + exec(bot, *this, { + "onJoin", + event.server->get_id(), + event.origin, + event.channel + }); +} + +void hook::handle_kick(bot& bot, const kick_event& event) +{ + exec(bot, *this, { + "onKick", + event.server->get_id(), + event.origin, + event.channel, + event.target, + event.reason + }); +} + +void hook::handle_message(bot& bot, const message_event& event) +{ + exec(bot, *this, { + "onMessage", + event.server->get_id(), + event.origin, + event.channel, + event.message + }); +} + +void hook::handle_me(bot& bot, const me_event& event) +{ + exec(bot, *this, { + "onMe", + event.server->get_id(), + event.origin, + event.channel, + event.message + }); +} + +void hook::handle_mode(bot& bot, const mode_event& event) +{ + exec(bot, *this, { + "onMode", + event.server->get_id(), + event.origin, + event.channel, + event.mode, + event.limit, + event.user, + event.mask + }); +} + +void hook::handle_nick(bot& bot, const nick_event& event) +{ + exec(bot, *this, { + "onNick", + event.server->get_id(), + event.origin, + event.nickname + }); +} + +void hook::handle_notice(bot& bot, const notice_event& event) +{ + exec(bot, *this, { + "onNotice", + event.server->get_id(), + event.origin, + event.channel, + event.message + }); +} + +void hook::handle_part(bot& bot, const part_event& event) +{ + exec(bot, *this, { + "onPart", + event.server->get_id(), + event.origin, + event.channel, + event.reason + }); +} + +void hook::handle_topic(bot& bot, const topic_event& event) +{ + exec(bot, *this, { + "onTopic", + event.server->get_id(), + event.origin, + event.channel, + event.topic + }); +} + +auto operator==(const hook& h1, const hook& h2) noexcept -> bool +{ + return h1.get_id() == h2.get_id() && + h1.get_path() == h2.get_path(); +} + +auto operator!=(const hook& h1, const hook& h2) noexcept -> bool +{ + return !(h1 == h2); +} + +hook_error::hook_error(error errc, std::string id, std::string message) + : system_error(make_error_code(errc)) + , id_(std::move(id)) + , message_(std::move(message)) +{ +} + +auto hook_error::get_id() const noexcept -> const std::string& +{ + return id_; +} + +auto hook_error::get_message() const noexcept -> const std::string& +{ + return message_; +} + +auto hook_error::what() const noexcept -> const char* +{ + return message_.c_str(); +} + +auto hook_category() -> const std::error_category& +{ + static const class category : public std::error_category { + public: + auto name() const noexcept -> const char* override + { + return "hook"; + } + + auto message(int e) const -> std::string override + { + switch (static_cast<hook_error::error>(e)) { + case hook_error::not_found: + return "hook not found"; + case hook_error::invalid_path: + return "invalid path given"; + case hook_error::invalid_identifier: + return "invalid hook identifier"; + case hook_error::exec_error: + return "hook exec error"; + case hook_error::already_exists: + return "hook already exists"; + default: + return "no error"; + } + } + } category; + + return category; +} + +auto make_error_code(hook_error::error e) -> std::error_code +{ + return { static_cast<int>(e), hook_category() }; +} + +namespace logger { + +auto type_traits<hook>::get_category(const hook&) -> std::string_view +{ + return "hook"; +} + +auto type_traits<hook>::get_component(const hook& hook) -> std::string_view +{ + return hook.get_id(); +} + +} // !logger + +} // !irccd::daemon
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libirccd-daemon/irccd/daemon/hook.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,331 @@ +/* + * hook.hpp -- irccd hooks + * + * 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_HOOK_HPP +#define IRCCD_DAEMON_HOOK_HPP + +/** + * \file irccd/daemon/hook.hpp + * \brief irccd hooks + */ + +#include <irccd/sysconfig.hpp> + +#include <string> +#include <string_view> +#include <system_error> +#include <type_traits> + +namespace irccd::daemon { + +class bot; + +struct connect_event; +struct disconnect_event; +struct invite_event; +struct join_event; +struct kick_event; +struct me_event; +struct message_event; +struct mode_event; +struct nick_event; +struct notice_event; +struct part_event; +struct topic_event; + +/** + * \brief Event hook. + * + * A hook is a lightweight alternative to plugins, it is executed once an event + * arrive and can be written in any language. + */ +class hook { +private: + std::string id_; + std::string path_; + +public: + /** + * Construct a hook. + * + * This does not check the presence of the script. + * + * \pre id must be a valid identifier + * \pre path must not be empty + * \param id the hook id + * \param path the path to the hook + */ + hook(std::string id, std::string path) noexcept; + + /** + * Get user unique id. + * + * \return the hook id + */ + auto get_id() const noexcept -> const std::string&; + + /** + * Get path to the hook. + * + * \return the path + */ + auto get_path() const noexcept -> const std::string&; + + /** + * Similar interface to plugin::handle_connect. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_connect(bot& bot, const connect_event& event); + + /** + * Similar interface to plugin::handle_disconnect. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_disconnect(bot& bot, const disconnect_event& event); + + /** + * Similar interface to plugin::handle_invite. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_invite(bot& bot, const invite_event& event); + + /** + * Similar interface to plugin::handle_join. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_join(bot& bot, const join_event& event); + + /** + * Similar interface to plugin::handle_kick. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_kick(bot& bot, const kick_event& event); + + /** + * Similar interface to plugin::handle_message. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_message(bot& bot, const message_event& event); + + /** + * Similar interface to plugin::handle_me. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_me(bot& bot, const me_event& event); + + /** + * Similar interface to plugin::handle_mode. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_mode(bot& bot, const mode_event& event); + + /** + * Similar interface to plugin::handle_nick. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_nick(bot& bot, const nick_event& event); + + /** + * Similar interface to plugin::handle_notice. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_notice(bot& bot, const notice_event& event); + + /** + * Similar interface to plugin::handle_part. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_part(bot& bot, const part_event& event); + + /** + * Similar interface to plugin::handle_topic. + * + * \param bot the irccd instance + * \param event the event + */ + void handle_topic(bot& bot, const topic_event& event); +}; + +/** + * Equality operator. + * + * \param lhs the left side + * \param rhs the right side + * \return true if they equals + */ +auto operator==(const hook& lhs, const hook& rhs) noexcept -> bool; + +/** + * Equality operator. + * + * \param lhs the left side + * \param rhs the right side + * \return false if they equals + */ +auto operator!=(const hook& lhs, const hook& rhs) noexcept -> bool; + +/** + * \brief Hook error. + */ +class hook_error : public std::system_error { +public: + /** + * \brief Plugin related errors. + */ + enum error { + //!< No error. + no_error = 0, + + //!< The specified identifier is invalid. + invalid_identifier, + + //!< The specified hook is not found. + not_found, + + //!< Invalid path given. + invalid_path, + + //!< The hook was unable to run the function. + exec_error, + + //!< The hook is already loaded. + already_exists, + }; + +private: + std::string id_; + std::string message_; + +public: + /** + * Constructor. + * + * \param code the error code + * \param id the hook id + * \param message the optional message (e.g. error from hook) + */ + hook_error(error code, std::string id, std::string message = ""); + + /** + * Get the hook identifier. + * + * \return the id + */ + auto get_id() const noexcept -> const std::string&; + + /** + * Get the additional message. + * + * \return the message + */ + auto get_message() const noexcept -> const std::string&; + + /** + * Get message appropriate for use with logger. + * + * \return the error message + */ + auto what() const noexcept -> const char* override; +}; + +/** + * Get the hook error category singleton. + * + * \return the singleton + */ +auto hook_category() -> const std::error_category&; + +/** + * Create a std::error_code from hook_error::error enum. + * + * \param e the error code + * \return the error code + */ +auto make_error_code(hook_error::error e) -> std::error_code; + +namespace logger { + +template <typename T> +struct type_traits; + +/** + * \brief Specialization for hook. + * \ingroup daemon-loggers-traits + */ +template <> +struct type_traits<hook> { + /** + * Get 'hook' category. + * + * \param hook the hook + * \return hook + */ + static auto get_category(const hook& hook) -> std::string_view; + + /** + * Get the hook id. + * + * \param hook the hook + * \return the hook id + */ + static auto get_component(const hook& hook) -> std::string_view; +}; + +} // !logger + +} // !irccd::daemon + +/** + * \cond IRCCD_HIDDEN_SYMBOLS + */ + +namespace std { + +template <> +struct is_error_code_enum<irccd::daemon::hook_error::error> : public std::true_type { +}; + +} // !std + +/** + * \endcond + */ + +#endif // !IRCCD_DAEMON_PLUGIN_HPP
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libirccd-daemon/irccd/daemon/hook_service.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,66 @@ +/* + * hook_service.cpp -- irccd hook service + * + * 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 <algorithm> + +#include "hook_service.hpp" + +using std::find; +using std::move; + +namespace irccd::daemon { + +hook_service::hook_service(bot& bot) noexcept + : bot_(bot) +{ +} + +auto hook_service::has(const hook& hook) const noexcept -> bool +{ + return find(hooks_.begin(), hooks_.end(), hook) != hooks_.end(); +} + +void hook_service::add(hook hook) +{ + if (has(hook)) + throw hook_error(hook_error::already_exists, hook.get_id(), ""); + + hooks_.push_back(move(hook)); +} + +void hook_service::remove(const hook& hook) noexcept +{ + hooks_.erase(std::remove(hooks_.begin(), hooks_.end(), hook), hooks_.end()); +} + +auto hook_service::list() const noexcept -> const hooks& +{ + return hooks_; +} + +auto hook_service::list() noexcept -> hooks& +{ + return hooks_; +} + +void hook_service::clear() noexcept +{ + hooks_.clear(); +} + +} // !irccd::daemon
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libirccd-daemon/irccd/daemon/hook_service.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,134 @@ +/* + * hook_service.hpp -- irccd hook service + * + * 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_HOOK_SERVICE_HPP +#define IRCCD_DAEMON_HOOK_SERVICE_HPP + +/** + * \file irccd/daemon/hook_service.hpp + * \brief Irccd hook service. + */ + +#include <functional> +#include <vector> +#include <utility> + +#include "bot.hpp" +#include "hook.hpp" +#include "logger.hpp" + +namespace irccd::daemon { + +class bot; + +/** + * \brief Irccd hook service. + */ +class hook_service { +public: + /** + * List of hooks. + */ + using hooks = std::vector<hook>; + +private: + bot& bot_; + hooks hooks_; + +public: + /** + * Constructor. + * + * \param bot the bot + */ + hook_service(bot& bot) noexcept; + + /** + * Tells if a hook already exists. + * + * \param hook the hook to check + * \return true if hook is already present + */ + auto has(const hook& hook) const noexcept -> bool; + + /** + * Add a new hook. + * + * \param hook the hook + * \throw hook_error if the hook is already present + */ + void add(hook hook); + + /** + * Remove the specified hook. + * + * \param hook the hook to remove + */ + void remove(const hook& hook) noexcept; + + /** + * Get the list of hooks. + * + * \return the hooks + */ + auto list() const noexcept -> const hooks&; + + /** + * Overloaded function. + * + * \return the hooks + */ + auto list() noexcept -> hooks&; + + /** + * Remove all hooks. + */ + void clear() noexcept; + + /** + * Convenient function to call a hook member function for all hook + * present in the list. + * + * \param func the function to call (e.g. hook::handle_connect) + * \param args the arguments to the hook function + * \throw hook_error on errors + */ + template <typename Func, typename... Args> + void dispatch(Func&& func, Args&&... args); +}; + +template <typename Func, typename... Args> +void hook_service::dispatch(Func&& func, Args&&... args) +{ + using std::invoke; + using std::forward; + using std::exception; + + for (auto& hook : hooks_) { + // Protect to avoid stopping all next hooks. + try { + invoke(forward<Func>(func), hook, bot_, forward<Args>(args)...); + } catch (const exception& ex) { + bot_.get_log().warning(hook) << ex.what() << std::endl; + } + } +} + +} // !irccd::daemon + +#endif // !IRCCD_DAEMON_HOOK_SERVICE_HPP
--- a/libirccd-daemon/irccd/daemon/server_service.cpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/irccd/daemon/server_service.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -22,6 +22,7 @@ #include <irccd/string_util.hpp> #include "bot.hpp" +#include "hook_service.hpp" #include "logger.hpp" #include "plugin_service.hpp" #include "rule_service.hpp" @@ -102,6 +103,7 @@ { "event", "onConnect" }, { "server", ev.server->get_id() } })); + bot_.get_hooks().dispatch(&hook::handle_connect, ev); dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "", [=] (plugin&) -> std::string { @@ -120,6 +122,7 @@ { "event", "onDisconnect" }, { "server", ev.server->get_id() } })); + bot_.get_hooks().dispatch(&hook::handle_disconnect, ev); dispatch(ev.server->get_id(), /* origin */ "", /* channel */ "", [=] (plugin&) -> std::string { @@ -144,6 +147,7 @@ { "origin", ev.origin }, { "channel", ev.channel } })); + bot_.get_hooks().dispatch(&hook::handle_invite, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin&) -> std::string { @@ -167,6 +171,7 @@ { "origin", ev.origin }, { "channel", ev.channel } })); + bot_.get_hooks().dispatch(&hook::handle_join, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin&) -> std::string { @@ -194,6 +199,7 @@ { "target", ev.target }, { "reason", ev.reason } })); + bot_.get_hooks().dispatch(&hook::handle_kick, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin&) -> std::string { @@ -219,6 +225,7 @@ { "channel", ev.channel }, { "message", ev.message } })); + bot_.get_hooks().dispatch(&hook::handle_message, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin& plugin) -> std::string { @@ -260,6 +267,7 @@ { "target", ev.channel }, { "message", ev.message } })); + bot_.get_hooks().dispatch(&hook::handle_me, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin&) -> std::string { @@ -291,6 +299,7 @@ { "user", ev.user }, { "mask", ev.mask } })); + bot_.get_hooks().dispatch(&hook::handle_mode, ev); dispatch(ev.server->get_id(), ev.origin, /* channel */ "", [=] (plugin &) -> std::string { @@ -342,6 +351,7 @@ { "origin", ev.origin }, { "nickname", ev.nickname } })); + bot_.get_hooks().dispatch(&hook::handle_nick, ev); dispatch(ev.server->get_id(), ev.origin, /* channel */ "", [=] (plugin&) -> std::string { @@ -367,6 +377,7 @@ { "channel", ev.channel }, { "message", ev.message } })); + bot_.get_hooks().dispatch(&hook::handle_notice, ev); dispatch(ev.server->get_id(), ev.origin, /* channel */ "", [=] (plugin&) -> std::string { @@ -392,6 +403,7 @@ { "channel", ev.channel }, { "reason", ev.reason } })); + bot_.get_hooks().dispatch(&hook::handle_part, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin&) -> std::string { @@ -417,6 +429,7 @@ { "channel", ev.channel }, { "topic", ev.topic } })); + bot_.get_hooks().dispatch(&hook::handle_topic, ev); dispatch(ev.server->get_id(), ev.origin, ev.channel, [=] (plugin&) -> std::string {
--- a/libirccd-daemon/irccd/daemon/transport_command.cpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/irccd/daemon/transport_command.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -21,6 +21,7 @@ #include <irccd/string_util.hpp> #include "bot.hpp" +#include "hook_service.hpp" #include "plugin.hpp" #include "plugin_service.hpp" #include "rule.hpp" @@ -93,6 +94,9 @@ auto transport_command::registry() noexcept -> const std::vector<constructor>& { static const std::vector<transport_command::constructor> list{ + bind<hook_add_command>(), + bind<hook_list_command>(), + bind<hook_remove_command>(), bind<plugin_config_command>(), bind<plugin_info_command>(), bind<plugin_list_command>(), @@ -126,6 +130,84 @@ return list; } +// {{{ hook_add_command + +auto hook_add_command::get_name() const noexcept -> std::string_view +{ + return "hook-add"; +} + +void hook_add_command::exec(bot& bot, transport_client& client, const document& args) +{ + const auto id = args.get<std::string>("id"); + const auto path = args.get<std::string>("path"); + + if (!id || !string_util::is_identifier(*id)) + throw hook_error(hook_error::invalid_identifier, id.value_or("")); + if (!path || path->empty()) + throw hook_error(hook_error::invalid_path, *id, path.value_or("")); + + // The add function may throws already_exists + bot.get_hooks().add(hook(*id, *path)); + client.success("hook-add"); +} + +// }}} + +// {{{ hook_list_command + +auto hook_list_command::get_name() const noexcept -> std::string_view +{ + return "hook-list"; +} + +void hook_list_command::exec(bot& bot, transport_client& client, const document&) +{ + auto array = nlohmann::json::array(); + + for (const auto& hook : bot.get_hooks().list()) + array.push_back(nlohmann::json::object({ + { "id", hook.get_id() }, + { "path", hook.get_path() } + })); + + client.write({ + { "command", "hook-list" }, + { "list", std::move(array) } + }); +} + +// }}} + +// {{{ hook_remove_command + +auto hook_remove_command::get_name() const noexcept -> std::string_view +{ + return "hook-remove"; +} + +void hook_remove_command::exec(bot& bot, transport_client& client, const document& args) +{ + const auto id = args.get<std::string>("id"); + + if (!id || !string_util::is_identifier(*id)) + throw hook_error(hook_error::invalid_identifier, id.value_or("")); + + // We don't use hook_service::remove because it never fails. + auto& hooks = bot.get_hooks().list(); + auto it = std::find_if(hooks.begin(), hooks.end(), [&] (const auto& hook) { + return hook.get_id() == *id; + }); + + if (it == hooks.end()) + throw hook_error(hook_error::not_found, *id); + + hooks.erase(it); + client.success("hook-remove"); +} + +// }}} + // {{{ plugin_config_command auto plugin_config_command::get_name() const noexcept -> std::string_view
--- a/libirccd-daemon/irccd/daemon/transport_command.hpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-daemon/irccd/daemon/transport_command.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -91,6 +91,80 @@ // }}} +// {{{ hook_add_command + +/** + * \brief Implementation of hook-add transport command. + * \ingroup daemon-transport-commands + * + * Replies: + * + * - hook_error::already_exists + * - hook_error::invalid_identifier + * - hook_error::invalid_path + */ +class hook_add_command : public transport_command { +public: + /** + * \copydoc transport_command::get_name + */ + auto get_name() const noexcept -> std::string_view override; + + /** + * \copydoc transport_command::exec + */ + void exec(bot& bot, transport_client& client, const document& args) override; +}; + +// }}} + +// {{{ hook_list_command + +/** + * \brief Implementation of hook-list transport command. + * \ingroup daemon-transport-commands + */ +class hook_list_command : public transport_command { +public: + /** + * \copydoc transport_command::get_name + */ + auto get_name() const noexcept -> std::string_view override; + + /** + * \copydoc transport_command::exec + */ + void exec(bot& bot, transport_client& client, const document& args) override; +}; + +// }}} + +// {{{ hook_remove_command + +/** + * \brief Implementation of hook-remove transport command. + * \ingroup daemon-transport-commands + * + * Replies: + * + * - hook_error::invalid_identifier + * - hook_error::not_found + */ +class hook_remove_command : public transport_command { +public: + /** + * \copydoc transport_command::get_name + */ + auto get_name() const noexcept -> std::string_view override; + + /** + * \copydoc transport_command::exec + */ + void exec(bot& bot, transport_client& client, const document& args) override; +}; + +// }}} + // {{{ plugin_config_command /**
--- a/libirccd-test/irccd/test/cli_fixture.hpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-test/irccd/test/cli_fixture.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -31,6 +31,7 @@ #include <boost/asio.hpp> #include <irccd/daemon/bot.hpp> +#include <irccd/daemon/hook_service.hpp> #include <irccd/daemon/plugin_service.hpp> #include <irccd/daemon/rule_service.hpp> #include <irccd/daemon/server_service.hpp>
--- a/libirccd-test/irccd/test/command_fixture.hpp Sun Sep 01 17:23:37 2019 +0200 +++ b/libirccd-test/irccd/test/command_fixture.hpp Thu Sep 05 13:39:32 2019 +0200 @@ -28,6 +28,7 @@ #include <irccd/daemon/rule_service.hpp> #include <irccd/daemon/server_service.hpp> #include <irccd/daemon/transport_client.hpp> +#include <irccd/daemon/hook_service.hpp> #include "irccd_fixture.hpp" #include "mock_server.hpp" @@ -58,6 +59,7 @@ * \brief The fake transport_client stream. */ std::shared_ptr<mock_stream> stream_; + /** * \brief Client sending request. */
--- a/man/irccd.1 Sun Sep 01 17:23:37 2019 +0200 +++ b/man/irccd.1 Thu Sep 05 13:39:32 2019 +0200 @@ -61,7 +61,7 @@ .Sh PLUGINS The .Nm -program can runs plugins once IRC events are received. For example, if someone +program can run plugins once IRC events are received. For example, if someone sends you a private message plugins will be invoked with that event. Both native plugins written in C++ and Javascript are supported (if enabled at compile time). @@ -173,6 +173,38 @@ See additional documentation in their own manual page in the form .Xr irccd-plugin-name 7 where name is the actual plugin name. +.\" HOOKS +.Sh HOOKS +Hooks are a different and more lightweight approach to plugins, they are +executed upon incoming events and spawned each time a new event arrives. +.Pp +In contrast to plugins, differences are: +.Pp +.Bl -bullet -compact +.It +Hooks can not be filtered with rules. +.It +Hooks does not support all events. These events are not supported: +.Em onLoad , onUnload , onReload , onCommand , onNames , onWhois . +.It +Hooks can be written in any language. +.It +Execution may be slower since scripting languages require to fire up the +interpreter each time a new event is available. +.El +.Pp +Each hook will receive as positional argument the event name (similar to plugin +events) and the event arguments. +.Pp +An example of hook in shell script is available as +.Pa @CMAKE_INSTALL_FULL_DOCDIR@/examples/sample-hook.sh +file. +.Pp +See also the section +.Va [hooks] +in +.Xr irccd.conf 5 +manual page to enable hooks. .\" TRANSPORTS .Sh TRANSPORTS The daemon can be controlled at runtime using the dedicated
--- a/man/irccd.conf.5 Sun Sep 01 17:23:37 2019 +0200 +++ b/man/irccd.conf.5 Thu Sep 05 13:39:32 2019 +0200 @@ -208,6 +208,7 @@ The section is redefinable per plugin basis using the .Va [paths.<plugin>] syntax. +.\" [plugins] .Ss plugins This section is used to load plugins. .Pp @@ -216,6 +217,10 @@ path (including the .js extension). .Pp Warning: remember to add an empty string for searching plugins. +.\" [hooks] +.Ss hooks +This sections stores every hooks in key-value pairs. The option key denotes the +hook id and the value must be a path to the actual hook file. .\" [transport] .Ss transport This section defines transports that are used to communicate through clients @@ -355,6 +360,11 @@ channels = "#staff" plugins = "reboot" action = accept + +# Example of hooks +# This create an hook named "mail" with the given path. +[hooks] +mail = "/path/to/mail.py" .Ed .\" SEE ALSO .Sh SEE ALSO
--- a/man/irccdctl.1 Sun Sep 01 17:23:37 2019 +0200 +++ b/man/irccdctl.1 Thu Sep 05 13:39:32 2019 +0200 @@ -22,8 +22,20 @@ .Nd irccd controller agent .\" SYNOPSIS .Sh SYNOPSIS +.\" hook-add .Nm +.Cm hook-add +.Ar id +.Ar path +.\" hook-list +.Nm +.Cm hook-list +.\" hook-remove +.Nm +.Cm hook-remove +.Ar id .\" plugin-config +.Nm .Cm plugin-config .Ar id .Op Ar variable @@ -223,6 +235,20 @@ .\" COMMANDS .Sh COMMANDS .Bl -tag -width xxxxxxxx-yyyyyyyyy +.\" hook-add +.It Cm hook-add +Add a new hook with +.Ar id +as unique identifier and +.Ar path +as local path (on the machine where irccd is running). +.\" hook-list +.It Cm hook-list +List active hooks. +.\" hook-remove +.It Cm hook-remove +Remove a hook with identifier +.Ar id . .\" plugin-config .It Cm plugin-config Manipulate a configuration variable for the plugin specified by
--- a/tests/src/irccdctl/CMakeLists.txt Sun Sep 01 17:23:37 2019 +0200 +++ b/tests/src/irccdctl/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -25,6 +25,10 @@ add_subdirectory(cli-plugin-unload) endif () +add_subdirectory(cli-hook-add) +add_subdirectory(cli-hook-list) +add_subdirectory(cli-hook-remove) + add_subdirectory(cli-rule-add) add_subdirectory(cli-rule-edit) add_subdirectory(cli-rule-info)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/irccdctl/cli-hook-add/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,25 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +irccd_define_test( + NAME cli-hook-add + SOURCES main.cpp + LIBRARIES libirccd + DEPENDS irccd irccdctl + FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>" +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/irccdctl/cli-hook-add/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,117 @@ +/* + * main.cpp -- test irccdctl hook-add + * + * 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. + */ + +#define BOOST_TEST_MODULE "irccdctl hook-add" +#include <boost/test/unit_test.hpp> + +#include <irccd/test/cli_fixture.hpp> + +using namespace irccd::test; + +using irccd::daemon::hook; + +namespace irccd { + +namespace { + +class hook_add_fixture : public cli_fixture { +public: + hook_add_fixture() + : cli_fixture(IRCCDCTL_EXECUTABLE) + { + } +}; + +BOOST_FIXTURE_TEST_SUITE(hook_add_suite, hook_add_fixture) + +BOOST_AUTO_TEST_CASE(basic) +{ + start(); + + // true -> /bin/true + { + const auto [code, out, err] = exec({ "hook-add", "true", "/bin/true" }); + + BOOST_TEST(!code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 0U); + } + + // false -> /bin/false + { + const auto [code, out, err] = exec({ "hook-add", "false", "/bin/false" }); + + BOOST_TEST(!code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 0U); + } + + BOOST_TEST(bot_.get_hooks().list().size() == 2U); + BOOST_TEST(bot_.get_hooks().list()[0].get_id() == "true"); + BOOST_TEST(bot_.get_hooks().list()[0].get_path() == "/bin/true"); + BOOST_TEST(bot_.get_hooks().list()[1].get_id() == "false"); + BOOST_TEST(bot_.get_hooks().list()[1].get_path() == "/bin/false"); +} + +BOOST_AUTO_TEST_SUITE(errors) + +BOOST_AUTO_TEST_CASE(invalid_identifier) +{ + start(); + + const auto [code, out, err] = exec({ "hook-add", "#@#@", "/bin/true" }); + + BOOST_TEST(code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 1U); + BOOST_TEST(err[0] == "abort: invalid hook identifier"); +} + +BOOST_AUTO_TEST_CASE(invalid_path) +{ + start(); + + const auto [code, out, err] = exec({ "hook-add", "true", "\"\"" }); + + BOOST_TEST(code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 1U); + BOOST_TEST(err[0] == "abort: invalid path given"); +} + +BOOST_AUTO_TEST_CASE(already_exists) +{ + bot_.get_hooks().add(hook("true", "/bin/true")); + + start(); + + const auto [code, out, err] = exec({ "hook-add", "true", "/bin/true" }); + + BOOST_TEST(code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 1U); + BOOST_TEST(err[0] == "abort: hook already exists"); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/irccdctl/cli-hook-list/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,25 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +irccd_define_test( + NAME cli-hook-list + SOURCES main.cpp + LIBRARIES libirccd + DEPENDS irccd irccdctl + FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>" +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/irccdctl/cli-hook-list/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,61 @@ +/* + * main.cpp -- test irccdctl hook-list + * + * 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. + */ + +#define BOOST_TEST_MODULE "irccdctl hook-list" +#include <boost/test/unit_test.hpp> + +#include <irccd/test/cli_fixture.hpp> + +using namespace irccd::test; + +using irccd::daemon::hook; + +namespace irccd { + +namespace { + +class hook_list_fixture : public cli_fixture { +public: + hook_list_fixture() + : cli_fixture(IRCCDCTL_EXECUTABLE) + { + bot_.get_hooks().add(hook("true", "/bin/true")); + bot_.get_hooks().add(hook("false", "/bin/false")); + } +}; + +BOOST_FIXTURE_TEST_SUITE(hook_list_suite, hook_list_fixture) + +BOOST_AUTO_TEST_CASE(basic) +{ + start(); + + const auto [code, out, err] = exec({ "hook-list" }); + + BOOST_TEST(!code); + BOOST_TEST(out.size() == 2U); + BOOST_TEST(err.size() == 0U); + BOOST_TEST(out[0] == "true /bin/true"); + BOOST_TEST(out[1] == "false /bin/false"); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/irccdctl/cli-hook-remove/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,25 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +irccd_define_test( + NAME cli-hook-remove + SOURCES main.cpp + LIBRARIES libirccd + DEPENDS irccd irccdctl + FLAGS IRCCDCTL_EXECUTABLE="$<TARGET_FILE:irccdctl>" +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/irccdctl/cli-hook-remove/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,90 @@ +/* + * main.cpp -- test irccdctl hook-remove + * + * 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. + */ + +#define BOOST_TEST_MODULE "irccdctl hook-remove" +#include <boost/test/unit_test.hpp> + +#include <irccd/test/cli_fixture.hpp> + +using namespace irccd::test; + +using irccd::daemon::hook; + +namespace irccd { + +namespace { + +class hook_remove_fixture : public cli_fixture { +public: + hook_remove_fixture() + : cli_fixture(IRCCDCTL_EXECUTABLE) + { + bot_.get_hooks().add(hook("true", "/bin/true")); + bot_.get_hooks().add(hook("false", "/bin/false")); + } +}; + +BOOST_FIXTURE_TEST_SUITE(hook_remove_suite, hook_remove_fixture) + +BOOST_AUTO_TEST_CASE(basic) +{ + start(); + + const auto [code, out, err] = exec({ "hook-remove", "false" }); + + BOOST_TEST(!code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 0U); + BOOST_TEST(bot_.get_hooks().list().size() == 1U); + BOOST_TEST(bot_.get_hooks().list()[0].get_id() == "true"); + BOOST_TEST(bot_.get_hooks().list()[0].get_path() == "/bin/true"); +} + +BOOST_AUTO_TEST_SUITE(errors) + +BOOST_AUTO_TEST_CASE(invalid_identifier) +{ + start(); + + const auto [code, out, err] = exec({ "hook-remove", "#@#@" }); + + BOOST_TEST(code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 1U); + BOOST_TEST(err[0] == "abort: invalid hook identifier"); +} + +BOOST_AUTO_TEST_CASE(not_found) +{ + start(); + + const auto [code, out, err] = exec({ "hook-remove", "nonexistent" }); + + BOOST_TEST(code); + BOOST_TEST(out.size() == 0U); + BOOST_TEST(err.size() == 1U); + BOOST_TEST(err[0] == "abort: hook not found"); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd
--- a/tests/src/libirccd-daemon/CMakeLists.txt Sun Sep 01 17:23:37 2019 +0200 +++ b/tests/src/libirccd-daemon/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -16,18 +16,24 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # +add_subdirectory(command-hook-add) +add_subdirectory(command-hook-list) +add_subdirectory(command-hook-remove) + add_subdirectory(command-plugin-config) add_subdirectory(command-plugin-info) add_subdirectory(command-plugin-list) add_subdirectory(command-plugin-load) add_subdirectory(command-plugin-reload) add_subdirectory(command-plugin-unload) + add_subdirectory(command-rule-add) add_subdirectory(command-rule-edit) add_subdirectory(command-rule-info) add_subdirectory(command-rule-list) add_subdirectory(command-rule-move) add_subdirectory(command-rule-remove) + add_subdirectory(command-server-connect) add_subdirectory(command-server-disconnect) add_subdirectory(command-server-info) @@ -45,12 +51,13 @@ add_subdirectory(command-server-topic) add_subdirectory(dynlib-plugin) +add_subdirectory(hook) add_subdirectory(irc) add_subdirectory(logger) add_subdirectory(plugin-service) add_subdirectory(rule-service) add_subdirectory(rule-util) add_subdirectory(server) +add_subdirectory(server-service) add_subdirectory(server-util) -add_subdirectory(server-service) add_subdirectory(transports)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/command-hook-add/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,23 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +irccd_define_test( + NAME command-hook-add + SOURCES main.cpp + LIBRARIES libirccd libirccd-ctl +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/command-hook-add/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,106 @@ +/* + * main.cpp -- test hook-add 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. + */ + +#define BOOST_TEST_MODULE "hook-add" +#include <boost/test/unit_test.hpp> + +#include <irccd/test/command_fixture.hpp> + +using std::string; + +using irccd::daemon::hook; +using irccd::daemon::hook_error; + +namespace irccd { + +namespace { + +BOOST_FIXTURE_TEST_SUITE(hook_add_fixture_suite, test::command_fixture) + +BOOST_AUTO_TEST_CASE(basic) +{ + request({ + { "command", "hook-add" }, + { "id", "true" }, + { "path", "/bin/true" } + }); + request({ + { "command", "hook-add" }, + { "id", "false" }, + { "path", "/bin/false" } + }); + + BOOST_TEST(bot_.get_hooks().list().size() == 2U); + BOOST_TEST(bot_.get_hooks().list()[0].get_id() == "true"); + BOOST_TEST(bot_.get_hooks().list()[0].get_path() == "/bin/true"); + BOOST_TEST(bot_.get_hooks().list()[1].get_id() == "false"); + BOOST_TEST(bot_.get_hooks().list()[1].get_path() == "/bin/false"); +} + +BOOST_AUTO_TEST_SUITE(errors) + +BOOST_AUTO_TEST_CASE(invalid_identifier) +{ + const auto json = request({ + { "command", "hook-add" }, + { "id" , "#@#@" } + }); + + BOOST_TEST(json.size() == 4U); + BOOST_TEST(json["command"].get<string>() == "hook-add"); + BOOST_TEST(json["error"].get<int>() == hook_error::invalid_identifier); + BOOST_TEST(json["errorCategory"].get<string>() == "hook"); +} + +BOOST_AUTO_TEST_CASE(invalid_path) +{ + const auto json = request({ + { "command", "hook-add" }, + { "id", "true" }, + { "path", 1234 } + }); + + BOOST_TEST(json.size() == 4U); + BOOST_TEST(json["command"].get<string>() == "hook-add"); + BOOST_TEST(json["error"].get<int>() == hook_error::invalid_path); + BOOST_TEST(json["errorCategory"].get<string>() == "hook"); +} + +BOOST_AUTO_TEST_CASE(already_exists) +{ + bot_.get_hooks().add(hook("true", "/bin/true")); + + const auto json = request({ + { "command", "hook-add" }, + { "id", "true" }, + { "path", "/bin/true" } + }); + + BOOST_TEST(json.size() == 4U); + BOOST_TEST(json["command"].get<string>() == "hook-add"); + BOOST_TEST(json["error"].get<int>() == hook_error::already_exists); + BOOST_TEST(json["errorCategory"].get<string>() == "hook"); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/command-hook-list/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,23 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +irccd_define_test( + NAME command-hook-list + SOURCES main.cpp + LIBRARIES libirccd libirccd-ctl +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/command-hook-list/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,54 @@ +/* + * main.cpp -- test hook-list 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. + */ + +#define BOOST_TEST_MODULE "hook-list" +#include <boost/test/unit_test.hpp> + +#include <irccd/test/command_fixture.hpp> + +using std::string; + +using irccd::daemon::hook; + +namespace irccd { + +namespace { + +BOOST_FIXTURE_TEST_SUITE(hook_list_fixture_suite, test::command_fixture) + +BOOST_AUTO_TEST_CASE(basic) +{ + bot_.get_hooks().add(hook("true", "/bin/true")); + bot_.get_hooks().add(hook("false", "/bin/false")); + + const auto json = request({{"command", "hook-list"}}); + + BOOST_TEST(json.size() == 2U); + BOOST_TEST(json["command"].get<string>() == "hook-list"); + BOOST_TEST(json["list"].size() == 2U); + BOOST_TEST(json["list"][0]["id"].get<string>() == "true"); + BOOST_TEST(json["list"][0]["path"].get<string>() == "/bin/true"); + BOOST_TEST(json["list"][1]["id"].get<string>() == "false"); + BOOST_TEST(json["list"][1]["path"].get<string>() == "/bin/false"); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/command-hook-remove/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,23 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +irccd_define_test( + NAME command-hook-remove + SOURCES main.cpp + LIBRARIES libirccd libirccd-ctl +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/command-hook-remove/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,94 @@ +/* + * main.cpp -- test hook-remove 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. + */ + +#define BOOST_TEST_MODULE "hook-remove" +#include <boost/test/unit_test.hpp> + +#include <irccd/test/command_fixture.hpp> + +using std::string; + +using irccd::daemon::hook; +using irccd::daemon::hook_error; + +namespace irccd { + +namespace { + +BOOST_FIXTURE_TEST_SUITE(hook_remove_fixture_suite, test::command_fixture) + +BOOST_AUTO_TEST_CASE(basic) +{ + bot_.get_hooks().add(hook("true", "/bin/true")); + bot_.get_hooks().add(hook("false", "/bin/false")); + + const auto json = request({ + { "command", "hook-remove" }, + { "id", "false" }, + }); + + BOOST_TEST(json.size() == 1U); + BOOST_TEST(json["command"].get<string>() == "hook-remove"); + BOOST_TEST(bot_.get_hooks().list().size() == 1U); + BOOST_TEST(bot_.get_hooks().list()[0].get_id() == "true"); + BOOST_TEST(bot_.get_hooks().list()[0].get_path() == "/bin/true"); +} + +BOOST_AUTO_TEST_SUITE(errors) + +BOOST_AUTO_TEST_CASE(invalid_identifier) +{ + const auto json = request({ + { "command", "hook-remove" }, + { "action", "#@#@" } + }); + + BOOST_TEST(json.size() == 4U); + BOOST_TEST(json["command"].get<string>() == "hook-remove"); + BOOST_TEST(json["error"].get<int>() == hook_error::invalid_identifier); + BOOST_TEST(json["errorCategory"].get<string>() == "hook"); +} + +BOOST_AUTO_TEST_CASE(not_found) +{ + request({ + { "command", "hook-add" }, + { "id", "true" }, + { "path", "/bin/true" } + }); + + stream_->clear(); + + const auto json = request({ + { "command", "hook-remove" }, + { "id", "nonexistent" }, + }); + + BOOST_TEST(json.size() == 4U); + BOOST_TEST(json["command"].get<string>() == "hook-remove"); + BOOST_TEST(json["error"].get<int>() == hook_error::not_found); + BOOST_TEST(json["errorCategory"].get<string>() == "hook"); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/hook/CMakeLists.txt Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,48 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# 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. +# + +find_package(Boost REQUIRED QUIET) + +add_executable(sample-hook sample_hook.cpp) + +set_target_properties( + sample-hook + PROPERTIES + PREFIX "" + FOLDER "test" + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + +foreach (cfg ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${cfg} cfg) + set_target_properties( + sample-hook + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY_${cfg} ${CMAKE_CURRENT_BINARY_DIR} + LIBRARY_OUTPUT_DIRECTORY_${cfg} ${CMAKE_CURRENT_BINARY_DIR} + ) +endforeach () + +irccd_define_test( + NAME hook + SOURCES main.cpp + LIBRARIES libirccd libirccd-test + DEPENDS sample-hook + FLAGS HOOK_FILE="$<TARGET_FILE:sample-hook>" +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/hook/main.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,280 @@ +/* + * main.cpp -- test hook functions + * + * 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. + */ + +#define BOOST_TEST_MODULE "hook" +#include <boost/test/unit_test.hpp> + +#include <string> +#include <vector> + +#include <irccd/daemon/bot.hpp> +#include <irccd/daemon/hook.hpp> +#include <irccd/daemon/logger.hpp> + +#include <irccd/test/mock_server.hpp> + +using std::string; +using std::vector; +using std::shared_ptr; +using std::make_unique; +using std::make_shared; + +using boost::asio::io_context; + +using irccd::daemon::logger::sink; + +using irccd::test::mock_server; + +namespace irccd::daemon { + +namespace { + +/* + * Since stdout/stderr from the hook is logger through the irccd's logger, we'll + * gonna store every message logged into it and compare if the values are + * appropriate. + */ +class memory_sink : public logger::sink { +public: + using list = vector<string>; + +private: + list debug_; + list warning_; + list info_; + +public: + auto get_debug() const noexcept -> const list&; + auto get_info() const noexcept -> const list&; + auto get_warning() const noexcept -> const list&; + void write_debug(const std::string& line) override; + void write_info(const std::string& line) override; + void write_warning(const std::string& line) override; +}; + +auto memory_sink::get_debug() const noexcept -> const list& +{ + return debug_; +} + +auto memory_sink::get_info() const noexcept -> const list& +{ + return info_; +} + +auto memory_sink::get_warning() const noexcept -> const list& +{ + return warning_; +} + +void memory_sink::write_debug(const std::string& line) +{ + debug_.push_back(line); +} + +void memory_sink::write_info(const std::string& line) +{ + info_.push_back(line); +} + +void memory_sink::write_warning(const std::string& line) +{ + warning_.push_back(line); +} + +class hook_fixture { +protected: + io_context io_; + bot bot_{io_}; + hook hook_{"test", HOOK_FILE}; + memory_sink* sink_{nullptr}; + shared_ptr<mock_server> server_; + +public: + hook_fixture(); +}; + +hook_fixture::hook_fixture() + : server_(make_shared<mock_server>(io_, "test")) +{ + auto sink = make_unique<memory_sink>(); + + sink_ = sink.get(); + bot_.set_log(std::move(sink)); + bot_.get_log().set_verbose(true); +} + +BOOST_FIXTURE_TEST_SUITE(hook_fixture_suite, hook_fixture) + +BOOST_AUTO_TEST_CASE(connect) +{ + hook_.handle_connect(bot_, {server_}); + + BOOST_TEST(sink_->get_info().size() == 2U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onConnect"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); +} + +BOOST_AUTO_TEST_CASE(disconnect) +{ + hook_.handle_disconnect(bot_, {server_}); + + BOOST_TEST(sink_->get_info().size() == 2U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onDisconnect"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); +} + +BOOST_AUTO_TEST_CASE(invite) +{ + hook_.handle_invite(bot_, {server_, "jean", "#staff", "NiReaS"}); + + BOOST_TEST(sink_->get_info().size() == 5U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onInvite"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); + BOOST_TEST(sink_->get_info()[4] == "hook test: target: NiReaS"); +} + +BOOST_AUTO_TEST_CASE(join) +{ + hook_.handle_join(bot_, {server_, "jean", "#staff"}); + + BOOST_TEST(sink_->get_info().size() == 4U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onJoin"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); +} + +BOOST_AUTO_TEST_CASE(kick) +{ + hook_.handle_kick(bot_, {server_, "jean", "#staff", "NiReaS", "stop it"}); + + BOOST_TEST(sink_->get_info().size() == 6U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onKick"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); + BOOST_TEST(sink_->get_info()[4] == "hook test: target: NiReaS"); + BOOST_TEST(sink_->get_info()[5] == "hook test: reason: stop it"); +} + +BOOST_AUTO_TEST_CASE(message) +{ + hook_.handle_message(bot_, {server_, "jean", "#staff", "coucou tout le monde"}); + + BOOST_TEST(sink_->get_info().size() == 5U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onMessage"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); + BOOST_TEST(sink_->get_info()[4] == "hook test: message: coucou tout le monde"); +} + +BOOST_AUTO_TEST_CASE(me) +{ + hook_.handle_me(bot_, {server_, "jean", "#staff", "coucou tout le monde"}); + + BOOST_TEST(sink_->get_info().size() == 5U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onMe"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); + BOOST_TEST(sink_->get_info()[4] == "hook test: message: coucou tout le monde"); +} + +BOOST_AUTO_TEST_CASE(mode) +{ + hook_.handle_mode(bot_, {server_, "jean", "#staff", "+o", "franck", "abc", "xyz" }); + + BOOST_TEST(sink_->get_info().size() == 8U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onMode"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); + BOOST_TEST(sink_->get_info()[4] == "hook test: mode: +o"); + BOOST_TEST(sink_->get_info()[5] == "hook test: limit: franck"); + BOOST_TEST(sink_->get_info()[6] == "hook test: user: abc"); + BOOST_TEST(sink_->get_info()[7] == "hook test: mask: xyz"); +} + +BOOST_AUTO_TEST_CASE(nick) +{ + hook_.handle_nick(bot_, {server_, "jean", "doctor"}); + + BOOST_TEST(sink_->get_info().size() == 4U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onNick"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: nick: doctor"); +} + +BOOST_AUTO_TEST_CASE(notice) +{ + hook_.handle_notice(bot_, {server_, "jean", "#staff", "coucou tout le monde"}); + + BOOST_TEST(sink_->get_info().size() == 5U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onNotice"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #staff"); + BOOST_TEST(sink_->get_info()[4] == "hook test: message: coucou tout le monde"); +} + +BOOST_AUTO_TEST_CASE(part) +{ + hook_.handle_part(bot_, {server_, "jean", "#windows", "je n'aime pas ici"}); + + BOOST_TEST(sink_->get_info().size() == 5U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onPart"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #windows"); + BOOST_TEST(sink_->get_info()[4] == "hook test: reason: je n'aime pas ici"); +} + +BOOST_AUTO_TEST_CASE(topic) +{ + hook_.handle_topic(bot_, {server_, "jean", "#windows", "attention Windows est un malware"}); + + BOOST_TEST(sink_->get_info().size() == 5U); + BOOST_TEST(sink_->get_warning().empty()); + BOOST_TEST(sink_->get_info()[0] == "hook test: event: onTopic"); + BOOST_TEST(sink_->get_info()[1] == "hook test: server: test"); + BOOST_TEST(sink_->get_info()[2] == "hook test: origin: jean"); + BOOST_TEST(sink_->get_info()[3] == "hook test: channel: #windows"); + BOOST_TEST(sink_->get_info()[4] == "hook test: topic: attention Windows est un malware"); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // !namespace + +} // !irccd::daemon
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/libirccd-daemon/hook/sample_hook.cpp Thu Sep 05 13:39:32 2019 +0200 @@ -0,0 +1,177 @@ +/* + * sample_hook.cpp -- sample hook for unit tests + * + * 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 <iostream> +#include <functional> +#include <string_view> +#include <unordered_map> + +using std::function; +using std::string_view; +using std::unordered_map; +using std::cerr; +using std::cout; +using std::endl; + +namespace { + +auto print(int argc, char** argv, int index) -> string_view +{ + return argc > index ? argv[index] : ""; +} + +void handle_onConnect(int argc, char** argv) +{ + cout << "event: onConnect" << endl; + cout << "server: " << print(argc, argv, 0) << endl; +} + +void handle_onDisconnect(int argc, char** argv) +{ + cout << "event: onDisconnect" << endl; + cout << "server: " << print(argc, argv, 0) << endl; +} + +void handle_onInvite(int argc, char** argv) +{ + cout << "event: onInvite" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "target: " << print(argc, argv, 3) << endl; +} + +void handle_onJoin(int argc, char** argv) +{ + cout << "event: onJoin" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; +} + +void handle_onKick(int argc, char** argv) +{ + cout << "event: onKick" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "target: " << print(argc, argv, 3) << endl; + cout << "reason: " << print(argc, argv, 4) << endl; +} + +void handle_onMessage(int argc, char** argv) +{ + cout << "event: onMessage" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "message: " << print(argc, argv, 3) << endl; +} + +void handle_onMe(int argc, char** argv) +{ + cout << "event: onMe" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "message: " << print(argc, argv, 3) << endl; +} + +void handle_onMode(int argc, char** argv) +{ + cout << "event: onMode" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "mode: " << print(argc, argv, 3) << endl; + cout << "limit: " << print(argc, argv, 4) << endl; + cout << "user: " << print(argc, argv, 5) << endl; + cout << "mask: " << print(argc, argv, 6) << endl; +} + +void handle_onNick(int argc, char** argv) +{ + cout << "event: onNick" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "nick: " << print(argc, argv, 2) << endl; +} + +void handle_onNotice(int argc, char** argv) +{ + cout << "event: onNotice" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "message: " << print(argc, argv, 3) << endl; +} + +void handle_onPart(int argc, char** argv) +{ + cout << "event: onPart" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "reason: " << print(argc, argv, 3) << endl; +} + +void handle_onTopic(int argc, char** argv) +{ + cout << "event: onTopic" << endl; + cout << "server: " << print(argc, argv, 0) << endl; + cout << "origin: " << print(argc, argv, 1) << endl; + cout << "channel: " << print(argc, argv, 2) << endl; + cout << "topic: " << print(argc, argv, 3) << endl; +} + +unordered_map<string_view, function<void (int, char**)>> handlers{ + { "onConnect", handle_onConnect }, + { "onDisconnect", handle_onDisconnect }, + { "onInvite", handle_onInvite }, + { "onJoin", handle_onJoin }, + { "onKick", handle_onKick }, + { "onMessage", handle_onMessage }, + { "onMe", handle_onMe }, + { "onMode", handle_onMode }, + { "onNick", handle_onNick }, + { "onNotice", handle_onNotice }, + { "onPart", handle_onPart }, + { "onTopic", handle_onTopic } +}; + +} // !namespace + +int main(int argc, char** argv) +{ + --argc; + ++argv; + + if (argc == 0) { + cerr << "abort: no command given" << endl; + return 1; + } + + const auto handler = handlers.find(argv[0]); + + if (handler == handlers.end()) { + cerr << "abort: unknown message hook: " << argv[0] << endl; + return 1; + } + + handler->second(--argc, ++argv); +}