diff --git a/CMakeLists.txt b/CMakeLists.txt index f95cf38..3fe509f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,12 +38,17 @@ add_custom_command( add_subdirectory(mybase64) add_executable(xbot - main.cpp irc_commands.inc ircmsg.cpp settings.cpp connection.cpp - registration_thread.cpp - snote.cpp - self_thread.cpp - sasl_mechanism.cpp + main.cpp + irc_commands.inc + bot.cpp + connection.cpp irc_coroutine.cpp + ircmsg.cpp + registration_thread.cpp + sasl_mechanism.cpp + self_thread.cpp + settings.cpp + snote.cpp ) target_include_directories(xbot PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/bot.cpp b/bot.cpp new file mode 100644 index 0000000..6cbc4d1 --- /dev/null +++ b/bot.cpp @@ -0,0 +1,55 @@ +#include "bot.hpp" + +#include + +auto Bot::start(std::shared_ptr self) -> std::shared_ptr +{ + const auto thread = std::make_shared(std::move(self)); + + auto &connection = *thread->self_->get_connection(); + + connection.sig_ircmsg.connect([thread](auto cmd, auto &msg) { + thread->on_ircmsg(cmd, msg); + }); + + return thread; +} + +auto Bot::process_command(std::string_view message, const IrcMsg &msg) -> void +{ + const auto cmdstart = message.find_first_not_of(' '); + if (cmdstart == message.npos) return; + message = message.substr(cmdstart); + + if (not message.starts_with(command_prefix_)) return; + + auto cmdend = message.find_first_of(' ', 1); + + std::string_view command = + cmdend == message.npos ? message : message.substr(0, cmdend); + + + + std::string_view arguments = + cmdend == message.npos ? std::string_view{} : message.substr(cmdend + 1); + + BOOST_LOG_TRIVIAL(debug) << "COMMAND: " << command << " -- [" << arguments << "]"; +} + +auto Bot::on_ircmsg(IrcCommand cmd, const IrcMsg &msg) -> void +{ + if (cmd == IrcCommand::PRIVMSG) + { + const auto target = msg.args[0]; + const auto message = msg.args[1]; + if (self_->is_my_nick(target)) + { + process_command(message, msg); + } else if (self_->is_channel(target)) { + const auto colon = message.find(':'); + if (colon == message.npos) return; + if (not self_->is_my_nick(message.substr(0, colon))) return; + process_command(message.substr(colon+1), msg); + } + } +} \ No newline at end of file diff --git a/bot.hpp b/bot.hpp new file mode 100644 index 0000000..3e03319 --- /dev/null +++ b/bot.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "self_thread.hpp" + +#include + +#include + +struct Command +{ + std::string_view source; + std::string_view target; + std::string_view oper; + std::string_view account; + std::string_view command; + std::string_view arguments; +}; + +struct Bot : std::enable_shared_from_this +{ + std::shared_ptr self_; + char command_prefix_; + + Bot(std::shared_ptr self) + : self_{std::move(self)} + , command_prefix_{'!'} + {} + + auto on_ircmsg(IrcCommand, const IrcMsg &) -> void; + auto process_command(std::string_view message, const IrcMsg &msg) -> void; + static auto start(std::shared_ptr) -> std::shared_ptr; +}; diff --git a/connection.cpp b/connection.cpp index a227094..451f364 100644 --- a/connection.cpp +++ b/connection.cpp @@ -265,6 +265,11 @@ auto Connection::send_authenticate(std::string_view message) -> void write_irc("AUTHENTICATE", message); } +auto Connection::send_join(std::string_view channel) -> void +{ + write_irc("JOIN", channel); +} + auto Connection::on_authenticate(const std::string_view chunk) -> void { if (chunk != "+"sv) diff --git a/connection.hpp b/connection.hpp index 849d69b..c992a02 100644 --- a/connection.hpp +++ b/connection.hpp @@ -33,16 +33,16 @@ private: auto watchdog() -> void; auto watchdog_activity() -> void; - /// Write bytes into the socket. Messages should be properly newline terminated. - auto write_line(std::string message) -> void; - +public: /// 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: + /// Write bytes into the socket. + auto write_line(std::string message) -> void; + Connection(boost::asio::io_context &io); boost::signals2::signal sig_connect; @@ -71,6 +71,7 @@ public: 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_join(std::string_view) -> void; auto send_cap_ls() -> void; auto send_cap_end() -> void; auto send_cap_req(std::string_view) -> void; diff --git a/main.cpp b/main.cpp index c27c44c..c3db272 100644 --- a/main.cpp +++ b/main.cpp @@ -10,6 +10,7 @@ #include "registration_thread.hpp" #include "self_thread.hpp" +#include "bot.hpp" using namespace std::chrono_literals; @@ -19,6 +20,7 @@ auto start(boost::asio::io_context &io, const Settings &settings) -> void const auto selfThread = SelfThread::start(*connection); RegistrationThread::start(*connection, settings, selfThread); + Bot::start(selfThread); connection->sig_snote.connect([](auto &match) { std::cout << "SNOTE " << static_cast(match.get_tag()) << std::endl; diff --git a/self_thread.cpp b/self_thread.cpp index fc93c7c..43a24ad 100644 --- a/self_thread.cpp +++ b/self_thread.cpp @@ -14,6 +14,11 @@ auto SelfThread::on_welcome(const IrcMsg &irc) -> void nickname_ = irc.args[0]; } +auto SelfThread::on_registered() -> void +{ + connection_.send_join("#lobby"); +} + auto SelfThread::on_nick(const IrcMsg &irc) -> void { if (is_my_mask(irc.source)) @@ -143,6 +148,9 @@ auto SelfThread::start(Connection &connection) -> std::shared_ptr case IrcCommand::RPL_WELCOME: thread->on_welcome(msg); break; + case IrcCommand::RPL_ENDOFMOTD: + thread->on_registered(); + break; default: break; } @@ -172,7 +180,7 @@ auto SelfThread::get_my_channels() const -> const std::unordered_set bool { - return nick == nickname_; + return casemap_compare(nick, nickname_) == 0; } auto SelfThread::is_my_mask(std::string_view mask) const -> bool @@ -181,6 +189,11 @@ auto SelfThread::is_my_mask(std::string_view mask) const -> bool return bang != std::string_view::npos && nickname_ == mask.substr(0, bang); } +auto SelfThread::is_channel(std::string_view name) const -> bool +{ + return not name.empty() && channel_prefix_.find(name[0]) != channel_prefix_.npos; +} + auto SelfThread::on_authenticate(const std::string_view body) -> void { if (not sasl_mechanism_) @@ -213,3 +226,57 @@ auto SelfThread::start_sasl(std::unique_ptr mechanism) -> void sasl_mechanism_ = std::move(mechanism); connection_.send_authenticate(sasl_mechanism_->mechanism_name()); } + +static auto tolower_rfc1459(int c) -> int +{ + return 97 <= c && c <= 126 ? c - 32 : c; +} + +static auto tolower_rfc1459_strict(int c) -> int +{ + return 97 <= c && c <= 125 ? c - 32 : c; +} + +template +static auto casemap_impl(std::string_view str) -> std::string +{ + std::string result; + result.reserve(str.size()); + for (auto c : str) { + result.push_back(lower(c)); + } + return result; +} + +auto SelfThread::casemap(std::string_view str) const -> std::string +{ + switch (casemap_) { + case Casemap::Ascii: return casemap_impl(str); + case Casemap::Rfc1459: return casemap_impl(str); + case Casemap::Rfc1459_Strict: return casemap_impl(str); + } +} + +template +static auto casemap_compare_impl(std::string_view lhs, std::string_view rhs) -> int +{ + size_t n1 = lhs.size(); + size_t n2 = rhs.size(); + size_t n = std::min(n1, n2); + for (size_t i = 0; i < n; ++i) { + if (lower(lhs[i]) < lower(rhs[i])) return -1; + if (lower(lhs[i]) > lower(rhs[i])) return 1; + } + if (n1 < n2) return -1; + if (n1 > n2) return 1; + return 0; +} + +auto SelfThread::casemap_compare(std::string_view lhs, std::string_view rhs) const -> int +{ + switch (casemap_) { + case Casemap::Ascii: return casemap_compare_impl(lhs, rhs); + case Casemap::Rfc1459: return casemap_compare_impl(lhs, rhs); + case Casemap::Rfc1459_Strict: return casemap_compare_impl(lhs, rhs); + } +} \ No newline at end of file diff --git a/self_thread.hpp b/self_thread.hpp index bdfa7c0..a9cda10 100644 --- a/self_thread.hpp +++ b/self_thread.hpp @@ -9,6 +9,13 @@ struct Connection; struct IrcMsg; +enum class Casemap +{ + Rfc1459, + Rfc1459_Strict, + Ascii, +}; + /** * @brief Thread to track this connection's identity, and IRC state. * @@ -23,9 +30,11 @@ class SelfThread // RPL_ISUPPORT state std::unordered_map isupport_; - std::unique_ptr sasl_mechanism_; + Casemap casemap_; + std::string channel_prefix_; + auto on_welcome(const IrcMsg &irc) -> void; auto on_isupport(const IrcMsg &irc) -> void; auto on_nick(const IrcMsg &irc) -> void; @@ -35,20 +44,32 @@ class SelfThread auto on_part(const IrcMsg &irc) -> void; auto on_mode(const IrcMsg &irc) -> void; auto on_authenticate(std::string_view) -> void; + auto on_registered() -> void; public: SelfThread(Connection &connection) : connection_{connection} + , casemap_{Casemap::Rfc1459} + , channel_prefix_{"#&"} { } static auto start(Connection &) -> std::shared_ptr; auto start_sasl(std::unique_ptr mechanism) -> void; + auto get_connection() const -> std::shared_ptr + { + return connection_.shared_from_this(); + } + auto get_my_nickname() const -> const std::string &; auto get_my_mode() const -> const std::string &; auto get_my_channels() const -> const std::unordered_set &; auto is_my_nick(std::string_view nick) const -> bool; - auto is_my_mask(std::string_view nick) const -> bool; + auto is_my_mask(std::string_view mask) const -> bool; + auto is_channel(std::string_view name) const -> bool; + + auto casemap(std::string_view) const -> std::string; + auto casemap_compare(std::string_view, std::string_view) const -> int; }; diff --git a/snote.hpp b/snote.hpp index 28b8e71..15519f4 100644 --- a/snote.hpp +++ b/snote.hpp @@ -34,28 +34,29 @@ enum class SnoteTag class SnoteMatch { SnoteTag tag_; - std::variant, std::match_results> components_; + std::variant, std::match_results> components_; public: - SnoteMatch(SnoteTag tag, std::regex const& regex, std::string_view full) - : tag_{tag} - , components_{std::make_pair(std::ref(regex), full)} - {} + SnoteMatch(SnoteTag tag, const std::regex ®ex, std::string_view full) + : tag_{tag} + , components_{std::make_pair(std::ref(regex), full)} + { + } auto get_tag() -> SnoteTag { return tag_; } - auto get_results() -> std::match_results const&; + auto get_results() -> const std::match_results &; }; struct SnoteCore { struct DbDeleter { - auto operator()(hs_database * db) const -> void; + auto operator()(hs_database *db) const -> void; }; struct ScratchDeleter { - auto operator()(hs_scratch * scratch) const -> void; + auto operator()(hs_scratch *scratch) const -> void; }; /// @brief Database of server notice patterns