summaryrefslogtreecommitdiff
path: root/bot/src/irc
diff options
context:
space:
mode:
authorilotterytea <iltsu@alright.party>2024-05-18 14:48:12 +0500
committerilotterytea <iltsu@alright.party>2024-05-18 14:48:12 +0500
commitd1793df1eda463b10107d41785ad1d7f055ed476 (patch)
treefd3e41c3b4a05924748ae4b762e1ae55a0bc815c /bot/src/irc
parentd7a2de17e9b7931f68b5b4079b1c36866a19d343 (diff)
upd: moved the bot part to a relative subfolder
Diffstat (limited to 'bot/src/irc')
-rw-r--r--bot/src/irc/client.cpp155
-rw-r--r--bot/src/irc/client.hpp58
-rw-r--r--bot/src/irc/message.cpp30
-rw-r--r--bot/src/irc/message.hpp130
4 files changed, 373 insertions, 0 deletions
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 <ixwebsocket/IXWebSocketMessage.h>
+#include <ixwebsocket/IXWebSocketMessageType.h>
+
+#include <algorithm>
+#include <optional>
+#include <string>
+#include <vector>
+
+#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<std::string>());
+ 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<std::string> 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<MessageType> type = define_message_type(line);
+
+ if (!type.has_value()) {
+ break;
+ }
+
+ MessageType m_type = type.value();
+
+ if (m_type == MessageType::Privmsg) {
+ std::optional<Message<MessageType::Privmsg>> message =
+ parse_message<MessageType::Privmsg>(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 <ixwebsocket/IXWebSocket.h>
+
+#include <string>
+#include <vector>
+
+#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 <MessageType T>
+ void on(typename MessageHandler<T>::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<std::string> pool;
+
+ std::vector<std::string> joined_channels;
+
+ // Message handlers
+ typename MessageHandler<MessageType::Privmsg>::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 <optional>
+#include <string>
+#include <vector>
+
+namespace bot {
+ namespace irc {
+ std::optional<MessageType> define_message_type(const std::string &msg) {
+ std::vector<std::string> 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 <functional>
+#include <map>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "../utils/string.hpp"
+
+namespace bot {
+ namespace irc {
+ enum MessageType { Privmsg, Notice };
+ std::optional<MessageType> define_message_type(const std::string &msg);
+
+ struct MessageSender {
+ std::string login;
+ std::string display_name;
+ int id;
+
+ std::map<std::string, int> badges;
+
+ // More fields will be here
+ };
+
+ struct MessageSource {
+ std::string login;
+ int id;
+ };
+
+ template <MessageType T>
+ struct Message;
+
+ template <>
+ struct Message<MessageType::Privmsg> {
+ MessageSender sender;
+ MessageSource source;
+ std::string message;
+ };
+
+ template <MessageType T>
+ std::optional<Message<T>> parse_message(const std::string &msg) {
+ std::vector<std::string> parts = utils::string::split_text(msg, ' ');
+
+ if (T == MessageType::Privmsg) {
+ MessageSender sender;
+ MessageSource source;
+
+ Message<MessageType::Privmsg> 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<std::string> 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<std::string> 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<std::string> badges =
+ utils::string::split_text(value, ',');
+
+ std::map<std::string, int> 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 <MessageType T>
+ struct MessageHandler;
+
+ template <>
+ struct MessageHandler<MessageType::Privmsg> {
+ using fn = std::function<void(Message<Privmsg> message)>;
+ };
+
+ }
+}