#include "twitcheventsub.h" #include #include #include #include #include #include TwitchEventSub::TwitchEventSub(QObject *parent) : QObject(parent) , m_connected(false) { connect(&m_webSocket, &QWebSocket::connected, this, &TwitchEventSub::onWebSocketConnected); connect(&m_webSocket, &QWebSocket::disconnected, this, &TwitchEventSub::onWebSocketDisconnected); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &TwitchEventSub::onWebSocketTextMessageReceived); connect(&m_webSocket, QOverload::of(&QWebSocket::error), this, &TwitchEventSub::onWebSocketError); m_pingTimer.setInterval(30000); // 30 секунд connect(&m_pingTimer, &QTimer::timeout, this, &TwitchEventSub::onPingTimer); } TwitchEventSub::~TwitchEventSub() { disconnectFromTwitch(); } void TwitchEventSub::init(const QString &accessToken, const QString &clientId, const QString &broadcasterId) { m_accessToken = accessToken; m_clientId = clientId; m_broadcasterId = broadcasterId; } void TwitchEventSub::connectToTwitch() { if (m_webSocket.state() == QAbstractSocket::ConnectedState) return; emit onLog(0, "connectToTwitch", "Connecting to EventSub WebSocket..."); m_webSocket.open(QUrl("wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=60")); } void TwitchEventSub::disconnectFromTwitch() { m_pingTimer.stop(); m_webSocket.close(); } void TwitchEventSub::onWebSocketConnected() { emit onLog(0, "onWebSocketConnected", "WebSocket connected"); m_pingTimer.start(); m_connected = true; emit onConnected(); } void TwitchEventSub::onWebSocketDisconnected() { m_pingTimer.stop(); m_connected = false; emit onLog(1, "onWebSocketDisconnected", "WebSocket disconnected"); emit onDisconnected(); } void TwitchEventSub::onWebSocketTextMessageReceived(const QString &message) { emit onRawMessage(message); parseMessage(message); } void TwitchEventSub::onWebSocketError(QAbstractSocket::SocketError error) { emit onLog(2, "onWebSocketError", m_webSocket.errorString()); emit onError(m_webSocket.errorString()); } void TwitchEventSub::onPingTimer() { if (m_webSocket.state() == QAbstractSocket::ConnectedState) { m_webSocket.ping(); // отправляем Ping-фрейм emit onLog(3, "onPingTimer", "Ping sent"); } } void TwitchEventSub::parseMessage(const QString &message) { QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); if (!doc.isObject()) return; QJsonObject root = doc.object(); QJsonObject metadata = root.value("metadata").toObject(); QString messageType = metadata.value("message_type").toString(); QString subscriptionType = metadata.value("subscription_type").toString(); emit onLog(0, "parseMessage", QString("Message type: %1, Subscription: %2").arg(messageType, subscriptionType)); if (messageType == "session_welcome") { QJsonObject payload = root.value("payload").toObject(); QJsonObject session = payload.value("session").toObject(); m_sessionId = session.value("id").toString(); emit onLog(0, "parseMessage", "Received session_welcome, session_id: " + m_sessionId); performSubscriptions(); } else if (messageType == "notification") { QJsonObject payload = root.value("payload").toObject(); if (subscriptionType == "channel.channel_points_custom_reward_redemption.add") { emit onCustomReward(parseCustomReward(payload)); } else if (subscriptionType == "channel.follow") { emit onFollow(parseFollow(payload)); } else if (subscriptionType == "channel.subscribe") { emit onSubscribe(parseSubscribe(payload)); } else if (subscriptionType == "channel.subscription.gift") { emit onGift(parseGift(payload)); } else if (subscriptionType == "channel.raid") { emit onRaid(parseRaid(payload)); } } else if (messageType == "session_keepalive") { emit onLog(3, "parseMessage", "Received keepalive"); } } void TwitchEventSub::performSubscriptions() { if (m_sessionId.isEmpty()) { emit onLog(2, "performSubscriptions", "No session_id, cannot subscribe"); return; } // Формируем условие для каждого типа подписки subscribeTo("channel.channel_points_custom_reward_redemption.add", "1", QString("{\"broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId)); _sleep(500); subscribeTo("channel.raid", "1", QString("{\"to_broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId)); _sleep(500); subscribeTo("channel.follow", "2", QString("{\"broadcaster_user_id\":\"%1\",\"moderator_user_id\":\"%1\"}").arg(m_broadcasterId)); _sleep(500); subscribeTo("channel.subscribe", "1", QString("{\"broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId)); _sleep(500); subscribeTo("channel.subscription.gift", "1", QString("{\"broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId)); _sleep(500); } // --- Парсеры событий --- TCustomRewardEvent2 TwitchEventSub::parseCustomReward(const QJsonObject &payload) { TCustomRewardEvent2 result; QJsonObject subscription = payload.value("subscription").toObject(); result.subscription.id = subscription.value("id").toString(); result.subscription.type = subscription.value("type").toString(); result.subscription.version = subscription.value("version").toString(); result.subscription.status = subscription.value("status").toString(); result.subscription.cost = subscription.value("cost").toInt(); result.subscription.created_at = subscription.value("created_at").toString(); QJsonObject cond = subscription.value("condition").toObject(); result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString(); result.subscription.condition.reward_id = cond.value("reward_id").toString(); QJsonObject transp = subscription.value("transport").toObject(); result.subscription.transport.method = transp.value("method").toString(); QJsonObject event = payload.value("event").toObject(); result.event.id = event.value("id").toString(); result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString(); result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString(); result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString(); result.event.user_id = event.value("user_id").toString(); result.event.user_login = event.value("user_login").toString(); result.event.user_name = event.value("user_name").toString(); result.event.user_input = event.value("user_input").toString(); QJsonObject reward = event.value("reward").toObject(); result.event.reward.id = reward.value("id").toString(); result.event.reward.title = reward.value("title").toString(); result.event.reward.cost = reward.value("cost").toInt(); result.event.reward.prompt = reward.value("prompt").toString(); return result; } TFollowEvent TwitchEventSub::parseFollow(const QJsonObject &payload) { TFollowEvent result; QJsonObject subscription = payload.value("subscription").toObject(); result.subscription.id = subscription.value("id").toString(); result.subscription.type = subscription.value("type").toString(); result.subscription.version = subscription.value("version").toString(); result.subscription.status = subscription.value("status").toString(); result.subscription.cost = subscription.value("cost").toInt(); result.subscription.created_at = subscription.value("created_at").toString(); QJsonObject cond = subscription.value("condition").toObject(); result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString(); QJsonObject transp = subscription.value("transport").toObject(); result.subscription.transport.method = transp.value("method").toString(); QJsonObject event = payload.value("event").toObject(); result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString(); result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString(); result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString(); result.event.user_id = event.value("user_id").toString(); result.event.user_login = event.value("user_login").toString(); result.event.user_name = event.value("user_name").toString(); result.event.followed_at = event.value("followed_at").toString(); return result; } TSubEvent TwitchEventSub::parseSubscribe(const QJsonObject &payload) { TSubEvent result; QJsonObject subscription = payload.value("subscription").toObject(); result.subscription.id = subscription.value("id").toString(); result.subscription.type = subscription.value("type").toString(); result.subscription.version = subscription.value("version").toString(); result.subscription.status = subscription.value("status").toString(); result.subscription.cost = subscription.value("cost").toInt(); result.subscription.created_at = subscription.value("created_at").toString(); QJsonObject cond = subscription.value("condition").toObject(); result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString(); QJsonObject transp = subscription.value("transport").toObject(); result.subscription.transport.method = transp.value("method").toString(); QJsonObject event = payload.value("event").toObject(); result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString(); result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString(); result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString(); result.event.user_id = event.value("user_id").toString(); result.event.user_login = event.value("user_login").toString(); result.event.user_name = event.value("user_name").toString(); result.event.tier = event.value("tier").toString(); result.event.is_gift = event.value("is_gift").toBool(); return result; } TGiftEvent TwitchEventSub::parseGift(const QJsonObject &payload) { TGiftEvent result; QJsonObject subscription = payload.value("subscription").toObject(); result.subscription.id = subscription.value("id").toString(); result.subscription.type = subscription.value("type").toString(); result.subscription.version = subscription.value("version").toString(); result.subscription.status = subscription.value("status").toString(); result.subscription.cost = subscription.value("cost").toInt(); result.subscription.created_at = subscription.value("created_at").toString(); QJsonObject cond = subscription.value("condition").toObject(); result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString(); QJsonObject transp = subscription.value("transport").toObject(); result.subscription.transport.method = transp.value("method").toString(); QJsonObject event = payload.value("event").toObject(); result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString(); result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString(); result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString(); result.event.user_id = event.value("user_id").toString(); result.event.user_login = event.value("user_login").toString(); result.event.user_name = event.value("user_name").toString(); result.event.total = event.value("total").toInt(); result.event.tier = event.value("tier").toString(); result.event.cumulative_total = event.value("cumulative_total").toInt(); result.event.is_anonymous = event.value("is_anonymous").toBool(); return result; } TRaidEvent TwitchEventSub::parseRaid(const QJsonObject &payload) { TRaidEvent result; QJsonObject subscription = payload.value("subscription").toObject(); result.subscription.id = subscription.value("id").toString(); result.subscription.type = subscription.value("type").toString(); result.subscription.version = subscription.value("version").toString(); result.subscription.status = subscription.value("status").toString(); result.subscription.cost = subscription.value("cost").toInt(); result.subscription.created_at = subscription.value("created_at").toString(); QJsonObject cond = subscription.value("condition").toObject(); result.subscription.condition.to_broadcaster_user_id = cond.value("to_broadcaster_user_id").toString(); QJsonObject transp = subscription.value("transport").toObject(); result.subscription.transport.method = transp.value("method").toString(); QJsonObject event = payload.value("event").toObject(); result.event.from_broadcaster_user_id = event.value("from_broadcaster_user_id").toString(); result.event.from_broadcaster_user_login = event.value("from_broadcaster_user_login").toString(); result.event.from_broadcaster_user_name = event.value("from_broadcaster_user_name").toString(); result.event.to_broadcaster_user_id = event.value("to_broadcaster_user_id").toString(); result.event.to_broadcaster_user_login = event.value("to_broadcaster_user_login").toString(); result.event.to_broadcaster_user_name = event.value("to_broadcaster_user_name").toString(); result.event.viewers = event.value("viewers").toInt(); return result; } bool TwitchEventSub::subscribeTo(const QString &eventType, const QString &version, const QString &condition) { emit onLog(0, "subscribeTo", QString("Subscribing to %1").arg(eventType)); QJsonObject transport; transport["method"] = "websocket"; transport["session_id"] = m_sessionId; QJsonObject requestBody; requestBody["type"] = eventType; requestBody["version"] = version; requestBody["condition"] = QJsonDocument::fromJson(condition.toUtf8()).object(); requestBody["transport"] = transport; QNetworkRequest request(QUrl("https://api.twitch.tv/helix/eventsub/subscriptions")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); request.setRawHeader("Client-Id", m_clientId.toUtf8()); QNetworkReply *reply = m_networkManager.post(request, QJsonDocument(requestBody).toJson()); connect(reply, &QNetworkReply::finished, this, [this, reply, eventType]() { if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); emit onLog(3, "subscribeTo", QString("Response: %1").arg(QString(data))); if (data.contains("\"status\":\"enabled\"")) { emit onLog(0, "subscribeTo", QString("Subscription %1 successful").arg(eventType)); } else { emit onLog(1, "subscribeTo", QString("Subscription %1 failed: %2").arg(eventType, QString(data))); } } else { emit onLog(2, "subscribeTo", QString("Network error: %1").arg(reply->errorString())); } reply->deleteLater(); // обязательно удаляем }); return true; }