summaryrefslogtreecommitdiff
path: root/bot/src/api
diff options
context:
space:
mode:
authorilotterytea <iltsu@alright.party>2025-07-01 18:54:10 +0500
committerilotterytea <iltsu@alright.party>2025-07-01 18:54:10 +0500
commit8c2e0d8c1faabb76f2e75cddc8b7da2caabf8da6 (patch)
tree5d227bd7529c0fa7e248737d430212275621bb93 /bot/src/api
parent25a25a07a7b68443791974800dbfc77d223392d6 (diff)
feat: KICK SUPPORT RAAAAH!!!! emojiAngry
Diffstat (limited to 'bot/src/api')
-rw-r--r--bot/src/api/kick.cpp125
-rw-r--r--bot/src/api/kick.hpp39
2 files changed, 164 insertions, 0 deletions
diff --git a/bot/src/api/kick.cpp b/bot/src/api/kick.cpp
new file mode 100644
index 0000000..9b64f48
--- /dev/null
+++ b/bot/src/api/kick.cpp
@@ -0,0 +1,125 @@
+#include "api/kick.hpp"
+
+#include <chrono>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "cpr/api.h"
+#include "cpr/cprtypes.h"
+#include "cpr/response.h"
+#include "logger.hpp"
+#include "nlohmann/json.hpp"
+#include "nlohmann/json_fwd.hpp"
+#include "utils/chrono.hpp"
+#include "utils/string.hpp"
+
+namespace bot::api {
+ std::vector<KickChannel> parse_json_channels(const nlohmann::json &value) {
+ if (!value.contains("data")) {
+ log::warn("api/kick", "No data object in Kick API get_channels response");
+ return {};
+ }
+
+ std::vector<KickChannel> channels;
+ nlohmann::json d = value["data"];
+
+ for (const nlohmann::json &c : d) {
+ channels.push_back(
+ {c["broadcaster_user_id"], c["slug"], c["stream_title"],
+ c["category"]["name"], c["stream"]["is_live"],
+ utils::chrono::string_to_time_point(c["stream"]["start_time"],
+ "%Y-%m-%dT%H:%M:%SZ")});
+ }
+
+ return channels;
+ }
+
+ std::vector<KickChannel> KickAPIClient::get_channels(
+ const std::vector<int> &ids) const {
+ if (this->authorization_key.empty()) {
+ log::error("api/kick", "You must be authorized before using Kick API");
+ return {};
+ }
+
+ if (ids.empty()) {
+ return {};
+ }
+
+ cpr::Response r = cpr::Get(
+ cpr::Url{this->base_url + "/public/v1/channels?broadcaster_user_id=" +
+ utils::string::str(ids.begin(), ids.end(), ',')},
+ cpr::Header{{"Authorization", "Bearer " + this->authorization_key}});
+
+ if (r.status_code != 200) {
+ log::error("api/kick", "Failed to get Kick channels. Status code: " +
+ std::to_string(r.status_code));
+ return {};
+ }
+
+ nlohmann::json j = nlohmann::json::parse(r.text);
+ return parse_json_channels(j);
+ }
+
+ std::vector<KickChannel> KickAPIClient::get_channels(
+ const std::string &slug) const {
+ if (this->authorization_key.empty()) {
+ log::error("api/kick", "You must be authorized before using Kick API");
+ return {};
+ }
+
+ if (slug.empty()) {
+ return {};
+ }
+
+ cpr::Response r = cpr::Get(
+ cpr::Url{this->base_url + "/public/v1/channels?slug=" + slug},
+ cpr::Header{{"Authorization", "Bearer " + this->authorization_key}});
+
+ if (r.status_code != 200) {
+ log::error("api/kick", "Failed to get Kick channels. Status code: " +
+ std::to_string(r.status_code));
+ return {};
+ }
+
+ nlohmann::json j = nlohmann::json::parse(r.text);
+ return parse_json_channels(j);
+ }
+
+ void KickAPIClient::authorize() {
+ cpr::Response r = cpr::Post(
+ cpr::Url{"https://id.kick.com/oauth/"
+ "token?grant_type=client_credentials&client_id=" +
+ this->client_id + "&client_secret=" + this->client_secret});
+
+ if (r.status_code != 200) {
+ throw std::runtime_error(
+ "Failed to authorize in Kick API. Status code: " +
+ std::to_string(r.status_code));
+ }
+
+ nlohmann::json j = nlohmann::json::parse(r.text);
+
+ this->authorization_key = j["access_token"];
+ this->expires_in = j["expires_in"];
+ this->token_acquired_timestamp = std::time(nullptr);
+
+ log::info("api/kick", "Successfully authorized in Kick API!");
+ }
+
+ void KickAPIClient::refresh_token_thread() {
+ while (true) {
+ std::this_thread::sleep_for(std::chrono::seconds(60));
+ if (this->client_id.empty() || this->client_secret.empty()) {
+ break;
+ } else if (std::time(nullptr) - this->token_acquired_timestamp <
+ this->expires_in - 300) {
+ continue;
+ }
+
+ log::info("api/kick", "Kick token is going to expire. Refreshing...");
+ this->authorize();
+ }
+ }
+} \ No newline at end of file
diff --git a/bot/src/api/kick.hpp b/bot/src/api/kick.hpp
new file mode 100644
index 0000000..9b29b24
--- /dev/null
+++ b/bot/src/api/kick.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <chrono>
+#include <string>
+#include <vector>
+
+namespace bot::api {
+ struct KickChannel {
+ int broadcaster_user_id;
+ std::string slug, stream_title, stream_game_name;
+ bool is_live;
+ std::chrono::system_clock::time_point start_time;
+ };
+
+ class KickAPIClient {
+ public:
+ KickAPIClient(const std::string &client_id,
+ const std::string &client_secret)
+ : client_id(client_id), client_secret(client_secret) {
+ this->authorize();
+ };
+
+ ~KickAPIClient() = default;
+
+ std::vector<KickChannel> get_channels(const std::vector<int> &ids) const;
+ std::vector<KickChannel> get_channels(const std::string &slug) const;
+
+ void refresh_token_thread();
+
+ private:
+ void authorize();
+
+ std::string authorization_key;
+ int expires_in;
+ long token_acquired_timestamp;
+ const std::string base_url = "https://api.kick.com", client_id,
+ client_secret;
+ };
+} \ No newline at end of file