diff options
| author | ilotterytea <iltsu@alright.party> | 2024-05-18 14:48:12 +0500 |
|---|---|---|
| committer | ilotterytea <iltsu@alright.party> | 2024-05-18 14:48:12 +0500 |
| commit | d1793df1eda463b10107d41785ad1d7f055ed476 (patch) | |
| tree | fd3e41c3b4a05924748ae4b762e1ae55a0bc815c /bot/src/commands | |
| parent | d7a2de17e9b7931f68b5b4079b1c36866a19d343 (diff) | |
upd: moved the bot part to a relative subfolder
Diffstat (limited to 'bot/src/commands')
| -rw-r--r-- | bot/src/commands/command.cpp | 104 | ||||
| -rw-r--r-- | bot/src/commands/command.hpp | 55 | ||||
| -rw-r--r-- | bot/src/commands/request.hpp | 25 | ||||
| -rw-r--r-- | bot/src/commands/request_util.cpp | 205 | ||||
| -rw-r--r-- | bot/src/commands/request_util.hpp | 13 | ||||
| -rw-r--r-- | bot/src/commands/response_error.hpp | 222 |
6 files changed, 624 insertions, 0 deletions
diff --git a/bot/src/commands/command.cpp b/bot/src/commands/command.cpp new file mode 100644 index 0000000..e3b45b1 --- /dev/null +++ b/bot/src/commands/command.cpp @@ -0,0 +1,104 @@ +#include "command.hpp" + +#include <algorithm> +#include <chrono> +#include <ctime> +#include <memory> +#include <optional> +#include <pqxx/pqxx> +#include <string> + +#include "../bundle.hpp" +#include "../modules/custom_command.hpp" +#include "../modules/event.hpp" +#include "../modules/help.hpp" +#include "../modules/join.hpp" +#include "../modules/massping.hpp" +#include "../modules/notify.hpp" +#include "../modules/ping.hpp" +#include "../modules/timer.hpp" +#include "../utils/chrono.hpp" +#include "request.hpp" + +namespace bot { + namespace command { + CommandLoader::CommandLoader() { + this->add_command(std::make_unique<mod::Ping>()); + this->add_command(std::make_unique<mod::Massping>()); + this->add_command(std::make_unique<mod::Event>()); + this->add_command(std::make_unique<mod::Notify>()); + this->add_command(std::make_unique<mod::Join>()); + this->add_command(std::make_unique<mod::CustomCommand>()); + this->add_command(std::make_unique<mod::Timer>()); + this->add_command(std::make_unique<mod::Help>()); + } + + void CommandLoader::add_command(std::unique_ptr<Command> command) { + this->commands.push_back(std::move(command)); + } + + std::optional<std::variant<std::vector<std::string>, std::string>> + CommandLoader::run(const InstanceBundle &bundle, + const Request &request) const { + auto command = std::find_if( + this->commands.begin(), this->commands.end(), + [&](const auto &x) { return x->get_name() == request.command_id; }); + + if (command == this->commands.end()) { + return std::nullopt; + } + + if ((*command)->get_permission_level() > + request.user_rights.get_level()) { + return std::nullopt; + } + + pqxx::work work(request.conn); + + pqxx::result action_query = work.exec( + "SELECT sent_at FROM actions WHERE user_id = " + + std::to_string(request.user.get_id()) + + " AND channel_id = " + std::to_string(request.channel.get_id()) + + " AND command = '" + request.command_id + "' ORDER BY sent_at DESC"); + + if (!action_query.empty()) { + auto last_sent_at = utils::chrono::string_to_time_point( + action_query[0][0].as<std::string>()); + + auto now = std::chrono::system_clock::now(); + auto now_time_it = std::chrono::system_clock::to_time_t(now); + auto now_tm = std::gmtime(&now_time_it); + now = std::chrono::system_clock::from_time_t(std::mktime(now_tm)); + + auto difference = std::chrono::duration_cast<std::chrono::seconds>( + now - last_sent_at); + + if (difference.count() < command->get()->get_delay_seconds()) { + return std::nullopt; + } + } + + std::string arguments; + + if (request.subcommand_id.has_value()) { + arguments += request.subcommand_id.value() + " "; + } + + if (request.message.has_value()) { + arguments += request.message.value(); + } + + work.exec( + "INSERT INTO actions(user_id, channel_id, command, arguments, " + "full_message) VALUES (" + + std::to_string(request.user.get_id()) + ", " + + std::to_string(request.channel.get_id()) + ", '" + + request.command_id + "', '" + arguments + "', '" + + request.irc_message.message + "')"); + + work.commit(); + + return (*command)->run(bundle, request); + } + } +} diff --git a/bot/src/commands/command.hpp b/bot/src/commands/command.hpp new file mode 100644 index 0000000..40ec114 --- /dev/null +++ b/bot/src/commands/command.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include <memory> +#include <optional> +#include <string> +#include <variant> +#include <vector> + +#include "../bundle.hpp" +#include "request.hpp" + +namespace bot { + namespace command { + enum CommandArgument { + SUBCOMMAND, + MESSAGE, + INTERVAL, + NAME, + TARGET, + VALUE, + AMOUNT, + }; + + class Command { + public: + virtual std::string get_name() const = 0; + virtual std::variant<std::vector<std::string>, std::string> run( + const InstanceBundle &bundle, const Request &request) const = 0; + virtual schemas::PermissionLevel get_permission_level() const { + return schemas::PermissionLevel::USER; + } + virtual int get_delay_seconds() const { return 5; } + virtual std::vector<std::string> get_subcommand_ids() const { + return {}; + } + }; + + class CommandLoader { + public: + CommandLoader(); + ~CommandLoader() = default; + + void add_command(std::unique_ptr<Command> cmd); + std::optional<std::variant<std::vector<std::string>, std::string>> run( + const InstanceBundle &bundle, const Request &msg) const; + + const std::vector<std::unique_ptr<Command>> &get_commands() const { + return this->commands; + }; + + private: + std::vector<std::unique_ptr<Command>> commands; + }; + } +} diff --git a/bot/src/commands/request.hpp b/bot/src/commands/request.hpp new file mode 100644 index 0000000..e2685f1 --- /dev/null +++ b/bot/src/commands/request.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include <optional> +#include <pqxx/pqxx> +#include <string> + +#include "../irc/message.hpp" +#include "../schemas/channel.hpp" +#include "../schemas/user.hpp" + +namespace bot::command { + struct Request { + std::string command_id; + std::optional<std::string> subcommand_id; + std::optional<std::string> message; + const irc::Message<irc::MessageType::Privmsg> &irc_message; + + schemas::Channel channel; + schemas::ChannelPreferences channel_preferences; + schemas::User user; + schemas::UserRights user_rights; + + pqxx::connection &conn; + }; +} diff --git a/bot/src/commands/request_util.cpp b/bot/src/commands/request_util.cpp new file mode 100644 index 0000000..90750e5 --- /dev/null +++ b/bot/src/commands/request_util.cpp @@ -0,0 +1,205 @@ +#include "request_util.hpp" + +#include <algorithm> +#include <optional> +#include <pqxx/pqxx> +#include <string> + +#include "../constants.hpp" +#include "../irc/message.hpp" +#include "../schemas/channel.hpp" +#include "command.hpp" +#include "request.hpp" + +namespace bot::command { + std::optional<Request> generate_request( + const command::CommandLoader &command_loader, + const irc::Message<irc::MessageType::Privmsg> &irc_message, + pqxx::connection &conn) { + pqxx::work *work; + + work = new pqxx::work(conn); + + std::vector<std::string> parts = + utils::string::split_text(irc_message.message, ' '); + + std::string command_id = parts[0]; + + if (command_id.substr(0, DEFAULT_PREFIX.length()) != DEFAULT_PREFIX) { + delete work; + return std::nullopt; + } + + command_id = + command_id.substr(DEFAULT_PREFIX.length(), command_id.length()); + + auto cmd = std::find_if( + command_loader.get_commands().begin(), + command_loader.get_commands().end(), + [&](const auto &command) { return command->get_name() == command_id; }); + + if (cmd == command_loader.get_commands().end()) { + delete work; + return std::nullopt; + } + + parts.erase(parts.begin()); + + pqxx::result query = work->exec("SELECT * FROM channels WHERE alias_id = " + + std::to_string(irc_message.source.id)); + + // Create new channel data in the database if it didn't exist b4 + if (query.empty()) { + work->exec("INSERT INTO channels (alias_id, alias_name) VALUES (" + + std::to_string(irc_message.source.id) + ", '" + + irc_message.source.login + "')"); + + work->commit(); + + delete work; + work = new pqxx::work(conn); + + query = work->exec("SELECT * FROM channels WHERE alias_id = " + + std::to_string(irc_message.source.id)); + } + + schemas::Channel channel(query[0]); + + if (channel.get_opted_out_at().has_value()) { + delete work; + return std::nullopt; + } + + query = work->exec("SELECT * FROM channel_preferences WHERE channel_id = " + + std::to_string(channel.get_id())); + + // Create new channel preference data in the database if it didn't exist b4 + if (query.empty()) { + work->exec( + "INSERT INTO channel_preferences (channel_id, prefix, locale) VALUES " + "(" + + std::to_string(channel.get_id()) + ", '" + DEFAULT_PREFIX + "', '" + + DEFAULT_LOCALE_ID + "')"); + + work->commit(); + + delete work; + work = new pqxx::work(conn); + + query = + work->exec("SELECT * FROM channel_preferences WHERE channel_id = " + + std::to_string(channel.get_id())); + } + + schemas::ChannelPreferences channel_preferences(query[0]); + + query = work->exec("SELECT * FROM users WHERE alias_id = " + + std::to_string(irc_message.sender.id)); + + // Create new user data in the database if it didn't exist before + if (query.empty()) { + work->exec("INSERT INTO users (alias_id, alias_name) VALUES (" + + std::to_string(irc_message.sender.id) + ", '" + + irc_message.sender.login + "')"); + + work->commit(); + + delete work; + work = new pqxx::work(conn); + + query = work->exec("SELECT * FROM users WHERE alias_id = " + + std::to_string(irc_message.sender.id)); + } + + schemas::User user(query[0]); + + if (user.get_alias_name() != irc_message.sender.login) { + work->exec("UPDATE users SET alias_name = '" + irc_message.sender.login + + "' WHERE id = " + std::to_string(user.get_id())); + work->commit(); + + delete work; + work = new pqxx::work(conn); + + user.set_alias_name(irc_message.sender.login); + } + + schemas::PermissionLevel level = schemas::PermissionLevel::USER; + const auto &badges = irc_message.sender.badges; + + if (user.get_alias_id() == channel.get_alias_id()) { + level = schemas::PermissionLevel::BROADCASTER; + } else if (std::any_of(badges.begin(), badges.end(), [&](const auto &x) { + return x.first == "moderator"; + })) { + level = schemas::PermissionLevel::MODERATOR; + } else if (std::any_of(badges.begin(), badges.end(), + [&](const auto &x) { return x.first == "vip"; })) { + level = schemas::PermissionLevel::VIP; + } + + query = work->exec("SELECT * FROM user_rights WHERE user_id = " + + std::to_string(user.get_id()) + + " AND channel_id = " + std::to_string(channel.get_id())); + + if (query.empty()) { + work->exec( + "INSERT INTO user_rights (user_id, channel_id, level) VALUES (" + + std::to_string(user.get_id()) + ", " + + std::to_string(channel.get_id()) + ", " + std::to_string(level) + + ")"); + + work->commit(); + + delete work; + work = new pqxx::work(conn); + + query = work->exec("SELECT * FROM user_rights WHERE user_id = " + + std::to_string(user.get_id()) + " AND channel_id = " + + std::to_string(channel.get_id())); + } + + schemas::UserRights user_rights(query[0]); + + if (user_rights.get_level() != level) { + work->exec("UPDATE user_rights SET level = " + std::to_string(level) + + " WHERE id = " + std::to_string(query[0][0].as<int>())); + + work->commit(); + + user_rights.set_level(level); + } + + delete work; + + if (parts.empty()) { + Request req{command_id, std::nullopt, std::nullopt, + irc_message, channel, channel_preferences, + user, user_rights, conn}; + + return req; + } + + std::optional<std::string> subcommand_id = parts[0]; + auto subcommand_ids = (*cmd)->get_subcommand_ids(); + + if (std::any_of( + subcommand_ids.begin(), subcommand_ids.end(), + [&](const auto &x) { return x == subcommand_id.value(); })) { + parts.erase(parts.begin()); + } else { + subcommand_id = std::nullopt; + } + + std::optional<std::string> message = utils::string::join_vector(parts, ' '); + + if (message->empty()) { + message = std::nullopt; + } + + Request req{command_id, subcommand_id, message, + irc_message, channel, channel_preferences, + user, user_rights, conn}; + return req; + } +} diff --git a/bot/src/commands/request_util.hpp b/bot/src/commands/request_util.hpp new file mode 100644 index 0000000..dea6e12 --- /dev/null +++ b/bot/src/commands/request_util.hpp @@ -0,0 +1,13 @@ +#include <optional> +#include <pqxx/pqxx> + +#include "../irc/message.hpp" +#include "command.hpp" +#include "request.hpp" + +namespace bot::command { + std::optional<Request> generate_request( + const command::CommandLoader &command_loader, + const irc::Message<irc::MessageType::Privmsg> &irc_message, + pqxx::connection &conn); +} diff --git a/bot/src/commands/response_error.hpp b/bot/src/commands/response_error.hpp new file mode 100644 index 0000000..ae2c3ee --- /dev/null +++ b/bot/src/commands/response_error.hpp @@ -0,0 +1,222 @@ +#pragma once + +#include <exception> +#include <optional> +#include <string> +#include <type_traits> +#include <vector> + +#include "command.hpp" +#include "request.hpp" + +namespace bot { + enum ResponseError { + NOT_ENOUGH_ARGUMENTS, + INCORRECT_ARGUMENT, + + INCOMPATIBLE_NAME, + NAMESAKE_CREATION, + NOT_FOUND, + + SOMETHING_WENT_WRONG, + + EXTERNAL_API_ERROR, + INSUFFICIENT_RIGHTS, + + ILLEGAL_COMMAND + }; + + template <ResponseError T, class Enable = void> + class ResponseException; + + template <ResponseError T> + class ResponseException< + T, typename std::enable_if< + T == INCORRECT_ARGUMENT || T == INCOMPATIBLE_NAME || + T == NAMESAKE_CREATION || T == NOT_FOUND>::type> + : public std::exception { + public: + ResponseException(const command::Request &request, + const loc::Localization &localizator, + const std::string &message) + : request(request), + localizator(localizator), + message(message), + error(T) { + loc::LineId line_id; + + switch (this->error) { + case INCORRECT_ARGUMENT: + line_id = loc::LineId::ErrorIncorrectArgument; + break; + case INCOMPATIBLE_NAME: + line_id = loc::LineId::ErrorIncompatibleName; + break; + case NAMESAKE_CREATION: + line_id = loc::LineId::ErrorNamesakeCreation; + break; + case NOT_FOUND: + line_id = loc::LineId::ErrorNotFound; + break; + default: + line_id = loc::LineId::ErrorSomethingWentWrong; + break; + }; + + this->line = + this->localizator + .get_formatted_line(this->request, line_id, {this->message}) + .value(); + } + ~ResponseException() = default; + + const char *what() const noexcept override { return this->line.c_str(); } + + private: + const command::Request &request; + const loc::Localization &localizator; + std::string message, line; + ResponseError error; + }; + + template <ResponseError T> + class ResponseException<T, + typename std::enable_if<T == SOMETHING_WENT_WRONG || + T == INSUFFICIENT_RIGHTS || + T == ILLEGAL_COMMAND>::type> + : public std::exception { + public: + ResponseException(const command::Request &request, + const loc::Localization &localizator) + : request(request), localizator(localizator), error(T) { + loc::LineId line_id; + + switch (this->error) { + case INSUFFICIENT_RIGHTS: + line_id = loc::LineId::ErrorInsufficientRights; + break; + case ILLEGAL_COMMAND: + line_id = loc::LineId::ErrorIllegalCommand; + break; + default: + line_id = loc::LineId::ErrorSomethingWentWrong; + break; + } + + this->line = + this->localizator.get_formatted_line(this->request, line_id, {}) + .value(); + } + ~ResponseException() = default; + + const char *what() const noexcept override { return this->line.c_str(); } + + private: + const command::Request &request; + const loc::Localization &localizator; + std::string line; + ResponseError error; + }; + + template <ResponseError T> + class ResponseException< + T, typename std::enable_if<T == EXTERNAL_API_ERROR>::type> + : public std::exception { + public: + ResponseException( + const command::Request &request, const loc::Localization &localizator, + const int &code, + const std::optional<std::string> &message = std::nullopt) + : request(request), + localizator(localizator), + code(code), + message(message), + error(T) { + loc::LineId line_id = loc::LineId::ErrorExternalAPIError; + std::vector<std::string> args = {std::to_string(this->code)}; + + if (this->message.has_value()) { + args.push_back(" " + this->message.value()); + } + + this->line = + this->localizator.get_formatted_line(this->request, line_id, args) + .value(); + } + ~ResponseException() = default; + + const char *what() const noexcept override { return this->line.c_str(); } + + private: + const command::Request &request; + const loc::Localization &localizator; + int code; + std::optional<std::string> message; + std::string line; + ResponseError error; + }; + + template <ResponseError T> + class ResponseException< + T, typename std::enable_if<T == NOT_ENOUGH_ARGUMENTS>::type> + : public std::exception { + public: + ResponseException(const command::Request &request, + const loc::Localization &localizator, + command::CommandArgument argument) + : request(request), + localizator(localizator), + argument(argument), + error(T) { + loc::LineId line_id = loc::LineId::ErrorNotEnoughArguments; + loc::LineId arg_id; + + switch (this->argument) { + case command::SUBCOMMAND: + arg_id = loc::LineId::ArgumentSubcommand; + break; + case command::MESSAGE: + arg_id = loc::LineId::ArgumentMessage; + break; + case command::INTERVAL: + arg_id = loc::LineId::ArgumentInterval; + break; + case command::NAME: + arg_id = loc::LineId::ArgumentName; + break; + case command::TARGET: + arg_id = loc::LineId::ArgumentTarget; + break; + case command::VALUE: + arg_id = loc::LineId::ArgumentValue; + break; + case command::AMOUNT: + arg_id = loc::LineId::ArgumentAmount; + break; + default: + break; + } + + auto arg = + this->localizator + .get_localized_line( + this->request.channel_preferences.get_locale(), arg_id) + .value(); + + this->line = + this->localizator.get_formatted_line(this->request, line_id, {arg}) + .value(); + } + ~ResponseException() = default; + + const char *what() const noexcept override { return this->line.c_str(); } + + private: + const command::Request &request; + const loc::Localization &localizator; + command::CommandArgument argument; + ResponseError error; + std::string line; + }; + +} |
