From 60fc0d34eba30989ff634a797516d11aeb81e327 Mon Sep 17 00:00:00 2001 From: Green Sky Date: Sat, 13 Jan 2024 22:28:56 +0100 Subject: [PATCH] add mcd --- CMakeLists.txt | 3 + .../message3/message_command_dispatcher.cpp | 373 ++++++++++++++++++ .../message3/message_command_dispatcher.hpp | 85 ++++ 3 files changed, 461 insertions(+) create mode 100644 solanaceae/message3/message_command_dispatcher.cpp create mode 100644 solanaceae/message3/message_command_dispatcher.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index da8ca36..b14e24f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,9 @@ add_library(solanaceae_message3 ./solanaceae/message3/message_time_sort.hpp ./solanaceae/message3/message_time_sort.cpp + + ./solanaceae/message3/message_command_dispatcher.hpp + ./solanaceae/message3/message_command_dispatcher.cpp ) target_include_directories(solanaceae_message3 PUBLIC .) diff --git a/solanaceae/message3/message_command_dispatcher.cpp b/solanaceae/message3/message_command_dispatcher.cpp new file mode 100644 index 0000000..4ce5d3a --- /dev/null +++ b/solanaceae/message3/message_command_dispatcher.cpp @@ -0,0 +1,373 @@ +#include "./message_command_dispatcher.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +MessageCommandDispatcher::MessageCommandDispatcher( + Contact3Registry& cr, + RegistryMessageModel& rmm, + ConfigModelI& conf +) : + _cr(cr), _rmm(rmm), _conf(conf), _program_started_at(Message::getTimeMS()) +{ + // overwrite default admin and moderator to false + _conf.set("MessageCommandDispatcher", "admin", false); + _conf.set("MessageCommandDispatcher", "moderator", false); + + _rmm.subscribe(this, RegistryMessageModel_Event::message_construct); + + { // setup basic commands for bot + registerCommand( + "host", "", + "help", + [this](std::string_view params, Message3Handle m) -> bool { + return helpCommand(params, m); + }, + "Get help", + Perms::EVERYONE + ); + } +} + +MessageCommandDispatcher::~MessageCommandDispatcher(void) { +} + +void MessageCommandDispatcher::iterate(float) { + if (!_message_queue.empty()) { + _rmm.sendText( + _message_queue.front().to, + _message_queue.front().message + ); + _message_queue.pop_front(); + } +} + +static std::string_view get_first_word(std::string_view text, std::string_view::size_type& out_next) { + if (text.empty()) { + out_next = std::string_view::npos; + return text; + } + + // trim + const auto pos_first_non_space = text.find_first_not_of(' '); + if (pos_first_non_space == std::string_view::npos) { + // only contains spaces o.o + out_next = std::string_view::npos; + return ""; + } + + text = text.substr(pos_first_non_space); + out_next += pos_first_non_space; + + const auto pos_first_space = text.find_first_of(' '); + if (pos_first_space == 0 || pos_first_space == std::string_view::npos) { + // does not contain spaces + // command is whole message + out_next = std::string_view::npos; + return text; + } else { + out_next += pos_first_space; + return text.substr(0, pos_first_space); + } +} + +void MessageCommandDispatcher::registerCommand( + std::string_view m, // module + std::string_view m_prefix, // module prefix (if any) + std::string_view command, // command + std::function&& fn, + std::string_view help_text, + Perms perms +) { + std::string full_command_string = (m_prefix.empty() ? "" : std::string{m_prefix} + " ") + std::string{command}; + + if (_command_map.count(full_command_string)) { + std::cout << "MCD warning: overwriting existing '" << full_command_string << "'\n"; + } + + assert( + // needs atleast one "group" + (perms & ( + Perms::EVERYONE | + Perms::ADMIN | + Perms::MODERATOR + )) != 0u + ); + + assert( + // at most one "group" + (((perms & Perms::EVERYONE) != 0) + + ((perms & Perms::ADMIN) != 0) + + ((perms & Perms::MODERATOR) != 0)) + == 1 + ); + + _command_map[full_command_string] = Command{ + std::string{m}, + std::string{m_prefix}, + std::string{command}, + std::move(fn), + std::string{help_text}, + perms + }; +} + +bool MessageCommandDispatcher::helpCommand(std::string_view params, Message3Handle m) { + std::cout << "MCD: help got called '" << params << "'\n"; + + std::map> module_command_list; + for (auto it = _command_map.cbegin(); it != _command_map.cend(); it++) { + if (true) { // have permission + module_command_list[it->second.m].push_back(it); + } + } + + const auto contact_from = m.get().c; + + for (const auto& [module_name, command_list] : module_command_list) { + _message_queue.push_back({ + contact_from, + "=== " + module_name + " ===" + }); + + bool module_empty = true; + for (const auto& it : command_list) { + if (!hasPermission(it->second, contact_from)) { + continue; + } + + module_empty = false; + + std::string help_line {" !"}; + if (!it->second.m_prefix.empty()) { + help_line += it->second.m_prefix + " "; + } + + help_line += it->second.command; + + help_line += " - "; + help_line += it->second.help_text; + + _message_queue.push_back({ + contact_from, + help_line + }); + } + + if (module_empty) { + // unsend module cat title + _message_queue.pop_back(); + } + } + + return true; +} + +bool MessageCommandDispatcher::hasPermission(const Command& cmd, const Contact3 contact) { + if (!_cr.all_of(contact)) { + std::cerr << "MCD error: contact without ID\n"; + return false; // default to false + } + + const auto id_str = bin2hex(_cr.get(contact).data); + std::cout << "MCD: perm check for id '" << id_str << "'\n"; + + // TODO: blacklist here + // TODO: whitelist here + + if ((cmd.perms & Perms::EVERYONE) != 0) { + return true; + } + + if ((cmd.perms & Perms::ADMIN) != 0) { + auto is_admin_opt = _conf.get_bool("MessageCommandDispatcher", "admin", id_str); + assert(is_admin_opt.has_value); + + return is_admin_opt.value(); + } + + if ((cmd.perms & Perms::MODERATOR) != 0) { + auto is_mod_opt = _conf.get_bool("MessageCommandDispatcher", "moderator", id_str); + assert(is_mod_opt.has_value); + + return is_mod_opt.value(); + } + + return false; +} + +bool MessageCommandDispatcher::onEvent(const Message::Events::MessageConstruct& e) { + if (!e.e.all_of()) { + std::cout << "MCD: got message that is not"; + + if (!e.e.all_of()) { + std::cout << " contact_from"; + } + if (!e.e.all_of()) { + std::cout << " text"; + } + if (!e.e.all_of()) { + std::cout << " unread"; + } + + std::cout << "\n"; + return false; + } + + if (e.e.any_of()) { + std::cout << "MCD: got message that is"; + if (e.e.all_of()) { + std::cout << " action"; + } + std::cout << "\n"; + return false; + } + + if (e.e.any_of()) { + // test if message was written before program was started (-1s) + if (e.e.get().ts + 1'000 < _program_started_at) { + // message too old + return false; + } + } + + std::string_view message_text = e.e.get().text; + + if (message_text.empty()) { + std::cout << "MCD warning: empty message\n"; + // empty message? + return false; + } + + // skip unrelyable synced + if (e.e.all_of()) { + const auto& list = e.e.get().ts; + if ( + std::find_if( + list.cbegin(), list.cend(), + [this](const auto&& it) { + return _cr.any_of< + Contact::Components::TagSelfStrong, + Contact::Components::TagSelfWeak // trust weak self + >(it.first); + } + ) == list.cend() + ) { + // self not found + // TODO: config for self only + return false; + } + } + + const bool is_private = _cr.any_of(e.e.get().c); + + if (is_private) { + // check for command prefix + if ( + message_text.at(0) == '!' || + message_text.at(0) == '/' + ) { + // starts with command prefix + // remove c prefix + message_text = message_text.substr(1); + } + } else { + // check for command prefix + if ( + message_text.at(0) != '!' && + message_text.at(0) != '/' + ) { + // does not start with command prefix, not for us + return false; + } + + // remove c prefix + message_text = message_text.substr(1); + } + + if (message_text.empty()) { + // empty message? + std::cout << "MCD: got empty command\n"; + return false; + } + + std::cout << "MCD: got command '" << message_text << "'\n"; + + std::string_view first_word; + std::string_view second_word; + std::string_view::size_type pos_next = 0; + + first_word = get_first_word(message_text, pos_next); + std::cout << "------- first_word:'" << first_word << "' pos_next:" << pos_next << "\n"; + if (first_word.size() != message_text.size()) { + second_word = get_first_word( + message_text.substr(pos_next), + pos_next + ); + } + + std::cout << "------- second_word:'" << second_word << "' empty:" << second_word.empty() << " pos_next:" << pos_next << "\n"; + + std::string params; + if (pos_next != std::string_view::npos && message_text.size() > pos_next+1) { + auto tmp_params = message_text.substr(pos_next); + + const auto params_pos_first_non_space = tmp_params.find_first_not_of(' '); + if (params_pos_first_non_space == std::string_view::npos) { + tmp_params = {}; + } else if (params_pos_first_non_space != 0) { + // trim leading whitespace + tmp_params = tmp_params.substr(params_pos_first_non_space); + } + + params = tmp_params; + + std::cout << "------- params:'" << params << "'\n"; + } + + const auto contact_from = e.e.get().c; + + // first search first + space + second word + if (!second_word.empty()) { + std::string query {first_word}; + query += " "; + query += second_word; + + const auto command_it = _command_map.find(query); + if (command_it != _command_map.cend()) { + if (!hasPermission(command_it->second, contact_from)) { + return false; + } + + return command_it->second.fn(params, e.e); + } + } + + // then seach first word only + const auto command_it = _command_map.find(std::string{first_word}); + if (command_it != _command_map.cend()) { + if (!hasPermission(command_it->second, contact_from)) { + return false; + } + + params = std::string{second_word} + " " + params; + return command_it->second.fn(params, e.e); + } + + return false; +} + +bool MessageCommandDispatcher::onEvent(const Message::Events::MessageUpdated&) { + // do i need this? + return false; +} + diff --git a/solanaceae/message3/message_command_dispatcher.hpp b/solanaceae/message3/message_command_dispatcher.hpp new file mode 100644 index 0000000..e9e27d4 --- /dev/null +++ b/solanaceae/message3/message_command_dispatcher.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +// fwd +struct ConfigModelI; + +class MessageCommandDispatcher : public RegistryMessageModelEventI { + Contact3Registry& _cr; + RegistryMessageModel& _rmm; + ConfigModelI& _conf; + + public: + enum Perms { + BLACKLIST = 1 << 0, + WHITELIST = 1 << 1, + + // can only be trumped by blacklist + EVERYONE = 1 << 2, + + // can only be trumped by blacklist + // TODO: replace with groups? + ADMIN = 1 << 3, + MODERATOR = 1 << 4, + }; + + private: + struct Command { + std::string m; // module + std::string m_prefix; // module prefix (if any) + std::string command; // command + std::function fn; + std::string help_text; + + Perms perms = Perms::ADMIN; // default to highest + + //Command(const Command&) = delete; + }; + std::unordered_map _command_map; + + struct QueuedMessage { + Contact3 to; + std::string message; + }; + std::deque _message_queue; + + uint64_t _program_started_at {0}; + + public: + MessageCommandDispatcher(Contact3Registry& cr, RegistryMessageModel& rmm, ConfigModelI& conf); + ~MessageCommandDispatcher(void); + + void iterate(float time_delta); + + // TODO: think more about permissions? + // - user(s) + // - group(s) + // - everyone else? + + void registerCommand( + std::string_view m, // module + std::string_view m_prefix, // module prefix (if any) + std::string_view command, // command + std::function&& fn, + std::string_view help_text, + Perms perms = Perms::ADMIN + ); + + // generates a help + bool helpCommand(std::string_view params, Message3Handle m); + + bool hasPermission(const Command& cmd, const Contact3 contact); + + protected: // mm + bool onEvent(const Message::Events::MessageConstruct& e) override; + bool onEvent(const Message::Events::MessageUpdated& e) override; +}; +