diff --git a/CMakeLists.txt b/CMakeLists.txt index 15afc98..6b95ec0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,10 +31,8 @@ add_subdirectory(mybase64) add_executable(xbot main.cpp irc_commands.inc ircmsg.cpp settings.cpp connection.cpp - write_irc.cpp registration_thread.cpp - ping_thread.cpp - snote_thread.cpp + snote.cpp self_thread.cpp irc_coroutine.cpp watchdog_thread.cpp diff --git a/connection.cpp b/connection.cpp index 5f7f99c..f8746df 100644 --- a/connection.cpp +++ b/connection.cpp @@ -79,7 +79,9 @@ auto Connection::connect( sig_connect(); } + // Start the queue writer after connection self->writer(); + for(LineBuffer buffer{32'768};;) { boost::system::error_code error; @@ -108,11 +110,33 @@ auto Connection::dispatch_line(char *line) -> void && recognized->max_args >= msg.args.size() ? recognized->command : IrcCommand::UNKNOWN; - if (IrcCommand::UNKNOWN == command) - { + switch (command) { + + // Respond to pings immediate and discard + case IrcCommand::PING: + send_pong(msg.args[0]); + break; + + // Unknown message generate warnings but do not dispatch + // Messages can be unknown due to bad command or bad argument count + case IrcCommand::UNKNOWN: BOOST_LOG_TRIVIAL(warning) << "Unrecognized command: " << msg.command << " " << msg.args.size(); + break; + + // Server notice generate snote events but not IRC command events + case IrcCommand::NOTICE: + if (auto match = snoteCore.match(msg)) { + sig_snote(match->first, match->second); + break; + } + /* FALLTHROUGH */ + + // Normal IRC commands + default: + sig_ircmsg(command, msg); + break; } - sig_ircmsg(command, msg); + } auto Connection::write_line(std::string message) -> void @@ -131,3 +155,81 @@ auto Connection::close() -> void { stream_.close(); } + +static +auto is_invalid_last(char x) -> bool +{ + return x == '\0' || x == '\r' || x == '\n'; +} + +auto Connection::write_irc(std::string message) -> void +{ + write_line(std::move(message)); +} + +auto Connection::write_irc(std::string front, std::string_view last) -> void +{ + if (last.end() != std::find_if(last.begin(), last.end(), is_invalid_last)) + { + throw std::runtime_error{"bad irc argument"}; + } + + front += " :"; + front += last; + write_irc(std::move(front)); +} + +auto Connection::send_ping(std::string_view txt) -> void +{ + write_irc("PING", txt); +} + +auto Connection::send_pong(std::string_view txt) -> void +{ + write_irc("PONG", txt); +} + +auto Connection::send_pass(std::string_view password) -> void +{ + write_irc("PASS", password); +} + +auto Connection::send_user(std::string_view user, std::string_view real) -> void +{ + write_irc("USER", user, "*", "*", real); +} + +auto Connection::send_nick(std::string_view nick) -> void +{ + write_irc("NICK", nick); +} + +auto Connection::send_cap_ls() -> void +{ + write_irc("CAP", "LS", "302"); +} + +auto Connection::send_cap_end() -> void +{ + write_irc("CAP", "END"); +} + +auto Connection::send_cap_req(std::string_view caps) -> void +{ + write_irc("CAP", "REQ", caps); +} + +auto Connection::send_privmsg(std::string_view target, std::string_view message) -> void +{ + write_irc("PRIVMSG", target, message); +} + +auto Connection::send_notice(std::string_view target, std::string_view message) -> void +{ + write_irc("NOTICE", target, message); +} + +auto Connection::send_authenticate(std::string_view message) -> void +{ + write_irc("AUTHENTICATE", message); +} diff --git a/connection.hpp b/connection.hpp index bf79879..a98d00f 100644 --- a/connection.hpp +++ b/connection.hpp @@ -2,6 +2,7 @@ #include "ircmsg.hpp" #include "irc_command.hpp" +#include "snote.hpp" #include #include @@ -21,20 +22,27 @@ private: auto writer_immediate() -> void; auto dispatch_line(char * line) -> void; + /// Write bytes into the socket. Messages should be properly newline terminated. + auto write_line(std::string message) -> void; + + /// Build and send well-formed IRC message from individual parameters + auto write_irc(std::string) -> void; + auto write_irc(std::string, std::string_view) -> void; + template + auto write_irc(std::string front, std::string_view next, Args ...rest) -> void; + public: Connection(boost::asio::io_context & io); boost::signals2::signal sig_connect; boost::signals2::signal sig_disconnect; boost::signals2::signal sig_ircmsg; + boost::signals2::signal sig_snote; auto get_executor() -> boost::asio::any_io_executor { return stream_.get_executor(); } - /// Write bytes into the socket. Messages should be properly newline terminated. - auto write_line(std::string message) -> void; - auto connect( boost::asio::io_context & io, std::string host, @@ -42,5 +50,36 @@ public: ) -> boost::asio::awaitable; auto close() -> void; + + auto send_ping(std::string_view) -> void; + auto send_pong(std::string_view) -> void; + auto send_pass(std::string_view) -> void; + auto send_user(std::string_view, std::string_view) -> void; + auto send_nick(std::string_view) -> void; + auto send_cap_ls() -> void; + auto send_cap_end() -> void; + auto send_cap_req(std::string_view) -> void; + auto send_privmsg(std::string_view, std::string_view) -> void; + auto send_notice(std::string_view, std::string_view) -> void; + auto send_authenticate(std::string_view message) -> void; }; +template +auto Connection::write_irc(std::string front, std::string_view next, Args ...rest) -> void +{ + auto const is_invalid = [](char const x) -> bool + { + return x == '\0' || x == '\r' || x == '\n' || x == ' '; + }; + + if (next.empty() + || next.front() == ':' + || next.end() != std::find_if(next.begin(), next.end(), is_invalid)) + { + throw std::runtime_error{"bad irc argument"}; + } + + front += " "; + front += next; + write_irc(std::move(front), rest...); +} diff --git a/main.cpp b/main.cpp index 9619138..8af2f60 100644 --- a/main.cpp +++ b/main.cpp @@ -1,7 +1,6 @@ #include "connection.hpp" #include "ircmsg.hpp" #include "settings.hpp" -#include "write_irc.hpp" #include @@ -11,42 +10,27 @@ #include #include -#include "ping_thread.hpp" #include "registration_thread.hpp" #include "self_thread.hpp" -#include "snote_thread.hpp" #include "irc_coroutine.hpp" using namespace std::chrono_literals; -irc_coroutine example(Connection& connection) { - auto [cmd1, msg1] = co_await wait_command{IrcCommand::RPL_WELCOME}; - std::cout << "WELCOME " << msg1.args[0] << "\n"; - auto [cmd5, msg5] = co_await wait_command{IrcCommand::RPL_ISUPPORT}; - std::cout << "ISUPPORT " << msg5.args[0] << "\n"; -} - auto start(boost::asio::io_context & io, Settings const& settings) -> void { auto const connection = std::make_shared(io); RegistrationThread::start(*connection, settings.password, settings.username, settings.realname, settings.nickname); - PingThread::start(*connection); SelfThread::start(*connection); - auto const snote_thread = SnoteThread::start(*connection); - /* - snote_thread->sig_snote.connect([](auto tag, auto &match) { + connection->sig_snote.connect([](auto tag, auto &match) { std::cout << "SNOTE " << static_cast(tag) << std::endl; for (auto c : match.get_results()) { std::cout << " " << std::string_view{c.first, c.second} << std::endl; } }); - */ - auto logic = example(*connection); - logic.start(*connection); boost::asio::co_spawn( io, diff --git a/registration_thread.cpp b/registration_thread.cpp index 12929ed..885929d 100644 --- a/registration_thread.cpp +++ b/registration_thread.cpp @@ -2,7 +2,6 @@ #include "connection.hpp" #include "ircmsg.hpp" -#include "write_irc.hpp" #include #include @@ -25,10 +24,10 @@ RegistrationThread::RegistrationThread( auto RegistrationThread::on_connect() -> void { - send_cap_ls(connection_); - send_pass(connection_, password_); - send_user(connection_, username_, realname_); - send_nick(connection_, nickname_); + connection_.send_cap_ls(); + connection_.send_pass(password_); + connection_.send_user(username_, realname_); + connection_.send_nick(nickname_); } auto RegistrationThread::send_req() -> void @@ -63,13 +62,13 @@ auto RegistrationThread::send_req() -> void if (not outstanding.empty()) { request.pop_back(); - send_cap_req(connection_, request); + connection_.send_cap_req(request); listen_for_cap_ack(); } else { - send_cap_end(connection_); + connection_.send_cap_end(); } } @@ -86,7 +85,7 @@ auto RegistrationThread::on_msg_cap_ack(IrcMsg const& msg) -> void if (outstanding.empty()) { message_handle_.disconnect(); - send_cap_end(connection_); + connection_.send_cap_end(); } } diff --git a/snote_thread.cpp b/snote.cpp similarity index 69% rename from snote_thread.cpp rename to snote.cpp index 7f4a894..67b02f3 100644 --- a/snote_thread.cpp +++ b/snote.cpp @@ -1,6 +1,5 @@ -#include "snote_thread.hpp" +#include "snote.hpp" -#include "connection.hpp" #include "c_callback.hpp" #include @@ -9,6 +8,7 @@ #include #include +#include #include #include #include @@ -114,64 +114,60 @@ static auto setup_database() -> hs_database_t* } // namespace -auto SnoteThread::start(Connection& connection) -> std::shared_ptr +SnoteCore::SnoteCore() noexcept { - auto thread = std::make_shared(); - - thread->db_.reset(setup_database()); + db_.reset(setup_database()); hs_scratch_t* scratch = nullptr; - if (HS_SUCCESS != hs_alloc_scratch(thread->db_.get(), &scratch)) + if (HS_SUCCESS != hs_alloc_scratch(db_.get(), &scratch)) { abort(); } - thread->scratch_.reset(scratch); + scratch_.reset(scratch); +} +auto SnoteCore::match(const IrcMsg &msg) -> std::optional> +{ static char const* const prefix = "*** Notice -- "; - connection.sig_ircmsg.connect([&connection, thread](auto cmd, auto& msg) + + auto& args = msg.args; + if ("*" != args[0] || !args[1].starts_with(prefix)) { + return std::nullopt; + } + + auto const message = args[1].substr(strlen(prefix)); + + unsigned match_id; + auto cb = [&match_id](unsigned id, unsigned long long, unsigned long long, unsigned) -> int { - auto& args = msg.args; - if (IrcCommand::NOTICE == cmd && "*" == args[0] && args[1].starts_with(prefix)) - { - auto const message = args[1].substr(strlen(prefix)); + match_id = id; + return 1; // stop scanning + }; - unsigned match_id; - auto cb = [&match_id](unsigned id, unsigned long long, unsigned long long, unsigned) -> int - { - match_id = id; - return 1; // stop scanning - }; + auto const scan_result = + hs_scan( + db_.get(), + message.data(), message.size(), + 0, // no flags + scratch_.get(), + CCallback::invoke, &cb + ); - auto const scan_result = - hs_scan( - thread->db_.get(), - message.data(), message.size(), - 0, // no flags - thread->scratch_.get(), - CCallback::invoke, &cb - ); + switch (scan_result) + { + case HS_SUCCESS: + BOOST_LOG_TRIVIAL(warning) << "Unknown snote: " << message; + return std::nullopt; - switch (scan_result) - { - case HS_SUCCESS: - BOOST_LOG_TRIVIAL(warning) << "Unknown snote: " << message; - break; + case HS_SCAN_TERMINATED: + { + auto& pattern = patterns[match_id]; + return std::make_pair(pattern.tag, SnoteMatch{pattern.regex, message}); + } - case HS_SCAN_TERMINATED: - { - auto& pattern = patterns[match_id]; - auto match = SnoteMatch{pattern.regex, message}; - thread->sig_snote(pattern.tag, match); - break; - } - - default: - abort(); - } - } - }); - - return thread; + default: + abort(); + } } auto SnoteMatch::get_results() -> std::match_results const& @@ -191,7 +187,7 @@ auto SnoteMatch::get_results() -> std::match_results void +auto SnoteCore::DbDeleter::operator()(hs_database_t * db) const -> void { if (HS_SUCCESS != hs_free_database(db)) { @@ -199,10 +195,12 @@ auto SnoteThread::DbDeleter::operator()(hs_database_t * db) const -> void } } -auto SnoteThread::ScratchDeleter::operator()(hs_scratch_t * scratch) const -> void +auto SnoteCore::ScratchDeleter::operator()(hs_scratch_t * scratch) const -> void { if (HS_SUCCESS != hs_free_scratch(scratch)) { abort(); } } + +SnoteCore snoteCore; diff --git a/snote_thread.hpp b/snote.hpp similarity index 85% rename from snote_thread.hpp rename to snote.hpp index a4045b4..d88acfa 100644 --- a/snote_thread.hpp +++ b/snote.hpp @@ -1,11 +1,13 @@ #pragma once -#include +#include "ircmsg.hpp" #include #include #include #include +#include +#include class Connection; struct hs_database; @@ -44,7 +46,7 @@ private: std::variant, std::match_results> components_; }; -struct SnoteThread +struct SnoteCore { struct DbDeleter { @@ -62,7 +64,8 @@ struct SnoteThread /// @brief HyperScan scratch space std::unique_ptr scratch_; - boost::signals2::signal sig_snote; - - static auto start(Connection& connection) -> std::shared_ptr; + SnoteCore() noexcept; + auto match(const IrcMsg &msg) -> std::optional>; }; + +extern SnoteCore snoteCore; diff --git a/watchdog_thread.cpp b/watchdog_thread.cpp index c22521f..544722c 100644 --- a/watchdog_thread.cpp +++ b/watchdog_thread.cpp @@ -1,7 +1,6 @@ #include "watchdog_thread.hpp" #include "connection.hpp" -#include "write_irc.hpp" #include @@ -43,7 +42,7 @@ auto WatchdogThread::on_timeout() -> void } else { - send_ping(connection_, "watchdog"); + connection_.send_ping("watchdog"); stalled_ = true; timer_.expires_after(WatchdogThread::TIMEOUT); start_timer(); diff --git a/write_irc.cpp b/write_irc.cpp deleted file mode 100644 index 55a0e4a..0000000 --- a/write_irc.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "write_irc.hpp" - -#include "connection.hpp" - -#include -#include - -namespace { - -auto write_irc(Connection& connection, std::string message) -> void -{ - connection.write_line(std::move(message)); -} - -auto is_invalid_last(char x) -> bool -{ - return x == '\0' || x == '\r' || x == '\n'; -} - -auto is_invalid(char x) -> bool -{ - return x == '\0' || x == '\r' || x == '\n' || x == ' '; -} - -auto write_irc(Connection& connection, std::string front, std::string_view last) -> void -{ - if (last.end() != std::find_if(last.begin(), last.end(), is_invalid_last)) - { - throw std::runtime_error{"bad irc argument"}; - } - - front += " :"; - front += last; - write_irc(connection, std::move(front)); -} - -template -auto write_irc(Connection& connection, std::string front, std::string_view next, Args ...rest) -> void -{ - if (next.empty() - || next.front() == ':' - || next.end() != std::find_if(next.begin(), next.end(), is_invalid)) - { - throw std::runtime_error{"bad irc argument"}; - } - - front += " "; - front += next; - write_irc(connection, std::move(front), rest...); -} - -} // namespace - -auto send_ping(Connection& connection, std::string_view txt) -> void -{ - write_irc(connection, "PING", txt); -} - -auto send_pong(Connection& connection, std::string_view txt) -> void -{ - write_irc(connection, "PONG", txt); -} - -auto send_pass(Connection& connection, std::string_view password) -> void -{ - write_irc(connection, "PASS", password); -} - -auto send_user(Connection& connection, std::string_view user, std::string_view real) -> void -{ - write_irc(connection, "USER", user, "*", "*", real); -} - -auto send_nick(Connection& connection, std::string_view nick) -> void -{ - write_irc(connection, "NICK", nick); -} - -auto send_cap_ls(Connection& connection) -> void -{ - write_irc(connection, "CAP", "LS", "302"); -} - -auto send_cap_end(Connection& connection) -> void -{ - write_irc(connection, "CAP", "END"); -} - -auto send_cap_req(Connection& connection, std::string_view caps) -> void -{ - write_irc(connection, "CAP", "REQ", caps); -} - -auto send_privmsg(Connection& connection, std::string_view target, std::string_view message) -> void -{ - write_irc(connection, "PRIVMSG", target, message); -} - -auto send_notice(Connection& connection, std::string_view target, std::string_view message) -> void -{ - write_irc(connection, "NOTICE", target, message); -} - -auto send_authenticate(Connection& connection, std::string_view message) -> void -{ - write_irc(connection, "AUTHENTICATE", message); -} diff --git a/write_irc.hpp b/write_irc.hpp deleted file mode 100644 index e62600f..0000000 --- a/write_irc.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -class Connection; - -auto send_ping(Connection&, std::string_view) -> void; -auto send_pong(Connection&, std::string_view) -> void; -auto send_pass(Connection&, std::string_view) -> void; -auto send_user(Connection&, std::string_view, std::string_view) -> void; -auto send_nick(Connection&, std::string_view) -> void; -auto send_cap_ls(Connection&) -> void; -auto send_cap_end(Connection&) -> void; -auto send_cap_req(Connection&, std::string_view) -> void; -auto send_privmsg(Connection&, std::string_view, std::string_view) -> void; -auto send_notice(Connection&, std::string_view, std::string_view) -> void; -auto send_authenticate(Connection& connection, std::string_view message) -> void;