summaryrefslogtreecommitdiff
path: root/bot/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'bot/src/commands')
-rw-r--r--bot/src/commands/command.cpp104
-rw-r--r--bot/src/commands/command.hpp55
-rw-r--r--bot/src/commands/request.hpp25
-rw-r--r--bot/src/commands/request_util.cpp205
-rw-r--r--bot/src/commands/request_util.hpp13
-rw-r--r--bot/src/commands/response_error.hpp222
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;
+ };
+
+}