diff options
| author | moderndevslulw <moderndevslulw@alright.party> | 2025-04-05 15:21:42 +0500 |
|---|---|---|
| committer | moderndevslulw <moderndevslulw@alright.party> | 2025-04-05 15:21:42 +0500 |
| commit | 966ad0c6acee8bf19128b71e9710eb1065f76608 (patch) | |
| tree | f42657b14c4b63098721a1fa4b4d842125406828 | |
| parent | e67424780c981a8b032fc9f1e7ec7f09cd2ffd88 (diff) | |
| -rw-r--r-- | CMakeLists.txt | 9 | ||||
| -rw-r--r-- | include/emotespp/betterttv.hpp | 96 | ||||
| -rw-r--r-- | src/betterttv.cpp | 161 |
3 files changed, 266 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ec7505f..355ede1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,15 @@ file(GLOB_RECURSE SRC_FILES "src/*.cpp") add_library(${PROJECT_NAME} STATIC ${SRC_FILES}) target_include_directories(${PROJECT_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") +# BetterTTV Emote support +set(BUILD_BETTERTTV OFF BOOL "Enable BetterTTV emotes") +if (BUILD_BETTERTTV) + message("-- Building with BetterTTV clients") + target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_BETTERTTV=1) +endif() + +set(USE_TLS ON CACHE BOOL "Use TLS in websocket connections" FORCE) + # websockets FetchContent_Declare( ixwebsocket diff --git a/include/emotespp/betterttv.hpp b/include/emotespp/betterttv.hpp new file mode 100644 index 0000000..abf7e7f --- /dev/null +++ b/include/emotespp/betterttv.hpp @@ -0,0 +1,96 @@ +#ifdef BUILD_BETTERTTV +#pragma once + +#include <algorithm> +#include <map> +#include <nlohmann/json.hpp> +#include <stdexcept> +#include <string> +#include <vector> + +#include "cpr/cpr.h" +#include "emotespp/emotes.hpp" +#include "ixwebsocket/IXWebSocket.h" + +namespace emotespp { + class BetterTTVWebsocketClient : public RetrieveEmoteWebsocket<Emote> { + public: + BetterTTVWebsocketClient(); + + void subscribe_emote_set(const std::string &emote_set_id); + void unsubscribe_emote_set(const std::string &emote_set_id); + + void start(); + + const std::map<std::string, std::vector<Emote>> &get_ids() const; + + private: + std::map<std::string, std::vector<Emote>> ids; + ix::WebSocket websocket; + + bool is_connected = false; + }; + + class BetterTTVAPIClient : public RetrieveEmoteAPI<Emote> { + public: + BetterTTVAPIClient() = default; + ~BetterTTVAPIClient() = default; + + std::vector<Emote> get_channel_emotes( + std::string &channel_id) const override { + cpr::Response r = + cpr::Get(cpr::Url{base_url + "/cached/users/twitch/" + channel_id}); + + if (r.status_code != 200) { + throw std::runtime_error( + "Failed to get channel emotes. Status code: " + + std::to_string(r.status_code)); + } + + nlohmann::json j = nlohmann::json::parse(r.text); + + std::vector<Emote> emotes; + + nlohmann::json channel_emotes = j["channelEmotes"]; + + std::for_each(channel_emotes.begin(), channel_emotes.end(), + [this, &emotes](const nlohmann::json &v) { + emotes.push_back(this->parse_emote(v)); + }); + + nlohmann::json shared_emotes = j["sharedEmotes"]; + + std::for_each(shared_emotes.begin(), shared_emotes.end(), + [this, &emotes](const nlohmann::json &v) { + emotes.push_back(this->parse_emote(v)); + }); + + return emotes; + } + + std::vector<Emote> get_global_emotes() const override { + cpr::Response r = cpr::Get(cpr::Url{base_url + "/emote-sets/global"}); + + if (r.status_code != 200) { + throw std::runtime_error( + "Failed to get global emotes. Status code: " + + std::to_string(r.status_code)); + } + + nlohmann::json j = nlohmann::json::parse(r.text); + + std::vector<Emote> emotes; + std::for_each(j.begin(), j.end(), + [this, &emotes](const nlohmann::json &v) { + emotes.push_back(this->parse_emote(v)); + }); + + return emotes; + } + + private: + Emote parse_emote(const nlohmann::json &j) const; + const std::string base_url = "https://api.betterttv.net/3"; + }; +} +#endif
\ No newline at end of file diff --git a/src/betterttv.cpp b/src/betterttv.cpp new file mode 100644 index 0000000..7cb2453 --- /dev/null +++ b/src/betterttv.cpp @@ -0,0 +1,161 @@ +#ifdef BUILD_BETTERTTV +#include "emotespp/betterttv.hpp" + +#include <algorithm> +#include <map> +#include <nlohmann/json.hpp> +#include <optional> +#include <string> +#include <vector> + +#include "emotespp/emotes.hpp" + +namespace emotespp { + BetterTTVWebsocketClient::BetterTTVWebsocketClient() { + this->websocket.setUrl("wss://sockets.betterttv.net/ws"); + this->websocket.enableAutomaticReconnection(); + + this->websocket.setOnMessageCallback( + [this](const ix::WebSocketMessagePtr &msg) { + switch (msg->type) { + case ix::WebSocketMessageType::Message: { + nlohmann::json j = nlohmann::json::parse(msg->str); + + std::string name = j["name"]; + + nlohmann::json d = j["data"]; + std::string channel = d["channel"]; + + std::vector<Emote> &cache = this->ids.at(channel); + + if (name == "emote_create") { + nlohmann::json e = d["emote"]; + + Emote emote{e["id"], e["code"], std::nullopt}; + + cache.push_back(emote); + + if (this->emote_create.has_value()) { + this->emote_create.value()(channel, std::nullopt, emote); + } + } else if (name == "emote_update") { + nlohmann::json e = d["emote"]; + + Emote emote{e["id"], e["code"], std::nullopt}; + + auto cache_e = std::find_if( + cache.begin(), cache.end(), + [&emote](const Emote &e) { return emote.id == e.id; }); + + if (cache_e != cache.end()) { + emote.original_code = cache_e->code; + cache_e->code = emote.code; + } else { + cache.push_back(emote); + } + + if (this->emote_update.has_value()) { + this->emote_update.value()(channel, std::nullopt, emote); + } + } else if (name == "emote_delete") { + std::string emote_id = d["emoteId"]; + + Emote emote{emote_id, "-", std::nullopt}; + + auto cache_e = std::find_if( + cache.begin(), cache.end(), + [&emote](const Emote &e) { return emote.id == e.id; }); + + if (cache_e != cache.end()) { + emote.code = cache_e->code; + emote.original_code = cache_e->original_code; + + cache.erase(cache_e); + } + + if (this->emote_delete.has_value()) { + this->emote_delete.value()(channel, std::nullopt, emote); + } + } + + break; + } + case ix::WebSocketMessageType::Error: + case ix::WebSocketMessageType::Close: { + this->is_connected = false; + break; + } + case ix::WebSocketMessageType::Open: { + this->is_connected = true; + std::for_each(this->ids.begin(), this->ids.end(), + [this](const auto &pair) { + this->subscribe_emote_set(pair.first); + }); + break; + } + case ix::WebSocketMessageType::Ping: + case ix::WebSocketMessageType::Pong: + case ix::WebSocketMessageType::Fragment: + break; + } + }); + } + + void BetterTTVWebsocketClient::subscribe_emote_set( + const std::string &emote_set_id) { + if (!std::any_of(this->ids.begin(), this->ids.end(), + [&emote_set_id](const auto &pair) { + return pair.first == emote_set_id; + })) { + this->ids.insert({emote_set_id, {}}); + } + + if (this->is_connected) { + nlohmann::json j; + j["name"] = "join_channel"; + + nlohmann::json d; + d["name"] = emote_set_id; + + j["data"] = d; + + this->websocket.sendUtf8Text(j.dump()); + } + } + + void BetterTTVWebsocketClient::unsubscribe_emote_set( + const std::string &emote_set_id) { + if (this->is_connected) { + nlohmann::json j; + j["name"] = "part_channel"; + + nlohmann::json d; + d["name"] = emote_set_id; + + j["data"] = d; + + this->websocket.sendUtf8Text(j.dump()); + } + + auto it = std::find_if(this->ids.begin(), this->ids.end(), + [&emote_set_id](const auto &pair) { + return pair.first == emote_set_id; + }); + + if (it != this->ids.end()) { + this->ids.erase(it); + } + } + + void BetterTTVWebsocketClient::start() { this->websocket.start(); } + + const std::map<std::string, std::vector<Emote>> & + BetterTTVWebsocketClient::get_ids() const { + return this->ids; + } + + Emote BetterTTVAPIClient::parse_emote(const nlohmann::json &value) const { + return {value["id"], value["code"], std::nullopt}; + } +} +#endif
\ No newline at end of file |
