diff options
| author | ilotterytea <iltsu@alright.party> | 2024-05-18 14:48:12 +0500 |
|---|---|---|
| committer | ilotterytea <iltsu@alright.party> | 2024-05-18 14:48:12 +0500 |
| commit | d1793df1eda463b10107d41785ad1d7f055ed476 (patch) | |
| tree | fd3e41c3b4a05924748ae4b762e1ae55a0bc815c /bot/src/irc | |
| parent | d7a2de17e9b7931f68b5b4079b1c36866a19d343 (diff) | |
upd: moved the bot part to a relative subfolder
Diffstat (limited to 'bot/src/irc')
| -rw-r--r-- | bot/src/irc/client.cpp | 155 | ||||
| -rw-r--r-- | bot/src/irc/client.hpp | 58 | ||||
| -rw-r--r-- | bot/src/irc/message.cpp | 30 | ||||
| -rw-r--r-- | bot/src/irc/message.hpp | 130 |
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)>; + }; + + } +} |
