#include "ttw_api.h" #include "qeventloop.h" #include "ttw_types.h" #include #include #include #include #include #include #include #include TTwAPI::TTwAPI(QObject *parent) : QObject(parent) , m_networkManager(new QNetworkAccessManager(this)) { // Настройка таймаутов //m_networkManager->Timeout(30000); // 30 секунд } TTwAPI::~TTwAPI() { m_networkManager->deleteLater(); } void TTwAPI::init(const QString &clientId, const QString &token, const QString &streamerToken, const QString &channel, const QString &botName) { m_clientId = clientId; m_tokenApi = token; m_tokenApiStreamer = streamerToken; m_channelName = channel; m_botName = botName; toLog(1, "TTwAPI::init", "API инициализирован для канала: " + channel); } QString TTwAPI::sendRequest(const QString &url, const QString &method, const QByteArray &data, const QString &token) { QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Client-ID", m_clientId.toUtf8()); QString authHeader = token.startsWith("Bearer") ? token : "Bearer " + token; request.setRawHeader("Authorization", authHeader.toUtf8()); QNetworkReply *reply = nullptr; if (method.toUpper() == "GET") { reply = m_networkManager->get(request); } else if (method.toUpper() == "POST") { reply = m_networkManager->post(request, data); } else if (method.toUpper() == "DELETE") { reply = m_networkManager->sendCustomRequest(request, "DELETE", data); } else if (method.toUpper() == "PATCH") { reply = m_networkManager->sendCustomRequest(request, "PATCH", data); } else if (method.toUpper() == "PUT") { reply = m_networkManager->sendCustomRequest(request, "PUT", data); } if (!reply) { toLog(2, "TTwAPI::sendRequest", "Не удалось создать запрос: " + url); return QString(); } // Ожидание завершения запроса (синхронно) QEventLoop loop; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); loop.exec(); // Проверка ошибок if (reply->error() != QNetworkReply::NoError) { QString errorMsg = QString("Ошибка %1: %2").arg(reply->error()).arg(reply->errorString()); toLog(2, "TTwAPI::sendRequest", errorMsg); if (reply->error() == QNetworkReply::AuthenticationRequiredError) { emit tokenExpired(errorMsg); } reply->deleteLater(); return QString(); } QString response = reply->readAll(); reply->deleteLater(); return response; } QString TTwAPI::getTTW(const QString &method, const QString &clientId, bool isStreamer) { Q_UNUSED(clientId); QString baseUrl = "https://api.twitch.tv/helix/"; QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi; return sendRequest(baseUrl + method, "GET", QByteArray(), token); } QString TTwAPI::deleteTTW(const QString &method, const QString &clientId, bool isStreamer) { Q_UNUSED(clientId); QString baseUrl = "https://api.twitch.tv/helix/"; QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi; return sendRequest(baseUrl + method, "DELETE", QByteArray(), token); } QString TTwAPI::postTTW(const QString &method, const QString &clientId, const QByteArray &data, bool isStreamer) { Q_UNUSED(clientId); QString baseUrl = "https://api.twitch.tv/helix/"; QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi; return sendRequest(baseUrl + method, "POST", data, token); } QString TTwAPI::patchTTW(const QString &method, const QString &clientId, const QByteArray &data, bool isStreamer) { Q_UNUSED(clientId); QString baseUrl = "https://api.twitch.tv/helix/"; QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi; return sendRequest(baseUrl + method, "PATCH", data, token); } void TTwAPI::getTTWStat(const QString &channel, int &avgViewers, int &maxViewers, int &hoursWatched, int &followers, int &followersTotal) { Q_UNUSED(channel); QString response = getTTW("channels?broadcaster_id=" + getRoomId(), m_clientId); if (response.isEmpty()) { toLog(2, "TTwAPI::getTTWStat", "Пустой ответ от API"); return; } QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); if (!doc.isObject()) { toLog(2, "TTwAPI::getTTWStat", "Неверный JSON формат"); return; } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (data.isEmpty()) { toLog(2, "TTwAPI::getTTWStat", "Нет данных в ответе"); return; } QJsonObject channelData = data[0].toObject(); // Парсинг данных (заглушка - реальные поля могут отличаться) avgViewers = channelData["average_viewers"].toInt(); maxViewers = channelData["peak_viewers"].toInt(); hoursWatched = channelData["hours_watched"].toInt(); followers = channelData["followers_gained"].toInt(); followersTotal = channelData["followers_total"].toInt(); } QDate TTwAPI::getFollow(const QString &id) { // Правильный endpoint для проверки, является ли пользователь фоловером QString response = getTTW( QString("channels/followers?user_id=%1&broadcaster_id=%2") .arg(id) .arg(getRoomId()), m_clientId, true ); if (response.isEmpty()) { return QDate(); } QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); if (!doc.isObject()) { return QDate(); } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (data.isEmpty()) { // Пользователь не фоловер return QDate(); } QJsonObject followData = data[0].toObject(); QString followedAt = followData["followed_at"].toString(); return QDate::fromString(followedAt, Qt::ISODate); } User TTwAPI::getUserByLogin(const QString &login) { User user; user.login = login.toLower(); QString response = getTTW("users?login=" + login, m_clientId); if (response.isEmpty()) { toLog(2, "TTwAPI::getUserByLogin", "Не удалось получить данные пользователя: " + login); return user; } QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); if (!doc.isObject()) { return user; } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (data.isEmpty()) { return user; } QJsonObject userData = data[0].toObject(); user.id = userData["id"].toString(); user.displayName = userData["display_name"].toString(); user.createdAt = QDate::fromString(userData["created_at"].toString(), Qt::ISODate); // Получаем информацию о подписке user.followAt = getFollow(user.id); return user; } void TTwAPI::setModerator(const QString &id) { QJsonDocument doc((QJsonObject())); postTTW("moderation/moderators?broadcaster_id=" + getRoomId() + "&user_id=" + id, m_clientId, doc.toJson(), true); } void TTwAPI::delModerator(const QString &id) { deleteTTW("moderation/moderators?broadcaster_id=" + getRoomId() + "&user_id=" + id, m_clientId, true); } void TTwAPI::setVIP(const QString &id) { QJsonDocument doc((QJsonObject())); postTTW("channels/vips?broadcaster_id=" + getRoomId() + "&user_id=" + id, m_clientId, doc.toJson(), true); } void TTwAPI::delVIP(const QString &id) { deleteTTW("channels/vips?broadcaster_id=" + getRoomId() + "&user_id=" + id, m_clientId, true); } void TTwAPI::banUser(const QString &id) { QString roomId = getRoomId(); if (roomId.isEmpty()) { toLog(2, "TTwAPI::banUser", "Не удалось получить roomId"); return; } QString botId = getBotId(); // Нужно получить ID бота if (botId.isEmpty()) { toLog(2, "TTwAPI::banUser", "Не удалось получить botId"); return; } QJsonObject innerData; innerData["user_id"] = id; innerData["reason"] = "Нарушение правил чата"; QJsonObject dataObj; dataObj["data"] = innerData; QJsonDocument doc(dataObj); postTTW("moderation/bans?broadcaster_id=" + roomId + "&moderator_id=" + botId, m_clientId, doc.toJson(), false); } void TTwAPI::banUserTime(const QString &id, int timeMinutes) { QString roomId = getRoomId(); if (roomId.isEmpty()) { toLog(2, "TTwAPI::banUserTime", "Не удалось получить roomId"); return; } QString botId = getBotId(); if (botId.isEmpty()) { toLog(2, "TTwAPI::banUserTime", "Не удалось получить botId"); return; } toLog(2, "TTwAPI::баним", id); QJsonObject innerData; innerData["user_id"] = id; innerData["duration"] = timeMinutes * 60; innerData["reason"] = "Таймаут"; QJsonObject dataObj; dataObj["data"] = innerData; QJsonDocument doc(dataObj); postTTW("moderation/bans?broadcaster_id=" + roomId + "&moderator_id=" + botId, m_clientId, doc.toJson(), false); } void TTwAPI::warnUser(const QString &id) { QString roomId = getRoomId(); if (roomId.isEmpty()) { toLog(2, "TTwAPI::warnUser", "Не удалось получить roomId"); return; } QString botId = getBotId(); if (botId.isEmpty()) { toLog(2, "TTwAPI::warnUser", "Не удалось получить botId"); return; } QJsonObject innerData; innerData["user_id"] = id; QJsonObject dataObj; dataObj["data"] = innerData; QJsonDocument doc(dataObj); postTTW("moderation/warnings?broadcaster_id=" + roomId + "&moderator_id=" + botId, m_clientId, doc.toJson(), false); } void TTwAPI::unbanUser(const QString &id) { QString roomId = getRoomId(); if (roomId.isEmpty()) { toLog(2, "TTwAPI::unbanUser", "Не удалось получить roomId"); return; } QString botId = getBotId(); if (botId.isEmpty()) { toLog(2, "TTwAPI::unbanUser", "Не удалось получить botId"); return; } deleteTTW("moderation/bans?broadcaster_id=" + roomId + "&moderator_id=" + botId + "&user_id=" + id, m_clientId, false); } QString TTwAPI::getBotId() { static QString cachedBotId; if (!cachedBotId.isEmpty()) { return cachedBotId; } if (m_botName.isEmpty()) { toLog(2, "TTwAPI::getBotId", "Имя бота не установлено"); return QString(); } QString response = getTTW("users?login=" + m_botName, m_clientId); if (response.isEmpty()) { toLog(2, "TTwAPI::getBotId", "Не удалось получить данные бота"); return QString(); } QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); if (!doc.isObject()) { toLog(2, "TTwAPI::getBotId", "Неверный JSON формат"); return QString(); } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (data.isEmpty()) { toLog(2, "TTwAPI::getBotId", "Нет данных о боте"); return QString(); } cachedBotId = data[0].toObject()["id"].toString(); return cachedBotId; } QString TTwAPI::getFollowedAtFromJson(const QString &jsonString) { QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8()); if (!doc.isObject()) { return QString(); } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (data.isEmpty()) { return QString(); } QJsonObject followData = data[0].toObject(); return followData["followed_at"].toString(); } QString TTwAPI::getRoomId() { // Кэширование ID комнаты static QString cachedRoomId; if (!cachedRoomId.isEmpty()) { return cachedRoomId; } QString response = getTTW("users?login=" + m_channelName, m_clientId); if (response.isEmpty()) { return QString(); } QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); if (!doc.isObject()) { return QString(); } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (data.isEmpty()) { return QString(); } cachedRoomId = data[0].toObject()["id"].toString(); return cachedRoomId; } QString TTwAPI::getRoomAndBot() { QString roomId = getRoomId(); QString botId; QString response = getTTW("users?login=" + m_botName, m_clientId); if (!response.isEmpty()) { QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); if (!data.isEmpty()) { botId = data[0].toObject()["id"].toString(); } } return QString("RoomID: %1, BotID: %2").arg(roomId).arg(botId); } void TTwAPI::toLog(int level, const QString &method, const QString &message) { Q_UNUSED(level); Q_UNUSED(method); Q_UNUSED(message); qDebug() << message; // uGeneral.toLog("ttw_api", method, message, level); } void TTwAPI::sendAnnouncement(const QString &message) { QString roomId = getRoomId(); if (roomId.isEmpty()) { toLog(2, "TTwAPI::sendAnnouncement", "Не удалось получить roomId"); return; } QString botId = getBotId(); if (botId.isEmpty()) { toLog(2, "TTwAPI::sendAnnouncement", "Не удалось получить botId"); return; } QJsonObject json; json["message"] = message; json["color"] = "primary"; // Можно сделать настраиваемым: "primary", "blue", "green", "orange", "purple" QJsonDocument doc(json); QString otv = postTTW("chat/announcements?broadcaster_id=" + roomId + "&moderator_id=" + botId, m_clientId, doc.toJson(), false); } void TTwAPI::sendAnnouncementToChat(const QString &message) { // Этот метод может использоваться для обычных сообщений в чат // Но для Twitch API это тот же endpoint sendAnnouncement(message); } void TTwAPI::getGlobalChatBadges(QVector &badges) { QString response = getTTW("chat/badges/global", m_clientId, false); parseBadgesFromApi(response, badges); } void TTwAPI::getCustomChatBadges(QVector &badges) { QString roomId = getRoomId(); if (roomId.isEmpty()) { toLog(2, "TTwAPI::getCustomChatBadges", "Не удалось получить roomId"); return; } QString response = getTTW("chat/badges?broadcaster_id=" + roomId, m_clientId, false); parseBadgesFromApi(response, badges); } void TTwAPI::parseBadgesFromApi(const QString &jsonString, QVector &badges) { if (jsonString.isEmpty()) { return; } QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8()); if (!doc.isObject()) { toLog(2, "TTwAPI::parseBadgesFromApi", "Неверный JSON формат"); return; } QJsonObject root = doc.object(); QJsonArray data = root["data"].toArray(); for (const QJsonValue &value : data) { QJsonObject badgeObj = value.toObject(); ChatBadge badge; badge.setId = badgeObj["set_id"].toString(); QJsonArray versions = badgeObj["versions"].toArray(); for (const QJsonValue &versionValue : versions) { QJsonObject versionObj = versionValue.toObject(); BadgeVersion version; version.id = versionObj["id"].toString(); version.imageUrl1x = versionObj["image_url_1x"].toString(); version.imageUrl2x = versionObj["image_url_2x"].toString(); version.imageUrl4x = versionObj["image_url_4x"].toString(); version.title = versionObj["title"].toString(); version.description = versionObj["description"].toString(); badge.versions.append(version); } badges.append(badge); } } bool TTwAPI::validateTwitchToken(const QString &tokenName, const QString &tokenValue, int &daysValid) { daysValid = 0; if (tokenValue.trimmed().isEmpty()) { toLog(1, "TTwAPI::validateTwitchToken", "Пустой токен: " + tokenName); return false; } QUrl url("https://id.twitch.tv/oauth2/validate"); QNetworkRequest request(url); // Устанавливаем заголовки request.setHeader(QNetworkRequest::UserAgentHeader, "YourApp/1.0"); request.setRawHeader("Accept", "application/json"); // Формируем заголовок Authorization QString authHeader = "OAuth " + tokenValue; request.setRawHeader("Authorization", authHeader.toUtf8()); // Отправляем GET запрос QNetworkReply *reply = m_networkManager->get(request); if (!reply) { toLog(2, "TTwAPI::validateTwitchToken", "Не удалось создать запрос для токена: " + tokenName); return false; } // Ожидание завершения запроса QEventLoop loop; QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); loop.exec(); // Проверяем статус ответа int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QByteArray responseData = reply->readAll(); QString responseText = QString::fromUtf8(responseData); // Обрабатываем ошибки сети if (reply->error() != QNetworkReply::NoError) { QString errorMsg = QString("Ошибка сети: %1 - %2") .arg(reply->error()) .arg(reply->errorString()); toLog(2, "TTwAPI::validateTwitchToken", errorMsg); reply->deleteLater(); return false; } reply->deleteLater(); // Обработка HTTP статусов if (statusCode == 200) { // Успешная валидация QJsonDocument doc = QJsonDocument::fromJson(responseData); if (!doc.isObject()) { toLog(2, "TTwAPI::validateTwitchToken", "Ошибка парсинга JSON для токена: " + tokenName); return false; } QJsonObject jsonObj = doc.object(); // Извлекаем expires_in (в секундах) if (jsonObj.contains("expires_in") && jsonObj["expires_in"].isDouble()) { int expiresInSeconds = jsonObj["expires_in"].toInt(); daysValid = qRound(expiresInSeconds / 86400.0); // Конвертируем секунды в дни // Логируем информацию о токене QString logMsg = QString("Токен '%1' действителен. Осталось: %2 дней. " "Client ID: %3, Логин: %4") .arg(tokenName) .arg(daysValid) .arg(jsonObj.value("client_id").toString("N/A")) .arg(jsonObj.value("login").toString("N/A")); toLog(0, "TTwAPI::validateTwitchToken", logMsg); return true; } else { toLog(2, "TTwAPI::validateTwitchToken", QString("В ответе отсутствует expires_in для токена: %1").arg(tokenName)); return false; } } else if (statusCode == 401) { // Невалидный токен toLog(2, "TTwAPI::validateTwitchToken", QString("Токен '%1' невалиден (HTTP 401)").arg(tokenName)); return false; } else { // Другие HTTP ошибки QString errorMsg = QString("HTTP %1 для токена '%2': %3") .arg(statusCode) .arg(tokenName) .arg(responseText); toLog(2, "TTwAPI::validateTwitchToken", errorMsg); return false; } } void TTwAPI::getCustomRewards(QVector &rewards, bool onlyManageable) { QString roomId = getRoomId(); if (roomId.isEmpty()) return; QString url = "channel_points/custom_rewards?broadcaster_id=" + roomId; if (onlyManageable) { url += "&only_manageable_rewards=true"; } QString response = getTTW(url, m_clientId, true); if (response.isEmpty()) return; QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); if (!doc.isObject()) return; QJsonArray data = doc.object()["data"].toArray(); for (const QJsonValue &val : data) { QJsonObject obj = val.toObject(); TCustomReward *reward = new TCustomReward; reward->id = obj["id"].toString(); reward->title = obj["title"].toString(); reward->cost = obj["cost"].toInt(); reward->prompt = obj["prompt"].toString(); reward->isUserInputRequired = obj["is_user_input_required"].toBool(); reward->isEnabled = obj["is_enabled"].toBool(); // Для manageable запроса все награды по определению управляемы reward->isManagedByBroadcaster = onlyManageable; rewards.append(reward); } } TCustomReward* TTwAPI::createCustomReward(const QString &title, const QString &cost, const QString &prompt, bool isUserInput) { QString roomId = getRoomId(); if (roomId.isEmpty()) return nullptr; QJsonObject body; body["title"] = title; body["cost"] = cost.toInt(); if (!prompt.isEmpty()) body["prompt"] = prompt; body["is_user_input_required"] = isUserInput; // Можно добавить и другие параметры: is_enabled, color и т.д. QJsonDocument doc(body); QString response = postTTW("channel_points/custom_rewards?broadcaster_id=" + roomId, m_clientId, doc.toJson(), true); if (response.isEmpty()) return nullptr; QJsonDocument respDoc = QJsonDocument::fromJson(response.toUtf8()); if (!respDoc.isObject()) return nullptr; QJsonArray data = respDoc.object()["data"].toArray(); if (data.isEmpty()) return nullptr; QJsonObject obj = data[0].toObject(); TCustomReward *reward = new TCustomReward; reward->id = obj["id"].toString(); reward->title = obj["title"].toString(); reward->cost = obj["cost"].toInt(); reward->prompt = obj["prompt"].toString(); reward->isUserInputRequired = obj["is_user_input_required"].toBool(); reward->isEnabled = obj["is_enabled"].toBool(); return reward; } void TTwAPI::updateCustomReward(TCustomReward* reward) { if (!reward) return; QString roomId = getRoomId(); if (roomId.isEmpty()) return; QJsonObject body; if (!reward->title.isEmpty()) body["title"] = reward->title; body["cost"] = reward->cost; if (!reward->prompt.isEmpty()) body["prompt"] = reward->prompt; body["is_user_input_required"] = reward->isUserInputRequired; body["is_enabled"] = reward->isEnabled; QJsonDocument doc(body); patchTTW(QString("channel_points/custom_rewards?broadcaster_id=%1&id=%2") .arg(roomId, reward->id), m_clientId, doc.toJson(), true); } bool TTwAPI::deleteCustomReward(const QString &id) { QString roomId = getRoomId(); if (roomId.isEmpty()) return false; deleteTTW("channel_points/custom_rewards?broadcaster_id=" + roomId + "&id=" + id, m_clientId, true); return true; // или проверка HTTP-статуса, но deleteTTW возвращает пустую строку при ошибке } void TTwAPI::updateRedemptionStatus(TCustomRewardEvent* event) { // event содержит id награды, id redemption и новый статус (COMPLETED/CANCELED) QString roomId = getRoomId(); if (roomId.isEmpty()) return; QJsonObject body; body["status"] = event->status; // "COMPLETED" или "CANCELED" QJsonDocument doc(body); patchTTW(QString("channel_points/custom_rewards/redemptions?broadcaster_id=%1&reward_id=%2&id=%3") .arg(roomId, event->rewardId, event->redemptionId), m_clientId, doc.toJson(), true); }