diff options
Diffstat (limited to 'bot')
| -rw-r--r-- | bot/src/config.cpp | 6 | ||||
| -rw-r--r-- | bot/src/config.hpp | 5 | ||||
| -rw-r--r-- | bot/src/github.cpp | 230 | ||||
| -rw-r--r-- | bot/src/github.hpp | 39 | ||||
| -rw-r--r-- | bot/src/main.cpp | 5 | ||||
| -rw-r--r-- | bot/src/modules/event.hpp | 6 | ||||
| -rw-r--r-- | bot/src/modules/notify.hpp | 4 | ||||
| -rw-r--r-- | bot/src/schemas/stream.cpp | 2 | ||||
| -rw-r--r-- | bot/src/schemas/stream.hpp | 2 | ||||
| -rw-r--r-- | bot/src/stream.cpp | 2 |
10 files changed, 294 insertions, 7 deletions
diff --git a/bot/src/config.cpp b/bot/src/config.cpp index 6c8e6d6..eef5142 100644 --- a/bot/src/config.cpp +++ b/bot/src/config.cpp @@ -24,6 +24,7 @@ namespace bot { CommandConfiguration cmd_cfg; OwnerConfiguration owner_cfg; UrlConfiguration url_cfg; + TokenConfiguration token_cfg; std::string line; while (std::getline(ifs, line, '\n')) { @@ -71,6 +72,10 @@ namespace bot { } else if (key == "url.chatters.paste_service") { url_cfg.paste_service = value; } + + else if (key == "token.github") { + token_cfg.github_token = value; + } } cfg.url = url_cfg; @@ -78,6 +83,7 @@ namespace bot { cfg.commands = cmd_cfg; cfg.twitch_credentials = ttv_crd_cfg; cfg.database = db_cfg; + cfg.tokens = token_cfg; log::info("Configuration", "Successfully loaded the file from '" + file_path + "'"); diff --git a/bot/src/config.hpp b/bot/src/config.hpp index 8707d02..db73f9a 100644 --- a/bot/src/config.hpp +++ b/bot/src/config.hpp @@ -42,12 +42,17 @@ namespace bot { std::optional<std::string> paste_service = std::nullopt; }; + struct TokenConfiguration { + std::optional<std::string> github_token = std::nullopt; + }; + struct Configuration { TwitchCredentialsConfiguration twitch_credentials; DatabaseConfiguration database; CommandConfiguration commands; OwnerConfiguration owner; UrlConfiguration url; + TokenConfiguration tokens; }; std::optional<Configuration> parse_configuration_from_file( diff --git a/bot/src/github.cpp b/bot/src/github.cpp new file mode 100644 index 0000000..e27289c --- /dev/null +++ b/bot/src/github.cpp @@ -0,0 +1,230 @@ +#include "github.hpp" + +#include <algorithm> +#include <chrono> +#include <iterator> +#include <pqxx/pqxx> +#include <string> +#include <thread> +#include <unordered_map> +#include <utility> +#include <vector> + +#include "config.hpp" +#include "cpr/api.h" +#include "cpr/cprtypes.h" +#include "cpr/response.h" +#include "irc/client.hpp" +#include "logger.hpp" +#include "nlohmann/json.hpp" +#include "pqxx/internal/statement_parameters.hxx" +#include "schemas/channel.hpp" +#include "utils/string.hpp" + +namespace bot { + void GithubListener::run() { + if (this->configuration.tokens.github_token->empty()) { + log::warn("Github Listener", + "Github token (token.github) must be set if you want to listen " + "for changes in repositories."); + return; + } + + log::info("Github Listener", "Listening for new commits..."); + + while (true) { + this->check_for_listeners(); + + std::unordered_map<std::string, std::vector<Commit>> new_commits = + this->check_new_commits(); + + this->notify_about_commits(new_commits); + + for (const auto &pair : new_commits) { + std::vector<std::string> &commits = this->commits.at(pair.first); + auto &s = pair.second; + commits.reserve(commits.size() + std::distance(s.begin(), s.end())); + + std::vector<std::string> commit_shas; + std::transform(s.begin(), s.end(), std::back_inserter(commit_shas), + [](const Commit &x) { return x.sha; }); + + // i could do it with .insert(), but im tired of these errors + for (std::string &sha : commit_shas) { + commits.push_back(sha); + } + } + + std::this_thread::sleep_for(std::chrono::seconds(60)); + } + } + + void GithubListener::check_for_listeners() { + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); + pqxx::work work(conn); + + pqxx::result repos = + work.exec("SELECT custom_alias_id FROM events WHERE event_type = 10"); + + // Adding new repos + for (const auto &repo : repos) { + std::string id = repo[0].as<std::string>(); + if (std::any_of(this->ids.begin(), this->ids.end(), + [&id](const auto &x) { return x == id; })) + continue; + + this->ids.push_back(id); + this->commits.insert({id, {}}); + } + + // Deleting old repos + std::vector<std::string> names_to_delete; + + for (const std::string &id : this->ids) { + if (std::any_of(repos.begin(), repos.end(), [&id](const pqxx::row &x) { + return x[0].as<std::string>() == id; + })) + continue; + + names_to_delete.push_back(id); + } + + for (const std::string &name : names_to_delete) { + auto id_pos = std::find(this->ids.begin(), this->ids.end(), name); + this->ids.erase(id_pos); + this->commits.erase(name); + } + + work.commit(); + conn.close(); + } + + std::unordered_map<std::string, std::vector<Commit>> + GithubListener::check_new_commits() { + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); + pqxx::work work(conn); + + pqxx::result repos = + work.exec("SELECT custom_alias_id FROM events WHERE event_type = 10"); + + std::unordered_map<std::string, std::vector<Commit>> new_commits; + + for (const std::string &id : this->ids) { + cpr::Response response = cpr::Get( + cpr::Url{"https://api.github.com/repos/" + id + "/commits"}, + cpr::Header{ + {"Authorization", + "Bearer " + this->configuration.tokens.github_token.value()}}, + cpr::Header{{"Accept", "application/vnd.github+json"}, + {"X-GitHub-Api-Version", "2022-11-28"}, + {"User-Agent", "https://github.com/ilotterytea/bot"}}); + + if (response.status_code != 200) { + log::error("Github Listener", "Got HTTP status " + + std::to_string(response.status_code) + + " for " + id); + continue; + } + + nlohmann::json j = nlohmann::json::parse(response.text); + const std::vector<std::string> &commit_cache = this->commits.at(id); + std::vector<Commit> repo_commits; + + for (const auto &commit : j) { + const std::string &sha = commit["sha"]; + + if (std::any_of(commit_cache.begin(), commit_cache.end(), + [&sha](const std::string &x) { return x == sha; })) + continue; + + const std::string &commiter_name = commit["committer"]["login"]; + const std::string &message = commit["commit"]["message"]; + + repo_commits.push_back({sha, commiter_name, message}); + } + + new_commits.insert({id, repo_commits}); + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + work.commit(); + conn.close(); + + return new_commits; + } + + void GithubListener::notify_about_commits( + const std::unordered_map<std::string, std::vector<Commit>> &new_commits) { + pqxx::connection conn(GET_DATABASE_CONNECTION_URL(this->configuration)); + pqxx::work work(conn); + + for (const auto &pair : new_commits) { + // don't notify on startup + if (this->commits.at(pair.first).size() == 0) continue; + + pqxx::result events = work.exec( + "SELECT id, channel_id, message, flags FROM events WHERE " + "custom_alias_id " + "= '" + + pair.first + "' AND event_type = 10"); + + for (const auto &event : events) { + schemas::Channel channel( + work.exec("SELECT * FROM channels WHERE id = " + + std::to_string(event[1].as<int>()))[0]); + + pqxx::result subscriber_ids = work.exec( + "SELECT user_id FROM event_subscriptions WHERE event_id = " + + std::to_string(event[0].as<int>())); + + std::vector<std::string> subscriber_names; + + for (const auto &subscriber_id : subscriber_ids) { + pqxx::result subscriber_name = + work.exec("SELECT alias_name FROM users WHERE id = " + + std::to_string(subscriber_id[0].as<int>())); + subscriber_names.push_back(subscriber_name[0][0].as<std::string>()); + } + + // TODO: implement massping flag + + for (const Commit &commit : pair.second) { + std::string message = event[2].as<std::string>(); + + // Replacing SHA placeholder + std::size_t pos = message.find("%0"); + if (pos != std::string::npos) + message.replace(pos, 2, commit.sha.substr(0, 7)); + + // Replacing committer placeholder + pos = message.find("%1"); + if (pos != std::string::npos) + message.replace(pos, 2, commit.commiter_name); + + // Replacing message placeholder + pos = message.find("%2"); + if (pos != std::string::npos) message.replace(pos, 2, commit.message); + + std::vector<std::vector<std::string>> ping_names = + utils::string::separate_by_length(subscriber_names, + 500 - message.length()); + + if (ping_names.empty()) { + this->irc_client.say(channel.get_alias_name(), message); + } else { + for (const std::vector<std::string> &ping_names_vec : ping_names) { + std::string pings = utils::string::str(ping_names_vec.begin(), + ping_names_vec.end(), ' '); + + this->irc_client.say(channel.get_alias_name(), + message + " ยท " + pings); + } + } + } + } + } + + work.commit(); + conn.close(); + } +}
\ No newline at end of file diff --git a/bot/src/github.hpp b/bot/src/github.hpp new file mode 100644 index 0000000..8014706 --- /dev/null +++ b/bot/src/github.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include <string> +#include <unordered_map> +#include <vector> + +#include "config.hpp" +#include "irc/client.hpp" + +namespace bot { + struct Commit { + std::string sha; + std::string commiter_name; + std::string message; + }; + + class GithubListener { + public: + GithubListener(const Configuration &configuration, + irc::Client &irc_client) + : configuration(configuration), irc_client(irc_client){}; + ~GithubListener(){}; + + void run(); + + private: + void check_for_listeners(); + std::unordered_map<std::string, std::vector<Commit>> check_new_commits(); + void notify_about_commits( + const std::unordered_map<std::string, std::vector<Commit>> + &new_commits); + + std::vector<std::string> ids; + std::unordered_map<std::string, std::vector<std::string>> commits; + + irc::Client &irc_client; + const Configuration &configuration; + }; +}
\ No newline at end of file diff --git a/bot/src/main.cpp b/bot/src/main.cpp index 3c8f5e7..a95e4a8 100644 --- a/bot/src/main.cpp +++ b/bot/src/main.cpp @@ -1,12 +1,14 @@ #include <optional> #include <pqxx/pqxx> #include <string> +#include <thread> #include <vector> #include "api/twitch/helix_client.hpp" #include "bundle.hpp" #include "commands/command.hpp" #include "config.hpp" +#include "github.hpp" #include "handlers.hpp" #include "irc/client.hpp" #include "irc/message.hpp" @@ -99,6 +101,8 @@ int main(int argc, char *argv[]) { bot::stream::StreamListenerClient stream_listener_client(helix_client, client, cfg); + bot::GithubListener github_listener(cfg, client); + client.on<bot::irc::MessageType::Privmsg>( [&client, &command_loader, &localization, &cfg, &helix_client]( const bot::irc::Message<bot::irc::MessageType::Privmsg> &message) { @@ -118,6 +122,7 @@ int main(int argc, char *argv[]) { threads.push_back(std::thread(bot::create_timer_thread, &client, &cfg)); threads.push_back(std::thread(&bot::stream::StreamListenerClient::run, &stream_listener_client)); + threads.push_back(std::thread(&bot::GithubListener::run, &github_listener)); for (auto &thread : threads) { thread.join(); diff --git a/bot/src/modules/event.hpp b/bot/src/modules/event.hpp index 58695d9..f2d33b8 100644 --- a/bot/src/modules/event.hpp +++ b/bot/src/modules/event.hpp @@ -59,7 +59,7 @@ namespace bot { auto channels = bundle.helix_client.get_users({target}); api::twitch::schemas::User channel; - if (channels.empty() && type != schemas::EventType::CUSTOM) { + if (channels.empty() && type < schemas::EventType::GITHUB) { throw ResponseException<ResponseError::NOT_FOUND>( request, bundle.localization, t); } @@ -67,7 +67,7 @@ namespace bot { pqxx::work work(request.conn); std::string query; - if (type != schemas::CUSTOM) { + if (type < schemas::GITHUB) { channel = channels[0]; query = "SELECT id FROM events WHERE channel_id = " + @@ -97,7 +97,7 @@ namespace bot { std::string m = utils::string::str(s.begin(), s.end(), ' '); - if (type != schemas::CUSTOM) { + if (type < schemas::GITHUB) { query = "INSERT INTO events (channel_id, target_alias_id, " "event_type, " diff --git a/bot/src/modules/notify.hpp b/bot/src/modules/notify.hpp index a25b34f..35b4d36 100644 --- a/bot/src/modules/notify.hpp +++ b/bot/src/modules/notify.hpp @@ -55,7 +55,7 @@ namespace bot { auto channels = bundle.helix_client.get_users({target}); api::twitch::schemas::User channel; - if (channels.empty() && type != schemas::EventType::CUSTOM) { + if (channels.empty() && type < schemas::EventType::GITHUB) { throw ResponseException<ResponseError::NOT_FOUND>( request, bundle.localization, t); } @@ -63,7 +63,7 @@ namespace bot { pqxx::work work(request.conn); std::string query; - if (type != schemas::CUSTOM) { + if (type < schemas::GITHUB) { channel = channels[0]; query = "SELECT id FROM events WHERE channel_id = " + diff --git a/bot/src/schemas/stream.cpp b/bot/src/schemas/stream.cpp index 6ef10dc..34d170a 100644 --- a/bot/src/schemas/stream.cpp +++ b/bot/src/schemas/stream.cpp @@ -10,6 +10,8 @@ namespace bot::schemas { return EventType::TITLE; } else if (type == "game") { return EventType::GAME; + } else if (type == "github") { + return EventType::GITHUB; } else { return EventType::CUSTOM; } diff --git a/bot/src/schemas/stream.hpp b/bot/src/schemas/stream.hpp index a636ea5..0811561 100644 --- a/bot/src/schemas/stream.hpp +++ b/bot/src/schemas/stream.hpp @@ -3,7 +3,7 @@ #include <string> namespace bot::schemas { - enum EventType { LIVE, OFFLINE, TITLE, GAME, CUSTOM = 99 }; + enum EventType { LIVE, OFFLINE, TITLE, GAME, GITHUB = 10, CUSTOM = 99 }; EventType string_to_event_type(const std::string &type); enum EventFlag { MASSPING }; diff --git a/bot/src/stream.cpp b/bot/src/stream.cpp index 6e48fb8..dc77328 100644 --- a/bot/src/stream.cpp +++ b/bot/src/stream.cpp @@ -178,7 +178,7 @@ namespace bot::stream { pqxx::work work(conn); pqxx::result ids = - work.exec("SELECT target_alias_id FROM events WHERE event_type < 99"); + work.exec("SELECT target_alias_id FROM events WHERE event_type < 10"); for (const auto &row : ids) { int id = row[0].as<int>(); |
