summaryrefslogtreecommitdiff
path: root/bot/src
diff options
context:
space:
mode:
authorilotterytea <iltsu@alright.party>2024-12-13 23:50:45 +0500
committerilotterytea <iltsu@alright.party>2024-12-13 23:50:45 +0500
commit2d40d39264de4eed0bdb9e8e0117a8c1e2f199d3 (patch)
tree890df967067371cd5ddb7b83d733db875a253e7a /bot/src
parentad05411350f1152cc3c86cebb5b8492d34507b33 (diff)
feat: now you can listen to github
Diffstat (limited to 'bot/src')
-rw-r--r--bot/src/config.cpp6
-rw-r--r--bot/src/config.hpp5
-rw-r--r--bot/src/github.cpp230
-rw-r--r--bot/src/github.hpp39
-rw-r--r--bot/src/main.cpp5
-rw-r--r--bot/src/modules/event.hpp6
-rw-r--r--bot/src/modules/notify.hpp4
-rw-r--r--bot/src/schemas/stream.cpp2
-rw-r--r--bot/src/schemas/stream.hpp2
-rw-r--r--bot/src/stream.cpp2
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>();