From d1793df1eda463b10107d41785ad1d7f055ed476 Mon Sep 17 00:00:00 2001 From: ilotterytea Date: Sat, 18 May 2024 14:48:12 +0500 Subject: upd: moved the bot part to a relative subfolder --- CMakeLists.txt | 83 +++++++------ bot/src/api/twitch/helix_client.cpp | 144 ++++++++++++++++++++++ bot/src/api/twitch/helix_client.hpp | 31 +++++ bot/src/api/twitch/schemas/stream.hpp | 35 ++++++ bot/src/api/twitch/schemas/user.hpp | 10 ++ bot/src/bundle.hpp | 15 +++ bot/src/commands/command.cpp | 104 ++++++++++++++++ bot/src/commands/command.hpp | 55 +++++++++ bot/src/commands/request.hpp | 25 ++++ bot/src/commands/request_util.cpp | 205 +++++++++++++++++++++++++++++++ bot/src/commands/request_util.hpp | 13 ++ bot/src/commands/response_error.hpp | 222 ++++++++++++++++++++++++++++++++++ bot/src/config.cpp | 84 +++++++++++++ bot/src/config.hpp | 54 +++++++++ bot/src/constants.hpp | 13 ++ bot/src/handlers.cpp | 85 +++++++++++++ bot/src/handlers.hpp | 14 +++ bot/src/irc/client.cpp | 155 ++++++++++++++++++++++++ bot/src/irc/client.hpp | 58 +++++++++ bot/src/irc/message.cpp | 30 +++++ bot/src/irc/message.hpp | 130 ++++++++++++++++++++ bot/src/localization/line_id.cpp | 100 +++++++++++++++ bot/src/localization/line_id.hpp | 56 +++++++++ bot/src/localization/localization.cpp | 132 ++++++++++++++++++++ bot/src/localization/localization.hpp | 37 ++++++ bot/src/logger.cpp | 96 +++++++++++++++ bot/src/logger.hpp | 16 +++ bot/src/main.cpp | 127 +++++++++++++++++++ bot/src/modules/custom_command.hpp | 96 +++++++++++++++ bot/src/modules/event.hpp | 145 ++++++++++++++++++++++ bot/src/modules/help.hpp | 31 +++++ bot/src/modules/join.hpp | 91 ++++++++++++++ bot/src/modules/massping.hpp | 62 ++++++++++ bot/src/modules/notify.hpp | 131 ++++++++++++++++++++ bot/src/modules/ping.hpp | 59 +++++++++ bot/src/modules/timer.hpp | 112 +++++++++++++++++ bot/src/schemas/channel.hpp | 76 ++++++++++++ bot/src/schemas/stream.cpp | 17 +++ bot/src/schemas/stream.hpp | 11 ++ bot/src/schemas/user.hpp | 73 +++++++++++ bot/src/stream.cpp | 200 ++++++++++++++++++++++++++++++ bot/src/stream.hpp | 41 +++++++ bot/src/timer.cpp | 70 +++++++++++ bot/src/timer.hpp | 9 ++ bot/src/utils/chrono.cpp | 48 ++++++++ bot/src/utils/chrono.hpp | 11 ++ bot/src/utils/string.cpp | 66 ++++++++++ bot/src/utils/string.hpp | 32 +++++ src/api/twitch/helix_client.cpp | 144 ---------------------- src/api/twitch/helix_client.hpp | 31 ----- src/api/twitch/schemas/stream.hpp | 35 ------ src/api/twitch/schemas/user.hpp | 10 -- src/bundle.hpp | 15 --- src/commands/command.cpp | 104 ---------------- src/commands/command.hpp | 55 --------- src/commands/request.hpp | 25 ---- src/commands/request_util.cpp | 205 ------------------------------- src/commands/request_util.hpp | 13 -- src/commands/response_error.hpp | 222 ---------------------------------- src/config.cpp | 84 ------------- src/config.hpp | 54 --------- src/constants.hpp | 13 -- src/handlers.cpp | 85 ------------- src/handlers.hpp | 14 --- src/irc/client.cpp | 155 ------------------------ src/irc/client.hpp | 58 --------- src/irc/message.cpp | 30 ----- src/irc/message.hpp | 130 -------------------- src/localization/line_id.cpp | 100 --------------- src/localization/line_id.hpp | 56 --------- src/localization/localization.cpp | 132 -------------------- src/localization/localization.hpp | 37 ------ src/logger.cpp | 96 --------------- src/logger.hpp | 16 --- src/main.cpp | 127 ------------------- src/modules/custom_command.hpp | 96 --------------- src/modules/event.hpp | 145 ---------------------- src/modules/help.hpp | 31 ----- src/modules/join.hpp | 91 -------------- src/modules/massping.hpp | 62 ---------- src/modules/notify.hpp | 131 -------------------- src/modules/ping.hpp | 59 --------- src/modules/timer.hpp | 112 ----------------- src/schemas/channel.hpp | 76 ------------ src/schemas/stream.cpp | 17 --- src/schemas/stream.hpp | 11 -- src/schemas/user.hpp | 73 ----------- src/stream.cpp | 200 ------------------------------ src/stream.hpp | 41 ------- src/timer.cpp | 70 ----------- src/timer.hpp | 9 -- src/utils/chrono.cpp | 48 -------- src/utils/chrono.hpp | 11 -- src/utils/string.cpp | 66 ---------- src/utils/string.hpp | 32 ----- 95 files changed, 3470 insertions(+), 3467 deletions(-) create mode 100644 bot/src/api/twitch/helix_client.cpp create mode 100644 bot/src/api/twitch/helix_client.hpp create mode 100644 bot/src/api/twitch/schemas/stream.hpp create mode 100644 bot/src/api/twitch/schemas/user.hpp create mode 100644 bot/src/bundle.hpp create mode 100644 bot/src/commands/command.cpp create mode 100644 bot/src/commands/command.hpp create mode 100644 bot/src/commands/request.hpp create mode 100644 bot/src/commands/request_util.cpp create mode 100644 bot/src/commands/request_util.hpp create mode 100644 bot/src/commands/response_error.hpp create mode 100644 bot/src/config.cpp create mode 100644 bot/src/config.hpp create mode 100644 bot/src/constants.hpp create mode 100644 bot/src/handlers.cpp create mode 100644 bot/src/handlers.hpp create mode 100644 bot/src/irc/client.cpp create mode 100644 bot/src/irc/client.hpp create mode 100644 bot/src/irc/message.cpp create mode 100644 bot/src/irc/message.hpp create mode 100644 bot/src/localization/line_id.cpp create mode 100644 bot/src/localization/line_id.hpp create mode 100644 bot/src/localization/localization.cpp create mode 100644 bot/src/localization/localization.hpp create mode 100644 bot/src/logger.cpp create mode 100644 bot/src/logger.hpp create mode 100644 bot/src/main.cpp create mode 100644 bot/src/modules/custom_command.hpp create mode 100644 bot/src/modules/event.hpp create mode 100644 bot/src/modules/help.hpp create mode 100644 bot/src/modules/join.hpp create mode 100644 bot/src/modules/massping.hpp create mode 100644 bot/src/modules/notify.hpp create mode 100644 bot/src/modules/ping.hpp create mode 100644 bot/src/modules/timer.hpp create mode 100644 bot/src/schemas/channel.hpp create mode 100644 bot/src/schemas/stream.cpp create mode 100644 bot/src/schemas/stream.hpp create mode 100644 bot/src/schemas/user.hpp create mode 100644 bot/src/stream.cpp create mode 100644 bot/src/stream.hpp create mode 100644 bot/src/timer.cpp create mode 100644 bot/src/timer.hpp create mode 100644 bot/src/utils/chrono.cpp create mode 100644 bot/src/utils/chrono.hpp create mode 100644 bot/src/utils/string.cpp create mode 100644 bot/src/utils/string.hpp delete mode 100644 src/api/twitch/helix_client.cpp delete mode 100644 src/api/twitch/helix_client.hpp delete mode 100644 src/api/twitch/schemas/stream.hpp delete mode 100644 src/api/twitch/schemas/user.hpp delete mode 100644 src/bundle.hpp delete mode 100644 src/commands/command.cpp delete mode 100644 src/commands/command.hpp delete mode 100644 src/commands/request.hpp delete mode 100644 src/commands/request_util.cpp delete mode 100644 src/commands/request_util.hpp delete mode 100644 src/commands/response_error.hpp delete mode 100644 src/config.cpp delete mode 100644 src/config.hpp delete mode 100644 src/constants.hpp delete mode 100644 src/handlers.cpp delete mode 100644 src/handlers.hpp delete mode 100644 src/irc/client.cpp delete mode 100644 src/irc/client.hpp delete mode 100644 src/irc/message.cpp delete mode 100644 src/irc/message.hpp delete mode 100644 src/localization/line_id.cpp delete mode 100644 src/localization/line_id.hpp delete mode 100644 src/localization/localization.cpp delete mode 100644 src/localization/localization.hpp delete mode 100644 src/logger.cpp delete mode 100644 src/logger.hpp delete mode 100644 src/main.cpp delete mode 100644 src/modules/custom_command.hpp delete mode 100644 src/modules/event.hpp delete mode 100644 src/modules/help.hpp delete mode 100644 src/modules/join.hpp delete mode 100644 src/modules/massping.hpp delete mode 100644 src/modules/notify.hpp delete mode 100644 src/modules/ping.hpp delete mode 100644 src/modules/timer.hpp delete mode 100644 src/schemas/channel.hpp delete mode 100644 src/schemas/stream.cpp delete mode 100644 src/schemas/stream.hpp delete mode 100644 src/schemas/user.hpp delete mode 100644 src/stream.cpp delete mode 100644 src/stream.hpp delete mode 100644 src/timer.cpp delete mode 100644 src/timer.hpp delete mode 100644 src/utils/chrono.cpp delete mode 100644 src/utils/chrono.hpp delete mode 100644 src/utils/string.cpp delete mode 100644 src/utils/string.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c4146b6..1fef134 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required(VERSION 3.10) +include(FetchContent) project( RedpilledBot @@ -13,53 +14,55 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_definitions(-DUSE_TLS=1) -add_executable(Bot) +file(GLOB_RECURSE BOT_SRC_FILES "bot/src/*.cpp" "bot/src/*.h" "bot/src/*.hpp") -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_definitions(Bot PRIVATE DEBUG_MODE) -endif() +option(BUILD_BOT "Build the bot" ON) -set_target_properties( - Bot PROPERTIES - DESCRIPTION ${PROJECT_DESCRIPTION} - OUTPUT_NAME "redpilledbot" -) +if (BUILD_BOT) + add_executable(Bot) -file(GLOB_RECURSE SRC_FILES "src/*.cpp" "src/*.h" "src/*.hpp") + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions(Bot PRIVATE DEBUG_MODE) + endif() -target_sources(Bot PRIVATE ${SRC_FILES}) + set_target_properties( + Bot PROPERTIES + DESCRIPTION ${PROJECT_DESCRIPTION} + OUTPUT_NAME "redpilledbot" + ) -# Getting libraries -include(FetchContent) + target_sources(Bot PRIVATE ${BOT_SRC_FILES}) -# json -FetchContent_Declare( - json - URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz -) -FetchContent_MakeAvailable(json) + # Getting libraries + # json + FetchContent_Declare( + json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + ) + FetchContent_MakeAvailable(json) -# http request maker -FetchContent_Declare( - cpr - GIT_REPOSITORY https://github.com/libcpr/cpr.git - GIT_TAG 1.10.5 -) -FetchContent_MakeAvailable(cpr) + # http request maker + FetchContent_Declare( + cpr + GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG 1.10.5 + ) + FetchContent_MakeAvailable(cpr) -# postgresql -FetchContent_Declare( - pqxx - GIT_REPOSITORY https://github.com/jtv/libpqxx.git - GIT_TAG 7.9.0 -) -FetchContent_MakeAvailable(pqxx) + # postgresql + FetchContent_Declare( + pqxx + GIT_REPOSITORY https://github.com/jtv/libpqxx.git + GIT_TAG 7.9.0 + ) + FetchContent_MakeAvailable(pqxx) -FetchContent_Declare( - ixwebsocket - GIT_REPOSITORY https://github.com/machinezone/IXWebSocket - GIT_TAG v11.4.5 -) -FetchContent_MakeAvailable(ixwebsocket) + FetchContent_Declare( + ixwebsocket + GIT_REPOSITORY https://github.com/machinezone/IXWebSocket + GIT_TAG v11.4.5 + ) + FetchContent_MakeAvailable(ixwebsocket) -target_link_libraries(Bot PRIVATE ixwebsocket::ixwebsocket pqxx nlohmann_json::nlohmann_json cpr::cpr) + target_link_libraries(Bot PRIVATE ixwebsocket::ixwebsocket pqxx nlohmann_json::nlohmann_json cpr::cpr) +endif() diff --git a/bot/src/api/twitch/helix_client.cpp b/bot/src/api/twitch/helix_client.cpp new file mode 100644 index 0000000..04d630b --- /dev/null +++ b/bot/src/api/twitch/helix_client.cpp @@ -0,0 +1,144 @@ +#include "helix_client.hpp" + +#include +#include +#include + +#include "cpr/api.h" +#include "cpr/bearer.h" +#include "cpr/cprtypes.h" +#include "cpr/response.h" +#include "schemas/stream.hpp" +#include "schemas/user.hpp" + +namespace bot::api::twitch { + HelixClient::HelixClient(const std::string &token, + const std::string &client_id) { + this->token = token; + this->client_id = client_id; + } + + std::vector HelixClient::get_users( + const std::vector &logins) const { + std::string s; + + for (auto i = logins.begin(); i != logins.end(); i++) { + std::string start; + if (i == logins.begin()) { + start = "?"; + } else { + start = "&"; + } + + s += start + "login=" + *i; + } + + return this->get_users_by_query(s); + } + + std::vector HelixClient::get_users( + const std::vector &ids) const { + std::string s; + + for (auto i = ids.begin(); i != ids.end(); i++) { + std::string start; + if (i == ids.begin()) { + start = "?"; + } else { + start = "&"; + } + + s += start + "id=" + std::to_string(*i); + } + + return this->get_users_by_query(s); + } + + std::vector HelixClient::get_users_by_query( + const std::string &query) const { + cpr::Response response = cpr::Get( + cpr::Url{this->base_url + "/users" + query}, cpr::Bearer{this->token}, + cpr::Header{{"Client-Id", this->client_id.c_str()}}); + + if (response.status_code != 200) { + return {}; + } + + std::vector users; + + nlohmann::json j = nlohmann::json::parse(response.text); + + for (const auto &d : j["data"]) { + schemas::User u{std::stoi(d["id"].get()), d["login"]}; + + users.push_back(u); + } + + return users; + } + + std::vector HelixClient::get_chatters( + const int &broadcaster_id, const int &moderator_id) const { + cpr::Response response = + cpr::Get(cpr::Url{this->base_url + "/chat/chatters?broadcaster_id=" + + std::to_string(broadcaster_id) + + "&moderator_id=" + std::to_string(moderator_id)}, + cpr::Bearer{this->token}, + cpr::Header{{"Client-Id", this->client_id.c_str()}}); + + if (response.status_code != 200) { + return {}; + } + + std::vector users; + + nlohmann::json j = nlohmann::json::parse(response.text); + + for (const auto &d : j["data"]) { + schemas::User u{std::stoi(d["user_id"].get()), + d["user_login"]}; + + users.push_back(u); + } + + return users; + } + + std::vector HelixClient::get_streams( + const std::vector &ids) const { + std::string s; + + for (auto i = ids.begin(); i != ids.end(); i++) { + std::string start; + if (i == ids.begin()) { + start = "?"; + } else { + start = "&"; + } + + s += start + "user_id=" + std::to_string(*i); + } + + cpr::Response response = cpr::Get( + cpr::Url{this->base_url + "/streams" + s}, cpr::Bearer{this->token}, + cpr::Header{{"Client-Id", this->client_id.c_str()}}); + + if (response.status_code != 200) { + return {}; + } + + std::vector streams; + + nlohmann::json j = nlohmann::json::parse(response.text); + + for (const auto &d : j["data"]) { + schemas::Stream u{std::stoi(d["user_id"].get()), + d["user_login"], d["game_name"], d["title"], + d["started_at"]}; + + streams.push_back(u); + } + + return streams; + } +} diff --git a/bot/src/api/twitch/helix_client.hpp b/bot/src/api/twitch/helix_client.hpp new file mode 100644 index 0000000..27a9fa3 --- /dev/null +++ b/bot/src/api/twitch/helix_client.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "schemas/stream.hpp" +#include "schemas/user.hpp" + +namespace bot::api::twitch { + class HelixClient { + public: + HelixClient(const std::string &token, const std::string &client_id); + ~HelixClient() = default; + + std::vector get_users( + const std::vector &logins) const; + std::vector get_users(const std::vector &ids) const; + + std::vector get_chatters(const int &broadcaster_id, + const int &moderator_id) const; + + std::vector get_streams( + const std::vector &ids) const; + + private: + std::vector get_users_by_query( + const std::string &query) const; + std::string token, client_id; + const std::string base_url = "https://api.twitch.tv/helix"; + }; +} diff --git a/bot/src/api/twitch/schemas/stream.hpp b/bot/src/api/twitch/schemas/stream.hpp new file mode 100644 index 0000000..e3d485e --- /dev/null +++ b/bot/src/api/twitch/schemas/stream.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "../../../utils/chrono.hpp" + +namespace bot::api::twitch::schemas { + class Stream { + public: + Stream(int user_id, std::string user_login, std::string game_name, + std::string title, std::string started_at) + : user_id(user_id), + user_login(user_login), + game_name(game_name), + title(title), + started_at(utils::chrono::string_to_time_point( + started_at, "%Y-%m-%dT%H:%M:%SZ")) {} + + Stream(int user_id) : user_id(user_id) {} + + const int &get_user_id() const { return this->user_id; } + const std::string &get_user_login() const { return this->user_login; } + const std::string &get_game_name() const { return this->game_name; } + const std::string &get_title() const { return this->title; } + const std::chrono::system_clock::time_point &get_started_at() const { + return this->started_at; + } + + private: + int user_id; + std::string user_login, game_name, title; + std::chrono::system_clock::time_point started_at; + }; +} diff --git a/bot/src/api/twitch/schemas/user.hpp b/bot/src/api/twitch/schemas/user.hpp new file mode 100644 index 0000000..288ec72 --- /dev/null +++ b/bot/src/api/twitch/schemas/user.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace bot::api::twitch::schemas { + struct User { + int id; + std::string login; + }; +} diff --git a/bot/src/bundle.hpp b/bot/src/bundle.hpp new file mode 100644 index 0000000..d30f5f8 --- /dev/null +++ b/bot/src/bundle.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "api/twitch/helix_client.hpp" +#include "config.hpp" +#include "irc/client.hpp" +#include "localization/localization.hpp" + +namespace bot { + struct InstanceBundle { + irc::Client &irc_client; + const api::twitch::HelixClient &helix_client; + const bot::loc::Localization &localization; + const Configuration &configuration; + }; +} 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 +#include +#include +#include +#include +#include +#include + +#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()); + this->add_command(std::make_unique()); + this->add_command(std::make_unique()); + this->add_command(std::make_unique()); + this->add_command(std::make_unique()); + this->add_command(std::make_unique()); + this->add_command(std::make_unique()); + this->add_command(std::make_unique()); + } + + void CommandLoader::add_command(std::unique_ptr command) { + this->commands.push_back(std::move(command)); + } + + std::optional, 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()); + + 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( + 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 +#include +#include +#include +#include + +#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::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 get_subcommand_ids() const { + return {}; + } + }; + + class CommandLoader { + public: + CommandLoader(); + ~CommandLoader() = default; + + void add_command(std::unique_ptr cmd); + std::optional, std::string>> run( + const InstanceBundle &bundle, const Request &msg) const; + + const std::vector> &get_commands() const { + return this->commands; + }; + + private: + std::vector> 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 +#include +#include + +#include "../irc/message.hpp" +#include "../schemas/channel.hpp" +#include "../schemas/user.hpp" + +namespace bot::command { + struct Request { + std::string command_id; + std::optional subcommand_id; + std::optional message; + const irc::Message &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 +#include +#include +#include + +#include "../constants.hpp" +#include "../irc/message.hpp" +#include "../schemas/channel.hpp" +#include "command.hpp" +#include "request.hpp" + +namespace bot::command { + std::optional generate_request( + const command::CommandLoader &command_loader, + const irc::Message &irc_message, + pqxx::connection &conn) { + pqxx::work *work; + + work = new pqxx::work(conn); + + std::vector 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())); + + 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 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 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 +#include + +#include "../irc/message.hpp" +#include "command.hpp" +#include "request.hpp" + +namespace bot::command { + std::optional generate_request( + const command::CommandLoader &command_loader, + const irc::Message &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 +#include +#include +#include +#include + +#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 + class ResponseException; + + template + 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 + class ResponseException::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 + class ResponseException< + T, typename std::enable_if::type> + : public std::exception { + public: + ResponseException( + const command::Request &request, const loc::Localization &localizator, + const int &code, + const std::optional &message = std::nullopt) + : request(request), + localizator(localizator), + code(code), + message(message), + error(T) { + loc::LineId line_id = loc::LineId::ErrorExternalAPIError; + std::vector 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 message; + std::string line; + ResponseError error; + }; + + template + class ResponseException< + T, typename std::enable_if::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; + }; + +} diff --git a/bot/src/config.cpp b/bot/src/config.cpp new file mode 100644 index 0000000..ec55913 --- /dev/null +++ b/bot/src/config.cpp @@ -0,0 +1,84 @@ +#include "config.hpp" + +#include +#include +#include +#include +#include + +#include "logger.hpp" + +namespace bot { + std::optional parse_configuration_from_file( + const std::string &file_path) { + std::ifstream ifs(file_path); + + if (!ifs.is_open()) { + log::error("Configuration", "Failed to open the file at " + file_path); + return std::nullopt; + } + + Configuration cfg; + TwitchCredentialsConfiguration ttv_crd_cfg; + DatabaseConfiguration db_cfg; + CommandConfiguration cmd_cfg; + OwnerConfiguration owner_cfg; + UrlConfiguration url_cfg; + + std::string line; + while (std::getline(ifs, line, '\n')) { + std::istringstream iss(line); + std::string key; + std::string value; + + std::getline(iss, key, '='); + std::getline(iss, value); + + for (char &c : key) { + c = tolower(c); + } + + if (key == "twitch_credentials.client_id") { + ttv_crd_cfg.client_id = value; + } else if (key == "twitch_credentials.token") { + ttv_crd_cfg.token = value; + } else if (key == "db_name") { + db_cfg.name = value; + } else if (key == "db_user") { + db_cfg.user = value; + } else if (key == "db_password") { + db_cfg.password = value; + } else if (key == "db_host") { + db_cfg.host = value; + } else if (key == "db_port") { + db_cfg.port = value; + } + + else if (key == "commands.join_allowed") { + cmd_cfg.join_allowed = std::stoi(value); + } else if (key == "commands.join_allow_from_other_chats") { + cmd_cfg.join_allow_from_other_chats = std::stoi(value); + } + + else if (key == "owner.name") { + owner_cfg.name = value; + } else if (key == "owner.id") { + owner_cfg.id = std::stoi(value); + } + + else if (key == "url.help") { + url_cfg.help = value; + } + } + + cfg.url = url_cfg; + cfg.owner = owner_cfg; + cfg.commands = cmd_cfg; + cfg.twitch_credentials = ttv_crd_cfg; + cfg.database = db_cfg; + + log::info("Configuration", + "Successfully loaded the file from '" + file_path + "'"); + return cfg; + } +} diff --git a/bot/src/config.hpp b/bot/src/config.hpp new file mode 100644 index 0000000..5c437d6 --- /dev/null +++ b/bot/src/config.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#define GET_DATABASE_CONNECTION_URL(c) \ + "dbname = " + c.database.name + " user = " + c.database.user + \ + " password = " + c.database.password + " host = " + c.database.host + \ + " port = " + c.database.port + +#define GET_DATABASE_CONNECTION_URL_POINTER(c) \ + "dbname = " + c->database.name + " user = " + c->database.user + \ + " password = " + c->database.password + " host = " + c->database.host + \ + " port = " + c->database.port + +namespace bot { + struct DatabaseConfiguration { + std::string name; + std::string user; + std::string password; + std::string host; + std::string port; + }; + + struct TwitchCredentialsConfiguration { + std::string client_id; + std::string token; + }; + + struct CommandConfiguration { + bool join_allowed = true; + bool join_allow_from_other_chats = false; + }; + + struct OwnerConfiguration { + std::optional name = std::nullopt; + std::optional id = std::nullopt; + }; + + struct UrlConfiguration { + std::optional help = std::nullopt; + }; + + struct Configuration { + TwitchCredentialsConfiguration twitch_credentials; + DatabaseConfiguration database; + CommandConfiguration commands; + OwnerConfiguration owner; + UrlConfiguration url; + }; + + std::optional parse_configuration_from_file( + const std::string &file_path); +} diff --git a/bot/src/constants.hpp b/bot/src/constants.hpp new file mode 100644 index 0000000..3c3462b --- /dev/null +++ b/bot/src/constants.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +#define DEFAULT_LOCALE_ID "english" + +#ifdef DEBUG_MODE +const std::string DEFAULT_PREFIX = "~"; +#else +const std::string DEFAULT_PREFIX = "!"; +#endif +const auto START_TIME = std::chrono::steady_clock::now(); diff --git a/bot/src/handlers.cpp b/bot/src/handlers.cpp new file mode 100644 index 0000000..c7820b4 --- /dev/null +++ b/bot/src/handlers.cpp @@ -0,0 +1,85 @@ +#include "handlers.hpp" + +#include +#include +#include +#include +#include + +#include "bundle.hpp" +#include "commands/command.hpp" +#include "commands/request.hpp" +#include "commands/request_util.hpp" +#include "irc/message.hpp" +#include "localization/line_id.hpp" +#include "logger.hpp" +#include "utils/string.hpp" + +namespace bot::handlers { + void handle_private_message( + const InstanceBundle &bundle, + const command::CommandLoader &command_loader, + const irc::Message &message, + pqxx::connection &conn) { + if (utils::string::string_contains_sql_injection(message.message)) { + log::warn("PrivateMessageHandler", + "Received the message in #" + message.source.login + + " with SQL injection: " + message.message); + return; + } + + std::optional request = + command::generate_request(command_loader, message, conn); + + if (request.has_value()) { + try { + auto response = command_loader.run(bundle, request.value()); + + if (response.has_value()) { + try { + auto str = std::get(*response); + bundle.irc_client.say(message.source.login, str); + } catch (const std::exception &e) { + } + + try { + auto strs = std::get>(*response); + for (const std::string &str : strs) { + bundle.irc_client.say(message.source.login, str); + } + } catch (const std::exception &e) { + } + } + } catch (const std::exception &e) { + std::string line = + bundle.localization + .get_formatted_line(request.value(), loc::LineId::ErrorTemplate, + {e.what()}) + .value(); + + bundle.irc_client.say(message.source.login, line); + } + } + + pqxx::work work(conn); + pqxx::result channels = + work.exec("SELECT id FROM channels WHERE alias_id = " + + std::to_string(message.source.id)); + + if (!channels.empty()) { + int channel_id = channels[0][0].as(); + pqxx::result cmds = + work.exec("SELECT message FROM custom_commands WHERE name = '" + + message.message + "' AND channel_id = '" + + std::to_string(channel_id) + "'"); + + if (!cmds.empty()) { + std::string msg = cmds[0][0].as(); + + bundle.irc_client.say(message.source.login, msg); + } + } + + work.commit(); + } +} diff --git a/bot/src/handlers.hpp b/bot/src/handlers.hpp new file mode 100644 index 0000000..a143f76 --- /dev/null +++ b/bot/src/handlers.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "bundle.hpp" +#include "commands/command.hpp" +#include "irc/message.hpp" +#include "pqxx/pqxx" + +namespace bot::handlers { + void handle_private_message( + const InstanceBundle &bundle, + const command::CommandLoader &command_loader, + const irc::Message &message, + pqxx::connection &conn); +} diff --git a/bot/src/irc/client.cpp b/bot/src/irc/client.cpp new file mode 100644 index 0000000..018736e --- /dev/null +++ b/bot/src/irc/client.cpp @@ -0,0 +1,155 @@ +#include "client.hpp" + +#include +#include + +#include +#include +#include +#include + +#include "../logger.hpp" +#include "cpr/api.h" +#include "cpr/cprtypes.h" +#include "cpr/response.h" +#include "message.hpp" +#include "nlohmann/json.hpp" + +using namespace bot::irc; + +Client::Client(std::string client_id, std::string token) { + this->client_id = client_id; + this->token = token; + + this->host = "wss://irc-ws.chat.twitch.tv"; + this->port = "443"; + + this->websocket.setUrl(this->host + ":" + this->port); + + // getting token owner + cpr::Response response = cpr::Get( + cpr::Url{"https://api.twitch.tv/helix/users"}, cpr::Bearer{this->token}, + cpr::Header{{"Client-Id", this->client_id}}); + + if (response.status_code != 200) { + log::warn("IRC", "Failed to get bot username from Twitch API: " + + std::to_string(response.status_code) + " " + + response.status_line); + } else { + nlohmann::json j = nlohmann::json::parse(response.text); + + auto d = j["data"][0]; + this->id = std::stoi(d["id"].get()); + this->username = d["login"]; + } +} + +void Client::run() { + this->websocket.setOnMessageCallback( + [this](const ix::WebSocketMessagePtr &msg) { + switch (msg->type) { + case ix::WebSocketMessageType::Message: { + log::debug("IRC", "Received message: " + msg->str); + + std::vector lines = + utils::string::split_text(msg->str, '\n'); + + for (std::string &line : lines) { + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { + return c == '\n' || c == '\r' || + c == '\t'; + }), + line.end()); + + std::optional type = define_message_type(line); + + if (!type.has_value()) { + break; + } + + MessageType m_type = type.value(); + + if (m_type == MessageType::Privmsg) { + std::optional> message = + parse_message(line); + + if (message.has_value()) { + this->onPrivmsg(message.value()); + } + } + } + + break; + } + case ix::WebSocketMessageType::Open: { + log::info("IRC", "Connected to Twitch IRC"); + this->is_connected = true; + this->authorize(); + for (const auto &msg : this->pool) { + this->websocket.send(msg); + } + this->pool.clear(); + break; + } + case ix::WebSocketMessageType::Close: { + log::info("IRC", "Twitch IRC connection closed"); + this->is_connected = false; + + for (const auto &x : this->joined_channels) { + this->raw("JOIN #" + x); + } + + break; + } + default: { + break; + } + } + }); + + this->websocket.start(); +} + +void Client::say(const std::string &channel_login, const std::string &message) { + this->raw("PRIVMSG #" + channel_login + " :" + message); + log::debug("IRC", "Sent '" + message + "' in #" + channel_login); +} + +bool Client::join(const std::string &channel_login) { + auto already_joined = + std::any_of(this->joined_channels.begin(), this->joined_channels.end(), + [&](const auto &x) { return x == channel_login; }); + + if (!already_joined) { + this->raw("JOIN #" + channel_login); + this->joined_channels.push_back(channel_login); + log::info("IRC", "Joined #" + channel_login); + } + + return !already_joined; +} + +void Client::raw(const std::string &raw_message) { + std::string msg = raw_message + "\r\n"; + if (this->is_connected) { + this->websocket.send(msg); + } else { + this->pool.push_back(msg); + } +} + +void Client::authorize() { + if (this->username.empty() || this->token.empty()) { + log::error("IRC", "Bot username and token must be set for authorization!"); + return; + } + + log::info("IRC", "Authorizing on Twitch IRC servers..."); + + this->raw("PASS oauth:" + this->token); + this->raw("NICK " + this->username); + this->raw("CAP REQ :twitch.tv/membership"); + this->raw("CAP REQ :twitch.tv/commands"); + this->raw("CAP REQ :twitch.tv/tags"); +} diff --git a/bot/src/irc/client.hpp b/bot/src/irc/client.hpp new file mode 100644 index 0000000..cff867f --- /dev/null +++ b/bot/src/irc/client.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include +#include + +#include "message.hpp" + +namespace bot { + namespace irc { + class Client { + public: + Client(std::string client_id, std::string token); + ~Client() = default; + + void run(); + + void say(const std::string &channel_login, const std::string &message); + bool join(const std::string &channel_login); + void raw(const std::string &raw_message); + + template + void on(typename MessageHandler::fn function) { + switch (T) { + case Privmsg: + this->onPrivmsg = function; + break; + default: + break; + } + } + + const std::string &get_bot_username() const { return this->username; }; + const int &get_bot_id() const { return this->id; } + + private: + void authorize(); + + std::string client_id, token, username; + + std::string host; + std::string port; + + int id; + + ix::WebSocket websocket; + + bool is_connected = false; + std::vector pool; + + std::vector joined_channels; + + // Message handlers + typename MessageHandler::fn onPrivmsg; + }; + } +} diff --git a/bot/src/irc/message.cpp b/bot/src/irc/message.cpp new file mode 100644 index 0000000..569e691 --- /dev/null +++ b/bot/src/irc/message.cpp @@ -0,0 +1,30 @@ +#include "message.hpp" + +#include +#include +#include + +namespace bot { + namespace irc { + std::optional define_message_type(const std::string &msg) { + std::vector parts = utils::string::split_text(msg, ' '); + int i; + + if (msg[0] == '@') { + i = 2; + } else if (msg[0] == ':') { + i = 1; + } else { + return std::nullopt; + } + + if (parts[i] == "NOTICE") { + return MessageType::Notice; + } else if (parts[i] == "PRIVMSG") { + return MessageType::Privmsg; + } + + return std::nullopt; + } + } +} diff --git a/bot/src/irc/message.hpp b/bot/src/irc/message.hpp new file mode 100644 index 0000000..164d7ca --- /dev/null +++ b/bot/src/irc/message.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../utils/string.hpp" + +namespace bot { + namespace irc { + enum MessageType { Privmsg, Notice }; + std::optional define_message_type(const std::string &msg); + + struct MessageSender { + std::string login; + std::string display_name; + int id; + + std::map badges; + + // More fields will be here + }; + + struct MessageSource { + std::string login; + int id; + }; + + template + struct Message; + + template <> + struct Message { + MessageSender sender; + MessageSource source; + std::string message; + }; + + template + std::optional> parse_message(const std::string &msg) { + std::vector parts = utils::string::split_text(msg, ' '); + + if (T == MessageType::Privmsg) { + MessageSender sender; + MessageSource source; + + Message message; + + std::string tags = parts[0]; + tags = tags.substr(1, tags.length()); + parts.erase(parts.begin()); + + std::string user = parts[0]; + user = user.substr(1, user.length()); + + std::vector user_parts = + utils::string::split_text(user, '!'); + + sender.login = user_parts[0]; + + parts.erase(parts.begin(), parts.begin() + 2); + + std::string channel_login = parts[0]; + source.login = channel_login.substr(1, channel_login.length()); + + parts.erase(parts.begin()); + + std::string chat_message = utils::string::join_vector(parts, ' '); + message.message = chat_message.substr(1, chat_message.length()); + + std::vector tags_parts = + utils::string::split_text(tags, ';'); + + for (const std::string &tag : tags_parts) { + std::istringstream iss(tag); + std::string key; + std::string value; + + std::getline(iss, key, '='); + std::getline(iss, value); + + if (key == "display-name") { + sender.display_name = value; + } else if (key == "room-id") { + source.id = std::stoi(value); + } else if (key == "user-id") { + sender.id = std::stoi(value); + } else if (key == "badges") { + std::vector badges = + utils::string::split_text(value, ','); + + std::map map; + + for (const auto &badge : badges) { + std::istringstream iss2(badge); + std::string name; + std::string value; + + std::getline(iss2, name, '/'); + std::getline(iss2, value); + + map.insert({name, std::stoi(value)}); + } + + sender.badges = map; + } + } + + message.sender = sender; + message.source = source; + + return message; + } + + return std::nullopt; + } + + template + struct MessageHandler; + + template <> + struct MessageHandler { + using fn = std::function message)>; + }; + + } +} diff --git a/bot/src/localization/line_id.cpp b/bot/src/localization/line_id.cpp new file mode 100644 index 0000000..567a3ba --- /dev/null +++ b/bot/src/localization/line_id.cpp @@ -0,0 +1,100 @@ +#include "line_id.hpp" + +#include +#include + +namespace bot { + namespace loc { + std::optional string_to_line_id(const std::string &str) { + if (str == "ping.response") { + return LineId::PingResponse; + } + + else if (str == "msg.owner") { + return LineId::MsgOwner; + } + + else if (str == "argument.subcommand") { + return LineId::ArgumentSubcommand; + } else if (str == "argument.message") { + return LineId::ArgumentMessage; + } else if (str == "argument.interval") { + return LineId::ArgumentInterval; + } else if (str == "argument.name") { + return LineId::ArgumentName; + } else if (str == "argument.target") { + return LineId::ArgumentTarget; + } else if (str == "argument.value") { + return LineId::ArgumentValue; + } else if (str == "argument.amount") { + return LineId::ArgumentAmount; + } + + else if (str == "error.template") { + return LineId::ErrorTemplate; + } else if (str == "error.not_enough_arguments") { + return LineId::ErrorNotEnoughArguments; + } else if (str == "error.incorrect_argument") { + return LineId::ErrorIncorrectArgument; + } else if (str == "error.incompatible_name") { + return LineId::ErrorIncompatibleName; + } else if (str == "error.namesake_creation") { + return LineId::ErrorNamesakeCreation; + } else if (str == "error.not_found") { + return LineId::ErrorNotFound; + } else if (str == "error.something_went_wrong") { + return LineId::ErrorSomethingWentWrong; + } else if (str == "error.insufficient_rights") { + return LineId::ErrorInsufficientRights; + } else if (str == "error.illegal_command") { + return LineId::ErrorIllegalCommand; + } + + else if (str == "event.on") { + return LineId::EventOn; + } else if (str == "event.off") { + return LineId::EventOff; + } + + else if (str == "notify.sub") { + return LineId::NotifySub; + } else if (str == "notify.unsub") { + return LineId::NotifyUnsub; + } + + else if (str == "join.response") { + return LineId::JoinResponse; + } else if (str == "join.response_in_chat") { + return LineId::JoinResponseInChat; + } else if (str == "join.already_in") { + return LineId::JoinAlreadyIn; + } else if (str == "join.rejoined") { + return LineId::JoinRejoined; + } else if (str == "join.from_other_chat") { + return LineId::JoinFromOtherChat; + } else if (str == "join.not_allowed") { + return LineId::JoinNotAllowed; + } + + else if (str == "custom_command.new") { + return LineId::CustomcommandNew; + } else if (str == "custom_command.delete") { + return LineId::CustomcommandDelete; + } + + else if (str == "timer.new") { + return LineId::TimerNew; + } else if (str == "timer.delete") { + return LineId::TimerDelete; + } + + else if (str == "help.response") { + return LineId::HelpResponse; + } + + else { + return std::nullopt; + } + } + } +} diff --git a/bot/src/localization/line_id.hpp b/bot/src/localization/line_id.hpp new file mode 100644 index 0000000..41ceec6 --- /dev/null +++ b/bot/src/localization/line_id.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace bot { + namespace loc { + enum LineId { + MsgOwner, + + ArgumentSubcommand, + ArgumentMessage, + ArgumentInterval, + ArgumentName, + ArgumentTarget, + ArgumentValue, + ArgumentAmount, + + ErrorTemplate, + ErrorNotEnoughArguments, + ErrorIncorrectArgument, + ErrorIncompatibleName, + ErrorNamesakeCreation, + ErrorNotFound, + ErrorSomethingWentWrong, + ErrorExternalAPIError, + ErrorInsufficientRights, + ErrorIllegalCommand, + + PingResponse, + + EventOn, + EventOff, + + NotifySub, + NotifyUnsub, + + JoinResponse, + JoinResponseInChat, + JoinAlreadyIn, + JoinRejoined, + JoinFromOtherChat, + JoinNotAllowed, + + CustomcommandNew, + CustomcommandDelete, + + TimerNew, + TimerDelete, + + HelpResponse + }; + + std::optional string_to_line_id(const std::string &str); + } +} diff --git a/bot/src/localization/localization.cpp b/bot/src/localization/localization.cpp new file mode 100644 index 0000000..2742602 --- /dev/null +++ b/bot/src/localization/localization.cpp @@ -0,0 +1,132 @@ +#include "localization.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../utils/string.hpp" +#include "line_id.hpp" + +namespace bot { + namespace loc { + Localization::Localization(const std::string &folder_path) { + for (const auto &entry : + std::filesystem::directory_iterator(folder_path)) { + std::vector file_name_parts = + utils::string::split_text(entry.path(), '/'); + std::string file_name = file_name_parts[file_name_parts.size() - 1]; + file_name = file_name.substr(0, file_name.length() - 5); + + std::unordered_map lines = + this->load_from_file(entry.path()); + + this->localizations[file_name] = lines; + } + } + + std::unordered_map Localization::load_from_file( + const std::string &file_path) { + std::ifstream ifs(file_path); + + std::unordered_map map; + + nlohmann::json json; + ifs >> json; + + for (auto it = json.begin(); it != json.end(); ++it) { + std::optional line_id = string_to_line_id(it.key()); + + if (line_id.has_value()) { + map[line_id.value()] = it.value(); + } + } + + ifs.close(); + return map; + } + + std::optional Localization::get_localized_line( + const std::string &locale_id, const LineId &line_id) const { + auto locale_it = + std::find_if(this->localizations.begin(), this->localizations.end(), + [&](const auto &x) { return x.first == locale_id; }); + + if (locale_it == this->localizations.end()) { + return std::nullopt; + } + + auto line_it = + std::find_if(locale_it->second.begin(), locale_it->second.end(), + [&](const auto &x) { return x.first == line_id; }); + + if (line_it == locale_it->second.end()) { + return std::nullopt; + } + + return line_it->second; + } + + std::optional Localization::get_formatted_line( + const std::string &locale_id, const LineId &line_id, + const std::vector &args) const { + std::optional o_line = + this->get_localized_line(locale_id, line_id); + + if (!o_line.has_value()) { + return std::nullopt; + } + + std::string line = o_line.value(); + + int pos = 0; + int index = 0; + + while ((pos = line.find("%s", pos)) != std::string::npos) { + line.replace(pos, 2, args[index]); + pos += args[index].size(); + ++index; + + if (index >= args.size()) { + break; + } + } + + return line; + } + + std::optional Localization::get_formatted_line( + const command::Request &request, const LineId &line_id, + const std::vector &args) const { + std::optional o_line = this->get_formatted_line( + request.channel_preferences.get_locale(), line_id, args); + + if (!o_line.has_value()) { + return std::nullopt; + } + + std::string line = o_line.value(); + + std::map token_map = { + {"{sender.alias_name}", request.user.get_alias_name()}, + {"{source.alias_name}", request.channel.get_alias_name()}, + {"{default.prefix}", DEFAULT_PREFIX}}; + + for (const auto &pair : token_map) { + int pos = line.find(pair.first); + + while (pos != std::string::npos) { + line.replace(pos, pair.first.length(), pair.second); + pos = line.find(pair.first, pos + pair.second.length()); + } + } + + return line; + } + } +} diff --git a/bot/src/localization/localization.hpp b/bot/src/localization/localization.hpp new file mode 100644 index 0000000..4626c68 --- /dev/null +++ b/bot/src/localization/localization.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +#include "../commands/request.hpp" +#include "line_id.hpp" + +namespace bot { + namespace loc { + class Localization { + public: + Localization(const std::string &folder_path); + ~Localization() = default; + + std::optional get_localized_line( + const std::string &locale_id, const LineId &line_id) const; + + std::optional get_formatted_line( + const std::string &locale_id, const LineId &line_id, + const std::vector &args) const; + + std::optional get_formatted_line( + const command::Request &request, const LineId &line_id, + const std::vector &args) const; + + private: + std::unordered_map load_from_file( + const std::string &file_path); + std::unordered_map> + localizations; + }; + } + +} diff --git a/bot/src/logger.cpp b/bot/src/logger.cpp new file mode 100644 index 0000000..3d142a2 --- /dev/null +++ b/bot/src/logger.cpp @@ -0,0 +1,96 @@ +#include "logger.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace bot::log { + void log(const LogLevel &level, const std::string &source, + const std::string &message) { + std::string dir_name = "logs"; + if (!std::filesystem::exists(dir_name)) { + std::filesystem::create_directory(dir_name); + } + + if (std::filesystem::exists(dir_name) && + !std::filesystem::is_directory(dir_name)) { + throw std::runtime_error("The path '" + dir_name + + "' is not a directory!"); + return; + } + + std::ostringstream line; + + // getting time + std::time_t current_time = std::time(nullptr); + std::tm *local_time = std::localtime(¤t_time); + + line << "[" << std::put_time(local_time, "%H:%M:%S") << "] "; + + std::string level_str; + + switch (level) { + case DEBUG: + level_str = "DEBUG"; + break; + case WARN: + level_str = "WARN"; + break; + case ERROR: + level_str = "ERROR"; + break; + default: + level_str = "INFO"; + break; + } + + line << level_str << " - "; + + line << source << ": " << message << "\n"; + +#ifdef DEBUG_MODE + std::cout << line.str(); +#else + if (level != LogLevel::DEBUG) { + std::cout << line.str(); + } +#endif + + // saving into the log file + std::ostringstream file_name_oss; + file_name_oss << dir_name << "/"; + file_name_oss << "log_"; + file_name_oss << std::put_time(local_time, "%Y-%m-%d"); + file_name_oss << ".log"; + + std::ofstream ofs; + ofs.open(file_name_oss.str(), std::ios::app); + + if (ofs.is_open()) { + ofs << line.str(); + ofs.close(); + } else { + std::cerr << "Failed to write to the log file!\n"; + } + } + + void info(const std::string &source, const std::string &message) { + log(LogLevel::INFO, source, message); + } + + void debug(const std::string &source, const std::string &message) { + log(LogLevel::DEBUG, source, message); + } + + void warn(const std::string &source, const std::string &message) { + log(LogLevel::WARN, source, message); + } + + void error(const std::string &source, const std::string &message) { + log(LogLevel::ERROR, source, message); + } +} diff --git a/bot/src/logger.hpp b/bot/src/logger.hpp new file mode 100644 index 0000000..91b4757 --- /dev/null +++ b/bot/src/logger.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace bot::log { + enum LogLevel { INFO, DEBUG, WARN, ERROR }; + + void log(const LogLevel &level, const std::string &source, + const std::string &message); + + // just shorthands + void info(const std::string &source, const std::string &message); + void debug(const std::string &source, const std::string &message); + void warn(const std::string &source, const std::string &message); + void error(const std::string &source, const std::string &message); +} diff --git a/bot/src/main.cpp b/bot/src/main.cpp new file mode 100644 index 0000000..3c8f5e7 --- /dev/null +++ b/bot/src/main.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include + +#include "api/twitch/helix_client.hpp" +#include "bundle.hpp" +#include "commands/command.hpp" +#include "config.hpp" +#include "handlers.hpp" +#include "irc/client.hpp" +#include "irc/message.hpp" +#include "localization/localization.hpp" +#include "logger.hpp" +#include "stream.hpp" +#include "timer.hpp" + +int main(int argc, char *argv[]) { + bot::log::info("Main", "Starting up..."); + + std::optional o_cfg = + bot::parse_configuration_from_file(".env"); + + if (!o_cfg.has_value()) { + return 1; + } + + bot::Configuration cfg = o_cfg.value(); + + if (cfg.twitch_credentials.client_id.empty() || + cfg.twitch_credentials.token.empty()) { + bot::log::error("Main", + "TWITCH_CREDENTIALS.CLIENT_ID and TWITCH_CREDENTIALS.TOKEN " + "must be set in environmental file!"); + return 1; + } + + if (cfg.database.name.empty() || cfg.database.user.empty() || + cfg.database.password.empty() || cfg.database.host.empty() || + cfg.database.port.empty()) { + bot::log::error("Main", + "DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_PORT " + "must be set in environmental file!"); + return 1; + } + + bot::irc::Client client(cfg.twitch_credentials.client_id, + cfg.twitch_credentials.token); + bot::command::CommandLoader command_loader; + bot::loc::Localization localization("localization"); + bot::api::twitch::HelixClient helix_client(cfg.twitch_credentials.token, + cfg.twitch_credentials.client_id); + + client.join(client.get_bot_username()); + + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(cfg)); + pqxx::work *work = new pqxx::work(conn); + + pqxx::result rows = work->exec( + "SELECT alias_id FROM channels WHERE opted_out_at is null AND alias_id " + "!= " + + std::to_string(client.get_bot_id())); + + std::vector ids; + + for (const auto &row : rows) { + ids.push_back(row[0].as()); + } + + auto helix_channels = helix_client.get_users(ids); + + // it could be optimized + for (const auto &helix_channel : helix_channels) { + auto channel = + work->exec("SELECT id, alias_name FROM channels WHERE alias_id = " + + std::to_string(helix_channel.id)); + + if (!channel.empty()) { + std::string name = channel[0][1].as(); + + if (name != helix_channel.login) { + work->exec("UPDATE channels SET alias_name = '" + helix_channel.login + + "' WHERE id = " + std::to_string(channel[0][0].as())); + work->commit(); + + delete work; + work = new pqxx::work(conn); + } + + client.join(helix_channel.login); + } + } + + work->commit(); + delete work; + + conn.close(); + + bot::stream::StreamListenerClient stream_listener_client(helix_client, client, + cfg); + + client.on( + [&client, &command_loader, &localization, &cfg, &helix_client]( + const bot::irc::Message &message) { + bot::InstanceBundle bundle{client, helix_client, localization, cfg}; + + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(cfg)); + + bot::handlers::handle_private_message(bundle, command_loader, message, + conn); + + conn.close(); + }); + + client.run(); + + std::vector threads; + threads.push_back(std::thread(bot::create_timer_thread, &client, &cfg)); + threads.push_back(std::thread(&bot::stream::StreamListenerClient::run, + &stream_listener_client)); + + for (auto &thread : threads) { + thread.join(); + } + + return 0; +} diff --git a/bot/src/modules/custom_command.hpp b/bot/src/modules/custom_command.hpp new file mode 100644 index 0000000..50b3692 --- /dev/null +++ b/bot/src/modules/custom_command.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../commands/response_error.hpp" + +namespace bot { + namespace mod { + class CustomCommand : public command::Command { + std::string get_name() const override { return "scmd"; } + + schemas::PermissionLevel get_permission_level() const override { + return schemas::PermissionLevel::MODERATOR; + } + + std::vector get_subcommand_ids() const override { + return {"new", "remove"}; + } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + if (!request.subcommand_id.has_value()) { + throw ResponseException( + request, bundle.localization, command::SUBCOMMAND); + } + + const std::string &subcommand_id = request.subcommand_id.value(); + + if (!request.message.has_value()) { + throw ResponseException( + request, bundle.localization, command::CommandArgument::NAME); + } + + const std::string &message = request.message.value(); + std::vector s = utils::string::split_text(message, ' '); + + std::string name = s[0]; + s.erase(s.begin()); + + pqxx::work work(request.conn); + pqxx::result cmds = work.exec( + "SELECT id FROM custom_commands WHERE name = '" + name + + "' AND channel_id = " + std::to_string(request.channel.get_id())); + + if (subcommand_id == "new") { + if (!cmds.empty()) { + throw ResponseException( + request, bundle.localization, name); + } + + if (s.empty()) { + throw ResponseException( + request, bundle.localization, + command::CommandArgument::MESSAGE); + } + + std::string m = utils::string::str(s.begin(), s.end(), ' '); + + work.exec( + "INSERT INTO custom_commands(channel_id, name, message) VALUES " + "(" + + std::to_string(request.channel.get_id()) + ", '" + name + + "', '" + m + "')"); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::CustomcommandNew, + {name}) + .value(); + } else if (subcommand_id == "remove") { + if (cmds.empty()) { + throw ResponseException( + request, bundle.localization, name); + } + + work.exec("DELETE FROM custom_commands WHERE id = " + + std::to_string(cmds[0][0].as())); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::CustomcommandDelete, + {name}) + .value(); + } + + throw ResponseException( + request, bundle.localization); + } + }; + } +} diff --git a/bot/src/modules/event.hpp b/bot/src/modules/event.hpp new file mode 100644 index 0000000..4242f07 --- /dev/null +++ b/bot/src/modules/event.hpp @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../commands/response_error.hpp" +#include "../schemas/stream.hpp" + +namespace bot { + namespace mod { + class Event : public command::Command { + std::string get_name() const override { return "event"; } + + schemas::PermissionLevel get_permission_level() const override { + return schemas::PermissionLevel::MODERATOR; + } + + std::vector get_subcommand_ids() const override { + return {"on", "off"}; + } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + if (!request.subcommand_id.has_value()) { + throw ResponseException( + request, bundle.localization, command::SUBCOMMAND); + } + + const std::string &subcommand_id = request.subcommand_id.value(); + + if (!request.message.has_value()) { + throw ResponseException( + request, bundle.localization, command::CommandArgument::TARGET); + } + + const std::string &message = request.message.value(); + std::vector s = utils::string::split_text(message, ' '); + + std::string target; + schemas::EventType type; + + std::vector target_and_type = + utils::string::split_text(s[0], ':'); + + if (target_and_type.size() != 2) { + throw ResponseException( + request, bundle.localization, s[0]); + } + + s.erase(s.begin()); + + target = target_and_type[0]; + type = schemas::string_to_event_type(target_and_type[1]); + + std::string t = target_and_type[0] + ":" + target_and_type[1]; + + auto channels = bundle.helix_client.get_users({target}); + api::twitch::schemas::User channel; + + if (channels.empty() && type != schemas::EventType::CUSTOM) { + throw ResponseException( + request, bundle.localization, t); + } + + pqxx::work work(request.conn); + std::string query; + + if (type != schemas::CUSTOM) { + channel = channels[0]; + + query = "SELECT id FROM events WHERE channel_id = " + + std::to_string(request.channel.get_id()) + + " AND target_alias_id = " + std::to_string(channel.id) + + " AND event_type = " + std::to_string(type); + } else { + query = "SELECT id FROM events WHERE channel_id = " + + std::to_string(request.channel.get_id()) + + " AND custom_alias_id = '" + target + + "' AND event_type = " + std::to_string(type); + } + + pqxx::result event = work.exec(query); + + if (subcommand_id == "on") { + if (!event.empty()) { + throw ResponseException( + request, bundle.localization, t); + } + + if (s.empty()) { + throw ResponseException( + request, bundle.localization, + command::CommandArgument::MESSAGE); + } + + std::string m = utils::string::str(s.begin(), s.end(), ' '); + + if (type != schemas::CUSTOM) { + query = + "INSERT INTO events (channel_id, target_alias_id, " + "event_type, " + "message) VALUES (" + + std::to_string(request.channel.get_id()) + ", " + + std::to_string(channel.id) + ", " + std::to_string(type) + + ", '" + m + "')"; + } else { + query = + "INSERT INTO events (channel_id, custom_alias_id, " + "event_type, " + "message) VALUES (" + + std::to_string(request.channel.get_id()) + ", '" + target + + "', " + std::to_string(type) + ", '" + m + "')"; + } + + work.exec(query); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::EventOn, {t}) + .value(); + } else if (subcommand_id == "off") { + if (event.empty()) { + throw ResponseException( + request, bundle.localization, t); + } + + work.exec("DELETE FROM events WHERE id = " + + std::to_string(event[0][0].as())); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::EventOff, {t}) + .value(); + } + + throw ResponseException( + request, bundle.localization); + } + }; + } +} diff --git a/bot/src/modules/help.hpp b/bot/src/modules/help.hpp new file mode 100644 index 0000000..13af228 --- /dev/null +++ b/bot/src/modules/help.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../commands/response_error.hpp" + +namespace bot { + namespace mod { + class Help : public command::Command { + std::string get_name() const override { return "help"; } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + if (!bundle.configuration.url.help.has_value()) { + throw ResponseException( + request, bundle.localization); + } + + return bundle.localization + .get_formatted_line(request, loc::LineId::HelpResponse, + {*bundle.configuration.url.help}) + .value(); + } + }; + } +} diff --git a/bot/src/modules/join.hpp b/bot/src/modules/join.hpp new file mode 100644 index 0000000..16e8b4a --- /dev/null +++ b/bot/src/modules/join.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../schemas/channel.hpp" + +namespace bot { + namespace mod { + class Join : public command::Command { + std::string get_name() const override { return "join"; } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + if (!bundle.configuration.commands.join_allowed) { + std::string owner = ""; + + if (bundle.configuration.owner.name.has_value()) { + owner = " " + bundle.localization + .get_formatted_line( + request, loc::LineId::MsgOwner, + {*bundle.configuration.owner.name}) + .value(); + } + + return bundle.localization + .get_formatted_line(request, loc::LineId::JoinNotAllowed, + {owner}) + .value(); + } + + if (!bundle.configuration.commands.join_allow_from_other_chats && + request.channel.get_alias_name() != + bundle.irc_client.get_bot_username()) { + return bundle.localization + .get_formatted_line(request, loc::LineId::JoinFromOtherChat, + {bundle.irc_client.get_bot_username()}) + .value(); + } + + pqxx::work work(request.conn); + + pqxx::result channels = + work.exec("SELECT * FROM channels WHERE alias_id = " + + std::to_string(request.user.get_alias_id())); + + if (!channels.empty()) { + schemas::Channel channel(channels[0]); + + if (channel.get_opted_out_at().has_value()) { + work.exec("UPDATE channels SET opted_out_at = null WHERE id = " + + std::to_string(channel.get_id())); + work.commit(); + + bundle.irc_client.join(channel.get_alias_name()); + + return bundle.localization + .get_formatted_line(request, loc::LineId::JoinRejoined, {}) + .value(); + } + + return bundle.localization + .get_formatted_line(request, loc::LineId::JoinAlreadyIn, {}) + .value(); + } + + work.exec("INSERT INTO channels(alias_id, alias_name) VALUES (" + + std::to_string(request.user.get_alias_id()) + ", '" + + request.user.get_alias_name() + "')"); + work.commit(); + + bundle.irc_client.join(request.user.get_alias_name()); + bundle.irc_client.say( + request.user.get_alias_name(), + bundle.localization + .get_formatted_line(request, loc::LineId::JoinResponseInChat, + {}) + .value()); + + return bundle.localization + .get_formatted_line(request, loc::LineId::JoinResponse, {}) + .value(); + } + }; + } +} diff --git a/bot/src/modules/massping.hpp b/bot/src/modules/massping.hpp new file mode 100644 index 0000000..2957e34 --- /dev/null +++ b/bot/src/modules/massping.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" + +namespace bot { + namespace mod { + class Massping : public command::Command { + std::string get_name() const override { return "massping"; } + + schemas::PermissionLevel get_permission_level() const override { + return schemas::PermissionLevel::MODERATOR; + } + + int get_delay_seconds() const override { return 1; } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + auto chatters = bundle.helix_client.get_chatters( + request.channel.get_alias_id(), bundle.irc_client.get_bot_id()); + + std::string m; + + if (request.message.has_value()) { + m = request.message.value() + " ·"; + } + + std::string base = "📣 " + m + " "; + std::vector msgs = {""}; + int index = 0; + + for (const auto &chatter : chatters) { + const std::string ¤t_msg = msgs.at(index); + std::string x = "@" + chatter.login; + + if (base.length() + current_msg.length() + 1 + x.length() >= 500) { + index += 1; + } + + if (index > msgs.size() - 1) { + msgs.push_back(x); + } else { + msgs[index] = current_msg + " " + x; + } + } + + std::vector msgs2; + + for (const auto &m : msgs) { + msgs2.push_back(base + m); + } + + return msgs2; + } + }; + } +} diff --git a/bot/src/modules/notify.hpp b/bot/src/modules/notify.hpp new file mode 100644 index 0000000..3587e73 --- /dev/null +++ b/bot/src/modules/notify.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../commands/response_error.hpp" +#include "../schemas/stream.hpp" + +namespace bot { + namespace mod { + class Notify : public command::Command { + std::string get_name() const override { return "notify"; } + + std::vector get_subcommand_ids() const override { + return {"sub", "unsub"}; + } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + if (!request.subcommand_id.has_value()) { + throw ResponseException( + request, bundle.localization, command::SUBCOMMAND); + } + + const std::string &subcommand_id = request.subcommand_id.value(); + + if (!request.message.has_value()) { + throw ResponseException( + request, bundle.localization, command::CommandArgument::TARGET); + } + + const std::string &message = request.message.value(); + std::vector s = utils::string::split_text(message, ' '); + + std::string target; + schemas::EventType type; + + std::vector target_and_type = + utils::string::split_text(s[0], ':'); + + if (target_and_type.size() != 2) { + throw ResponseException( + request, bundle.localization, s[0]); + } + + s.erase(s.begin()); + + target = target_and_type[0]; + type = schemas::string_to_event_type(target_and_type[1]); + + std::string t = target_and_type[0] + ":" + target_and_type[1]; + + auto channels = bundle.helix_client.get_users({target}); + api::twitch::schemas::User channel; + + if (channels.empty() && type != schemas::EventType::CUSTOM) { + throw ResponseException( + request, bundle.localization, t); + } + + pqxx::work work(request.conn); + std::string query; + + if (type != schemas::CUSTOM) { + channel = channels[0]; + + query = "SELECT id FROM events WHERE channel_id = " + + std::to_string(request.channel.get_id()) + + " AND target_alias_id = " + std::to_string(channel.id) + + " AND event_type = " + std::to_string(type); + } else { + query = "SELECT id FROM events WHERE channel_id = " + + std::to_string(request.channel.get_id()) + + " AND custom_alias_id = '" + target + + "' AND event_type = " + std::to_string(type); + } + + pqxx::result events = work.exec(query); + + if (events.empty()) { + throw ResponseException( + request, bundle.localization, t); + } + + pqxx::row event = events[0]; + + pqxx::result subs = + work.exec("SELECT id FROM event_subscriptions WHERE event_id = " + + std::to_string(event[0].as()) + " AND user_id = " + + std::to_string(request.user.get_id())); + + if (subcommand_id == "sub") { + if (!subs.empty()) { + throw ResponseException( + request, bundle.localization, t); + } + + work.exec( + "INSERT INTO event_subscriptions(event_id, user_id) VALUES (" + + std::to_string(event[0].as()) + ", " + + std::to_string(request.user.get_id())); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::NotifySub, {t}) + .value(); + } else if (subcommand_id == "unsub") { + if (subs.empty()) { + throw ResponseException( + request, bundle.localization, t); + } + + work.exec("DELETE FROM event_subscriptions WHERE id = " + + std::to_string(subs[0][0].as())); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::NotifyUnsub, {t}) + .value(); + } + + throw ResponseException( + request, bundle.localization); + } + }; + } +} diff --git a/bot/src/modules/ping.hpp b/bot/src/modules/ping.hpp new file mode 100644 index 0000000..836917d --- /dev/null +++ b/bot/src/modules/ping.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../utils/chrono.hpp" + +namespace bot { + namespace mod { + class Ping : public command::Command { + std::string get_name() const override { return "ping"; } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + auto now = std::chrono::steady_clock::now(); + auto duration = now - START_TIME; + auto seconds = + std::chrono::duration_cast(duration); + std::string uptime = utils::chrono::format_timestamp(seconds.count()); + + struct rusage usage; + getrusage(RUSAGE_SELF, &usage); + + int used_memory = usage.ru_maxrss / 1024; + + std::string cpp_info; + +#ifdef __cplusplus + cpp_info.append("C++" + std::to_string(__cplusplus).substr(2, 2)); +#endif + +#ifdef __VERSION__ + cpp_info.append(" (gcc " + + bot::utils::string::split_text(__VERSION__, ' ')[0] + + ")"); +#endif + + if (!cpp_info.empty()) { + cpp_info.append(" · "); + } + + return bundle.localization + .get_formatted_line( + request, loc::LineId::PingResponse, + {cpp_info, uptime, std::to_string(used_memory)}) + .value(); + } + }; + } +} diff --git a/bot/src/modules/timer.hpp b/bot/src/modules/timer.hpp new file mode 100644 index 0000000..36c3982 --- /dev/null +++ b/bot/src/modules/timer.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include + +#include "../bundle.hpp" +#include "../commands/command.hpp" +#include "../commands/response_error.hpp" + +namespace bot { + namespace mod { + class Timer : public command::Command { + std::string get_name() const override { return "timer"; } + + schemas::PermissionLevel get_permission_level() const override { + return schemas::PermissionLevel::MODERATOR; + } + + std::vector get_subcommand_ids() const override { + return {"new", "remove"}; + } + + std::variant, std::string> run( + const InstanceBundle &bundle, + const command::Request &request) const override { + if (!request.subcommand_id.has_value()) { + throw ResponseException( + request, bundle.localization, command::SUBCOMMAND); + } + + const std::string &subcommand_id = request.subcommand_id.value(); + + if (!request.message.has_value()) { + throw ResponseException( + request, bundle.localization, command::CommandArgument::NAME); + } + + const std::string &message = request.message.value(); + std::vector s = utils::string::split_text(message, ' '); + + std::string name = s[0]; + s.erase(s.begin()); + + pqxx::work work(request.conn); + pqxx::result timers = work.exec( + "SELECT id FROM timers WHERE name = '" + name + + "' AND channel_id = " + std::to_string(request.channel.get_id())); + + if (subcommand_id == "new") { + if (!timers.empty()) { + throw ResponseException( + request, bundle.localization, name); + } + + if (s.empty()) { + throw ResponseException( + request, bundle.localization, + command::CommandArgument::INTERVAL); + } + + int interval_s; + + try { + interval_s = std::stoi(s[0]); + } catch (std::exception e) { + throw ResponseException( + request, bundle.localization, s[0]); + } + + s.erase(s.begin()); + + if (s.empty()) { + throw ResponseException( + request, bundle.localization, + command::CommandArgument::MESSAGE); + } + + std::string m = utils::string::str(s.begin(), s.end(), ' '); + + work.exec( + "INSERT INTO timers(channel_id, name, message, interval_sec) " + "VALUES " + "(" + + std::to_string(request.channel.get_id()) + ", '" + name + + "', '" + m + "', " + std::to_string(interval_s) + ")"); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::TimerNew, {name}) + .value(); + } else if (subcommand_id == "remove") { + if (timers.empty()) { + throw ResponseException( + request, bundle.localization, name); + } + + work.exec("DELETE FROM timers WHERE id = " + + std::to_string(timers[0][0].as())); + work.commit(); + + return bundle.localization + .get_formatted_line(request, loc::LineId::TimerDelete, {name}) + .value(); + } + + throw ResponseException( + request, bundle.localization); + } + }; + } +} diff --git a/bot/src/schemas/channel.hpp b/bot/src/schemas/channel.hpp new file mode 100644 index 0000000..2560331 --- /dev/null +++ b/bot/src/schemas/channel.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include + +#include "../constants.hpp" +#include "../utils/chrono.hpp" + +namespace bot::schemas { + class Channel { + public: + Channel(const pqxx::row &row) { + this->id = row[0].as(); + this->alias_id = row[1].as(); + this->alias_name = row[2].as(); + + this->joined_at = + utils::chrono::string_to_time_point(row[3].as()); + + if (!row[4].is_null()) { + this->opted_out_at = + utils::chrono::string_to_time_point(row[4].as()); + } + } + + ~Channel() = default; + + const int &get_id() const { return this->id; } + const int &get_alias_id() const { return this->alias_id; } + const std::string &get_alias_name() const { return this->alias_name; } + const std::chrono::system_clock::time_point &get_joined_at() const { + return this->joined_at; + } + const std::optional & + get_opted_out_at() const { + return this->opted_out_at; + } + + private: + int id, alias_id; + std::string alias_name; + std::chrono::system_clock::time_point joined_at; + std::optional opted_out_at; + }; + + class ChannelPreferences { + public: + ChannelPreferences(const pqxx::row &row) { + this->channel_id = row[0].as(); + + if (!row[2].is_null()) { + this->prefix = row[1].as(); + } else { + this->prefix = DEFAULT_PREFIX; + } + + if (!row[3].is_null()) { + this->locale = row[2].as(); + } else { + this->locale = DEFAULT_LOCALE_ID; + } + } + + ~ChannelPreferences() = default; + + const int &get_channel_id() const { return this->channel_id; } + const std::string &get_prefix() const { return this->prefix; } + const std::string &get_locale() const { return this->locale; } + + private: + int channel_id; + std::string prefix, locale; + }; +} diff --git a/bot/src/schemas/stream.cpp b/bot/src/schemas/stream.cpp new file mode 100644 index 0000000..6ef10dc --- /dev/null +++ b/bot/src/schemas/stream.cpp @@ -0,0 +1,17 @@ +#include "stream.hpp" + +namespace bot::schemas { + EventType string_to_event_type(const std::string &type) { + if (type == "live") { + return EventType::LIVE; + } else if (type == "offline") { + return EventType::OFFLINE; + } else if (type == "title") { + return EventType::TITLE; + } else if (type == "game") { + return EventType::GAME; + } else { + return EventType::CUSTOM; + } + } +} diff --git a/bot/src/schemas/stream.hpp b/bot/src/schemas/stream.hpp new file mode 100644 index 0000000..a636ea5 --- /dev/null +++ b/bot/src/schemas/stream.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace bot::schemas { + enum EventType { LIVE, OFFLINE, TITLE, GAME, CUSTOM = 99 }; + EventType string_to_event_type(const std::string &type); + + enum EventFlag { MASSPING }; + +} diff --git a/bot/src/schemas/user.hpp b/bot/src/schemas/user.hpp new file mode 100644 index 0000000..0bd1368 --- /dev/null +++ b/bot/src/schemas/user.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +#include "../utils/chrono.hpp" + +namespace bot::schemas { + class User { + public: + User(const pqxx::row &row) { + this->id = row[0].as(); + this->alias_id = row[1].as(); + this->alias_name = row[2].as(); + + this->joined_at = + utils::chrono::string_to_time_point(row[3].as()); + + if (!row[4].is_null()) { + this->opted_out_at = + utils::chrono::string_to_time_point(row[4].as()); + } + } + + ~User() = default; + + const int &get_id() const { return this->id; } + const int &get_alias_id() const { return this->alias_id; } + const std::string &get_alias_name() const { return this->alias_name; } + void set_alias_name(const std::string &alias_name) { + this->alias_name = alias_name; + } + const std::chrono::system_clock::time_point &get_joined_at() const { + return this->joined_at; + } + const std::optional & + get_opted_out_at() const { + return this->opted_out_at; + } + + private: + int id, alias_id; + std::string alias_name; + std::chrono::system_clock::time_point joined_at; + std::optional opted_out_at; + }; + + enum PermissionLevel { SUSPENDED, USER, VIP, MODERATOR, BROADCASTER }; + + class UserRights { + public: + UserRights(const pqxx::row &row) { + this->id = row[0].as(); + this->user_id = row[1].as(); + this->channel_id = row[2].as(); + this->level = static_cast(row[3].as()); + } + + ~UserRights() = default; + + const int &get_id() const { return this->id; } + const int &get_user_id() const { return this->user_id; } + const int &get_channel_id() const { return this->channel_id; } + const PermissionLevel &get_level() const { return this->level; } + void set_level(PermissionLevel level) { this->level = level; } + + private: + int id, user_id, channel_id; + PermissionLevel level; + }; +} diff --git a/bot/src/stream.cpp b/bot/src/stream.cpp new file mode 100644 index 0000000..6e48fb8 --- /dev/null +++ b/bot/src/stream.cpp @@ -0,0 +1,200 @@ +#include "stream.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "api/twitch/schemas/stream.hpp" +#include "config.hpp" +#include "logger.hpp" +#include "schemas/stream.hpp" +#include "utils/string.hpp" + +namespace bot::stream { + void StreamListenerClient::listen_channel(const int &id) { + this->ids.push_back(id); + } + void StreamListenerClient::unlisten_channel(const int &id) { + auto x = std::find_if(this->ids.begin(), this->ids.end(), + [&](const auto &x) { return x == id; }); + + if (x != this->ids.end()) { + this->ids.erase(x); + } + + auto y = std::find_if(this->online_ids.begin(), this->online_ids.end(), + [&](const auto &x) { return x == id; }); + + if (y != this->online_ids.end()) { + this->online_ids.erase(y); + } + } + void StreamListenerClient::run() { + while (true) { + this->update_channel_ids(); + this->check(); + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + } + void StreamListenerClient::check() { + auto streams = this->helix_client.get_streams(this->ids); + 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)); + + // adding new ids + for (const auto &stream : streams) { + bool is_already_live = + std::any_of(this->online_ids.begin(), this->online_ids.end(), + [&](const auto &x) { return x == stream.get_user_id(); }); + + if (!is_already_live) { + this->online_ids.insert(stream.get_user_id()); + + auto difference = now - stream.get_started_at(); + auto difference_min = + std::chrono::duration_cast(difference); + + if (difference_min.count() < 1) { + this->handler(schemas::EventType::LIVE, stream); + } + } + } + + // removing old ids + for (auto i = this->online_ids.begin(); i != this->online_ids.end();) { + auto stream = + std::find_if(streams.begin(), streams.end(), + [&](const auto &x) { return x.get_user_id() == *i; }); + + if (stream == streams.end()) { + this->handler(schemas::EventType::OFFLINE, + api::twitch::schemas::Stream{*i}); + i = this->online_ids.erase(i); + } else { + ++i; + } + } + } + void StreamListenerClient::handler( + const schemas::EventType &type, + const api::twitch::schemas::Stream &stream) { + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); + pqxx::work work(conn); + + pqxx::result events = work.exec( + "SELECT id, channel_id, message, flags FROM events WHERE event_type " + "= " + + std::to_string(type) + + " AND target_alias_id = " + std::to_string(stream.get_user_id())); + + for (const auto &event : events) { + pqxx::row channel = work.exec1( + "SELECT alias_id, alias_name, opted_out_at FROM channels WHERE id " + "= " + + std::to_string(event[1].as())); + + if (!channel[2].is_null()) { + continue; + } + + pqxx::result subs = work.exec( + "SELECT user_id FROM event_subscriptions WHERE event_id = " + + std::to_string(event[0].as())); + + std::set user_ids; + if (!subs.empty()) { + for (const auto &sub : subs) { + user_ids.insert(std::to_string(sub[0].as())); + } + + pqxx::result users = work.exec( + "SELECT alias_name FROM users WHERE id IN (" + + utils::string::str(user_ids.begin(), user_ids.end(), ',') + ")"); + + user_ids.clear(); + + for (const auto &user : users) { + user_ids.insert(user[0].as()); + } + } + + auto flags = event[3].as_array(); + std::pair elem; + + do { + elem = flags.get_next(); + if (elem.first == pqxx::array_parser::juncture::string_value) { + if (std::stoi(elem.second) == schemas::EventFlag::MASSPING) { + auto chatters = this->helix_client.get_chatters( + channel[0].as(), this->irc_client.get_bot_id()); + + for (const auto &chatter : chatters) { + user_ids.insert(chatter.login); + } + } + } + } while (elem.first != pqxx::array_parser::juncture::done); + + std::string base = "⚡ " + event[2].as(); + std::vector msgs = {""}; + int index = 0; + + if (!user_ids.empty()) { + base.append(" · "); + } + + for (const auto &user_id : user_ids) { + const std::string ¤t_msg = msgs.at(index); + std::string x = "@" + user_id; + + if (base.length() + current_msg.length() + 1 + x.length() >= 500) { + index += 1; + } + + if (index > msgs.size() - 1) { + msgs.push_back(x); + } else { + msgs[index] = current_msg + " " + x; + } + } + + for (const auto &msg : msgs) { + this->irc_client.say(channel[1].as(), base + msg); + } + } + + work.commit(); + conn.close(); + } + void StreamListenerClient::update_channel_ids() { + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); + pqxx::work work(conn); + + pqxx::result ids = + work.exec("SELECT target_alias_id FROM events WHERE event_type < 99"); + + for (const auto &row : ids) { + int id = row[0].as(); + + if (std::any_of(this->ids.begin(), this->ids.end(), + [&](const auto &x) { return x == id; })) { + continue; + } + + log::info("TwitchStreamListener", + "Listening stream events for ID " + std::to_string(id)); + + this->ids.push_back(id); + } + + work.commit(); + conn.close(); + } +} diff --git a/bot/src/stream.hpp b/bot/src/stream.hpp new file mode 100644 index 0000000..73313ed --- /dev/null +++ b/bot/src/stream.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include "api/twitch/helix_client.hpp" +#include "api/twitch/schemas/stream.hpp" +#include "config.hpp" +#include "irc/client.hpp" +#include "schemas/stream.hpp" + +namespace bot::stream { + class StreamListenerClient { + public: + StreamListenerClient(const api::twitch::HelixClient &helix_client, + irc::Client &irc_client, + const Configuration &configuration) + : helix_client(helix_client), + irc_client(irc_client), + configuration(configuration){}; + ~StreamListenerClient() = default; + + void run(); + void listen_channel(const int &id); + void unlisten_channel(const int &id); + + private: + void check(); + void handler(const schemas::EventType &type, + const api::twitch::schemas::Stream &stream); + void update_channel_ids(); + + const api::twitch::HelixClient &helix_client; + irc::Client &irc_client; + const Configuration &configuration; + + std::vector ids; + + std::set online_ids; + }; +} diff --git a/bot/src/timer.cpp b/bot/src/timer.cpp new file mode 100644 index 0000000..055dde0 --- /dev/null +++ b/bot/src/timer.cpp @@ -0,0 +1,70 @@ +#include "timer.hpp" + +#include +#include +#include +#include + +#include "config.hpp" +#include "irc/client.hpp" +#include "utils/chrono.hpp" + +namespace bot { + void create_timer_thread(irc::Client *irc_client, + Configuration *configuration) { + while (true) { + pqxx::connection conn(GET_DATABASE_CONNECTION_URL_POINTER(configuration)); + pqxx::work *work = new pqxx::work(conn); + + pqxx::result timers = work->exec( + "SELECT id, interval_sec, message, channel_id, last_executed_at FROM " + "timers"); + + for (const auto &timer : timers) { + int id = timer[0].as(); + int interval_sec = timer[1].as(); + std::string message = timer[2].as(); + int channel_id = timer[3].as(); + + // it could be done in sql query + std::chrono::system_clock::time_point last_executed_at = + utils::chrono::string_to_time_point(timer[4].as()); + 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( + now - last_executed_at); + + if (difference.count() > interval_sec) { + pqxx::result channels = work->exec( + "SELECT alias_name, opted_out_at FROM channels WHERE id = " + + std::to_string(channel_id)); + + if (!channels.empty() && channels[0][1].is_null()) { + std::string alias_name = channels[0][0].as(); + + irc_client->say(alias_name, message); + } + + work->exec( + "UPDATE timers SET last_executed_at = timezone('utc', now()) " + "WHERE " + "id = " + + std::to_string(id)); + + work->commit(); + + delete work; + work = new pqxx::work(conn); + } + } + + delete work; + conn.close(); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } +} diff --git a/bot/src/timer.hpp b/bot/src/timer.hpp new file mode 100644 index 0000000..40a52ee --- /dev/null +++ b/bot/src/timer.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "config.hpp" +#include "irc/client.hpp" + +namespace bot { + void create_timer_thread(irc::Client *irc_client, + Configuration *configuration); +} diff --git a/bot/src/utils/chrono.cpp b/bot/src/utils/chrono.cpp new file mode 100644 index 0000000..7a7f2c9 --- /dev/null +++ b/bot/src/utils/chrono.cpp @@ -0,0 +1,48 @@ +#include "chrono.hpp" + +#include +#include +#include +#include +#include +#include + +namespace bot::utils::chrono { + std::string format_timestamp(int seconds) { + int d = round(seconds / (60 * 60 * 24)); + int h = round(seconds / (60 * 60) % 24); + int m = round(seconds % (60 * 60) / 60); + int s = round(seconds % 60); + + // Only seconds: + if (d == 0 && h == 0 && m == 0) { + return std::to_string(s) + "s"; + } + // Minutes and seconds: + else if (d == 0 && h == 0) { + return std::to_string(m) + "m" + std::to_string(s) + "s"; + } + // Hours and minutes: + else if (d == 0) { + return std::to_string(h) + "h" + std::to_string(m) + "m"; + } + // Days and hours: + else { + return std::to_string(d) + "d" + std::to_string(h) + "h"; + } + } + + std::chrono::system_clock::time_point string_to_time_point( + const std::string &value, const std::string &format) { + std::tm tm = {}; + std::stringstream ss(value); + + ss >> std::get_time(&tm, format.c_str()); + + if (ss.fail()) { + throw std::invalid_argument("Invalid time format"); + } + + return std::chrono::system_clock::from_time_t(std::mktime(&tm)); + } +} diff --git a/bot/src/utils/chrono.hpp b/bot/src/utils/chrono.hpp new file mode 100644 index 0000000..7e85e70 --- /dev/null +++ b/bot/src/utils/chrono.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace bot::utils::chrono { + std::string format_timestamp(int seconds); + std::chrono::system_clock::time_point string_to_time_point( + const std::string &value, + const std::string &format = "%Y-%m-%d %H:%M:%S"); +} diff --git a/bot/src/utils/string.cpp b/bot/src/utils/string.cpp new file mode 100644 index 0000000..71c06bf --- /dev/null +++ b/bot/src/utils/string.cpp @@ -0,0 +1,66 @@ +#include "string.hpp" + +#include +#include +#include + +namespace bot { + namespace utils { + namespace string { + std::vector split_text(const std::string &text, + char delimiter) { + std::vector parts; + + std::istringstream iss(text); + std::string part; + + while (std::getline(iss, part, delimiter)) { + parts.push_back(part); + } + + return parts; + } + + std::string join_vector(const std::vector &vec, + char delimiter) { + if (vec.empty()) { + return ""; + } + + std::string str; + + for (auto i = vec.begin(); i != vec.end() - 1; i++) { + str += *i + delimiter; + } + + str += vec[vec.size() - 1]; + + return str; + } + + std::string join_vector(const std::vector &vec) { + std::string str; + + for (const auto &e : vec) { + str += e; + } + + return str; + } + + bool string_contains_sql_injection(const std::string &input) { + std::string forbidden_strings[] = {";", "--", "'", "\"", + "/*", "*/", "xp_", "exec", + "sp_", "insert", "select", "delete"}; + + for (const auto &str : forbidden_strings) { + if (input.find(str) != std::string::npos) { + return true; + } + } + + return false; + } + } + } +} diff --git a/bot/src/utils/string.hpp b/bot/src/utils/string.hpp new file mode 100644 index 0000000..c8385ad --- /dev/null +++ b/bot/src/utils/string.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace bot { + namespace utils { + namespace string { + std::vector split_text(const std::string &text, + char delimiter); + std::string join_vector(const std::vector &vec, + char delimiter); + std::string join_vector(const std::vector &vec); + + template + std::string str(T begin, T end, char delimiter) { + std::stringstream ss; + bool first = true; + + for (; begin != end; begin++) { + if (!first) ss << delimiter; + ss << *begin; + first = false; + } + return ss.str(); + } + + bool string_contains_sql_injection(const std::string &input); + } + } +} diff --git a/src/api/twitch/helix_client.cpp b/src/api/twitch/helix_client.cpp deleted file mode 100644 index 04d630b..0000000 --- a/src/api/twitch/helix_client.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "helix_client.hpp" - -#include -#include -#include - -#include "cpr/api.h" -#include "cpr/bearer.h" -#include "cpr/cprtypes.h" -#include "cpr/response.h" -#include "schemas/stream.hpp" -#include "schemas/user.hpp" - -namespace bot::api::twitch { - HelixClient::HelixClient(const std::string &token, - const std::string &client_id) { - this->token = token; - this->client_id = client_id; - } - - std::vector HelixClient::get_users( - const std::vector &logins) const { - std::string s; - - for (auto i = logins.begin(); i != logins.end(); i++) { - std::string start; - if (i == logins.begin()) { - start = "?"; - } else { - start = "&"; - } - - s += start + "login=" + *i; - } - - return this->get_users_by_query(s); - } - - std::vector HelixClient::get_users( - const std::vector &ids) const { - std::string s; - - for (auto i = ids.begin(); i != ids.end(); i++) { - std::string start; - if (i == ids.begin()) { - start = "?"; - } else { - start = "&"; - } - - s += start + "id=" + std::to_string(*i); - } - - return this->get_users_by_query(s); - } - - std::vector HelixClient::get_users_by_query( - const std::string &query) const { - cpr::Response response = cpr::Get( - cpr::Url{this->base_url + "/users" + query}, cpr::Bearer{this->token}, - cpr::Header{{"Client-Id", this->client_id.c_str()}}); - - if (response.status_code != 200) { - return {}; - } - - std::vector users; - - nlohmann::json j = nlohmann::json::parse(response.text); - - for (const auto &d : j["data"]) { - schemas::User u{std::stoi(d["id"].get()), d["login"]}; - - users.push_back(u); - } - - return users; - } - - std::vector HelixClient::get_chatters( - const int &broadcaster_id, const int &moderator_id) const { - cpr::Response response = - cpr::Get(cpr::Url{this->base_url + "/chat/chatters?broadcaster_id=" + - std::to_string(broadcaster_id) + - "&moderator_id=" + std::to_string(moderator_id)}, - cpr::Bearer{this->token}, - cpr::Header{{"Client-Id", this->client_id.c_str()}}); - - if (response.status_code != 200) { - return {}; - } - - std::vector users; - - nlohmann::json j = nlohmann::json::parse(response.text); - - for (const auto &d : j["data"]) { - schemas::User u{std::stoi(d["user_id"].get()), - d["user_login"]}; - - users.push_back(u); - } - - return users; - } - - std::vector HelixClient::get_streams( - const std::vector &ids) const { - std::string s; - - for (auto i = ids.begin(); i != ids.end(); i++) { - std::string start; - if (i == ids.begin()) { - start = "?"; - } else { - start = "&"; - } - - s += start + "user_id=" + std::to_string(*i); - } - - cpr::Response response = cpr::Get( - cpr::Url{this->base_url + "/streams" + s}, cpr::Bearer{this->token}, - cpr::Header{{"Client-Id", this->client_id.c_str()}}); - - if (response.status_code != 200) { - return {}; - } - - std::vector streams; - - nlohmann::json j = nlohmann::json::parse(response.text); - - for (const auto &d : j["data"]) { - schemas::Stream u{std::stoi(d["user_id"].get()), - d["user_login"], d["game_name"], d["title"], - d["started_at"]}; - - streams.push_back(u); - } - - return streams; - } -} diff --git a/src/api/twitch/helix_client.hpp b/src/api/twitch/helix_client.hpp deleted file mode 100644 index 27a9fa3..0000000 --- a/src/api/twitch/helix_client.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -#include "schemas/stream.hpp" -#include "schemas/user.hpp" - -namespace bot::api::twitch { - class HelixClient { - public: - HelixClient(const std::string &token, const std::string &client_id); - ~HelixClient() = default; - - std::vector get_users( - const std::vector &logins) const; - std::vector get_users(const std::vector &ids) const; - - std::vector get_chatters(const int &broadcaster_id, - const int &moderator_id) const; - - std::vector get_streams( - const std::vector &ids) const; - - private: - std::vector get_users_by_query( - const std::string &query) const; - std::string token, client_id; - const std::string base_url = "https://api.twitch.tv/helix"; - }; -} diff --git a/src/api/twitch/schemas/stream.hpp b/src/api/twitch/schemas/stream.hpp deleted file mode 100644 index e3d485e..0000000 --- a/src/api/twitch/schemas/stream.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include - -#include "../../../utils/chrono.hpp" - -namespace bot::api::twitch::schemas { - class Stream { - public: - Stream(int user_id, std::string user_login, std::string game_name, - std::string title, std::string started_at) - : user_id(user_id), - user_login(user_login), - game_name(game_name), - title(title), - started_at(utils::chrono::string_to_time_point( - started_at, "%Y-%m-%dT%H:%M:%SZ")) {} - - Stream(int user_id) : user_id(user_id) {} - - const int &get_user_id() const { return this->user_id; } - const std::string &get_user_login() const { return this->user_login; } - const std::string &get_game_name() const { return this->game_name; } - const std::string &get_title() const { return this->title; } - const std::chrono::system_clock::time_point &get_started_at() const { - return this->started_at; - } - - private: - int user_id; - std::string user_login, game_name, title; - std::chrono::system_clock::time_point started_at; - }; -} diff --git a/src/api/twitch/schemas/user.hpp b/src/api/twitch/schemas/user.hpp deleted file mode 100644 index 288ec72..0000000 --- a/src/api/twitch/schemas/user.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -namespace bot::api::twitch::schemas { - struct User { - int id; - std::string login; - }; -} diff --git a/src/bundle.hpp b/src/bundle.hpp deleted file mode 100644 index d30f5f8..0000000 --- a/src/bundle.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "api/twitch/helix_client.hpp" -#include "config.hpp" -#include "irc/client.hpp" -#include "localization/localization.hpp" - -namespace bot { - struct InstanceBundle { - irc::Client &irc_client; - const api::twitch::HelixClient &helix_client; - const bot::loc::Localization &localization; - const Configuration &configuration; - }; -} diff --git a/src/commands/command.cpp b/src/commands/command.cpp deleted file mode 100644 index e3b45b1..0000000 --- a/src/commands/command.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "command.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#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()); - this->add_command(std::make_unique()); - this->add_command(std::make_unique()); - this->add_command(std::make_unique()); - this->add_command(std::make_unique()); - this->add_command(std::make_unique()); - this->add_command(std::make_unique()); - this->add_command(std::make_unique()); - } - - void CommandLoader::add_command(std::unique_ptr command) { - this->commands.push_back(std::move(command)); - } - - std::optional, 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()); - - 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( - 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/src/commands/command.hpp b/src/commands/command.hpp deleted file mode 100644 index 40ec114..0000000 --- a/src/commands/command.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#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::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 get_subcommand_ids() const { - return {}; - } - }; - - class CommandLoader { - public: - CommandLoader(); - ~CommandLoader() = default; - - void add_command(std::unique_ptr cmd); - std::optional, std::string>> run( - const InstanceBundle &bundle, const Request &msg) const; - - const std::vector> &get_commands() const { - return this->commands; - }; - - private: - std::vector> commands; - }; - } -} diff --git a/src/commands/request.hpp b/src/commands/request.hpp deleted file mode 100644 index e2685f1..0000000 --- a/src/commands/request.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../irc/message.hpp" -#include "../schemas/channel.hpp" -#include "../schemas/user.hpp" - -namespace bot::command { - struct Request { - std::string command_id; - std::optional subcommand_id; - std::optional message; - const irc::Message &irc_message; - - schemas::Channel channel; - schemas::ChannelPreferences channel_preferences; - schemas::User user; - schemas::UserRights user_rights; - - pqxx::connection &conn; - }; -} diff --git a/src/commands/request_util.cpp b/src/commands/request_util.cpp deleted file mode 100644 index 90750e5..0000000 --- a/src/commands/request_util.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#include "request_util.hpp" - -#include -#include -#include -#include - -#include "../constants.hpp" -#include "../irc/message.hpp" -#include "../schemas/channel.hpp" -#include "command.hpp" -#include "request.hpp" - -namespace bot::command { - std::optional generate_request( - const command::CommandLoader &command_loader, - const irc::Message &irc_message, - pqxx::connection &conn) { - pqxx::work *work; - - work = new pqxx::work(conn); - - std::vector 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())); - - 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 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 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/src/commands/request_util.hpp b/src/commands/request_util.hpp deleted file mode 100644 index dea6e12..0000000 --- a/src/commands/request_util.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -#include "../irc/message.hpp" -#include "command.hpp" -#include "request.hpp" - -namespace bot::command { - std::optional generate_request( - const command::CommandLoader &command_loader, - const irc::Message &irc_message, - pqxx::connection &conn); -} diff --git a/src/commands/response_error.hpp b/src/commands/response_error.hpp deleted file mode 100644 index ae2c3ee..0000000 --- a/src/commands/response_error.hpp +++ /dev/null @@ -1,222 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#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 - class ResponseException; - - template - 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 - class ResponseException::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 - class ResponseException< - T, typename std::enable_if::type> - : public std::exception { - public: - ResponseException( - const command::Request &request, const loc::Localization &localizator, - const int &code, - const std::optional &message = std::nullopt) - : request(request), - localizator(localizator), - code(code), - message(message), - error(T) { - loc::LineId line_id = loc::LineId::ErrorExternalAPIError; - std::vector 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 message; - std::string line; - ResponseError error; - }; - - template - class ResponseException< - T, typename std::enable_if::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; - }; - -} diff --git a/src/config.cpp b/src/config.cpp deleted file mode 100644 index ec55913..0000000 --- a/src/config.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "config.hpp" - -#include -#include -#include -#include -#include - -#include "logger.hpp" - -namespace bot { - std::optional parse_configuration_from_file( - const std::string &file_path) { - std::ifstream ifs(file_path); - - if (!ifs.is_open()) { - log::error("Configuration", "Failed to open the file at " + file_path); - return std::nullopt; - } - - Configuration cfg; - TwitchCredentialsConfiguration ttv_crd_cfg; - DatabaseConfiguration db_cfg; - CommandConfiguration cmd_cfg; - OwnerConfiguration owner_cfg; - UrlConfiguration url_cfg; - - std::string line; - while (std::getline(ifs, line, '\n')) { - std::istringstream iss(line); - std::string key; - std::string value; - - std::getline(iss, key, '='); - std::getline(iss, value); - - for (char &c : key) { - c = tolower(c); - } - - if (key == "twitch_credentials.client_id") { - ttv_crd_cfg.client_id = value; - } else if (key == "twitch_credentials.token") { - ttv_crd_cfg.token = value; - } else if (key == "db_name") { - db_cfg.name = value; - } else if (key == "db_user") { - db_cfg.user = value; - } else if (key == "db_password") { - db_cfg.password = value; - } else if (key == "db_host") { - db_cfg.host = value; - } else if (key == "db_port") { - db_cfg.port = value; - } - - else if (key == "commands.join_allowed") { - cmd_cfg.join_allowed = std::stoi(value); - } else if (key == "commands.join_allow_from_other_chats") { - cmd_cfg.join_allow_from_other_chats = std::stoi(value); - } - - else if (key == "owner.name") { - owner_cfg.name = value; - } else if (key == "owner.id") { - owner_cfg.id = std::stoi(value); - } - - else if (key == "url.help") { - url_cfg.help = value; - } - } - - cfg.url = url_cfg; - cfg.owner = owner_cfg; - cfg.commands = cmd_cfg; - cfg.twitch_credentials = ttv_crd_cfg; - cfg.database = db_cfg; - - log::info("Configuration", - "Successfully loaded the file from '" + file_path + "'"); - return cfg; - } -} diff --git a/src/config.hpp b/src/config.hpp deleted file mode 100644 index 5c437d6..0000000 --- a/src/config.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include - -#define GET_DATABASE_CONNECTION_URL(c) \ - "dbname = " + c.database.name + " user = " + c.database.user + \ - " password = " + c.database.password + " host = " + c.database.host + \ - " port = " + c.database.port - -#define GET_DATABASE_CONNECTION_URL_POINTER(c) \ - "dbname = " + c->database.name + " user = " + c->database.user + \ - " password = " + c->database.password + " host = " + c->database.host + \ - " port = " + c->database.port - -namespace bot { - struct DatabaseConfiguration { - std::string name; - std::string user; - std::string password; - std::string host; - std::string port; - }; - - struct TwitchCredentialsConfiguration { - std::string client_id; - std::string token; - }; - - struct CommandConfiguration { - bool join_allowed = true; - bool join_allow_from_other_chats = false; - }; - - struct OwnerConfiguration { - std::optional name = std::nullopt; - std::optional id = std::nullopt; - }; - - struct UrlConfiguration { - std::optional help = std::nullopt; - }; - - struct Configuration { - TwitchCredentialsConfiguration twitch_credentials; - DatabaseConfiguration database; - CommandConfiguration commands; - OwnerConfiguration owner; - UrlConfiguration url; - }; - - std::optional parse_configuration_from_file( - const std::string &file_path); -} diff --git a/src/constants.hpp b/src/constants.hpp deleted file mode 100644 index 3c3462b..0000000 --- a/src/constants.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include - -#define DEFAULT_LOCALE_ID "english" - -#ifdef DEBUG_MODE -const std::string DEFAULT_PREFIX = "~"; -#else -const std::string DEFAULT_PREFIX = "!"; -#endif -const auto START_TIME = std::chrono::steady_clock::now(); diff --git a/src/handlers.cpp b/src/handlers.cpp deleted file mode 100644 index c7820b4..0000000 --- a/src/handlers.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "handlers.hpp" - -#include -#include -#include -#include -#include - -#include "bundle.hpp" -#include "commands/command.hpp" -#include "commands/request.hpp" -#include "commands/request_util.hpp" -#include "irc/message.hpp" -#include "localization/line_id.hpp" -#include "logger.hpp" -#include "utils/string.hpp" - -namespace bot::handlers { - void handle_private_message( - const InstanceBundle &bundle, - const command::CommandLoader &command_loader, - const irc::Message &message, - pqxx::connection &conn) { - if (utils::string::string_contains_sql_injection(message.message)) { - log::warn("PrivateMessageHandler", - "Received the message in #" + message.source.login + - " with SQL injection: " + message.message); - return; - } - - std::optional request = - command::generate_request(command_loader, message, conn); - - if (request.has_value()) { - try { - auto response = command_loader.run(bundle, request.value()); - - if (response.has_value()) { - try { - auto str = std::get(*response); - bundle.irc_client.say(message.source.login, str); - } catch (const std::exception &e) { - } - - try { - auto strs = std::get>(*response); - for (const std::string &str : strs) { - bundle.irc_client.say(message.source.login, str); - } - } catch (const std::exception &e) { - } - } - } catch (const std::exception &e) { - std::string line = - bundle.localization - .get_formatted_line(request.value(), loc::LineId::ErrorTemplate, - {e.what()}) - .value(); - - bundle.irc_client.say(message.source.login, line); - } - } - - pqxx::work work(conn); - pqxx::result channels = - work.exec("SELECT id FROM channels WHERE alias_id = " + - std::to_string(message.source.id)); - - if (!channels.empty()) { - int channel_id = channels[0][0].as(); - pqxx::result cmds = - work.exec("SELECT message FROM custom_commands WHERE name = '" + - message.message + "' AND channel_id = '" + - std::to_string(channel_id) + "'"); - - if (!cmds.empty()) { - std::string msg = cmds[0][0].as(); - - bundle.irc_client.say(message.source.login, msg); - } - } - - work.commit(); - } -} diff --git a/src/handlers.hpp b/src/handlers.hpp deleted file mode 100644 index a143f76..0000000 --- a/src/handlers.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "bundle.hpp" -#include "commands/command.hpp" -#include "irc/message.hpp" -#include "pqxx/pqxx" - -namespace bot::handlers { - void handle_private_message( - const InstanceBundle &bundle, - const command::CommandLoader &command_loader, - const irc::Message &message, - pqxx::connection &conn); -} diff --git a/src/irc/client.cpp b/src/irc/client.cpp deleted file mode 100644 index 018736e..0000000 --- a/src/irc/client.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "client.hpp" - -#include -#include - -#include -#include -#include -#include - -#include "../logger.hpp" -#include "cpr/api.h" -#include "cpr/cprtypes.h" -#include "cpr/response.h" -#include "message.hpp" -#include "nlohmann/json.hpp" - -using namespace bot::irc; - -Client::Client(std::string client_id, std::string token) { - this->client_id = client_id; - this->token = token; - - this->host = "wss://irc-ws.chat.twitch.tv"; - this->port = "443"; - - this->websocket.setUrl(this->host + ":" + this->port); - - // getting token owner - cpr::Response response = cpr::Get( - cpr::Url{"https://api.twitch.tv/helix/users"}, cpr::Bearer{this->token}, - cpr::Header{{"Client-Id", this->client_id}}); - - if (response.status_code != 200) { - log::warn("IRC", "Failed to get bot username from Twitch API: " + - std::to_string(response.status_code) + " " + - response.status_line); - } else { - nlohmann::json j = nlohmann::json::parse(response.text); - - auto d = j["data"][0]; - this->id = std::stoi(d["id"].get()); - this->username = d["login"]; - } -} - -void Client::run() { - this->websocket.setOnMessageCallback( - [this](const ix::WebSocketMessagePtr &msg) { - switch (msg->type) { - case ix::WebSocketMessageType::Message: { - log::debug("IRC", "Received message: " + msg->str); - - std::vector lines = - utils::string::split_text(msg->str, '\n'); - - for (std::string &line : lines) { - line.erase(std::remove_if(line.begin(), line.end(), - [](char c) { - return c == '\n' || c == '\r' || - c == '\t'; - }), - line.end()); - - std::optional type = define_message_type(line); - - if (!type.has_value()) { - break; - } - - MessageType m_type = type.value(); - - if (m_type == MessageType::Privmsg) { - std::optional> message = - parse_message(line); - - if (message.has_value()) { - this->onPrivmsg(message.value()); - } - } - } - - break; - } - case ix::WebSocketMessageType::Open: { - log::info("IRC", "Connected to Twitch IRC"); - this->is_connected = true; - this->authorize(); - for (const auto &msg : this->pool) { - this->websocket.send(msg); - } - this->pool.clear(); - break; - } - case ix::WebSocketMessageType::Close: { - log::info("IRC", "Twitch IRC connection closed"); - this->is_connected = false; - - for (const auto &x : this->joined_channels) { - this->raw("JOIN #" + x); - } - - break; - } - default: { - break; - } - } - }); - - this->websocket.start(); -} - -void Client::say(const std::string &channel_login, const std::string &message) { - this->raw("PRIVMSG #" + channel_login + " :" + message); - log::debug("IRC", "Sent '" + message + "' in #" + channel_login); -} - -bool Client::join(const std::string &channel_login) { - auto already_joined = - std::any_of(this->joined_channels.begin(), this->joined_channels.end(), - [&](const auto &x) { return x == channel_login; }); - - if (!already_joined) { - this->raw("JOIN #" + channel_login); - this->joined_channels.push_back(channel_login); - log::info("IRC", "Joined #" + channel_login); - } - - return !already_joined; -} - -void Client::raw(const std::string &raw_message) { - std::string msg = raw_message + "\r\n"; - if (this->is_connected) { - this->websocket.send(msg); - } else { - this->pool.push_back(msg); - } -} - -void Client::authorize() { - if (this->username.empty() || this->token.empty()) { - log::error("IRC", "Bot username and token must be set for authorization!"); - return; - } - - log::info("IRC", "Authorizing on Twitch IRC servers..."); - - this->raw("PASS oauth:" + this->token); - this->raw("NICK " + this->username); - this->raw("CAP REQ :twitch.tv/membership"); - this->raw("CAP REQ :twitch.tv/commands"); - this->raw("CAP REQ :twitch.tv/tags"); -} diff --git a/src/irc/client.hpp b/src/irc/client.hpp deleted file mode 100644 index cff867f..0000000 --- a/src/irc/client.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include "message.hpp" - -namespace bot { - namespace irc { - class Client { - public: - Client(std::string client_id, std::string token); - ~Client() = default; - - void run(); - - void say(const std::string &channel_login, const std::string &message); - bool join(const std::string &channel_login); - void raw(const std::string &raw_message); - - template - void on(typename MessageHandler::fn function) { - switch (T) { - case Privmsg: - this->onPrivmsg = function; - break; - default: - break; - } - } - - const std::string &get_bot_username() const { return this->username; }; - const int &get_bot_id() const { return this->id; } - - private: - void authorize(); - - std::string client_id, token, username; - - std::string host; - std::string port; - - int id; - - ix::WebSocket websocket; - - bool is_connected = false; - std::vector pool; - - std::vector joined_channels; - - // Message handlers - typename MessageHandler::fn onPrivmsg; - }; - } -} diff --git a/src/irc/message.cpp b/src/irc/message.cpp deleted file mode 100644 index 569e691..0000000 --- a/src/irc/message.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "message.hpp" - -#include -#include -#include - -namespace bot { - namespace irc { - std::optional define_message_type(const std::string &msg) { - std::vector parts = utils::string::split_text(msg, ' '); - int i; - - if (msg[0] == '@') { - i = 2; - } else if (msg[0] == ':') { - i = 1; - } else { - return std::nullopt; - } - - if (parts[i] == "NOTICE") { - return MessageType::Notice; - } else if (parts[i] == "PRIVMSG") { - return MessageType::Privmsg; - } - - return std::nullopt; - } - } -} diff --git a/src/irc/message.hpp b/src/irc/message.hpp deleted file mode 100644 index 164d7ca..0000000 --- a/src/irc/message.hpp +++ /dev/null @@ -1,130 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "../utils/string.hpp" - -namespace bot { - namespace irc { - enum MessageType { Privmsg, Notice }; - std::optional define_message_type(const std::string &msg); - - struct MessageSender { - std::string login; - std::string display_name; - int id; - - std::map badges; - - // More fields will be here - }; - - struct MessageSource { - std::string login; - int id; - }; - - template - struct Message; - - template <> - struct Message { - MessageSender sender; - MessageSource source; - std::string message; - }; - - template - std::optional> parse_message(const std::string &msg) { - std::vector parts = utils::string::split_text(msg, ' '); - - if (T == MessageType::Privmsg) { - MessageSender sender; - MessageSource source; - - Message message; - - std::string tags = parts[0]; - tags = tags.substr(1, tags.length()); - parts.erase(parts.begin()); - - std::string user = parts[0]; - user = user.substr(1, user.length()); - - std::vector user_parts = - utils::string::split_text(user, '!'); - - sender.login = user_parts[0]; - - parts.erase(parts.begin(), parts.begin() + 2); - - std::string channel_login = parts[0]; - source.login = channel_login.substr(1, channel_login.length()); - - parts.erase(parts.begin()); - - std::string chat_message = utils::string::join_vector(parts, ' '); - message.message = chat_message.substr(1, chat_message.length()); - - std::vector tags_parts = - utils::string::split_text(tags, ';'); - - for (const std::string &tag : tags_parts) { - std::istringstream iss(tag); - std::string key; - std::string value; - - std::getline(iss, key, '='); - std::getline(iss, value); - - if (key == "display-name") { - sender.display_name = value; - } else if (key == "room-id") { - source.id = std::stoi(value); - } else if (key == "user-id") { - sender.id = std::stoi(value); - } else if (key == "badges") { - std::vector badges = - utils::string::split_text(value, ','); - - std::map map; - - for (const auto &badge : badges) { - std::istringstream iss2(badge); - std::string name; - std::string value; - - std::getline(iss2, name, '/'); - std::getline(iss2, value); - - map.insert({name, std::stoi(value)}); - } - - sender.badges = map; - } - } - - message.sender = sender; - message.source = source; - - return message; - } - - return std::nullopt; - } - - template - struct MessageHandler; - - template <> - struct MessageHandler { - using fn = std::function message)>; - }; - - } -} diff --git a/src/localization/line_id.cpp b/src/localization/line_id.cpp deleted file mode 100644 index 567a3ba..0000000 --- a/src/localization/line_id.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "line_id.hpp" - -#include -#include - -namespace bot { - namespace loc { - std::optional string_to_line_id(const std::string &str) { - if (str == "ping.response") { - return LineId::PingResponse; - } - - else if (str == "msg.owner") { - return LineId::MsgOwner; - } - - else if (str == "argument.subcommand") { - return LineId::ArgumentSubcommand; - } else if (str == "argument.message") { - return LineId::ArgumentMessage; - } else if (str == "argument.interval") { - return LineId::ArgumentInterval; - } else if (str == "argument.name") { - return LineId::ArgumentName; - } else if (str == "argument.target") { - return LineId::ArgumentTarget; - } else if (str == "argument.value") { - return LineId::ArgumentValue; - } else if (str == "argument.amount") { - return LineId::ArgumentAmount; - } - - else if (str == "error.template") { - return LineId::ErrorTemplate; - } else if (str == "error.not_enough_arguments") { - return LineId::ErrorNotEnoughArguments; - } else if (str == "error.incorrect_argument") { - return LineId::ErrorIncorrectArgument; - } else if (str == "error.incompatible_name") { - return LineId::ErrorIncompatibleName; - } else if (str == "error.namesake_creation") { - return LineId::ErrorNamesakeCreation; - } else if (str == "error.not_found") { - return LineId::ErrorNotFound; - } else if (str == "error.something_went_wrong") { - return LineId::ErrorSomethingWentWrong; - } else if (str == "error.insufficient_rights") { - return LineId::ErrorInsufficientRights; - } else if (str == "error.illegal_command") { - return LineId::ErrorIllegalCommand; - } - - else if (str == "event.on") { - return LineId::EventOn; - } else if (str == "event.off") { - return LineId::EventOff; - } - - else if (str == "notify.sub") { - return LineId::NotifySub; - } else if (str == "notify.unsub") { - return LineId::NotifyUnsub; - } - - else if (str == "join.response") { - return LineId::JoinResponse; - } else if (str == "join.response_in_chat") { - return LineId::JoinResponseInChat; - } else if (str == "join.already_in") { - return LineId::JoinAlreadyIn; - } else if (str == "join.rejoined") { - return LineId::JoinRejoined; - } else if (str == "join.from_other_chat") { - return LineId::JoinFromOtherChat; - } else if (str == "join.not_allowed") { - return LineId::JoinNotAllowed; - } - - else if (str == "custom_command.new") { - return LineId::CustomcommandNew; - } else if (str == "custom_command.delete") { - return LineId::CustomcommandDelete; - } - - else if (str == "timer.new") { - return LineId::TimerNew; - } else if (str == "timer.delete") { - return LineId::TimerDelete; - } - - else if (str == "help.response") { - return LineId::HelpResponse; - } - - else { - return std::nullopt; - } - } - } -} diff --git a/src/localization/line_id.hpp b/src/localization/line_id.hpp deleted file mode 100644 index 41ceec6..0000000 --- a/src/localization/line_id.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include -#include - -namespace bot { - namespace loc { - enum LineId { - MsgOwner, - - ArgumentSubcommand, - ArgumentMessage, - ArgumentInterval, - ArgumentName, - ArgumentTarget, - ArgumentValue, - ArgumentAmount, - - ErrorTemplate, - ErrorNotEnoughArguments, - ErrorIncorrectArgument, - ErrorIncompatibleName, - ErrorNamesakeCreation, - ErrorNotFound, - ErrorSomethingWentWrong, - ErrorExternalAPIError, - ErrorInsufficientRights, - ErrorIllegalCommand, - - PingResponse, - - EventOn, - EventOff, - - NotifySub, - NotifyUnsub, - - JoinResponse, - JoinResponseInChat, - JoinAlreadyIn, - JoinRejoined, - JoinFromOtherChat, - JoinNotAllowed, - - CustomcommandNew, - CustomcommandDelete, - - TimerNew, - TimerDelete, - - HelpResponse - }; - - std::optional string_to_line_id(const std::string &str); - } -} diff --git a/src/localization/localization.cpp b/src/localization/localization.cpp deleted file mode 100644 index 2742602..0000000 --- a/src/localization/localization.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "localization.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../utils/string.hpp" -#include "line_id.hpp" - -namespace bot { - namespace loc { - Localization::Localization(const std::string &folder_path) { - for (const auto &entry : - std::filesystem::directory_iterator(folder_path)) { - std::vector file_name_parts = - utils::string::split_text(entry.path(), '/'); - std::string file_name = file_name_parts[file_name_parts.size() - 1]; - file_name = file_name.substr(0, file_name.length() - 5); - - std::unordered_map lines = - this->load_from_file(entry.path()); - - this->localizations[file_name] = lines; - } - } - - std::unordered_map Localization::load_from_file( - const std::string &file_path) { - std::ifstream ifs(file_path); - - std::unordered_map map; - - nlohmann::json json; - ifs >> json; - - for (auto it = json.begin(); it != json.end(); ++it) { - std::optional line_id = string_to_line_id(it.key()); - - if (line_id.has_value()) { - map[line_id.value()] = it.value(); - } - } - - ifs.close(); - return map; - } - - std::optional Localization::get_localized_line( - const std::string &locale_id, const LineId &line_id) const { - auto locale_it = - std::find_if(this->localizations.begin(), this->localizations.end(), - [&](const auto &x) { return x.first == locale_id; }); - - if (locale_it == this->localizations.end()) { - return std::nullopt; - } - - auto line_it = - std::find_if(locale_it->second.begin(), locale_it->second.end(), - [&](const auto &x) { return x.first == line_id; }); - - if (line_it == locale_it->second.end()) { - return std::nullopt; - } - - return line_it->second; - } - - std::optional Localization::get_formatted_line( - const std::string &locale_id, const LineId &line_id, - const std::vector &args) const { - std::optional o_line = - this->get_localized_line(locale_id, line_id); - - if (!o_line.has_value()) { - return std::nullopt; - } - - std::string line = o_line.value(); - - int pos = 0; - int index = 0; - - while ((pos = line.find("%s", pos)) != std::string::npos) { - line.replace(pos, 2, args[index]); - pos += args[index].size(); - ++index; - - if (index >= args.size()) { - break; - } - } - - return line; - } - - std::optional Localization::get_formatted_line( - const command::Request &request, const LineId &line_id, - const std::vector &args) const { - std::optional o_line = this->get_formatted_line( - request.channel_preferences.get_locale(), line_id, args); - - if (!o_line.has_value()) { - return std::nullopt; - } - - std::string line = o_line.value(); - - std::map token_map = { - {"{sender.alias_name}", request.user.get_alias_name()}, - {"{source.alias_name}", request.channel.get_alias_name()}, - {"{default.prefix}", DEFAULT_PREFIX}}; - - for (const auto &pair : token_map) { - int pos = line.find(pair.first); - - while (pos != std::string::npos) { - line.replace(pos, pair.first.length(), pair.second); - pos = line.find(pair.first, pos + pair.second.length()); - } - } - - return line; - } - } -} diff --git a/src/localization/localization.hpp b/src/localization/localization.hpp deleted file mode 100644 index 4626c68..0000000 --- a/src/localization/localization.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "../commands/request.hpp" -#include "line_id.hpp" - -namespace bot { - namespace loc { - class Localization { - public: - Localization(const std::string &folder_path); - ~Localization() = default; - - std::optional get_localized_line( - const std::string &locale_id, const LineId &line_id) const; - - std::optional get_formatted_line( - const std::string &locale_id, const LineId &line_id, - const std::vector &args) const; - - std::optional get_formatted_line( - const command::Request &request, const LineId &line_id, - const std::vector &args) const; - - private: - std::unordered_map load_from_file( - const std::string &file_path); - std::unordered_map> - localizations; - }; - } - -} diff --git a/src/logger.cpp b/src/logger.cpp deleted file mode 100644 index 3d142a2..0000000 --- a/src/logger.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "logger.hpp" - -#include -#include -#include -#include -#include -#include -#include - -namespace bot::log { - void log(const LogLevel &level, const std::string &source, - const std::string &message) { - std::string dir_name = "logs"; - if (!std::filesystem::exists(dir_name)) { - std::filesystem::create_directory(dir_name); - } - - if (std::filesystem::exists(dir_name) && - !std::filesystem::is_directory(dir_name)) { - throw std::runtime_error("The path '" + dir_name + - "' is not a directory!"); - return; - } - - std::ostringstream line; - - // getting time - std::time_t current_time = std::time(nullptr); - std::tm *local_time = std::localtime(¤t_time); - - line << "[" << std::put_time(local_time, "%H:%M:%S") << "] "; - - std::string level_str; - - switch (level) { - case DEBUG: - level_str = "DEBUG"; - break; - case WARN: - level_str = "WARN"; - break; - case ERROR: - level_str = "ERROR"; - break; - default: - level_str = "INFO"; - break; - } - - line << level_str << " - "; - - line << source << ": " << message << "\n"; - -#ifdef DEBUG_MODE - std::cout << line.str(); -#else - if (level != LogLevel::DEBUG) { - std::cout << line.str(); - } -#endif - - // saving into the log file - std::ostringstream file_name_oss; - file_name_oss << dir_name << "/"; - file_name_oss << "log_"; - file_name_oss << std::put_time(local_time, "%Y-%m-%d"); - file_name_oss << ".log"; - - std::ofstream ofs; - ofs.open(file_name_oss.str(), std::ios::app); - - if (ofs.is_open()) { - ofs << line.str(); - ofs.close(); - } else { - std::cerr << "Failed to write to the log file!\n"; - } - } - - void info(const std::string &source, const std::string &message) { - log(LogLevel::INFO, source, message); - } - - void debug(const std::string &source, const std::string &message) { - log(LogLevel::DEBUG, source, message); - } - - void warn(const std::string &source, const std::string &message) { - log(LogLevel::WARN, source, message); - } - - void error(const std::string &source, const std::string &message) { - log(LogLevel::ERROR, source, message); - } -} diff --git a/src/logger.hpp b/src/logger.hpp deleted file mode 100644 index 91b4757..0000000 --- a/src/logger.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -namespace bot::log { - enum LogLevel { INFO, DEBUG, WARN, ERROR }; - - void log(const LogLevel &level, const std::string &source, - const std::string &message); - - // just shorthands - void info(const std::string &source, const std::string &message); - void debug(const std::string &source, const std::string &message); - void warn(const std::string &source, const std::string &message); - void error(const std::string &source, const std::string &message); -} diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 3c8f5e7..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,127 +0,0 @@ -#include -#include -#include -#include - -#include "api/twitch/helix_client.hpp" -#include "bundle.hpp" -#include "commands/command.hpp" -#include "config.hpp" -#include "handlers.hpp" -#include "irc/client.hpp" -#include "irc/message.hpp" -#include "localization/localization.hpp" -#include "logger.hpp" -#include "stream.hpp" -#include "timer.hpp" - -int main(int argc, char *argv[]) { - bot::log::info("Main", "Starting up..."); - - std::optional o_cfg = - bot::parse_configuration_from_file(".env"); - - if (!o_cfg.has_value()) { - return 1; - } - - bot::Configuration cfg = o_cfg.value(); - - if (cfg.twitch_credentials.client_id.empty() || - cfg.twitch_credentials.token.empty()) { - bot::log::error("Main", - "TWITCH_CREDENTIALS.CLIENT_ID and TWITCH_CREDENTIALS.TOKEN " - "must be set in environmental file!"); - return 1; - } - - if (cfg.database.name.empty() || cfg.database.user.empty() || - cfg.database.password.empty() || cfg.database.host.empty() || - cfg.database.port.empty()) { - bot::log::error("Main", - "DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_PORT " - "must be set in environmental file!"); - return 1; - } - - bot::irc::Client client(cfg.twitch_credentials.client_id, - cfg.twitch_credentials.token); - bot::command::CommandLoader command_loader; - bot::loc::Localization localization("localization"); - bot::api::twitch::HelixClient helix_client(cfg.twitch_credentials.token, - cfg.twitch_credentials.client_id); - - client.join(client.get_bot_username()); - - pqxx::connection conn(GET_DATABASE_CONNECTION_URL(cfg)); - pqxx::work *work = new pqxx::work(conn); - - pqxx::result rows = work->exec( - "SELECT alias_id FROM channels WHERE opted_out_at is null AND alias_id " - "!= " + - std::to_string(client.get_bot_id())); - - std::vector ids; - - for (const auto &row : rows) { - ids.push_back(row[0].as()); - } - - auto helix_channels = helix_client.get_users(ids); - - // it could be optimized - for (const auto &helix_channel : helix_channels) { - auto channel = - work->exec("SELECT id, alias_name FROM channels WHERE alias_id = " + - std::to_string(helix_channel.id)); - - if (!channel.empty()) { - std::string name = channel[0][1].as(); - - if (name != helix_channel.login) { - work->exec("UPDATE channels SET alias_name = '" + helix_channel.login + - "' WHERE id = " + std::to_string(channel[0][0].as())); - work->commit(); - - delete work; - work = new pqxx::work(conn); - } - - client.join(helix_channel.login); - } - } - - work->commit(); - delete work; - - conn.close(); - - bot::stream::StreamListenerClient stream_listener_client(helix_client, client, - cfg); - - client.on( - [&client, &command_loader, &localization, &cfg, &helix_client]( - const bot::irc::Message &message) { - bot::InstanceBundle bundle{client, helix_client, localization, cfg}; - - pqxx::connection conn(GET_DATABASE_CONNECTION_URL(cfg)); - - bot::handlers::handle_private_message(bundle, command_loader, message, - conn); - - conn.close(); - }); - - client.run(); - - std::vector threads; - threads.push_back(std::thread(bot::create_timer_thread, &client, &cfg)); - threads.push_back(std::thread(&bot::stream::StreamListenerClient::run, - &stream_listener_client)); - - for (auto &thread : threads) { - thread.join(); - } - - return 0; -} diff --git a/src/modules/custom_command.hpp b/src/modules/custom_command.hpp deleted file mode 100644 index 50b3692..0000000 --- a/src/modules/custom_command.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../commands/response_error.hpp" - -namespace bot { - namespace mod { - class CustomCommand : public command::Command { - std::string get_name() const override { return "scmd"; } - - schemas::PermissionLevel get_permission_level() const override { - return schemas::PermissionLevel::MODERATOR; - } - - std::vector get_subcommand_ids() const override { - return {"new", "remove"}; - } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - if (!request.subcommand_id.has_value()) { - throw ResponseException( - request, bundle.localization, command::SUBCOMMAND); - } - - const std::string &subcommand_id = request.subcommand_id.value(); - - if (!request.message.has_value()) { - throw ResponseException( - request, bundle.localization, command::CommandArgument::NAME); - } - - const std::string &message = request.message.value(); - std::vector s = utils::string::split_text(message, ' '); - - std::string name = s[0]; - s.erase(s.begin()); - - pqxx::work work(request.conn); - pqxx::result cmds = work.exec( - "SELECT id FROM custom_commands WHERE name = '" + name + - "' AND channel_id = " + std::to_string(request.channel.get_id())); - - if (subcommand_id == "new") { - if (!cmds.empty()) { - throw ResponseException( - request, bundle.localization, name); - } - - if (s.empty()) { - throw ResponseException( - request, bundle.localization, - command::CommandArgument::MESSAGE); - } - - std::string m = utils::string::str(s.begin(), s.end(), ' '); - - work.exec( - "INSERT INTO custom_commands(channel_id, name, message) VALUES " - "(" + - std::to_string(request.channel.get_id()) + ", '" + name + - "', '" + m + "')"); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::CustomcommandNew, - {name}) - .value(); - } else if (subcommand_id == "remove") { - if (cmds.empty()) { - throw ResponseException( - request, bundle.localization, name); - } - - work.exec("DELETE FROM custom_commands WHERE id = " + - std::to_string(cmds[0][0].as())); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::CustomcommandDelete, - {name}) - .value(); - } - - throw ResponseException( - request, bundle.localization); - } - }; - } -} diff --git a/src/modules/event.hpp b/src/modules/event.hpp deleted file mode 100644 index 4242f07..0000000 --- a/src/modules/event.hpp +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../commands/response_error.hpp" -#include "../schemas/stream.hpp" - -namespace bot { - namespace mod { - class Event : public command::Command { - std::string get_name() const override { return "event"; } - - schemas::PermissionLevel get_permission_level() const override { - return schemas::PermissionLevel::MODERATOR; - } - - std::vector get_subcommand_ids() const override { - return {"on", "off"}; - } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - if (!request.subcommand_id.has_value()) { - throw ResponseException( - request, bundle.localization, command::SUBCOMMAND); - } - - const std::string &subcommand_id = request.subcommand_id.value(); - - if (!request.message.has_value()) { - throw ResponseException( - request, bundle.localization, command::CommandArgument::TARGET); - } - - const std::string &message = request.message.value(); - std::vector s = utils::string::split_text(message, ' '); - - std::string target; - schemas::EventType type; - - std::vector target_and_type = - utils::string::split_text(s[0], ':'); - - if (target_and_type.size() != 2) { - throw ResponseException( - request, bundle.localization, s[0]); - } - - s.erase(s.begin()); - - target = target_and_type[0]; - type = schemas::string_to_event_type(target_and_type[1]); - - std::string t = target_and_type[0] + ":" + target_and_type[1]; - - auto channels = bundle.helix_client.get_users({target}); - api::twitch::schemas::User channel; - - if (channels.empty() && type != schemas::EventType::CUSTOM) { - throw ResponseException( - request, bundle.localization, t); - } - - pqxx::work work(request.conn); - std::string query; - - if (type != schemas::CUSTOM) { - channel = channels[0]; - - query = "SELECT id FROM events WHERE channel_id = " + - std::to_string(request.channel.get_id()) + - " AND target_alias_id = " + std::to_string(channel.id) + - " AND event_type = " + std::to_string(type); - } else { - query = "SELECT id FROM events WHERE channel_id = " + - std::to_string(request.channel.get_id()) + - " AND custom_alias_id = '" + target + - "' AND event_type = " + std::to_string(type); - } - - pqxx::result event = work.exec(query); - - if (subcommand_id == "on") { - if (!event.empty()) { - throw ResponseException( - request, bundle.localization, t); - } - - if (s.empty()) { - throw ResponseException( - request, bundle.localization, - command::CommandArgument::MESSAGE); - } - - std::string m = utils::string::str(s.begin(), s.end(), ' '); - - if (type != schemas::CUSTOM) { - query = - "INSERT INTO events (channel_id, target_alias_id, " - "event_type, " - "message) VALUES (" + - std::to_string(request.channel.get_id()) + ", " + - std::to_string(channel.id) + ", " + std::to_string(type) + - ", '" + m + "')"; - } else { - query = - "INSERT INTO events (channel_id, custom_alias_id, " - "event_type, " - "message) VALUES (" + - std::to_string(request.channel.get_id()) + ", '" + target + - "', " + std::to_string(type) + ", '" + m + "')"; - } - - work.exec(query); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::EventOn, {t}) - .value(); - } else if (subcommand_id == "off") { - if (event.empty()) { - throw ResponseException( - request, bundle.localization, t); - } - - work.exec("DELETE FROM events WHERE id = " + - std::to_string(event[0][0].as())); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::EventOff, {t}) - .value(); - } - - throw ResponseException( - request, bundle.localization); - } - }; - } -} diff --git a/src/modules/help.hpp b/src/modules/help.hpp deleted file mode 100644 index 13af228..0000000 --- a/src/modules/help.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../commands/response_error.hpp" - -namespace bot { - namespace mod { - class Help : public command::Command { - std::string get_name() const override { return "help"; } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - if (!bundle.configuration.url.help.has_value()) { - throw ResponseException( - request, bundle.localization); - } - - return bundle.localization - .get_formatted_line(request, loc::LineId::HelpResponse, - {*bundle.configuration.url.help}) - .value(); - } - }; - } -} diff --git a/src/modules/join.hpp b/src/modules/join.hpp deleted file mode 100644 index 16e8b4a..0000000 --- a/src/modules/join.hpp +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../schemas/channel.hpp" - -namespace bot { - namespace mod { - class Join : public command::Command { - std::string get_name() const override { return "join"; } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - if (!bundle.configuration.commands.join_allowed) { - std::string owner = ""; - - if (bundle.configuration.owner.name.has_value()) { - owner = " " + bundle.localization - .get_formatted_line( - request, loc::LineId::MsgOwner, - {*bundle.configuration.owner.name}) - .value(); - } - - return bundle.localization - .get_formatted_line(request, loc::LineId::JoinNotAllowed, - {owner}) - .value(); - } - - if (!bundle.configuration.commands.join_allow_from_other_chats && - request.channel.get_alias_name() != - bundle.irc_client.get_bot_username()) { - return bundle.localization - .get_formatted_line(request, loc::LineId::JoinFromOtherChat, - {bundle.irc_client.get_bot_username()}) - .value(); - } - - pqxx::work work(request.conn); - - pqxx::result channels = - work.exec("SELECT * FROM channels WHERE alias_id = " + - std::to_string(request.user.get_alias_id())); - - if (!channels.empty()) { - schemas::Channel channel(channels[0]); - - if (channel.get_opted_out_at().has_value()) { - work.exec("UPDATE channels SET opted_out_at = null WHERE id = " + - std::to_string(channel.get_id())); - work.commit(); - - bundle.irc_client.join(channel.get_alias_name()); - - return bundle.localization - .get_formatted_line(request, loc::LineId::JoinRejoined, {}) - .value(); - } - - return bundle.localization - .get_formatted_line(request, loc::LineId::JoinAlreadyIn, {}) - .value(); - } - - work.exec("INSERT INTO channels(alias_id, alias_name) VALUES (" + - std::to_string(request.user.get_alias_id()) + ", '" + - request.user.get_alias_name() + "')"); - work.commit(); - - bundle.irc_client.join(request.user.get_alias_name()); - bundle.irc_client.say( - request.user.get_alias_name(), - bundle.localization - .get_formatted_line(request, loc::LineId::JoinResponseInChat, - {}) - .value()); - - return bundle.localization - .get_formatted_line(request, loc::LineId::JoinResponse, {}) - .value(); - } - }; - } -} diff --git a/src/modules/massping.hpp b/src/modules/massping.hpp deleted file mode 100644 index 2957e34..0000000 --- a/src/modules/massping.hpp +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" - -namespace bot { - namespace mod { - class Massping : public command::Command { - std::string get_name() const override { return "massping"; } - - schemas::PermissionLevel get_permission_level() const override { - return schemas::PermissionLevel::MODERATOR; - } - - int get_delay_seconds() const override { return 1; } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - auto chatters = bundle.helix_client.get_chatters( - request.channel.get_alias_id(), bundle.irc_client.get_bot_id()); - - std::string m; - - if (request.message.has_value()) { - m = request.message.value() + " ·"; - } - - std::string base = "📣 " + m + " "; - std::vector msgs = {""}; - int index = 0; - - for (const auto &chatter : chatters) { - const std::string ¤t_msg = msgs.at(index); - std::string x = "@" + chatter.login; - - if (base.length() + current_msg.length() + 1 + x.length() >= 500) { - index += 1; - } - - if (index > msgs.size() - 1) { - msgs.push_back(x); - } else { - msgs[index] = current_msg + " " + x; - } - } - - std::vector msgs2; - - for (const auto &m : msgs) { - msgs2.push_back(base + m); - } - - return msgs2; - } - }; - } -} diff --git a/src/modules/notify.hpp b/src/modules/notify.hpp deleted file mode 100644 index 3587e73..0000000 --- a/src/modules/notify.hpp +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../commands/response_error.hpp" -#include "../schemas/stream.hpp" - -namespace bot { - namespace mod { - class Notify : public command::Command { - std::string get_name() const override { return "notify"; } - - std::vector get_subcommand_ids() const override { - return {"sub", "unsub"}; - } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - if (!request.subcommand_id.has_value()) { - throw ResponseException( - request, bundle.localization, command::SUBCOMMAND); - } - - const std::string &subcommand_id = request.subcommand_id.value(); - - if (!request.message.has_value()) { - throw ResponseException( - request, bundle.localization, command::CommandArgument::TARGET); - } - - const std::string &message = request.message.value(); - std::vector s = utils::string::split_text(message, ' '); - - std::string target; - schemas::EventType type; - - std::vector target_and_type = - utils::string::split_text(s[0], ':'); - - if (target_and_type.size() != 2) { - throw ResponseException( - request, bundle.localization, s[0]); - } - - s.erase(s.begin()); - - target = target_and_type[0]; - type = schemas::string_to_event_type(target_and_type[1]); - - std::string t = target_and_type[0] + ":" + target_and_type[1]; - - auto channels = bundle.helix_client.get_users({target}); - api::twitch::schemas::User channel; - - if (channels.empty() && type != schemas::EventType::CUSTOM) { - throw ResponseException( - request, bundle.localization, t); - } - - pqxx::work work(request.conn); - std::string query; - - if (type != schemas::CUSTOM) { - channel = channels[0]; - - query = "SELECT id FROM events WHERE channel_id = " + - std::to_string(request.channel.get_id()) + - " AND target_alias_id = " + std::to_string(channel.id) + - " AND event_type = " + std::to_string(type); - } else { - query = "SELECT id FROM events WHERE channel_id = " + - std::to_string(request.channel.get_id()) + - " AND custom_alias_id = '" + target + - "' AND event_type = " + std::to_string(type); - } - - pqxx::result events = work.exec(query); - - if (events.empty()) { - throw ResponseException( - request, bundle.localization, t); - } - - pqxx::row event = events[0]; - - pqxx::result subs = - work.exec("SELECT id FROM event_subscriptions WHERE event_id = " + - std::to_string(event[0].as()) + " AND user_id = " + - std::to_string(request.user.get_id())); - - if (subcommand_id == "sub") { - if (!subs.empty()) { - throw ResponseException( - request, bundle.localization, t); - } - - work.exec( - "INSERT INTO event_subscriptions(event_id, user_id) VALUES (" + - std::to_string(event[0].as()) + ", " + - std::to_string(request.user.get_id())); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::NotifySub, {t}) - .value(); - } else if (subcommand_id == "unsub") { - if (subs.empty()) { - throw ResponseException( - request, bundle.localization, t); - } - - work.exec("DELETE FROM event_subscriptions WHERE id = " + - std::to_string(subs[0][0].as())); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::NotifyUnsub, {t}) - .value(); - } - - throw ResponseException( - request, bundle.localization); - } - }; - } -} diff --git a/src/modules/ping.hpp b/src/modules/ping.hpp deleted file mode 100644 index 836917d..0000000 --- a/src/modules/ping.hpp +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../utils/chrono.hpp" - -namespace bot { - namespace mod { - class Ping : public command::Command { - std::string get_name() const override { return "ping"; } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - auto now = std::chrono::steady_clock::now(); - auto duration = now - START_TIME; - auto seconds = - std::chrono::duration_cast(duration); - std::string uptime = utils::chrono::format_timestamp(seconds.count()); - - struct rusage usage; - getrusage(RUSAGE_SELF, &usage); - - int used_memory = usage.ru_maxrss / 1024; - - std::string cpp_info; - -#ifdef __cplusplus - cpp_info.append("C++" + std::to_string(__cplusplus).substr(2, 2)); -#endif - -#ifdef __VERSION__ - cpp_info.append(" (gcc " + - bot::utils::string::split_text(__VERSION__, ' ')[0] + - ")"); -#endif - - if (!cpp_info.empty()) { - cpp_info.append(" · "); - } - - return bundle.localization - .get_formatted_line( - request, loc::LineId::PingResponse, - {cpp_info, uptime, std::to_string(used_memory)}) - .value(); - } - }; - } -} diff --git a/src/modules/timer.hpp b/src/modules/timer.hpp deleted file mode 100644 index 36c3982..0000000 --- a/src/modules/timer.hpp +++ /dev/null @@ -1,112 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../bundle.hpp" -#include "../commands/command.hpp" -#include "../commands/response_error.hpp" - -namespace bot { - namespace mod { - class Timer : public command::Command { - std::string get_name() const override { return "timer"; } - - schemas::PermissionLevel get_permission_level() const override { - return schemas::PermissionLevel::MODERATOR; - } - - std::vector get_subcommand_ids() const override { - return {"new", "remove"}; - } - - std::variant, std::string> run( - const InstanceBundle &bundle, - const command::Request &request) const override { - if (!request.subcommand_id.has_value()) { - throw ResponseException( - request, bundle.localization, command::SUBCOMMAND); - } - - const std::string &subcommand_id = request.subcommand_id.value(); - - if (!request.message.has_value()) { - throw ResponseException( - request, bundle.localization, command::CommandArgument::NAME); - } - - const std::string &message = request.message.value(); - std::vector s = utils::string::split_text(message, ' '); - - std::string name = s[0]; - s.erase(s.begin()); - - pqxx::work work(request.conn); - pqxx::result timers = work.exec( - "SELECT id FROM timers WHERE name = '" + name + - "' AND channel_id = " + std::to_string(request.channel.get_id())); - - if (subcommand_id == "new") { - if (!timers.empty()) { - throw ResponseException( - request, bundle.localization, name); - } - - if (s.empty()) { - throw ResponseException( - request, bundle.localization, - command::CommandArgument::INTERVAL); - } - - int interval_s; - - try { - interval_s = std::stoi(s[0]); - } catch (std::exception e) { - throw ResponseException( - request, bundle.localization, s[0]); - } - - s.erase(s.begin()); - - if (s.empty()) { - throw ResponseException( - request, bundle.localization, - command::CommandArgument::MESSAGE); - } - - std::string m = utils::string::str(s.begin(), s.end(), ' '); - - work.exec( - "INSERT INTO timers(channel_id, name, message, interval_sec) " - "VALUES " - "(" + - std::to_string(request.channel.get_id()) + ", '" + name + - "', '" + m + "', " + std::to_string(interval_s) + ")"); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::TimerNew, {name}) - .value(); - } else if (subcommand_id == "remove") { - if (timers.empty()) { - throw ResponseException( - request, bundle.localization, name); - } - - work.exec("DELETE FROM timers WHERE id = " + - std::to_string(timers[0][0].as())); - work.commit(); - - return bundle.localization - .get_formatted_line(request, loc::LineId::TimerDelete, {name}) - .value(); - } - - throw ResponseException( - request, bundle.localization); - } - }; - } -} diff --git a/src/schemas/channel.hpp b/src/schemas/channel.hpp deleted file mode 100644 index 2560331..0000000 --- a/src/schemas/channel.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "../constants.hpp" -#include "../utils/chrono.hpp" - -namespace bot::schemas { - class Channel { - public: - Channel(const pqxx::row &row) { - this->id = row[0].as(); - this->alias_id = row[1].as(); - this->alias_name = row[2].as(); - - this->joined_at = - utils::chrono::string_to_time_point(row[3].as()); - - if (!row[4].is_null()) { - this->opted_out_at = - utils::chrono::string_to_time_point(row[4].as()); - } - } - - ~Channel() = default; - - const int &get_id() const { return this->id; } - const int &get_alias_id() const { return this->alias_id; } - const std::string &get_alias_name() const { return this->alias_name; } - const std::chrono::system_clock::time_point &get_joined_at() const { - return this->joined_at; - } - const std::optional & - get_opted_out_at() const { - return this->opted_out_at; - } - - private: - int id, alias_id; - std::string alias_name; - std::chrono::system_clock::time_point joined_at; - std::optional opted_out_at; - }; - - class ChannelPreferences { - public: - ChannelPreferences(const pqxx::row &row) { - this->channel_id = row[0].as(); - - if (!row[2].is_null()) { - this->prefix = row[1].as(); - } else { - this->prefix = DEFAULT_PREFIX; - } - - if (!row[3].is_null()) { - this->locale = row[2].as(); - } else { - this->locale = DEFAULT_LOCALE_ID; - } - } - - ~ChannelPreferences() = default; - - const int &get_channel_id() const { return this->channel_id; } - const std::string &get_prefix() const { return this->prefix; } - const std::string &get_locale() const { return this->locale; } - - private: - int channel_id; - std::string prefix, locale; - }; -} diff --git a/src/schemas/stream.cpp b/src/schemas/stream.cpp deleted file mode 100644 index 6ef10dc..0000000 --- a/src/schemas/stream.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "stream.hpp" - -namespace bot::schemas { - EventType string_to_event_type(const std::string &type) { - if (type == "live") { - return EventType::LIVE; - } else if (type == "offline") { - return EventType::OFFLINE; - } else if (type == "title") { - return EventType::TITLE; - } else if (type == "game") { - return EventType::GAME; - } else { - return EventType::CUSTOM; - } - } -} diff --git a/src/schemas/stream.hpp b/src/schemas/stream.hpp deleted file mode 100644 index a636ea5..0000000 --- a/src/schemas/stream.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include - -namespace bot::schemas { - enum EventType { LIVE, OFFLINE, TITLE, GAME, CUSTOM = 99 }; - EventType string_to_event_type(const std::string &type); - - enum EventFlag { MASSPING }; - -} diff --git a/src/schemas/user.hpp b/src/schemas/user.hpp deleted file mode 100644 index 0bd1368..0000000 --- a/src/schemas/user.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "../utils/chrono.hpp" - -namespace bot::schemas { - class User { - public: - User(const pqxx::row &row) { - this->id = row[0].as(); - this->alias_id = row[1].as(); - this->alias_name = row[2].as(); - - this->joined_at = - utils::chrono::string_to_time_point(row[3].as()); - - if (!row[4].is_null()) { - this->opted_out_at = - utils::chrono::string_to_time_point(row[4].as()); - } - } - - ~User() = default; - - const int &get_id() const { return this->id; } - const int &get_alias_id() const { return this->alias_id; } - const std::string &get_alias_name() const { return this->alias_name; } - void set_alias_name(const std::string &alias_name) { - this->alias_name = alias_name; - } - const std::chrono::system_clock::time_point &get_joined_at() const { - return this->joined_at; - } - const std::optional & - get_opted_out_at() const { - return this->opted_out_at; - } - - private: - int id, alias_id; - std::string alias_name; - std::chrono::system_clock::time_point joined_at; - std::optional opted_out_at; - }; - - enum PermissionLevel { SUSPENDED, USER, VIP, MODERATOR, BROADCASTER }; - - class UserRights { - public: - UserRights(const pqxx::row &row) { - this->id = row[0].as(); - this->user_id = row[1].as(); - this->channel_id = row[2].as(); - this->level = static_cast(row[3].as()); - } - - ~UserRights() = default; - - const int &get_id() const { return this->id; } - const int &get_user_id() const { return this->user_id; } - const int &get_channel_id() const { return this->channel_id; } - const PermissionLevel &get_level() const { return this->level; } - void set_level(PermissionLevel level) { this->level = level; } - - private: - int id, user_id, channel_id; - PermissionLevel level; - }; -} diff --git a/src/stream.cpp b/src/stream.cpp deleted file mode 100644 index 6e48fb8..0000000 --- a/src/stream.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "stream.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "api/twitch/schemas/stream.hpp" -#include "config.hpp" -#include "logger.hpp" -#include "schemas/stream.hpp" -#include "utils/string.hpp" - -namespace bot::stream { - void StreamListenerClient::listen_channel(const int &id) { - this->ids.push_back(id); - } - void StreamListenerClient::unlisten_channel(const int &id) { - auto x = std::find_if(this->ids.begin(), this->ids.end(), - [&](const auto &x) { return x == id; }); - - if (x != this->ids.end()) { - this->ids.erase(x); - } - - auto y = std::find_if(this->online_ids.begin(), this->online_ids.end(), - [&](const auto &x) { return x == id; }); - - if (y != this->online_ids.end()) { - this->online_ids.erase(y); - } - } - void StreamListenerClient::run() { - while (true) { - this->update_channel_ids(); - this->check(); - std::this_thread::sleep_for(std::chrono::seconds(5)); - } - } - void StreamListenerClient::check() { - auto streams = this->helix_client.get_streams(this->ids); - 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)); - - // adding new ids - for (const auto &stream : streams) { - bool is_already_live = - std::any_of(this->online_ids.begin(), this->online_ids.end(), - [&](const auto &x) { return x == stream.get_user_id(); }); - - if (!is_already_live) { - this->online_ids.insert(stream.get_user_id()); - - auto difference = now - stream.get_started_at(); - auto difference_min = - std::chrono::duration_cast(difference); - - if (difference_min.count() < 1) { - this->handler(schemas::EventType::LIVE, stream); - } - } - } - - // removing old ids - for (auto i = this->online_ids.begin(); i != this->online_ids.end();) { - auto stream = - std::find_if(streams.begin(), streams.end(), - [&](const auto &x) { return x.get_user_id() == *i; }); - - if (stream == streams.end()) { - this->handler(schemas::EventType::OFFLINE, - api::twitch::schemas::Stream{*i}); - i = this->online_ids.erase(i); - } else { - ++i; - } - } - } - void StreamListenerClient::handler( - const schemas::EventType &type, - const api::twitch::schemas::Stream &stream) { - pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); - pqxx::work work(conn); - - pqxx::result events = work.exec( - "SELECT id, channel_id, message, flags FROM events WHERE event_type " - "= " + - std::to_string(type) + - " AND target_alias_id = " + std::to_string(stream.get_user_id())); - - for (const auto &event : events) { - pqxx::row channel = work.exec1( - "SELECT alias_id, alias_name, opted_out_at FROM channels WHERE id " - "= " + - std::to_string(event[1].as())); - - if (!channel[2].is_null()) { - continue; - } - - pqxx::result subs = work.exec( - "SELECT user_id FROM event_subscriptions WHERE event_id = " + - std::to_string(event[0].as())); - - std::set user_ids; - if (!subs.empty()) { - for (const auto &sub : subs) { - user_ids.insert(std::to_string(sub[0].as())); - } - - pqxx::result users = work.exec( - "SELECT alias_name FROM users WHERE id IN (" + - utils::string::str(user_ids.begin(), user_ids.end(), ',') + ")"); - - user_ids.clear(); - - for (const auto &user : users) { - user_ids.insert(user[0].as()); - } - } - - auto flags = event[3].as_array(); - std::pair elem; - - do { - elem = flags.get_next(); - if (elem.first == pqxx::array_parser::juncture::string_value) { - if (std::stoi(elem.second) == schemas::EventFlag::MASSPING) { - auto chatters = this->helix_client.get_chatters( - channel[0].as(), this->irc_client.get_bot_id()); - - for (const auto &chatter : chatters) { - user_ids.insert(chatter.login); - } - } - } - } while (elem.first != pqxx::array_parser::juncture::done); - - std::string base = "⚡ " + event[2].as(); - std::vector msgs = {""}; - int index = 0; - - if (!user_ids.empty()) { - base.append(" · "); - } - - for (const auto &user_id : user_ids) { - const std::string ¤t_msg = msgs.at(index); - std::string x = "@" + user_id; - - if (base.length() + current_msg.length() + 1 + x.length() >= 500) { - index += 1; - } - - if (index > msgs.size() - 1) { - msgs.push_back(x); - } else { - msgs[index] = current_msg + " " + x; - } - } - - for (const auto &msg : msgs) { - this->irc_client.say(channel[1].as(), base + msg); - } - } - - work.commit(); - conn.close(); - } - void StreamListenerClient::update_channel_ids() { - pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); - pqxx::work work(conn); - - pqxx::result ids = - work.exec("SELECT target_alias_id FROM events WHERE event_type < 99"); - - for (const auto &row : ids) { - int id = row[0].as(); - - if (std::any_of(this->ids.begin(), this->ids.end(), - [&](const auto &x) { return x == id; })) { - continue; - } - - log::info("TwitchStreamListener", - "Listening stream events for ID " + std::to_string(id)); - - this->ids.push_back(id); - } - - work.commit(); - conn.close(); - } -} diff --git a/src/stream.hpp b/src/stream.hpp deleted file mode 100644 index 73313ed..0000000 --- a/src/stream.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include - -#include "api/twitch/helix_client.hpp" -#include "api/twitch/schemas/stream.hpp" -#include "config.hpp" -#include "irc/client.hpp" -#include "schemas/stream.hpp" - -namespace bot::stream { - class StreamListenerClient { - public: - StreamListenerClient(const api::twitch::HelixClient &helix_client, - irc::Client &irc_client, - const Configuration &configuration) - : helix_client(helix_client), - irc_client(irc_client), - configuration(configuration){}; - ~StreamListenerClient() = default; - - void run(); - void listen_channel(const int &id); - void unlisten_channel(const int &id); - - private: - void check(); - void handler(const schemas::EventType &type, - const api::twitch::schemas::Stream &stream); - void update_channel_ids(); - - const api::twitch::HelixClient &helix_client; - irc::Client &irc_client; - const Configuration &configuration; - - std::vector ids; - - std::set online_ids; - }; -} diff --git a/src/timer.cpp b/src/timer.cpp deleted file mode 100644 index 055dde0..0000000 --- a/src/timer.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "timer.hpp" - -#include -#include -#include -#include - -#include "config.hpp" -#include "irc/client.hpp" -#include "utils/chrono.hpp" - -namespace bot { - void create_timer_thread(irc::Client *irc_client, - Configuration *configuration) { - while (true) { - pqxx::connection conn(GET_DATABASE_CONNECTION_URL_POINTER(configuration)); - pqxx::work *work = new pqxx::work(conn); - - pqxx::result timers = work->exec( - "SELECT id, interval_sec, message, channel_id, last_executed_at FROM " - "timers"); - - for (const auto &timer : timers) { - int id = timer[0].as(); - int interval_sec = timer[1].as(); - std::string message = timer[2].as(); - int channel_id = timer[3].as(); - - // it could be done in sql query - std::chrono::system_clock::time_point last_executed_at = - utils::chrono::string_to_time_point(timer[4].as()); - 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( - now - last_executed_at); - - if (difference.count() > interval_sec) { - pqxx::result channels = work->exec( - "SELECT alias_name, opted_out_at FROM channels WHERE id = " + - std::to_string(channel_id)); - - if (!channels.empty() && channels[0][1].is_null()) { - std::string alias_name = channels[0][0].as(); - - irc_client->say(alias_name, message); - } - - work->exec( - "UPDATE timers SET last_executed_at = timezone('utc', now()) " - "WHERE " - "id = " + - std::to_string(id)); - - work->commit(); - - delete work; - work = new pqxx::work(conn); - } - } - - delete work; - conn.close(); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - } -} diff --git a/src/timer.hpp b/src/timer.hpp deleted file mode 100644 index 40a52ee..0000000 --- a/src/timer.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "config.hpp" -#include "irc/client.hpp" - -namespace bot { - void create_timer_thread(irc::Client *irc_client, - Configuration *configuration); -} diff --git a/src/utils/chrono.cpp b/src/utils/chrono.cpp deleted file mode 100644 index 7a7f2c9..0000000 --- a/src/utils/chrono.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "chrono.hpp" - -#include -#include -#include -#include -#include -#include - -namespace bot::utils::chrono { - std::string format_timestamp(int seconds) { - int d = round(seconds / (60 * 60 * 24)); - int h = round(seconds / (60 * 60) % 24); - int m = round(seconds % (60 * 60) / 60); - int s = round(seconds % 60); - - // Only seconds: - if (d == 0 && h == 0 && m == 0) { - return std::to_string(s) + "s"; - } - // Minutes and seconds: - else if (d == 0 && h == 0) { - return std::to_string(m) + "m" + std::to_string(s) + "s"; - } - // Hours and minutes: - else if (d == 0) { - return std::to_string(h) + "h" + std::to_string(m) + "m"; - } - // Days and hours: - else { - return std::to_string(d) + "d" + std::to_string(h) + "h"; - } - } - - std::chrono::system_clock::time_point string_to_time_point( - const std::string &value, const std::string &format) { - std::tm tm = {}; - std::stringstream ss(value); - - ss >> std::get_time(&tm, format.c_str()); - - if (ss.fail()) { - throw std::invalid_argument("Invalid time format"); - } - - return std::chrono::system_clock::from_time_t(std::mktime(&tm)); - } -} diff --git a/src/utils/chrono.hpp b/src/utils/chrono.hpp deleted file mode 100644 index 7e85e70..0000000 --- a/src/utils/chrono.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include -#include - -namespace bot::utils::chrono { - std::string format_timestamp(int seconds); - std::chrono::system_clock::time_point string_to_time_point( - const std::string &value, - const std::string &format = "%Y-%m-%d %H:%M:%S"); -} diff --git a/src/utils/string.cpp b/src/utils/string.cpp deleted file mode 100644 index 71c06bf..0000000 --- a/src/utils/string.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "string.hpp" - -#include -#include -#include - -namespace bot { - namespace utils { - namespace string { - std::vector split_text(const std::string &text, - char delimiter) { - std::vector parts; - - std::istringstream iss(text); - std::string part; - - while (std::getline(iss, part, delimiter)) { - parts.push_back(part); - } - - return parts; - } - - std::string join_vector(const std::vector &vec, - char delimiter) { - if (vec.empty()) { - return ""; - } - - std::string str; - - for (auto i = vec.begin(); i != vec.end() - 1; i++) { - str += *i + delimiter; - } - - str += vec[vec.size() - 1]; - - return str; - } - - std::string join_vector(const std::vector &vec) { - std::string str; - - for (const auto &e : vec) { - str += e; - } - - return str; - } - - bool string_contains_sql_injection(const std::string &input) { - std::string forbidden_strings[] = {";", "--", "'", "\"", - "/*", "*/", "xp_", "exec", - "sp_", "insert", "select", "delete"}; - - for (const auto &str : forbidden_strings) { - if (input.find(str) != std::string::npos) { - return true; - } - } - - return false; - } - } - } -} diff --git a/src/utils/string.hpp b/src/utils/string.hpp deleted file mode 100644 index c8385ad..0000000 --- a/src/utils/string.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace bot { - namespace utils { - namespace string { - std::vector split_text(const std::string &text, - char delimiter); - std::string join_vector(const std::vector &vec, - char delimiter); - std::string join_vector(const std::vector &vec); - - template - std::string str(T begin, T end, char delimiter) { - std::stringstream ss; - bool first = true; - - for (; begin != end; begin++) { - if (!first) ss << delimiter; - ss << *begin; - first = false; - } - return ss.str(); - } - - bool string_contains_sql_injection(const std::string &input); - } - } -} -- cgit v1.2.3