initial command logic

This commit is contained in:
Eric Mertens 2025-01-25 21:24:33 -08:00
parent 093515c3ec
commit 8be5332692
9 changed files with 209 additions and 20 deletions

View File

@ -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})

55
bot.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "bot.hpp"
#include <boost/log/trivial.hpp>
auto Bot::start(std::shared_ptr<SelfThread> self) -> std::shared_ptr<Bot>
{
const auto thread = std::make_shared<Bot>(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);
}
}
}

32
bot.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include "self_thread.hpp"
#include <boost/signals2.hpp>
#include <memory>
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<Bot>
{
std::shared_ptr<SelfThread> self_;
char command_prefix_;
Bot(std::shared_ptr<SelfThread> 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<SelfThread>) -> std::shared_ptr<Bot>;
};

View File

@ -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)

View File

@ -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 <typename... Args>
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<void()> 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;

View File

@ -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<int>(match.get_tag()) << std::endl;

View File

@ -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<SelfThread>
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<std::string
auto SelfThread::is_my_nick(std::string_view nick) const -> 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<SaslMechanism> 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 <int lower(int)>
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<tolower>(str);
case Casemap::Rfc1459: return casemap_impl<tolower_rfc1459>(str);
case Casemap::Rfc1459_Strict: return casemap_impl<tolower_rfc1459_strict>(str);
}
}
template <int lower(int)>
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<tolower>(lhs, rhs);
case Casemap::Rfc1459: return casemap_compare_impl<tolower_rfc1459>(lhs, rhs);
case Casemap::Rfc1459_Strict: return casemap_compare_impl<tolower_rfc1459_strict>(lhs, rhs);
}
}

View File

@ -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<std::string, std::string> isupport_;
std::unique_ptr<SaslMechanism> 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<SelfThread>;
auto start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void;
auto get_connection() const -> std::shared_ptr<Connection>
{
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<std::string> &;
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;
};

View File

@ -34,28 +34,29 @@ enum class SnoteTag
class SnoteMatch
{
SnoteTag tag_;
std::variant<std::pair<std::regex const&, std::string_view>, std::match_results<std::string_view::const_iterator>> components_;
std::variant<std::pair<const std::regex &, std::string_view>, std::match_results<std::string_view::const_iterator>> components_;
public:
SnoteMatch(SnoteTag tag, std::regex const& regex, std::string_view full)
SnoteMatch(SnoteTag tag, const std::regex &regex, 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<std::string_view::const_iterator> const&;
auto get_results() -> const std::match_results<std::string_view::const_iterator> &;
};
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