summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormoderndevslulw <moderndevslulw@alright.party>2025-04-05 15:21:42 +0500
committermoderndevslulw <moderndevslulw@alright.party>2025-04-05 15:21:42 +0500
commit966ad0c6acee8bf19128b71e9710eb1065f76608 (patch)
treef42657b14c4b63098721a1fa4b4d842125406828
parente67424780c981a8b032fc9f1e7ec7f09cd2ffd88 (diff)
feat: betterttv websocket and api clientsHEADmaster
-rw-r--r--CMakeLists.txt9
-rw-r--r--include/emotespp/betterttv.hpp96
-rw-r--r--src/betterttv.cpp161
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