#include "twitchclient.h" #include #include #include #include #include TwitchClient::TwitchClient(QObject *parent) : QObject(parent) , m_socket(nullptr) , m_webSocketHandshakeDone(false) , m_ircAuthenticated(false) , m_channelJoined(false) { m_writeTimer = new QTimer(this); m_writeTimer->setInterval(10); // 10ms для отправки данных connect(m_writeTimer, &QTimer::timeout, this, &TwitchClient::sendQueuedData); m_pingTimer = new QTimer(this); m_pingTimer->setInterval(30000); // 30 секунд connect(m_pingTimer, &QTimer::timeout, this, &TwitchClient::sendPing); } TwitchClient::~TwitchClient() { disconnect(); } void TwitchClient::connectToTwitch(const QString &oauthToken, const QString &nickname, const QString &channel) { if (m_socket) { disconnect(); delete m_socket; } m_socket = new QSslSocket(this); m_nickname = nickname; m_channel = channel; m_oauthToken = oauthToken; m_webSocketHandshakeDone = false; m_ircAuthenticated = false; m_channelJoined = false; m_receiveBuffer.clear(); // Подключаем сигналы connect(m_socket, &QSslSocket::connected, this, &TwitchClient::onConnected); connect(m_socket, &QSslSocket::readyRead, this, &TwitchClient::onReadyRead); connect(m_socket, &QSslSocket::disconnected, this, &TwitchClient::onDisconnected); connect(m_socket, QOverload&>::of(&QSslSocket::sslErrors), this, &TwitchClient::onSslErrors); connect(m_socket, QOverload::of(&QSslSocket::error), this, &TwitchClient::onSocketError); // Настраиваем SSL m_socket->setPeerVerifyMode(QSslSocket::VerifyNone); m_socket->connectToHostEncrypted("irc-ws.chat.twitch.tv", 443); } void TwitchClient::onConnected() { // SSL handshake начинается автоматически } void TwitchClient::onReadyRead() { m_receiveBuffer.append(m_socket->readAll()); if (!m_webSocketHandshakeDone) { // Обработка WebSocket handshake int headerEnd = m_receiveBuffer.indexOf("\r\n\r\n"); if (headerEnd != -1) { QString response = QString::fromUtf8(m_receiveBuffer.left(headerEnd)); if (response.contains("101 Switching Protocols")) { m_webSocketHandshakeDone = true; m_receiveBuffer = m_receiveBuffer.mid(headerEnd + 4); // Запускаем таймеры m_writeTimer->start(); m_pingTimer->start(); emit systemMessage("WebSocket connected"); emit connected(); // Отправляем PING для проверки sendPing(); } } } else { // Обработка WebSocket фреймов while (m_receiveBuffer.size() >= 2) { // Простой парсинг WebSocket фреймов // Для продуктивного использования нужен полноценный парсер // Ищем конец фрейма (предполагаем текстовые сообщения) QString data = QString::fromUtf8(m_receiveBuffer); QStringList lines = data.split("\r\n", Qt::SkipEmptyParts); foreach (const QString &line, lines) { if (line.startsWith("PING")) { // Отвечаем на PING sendWebSocketFrame("PONG :tmi.twitch.tv"); emit systemMessage("PING/PONG"); } else if (!line.isEmpty() && line != "\n") { parseIrcMessage(line); } } m_receiveBuffer.clear(); break; } } } void TwitchClient::sendWebSocketFrame(const QByteArray &data, quint8 opcode) { QByteArray frame = createWebSocketFrame(data, opcode); m_writeQueue.enqueue(frame); if (!m_writeTimer->isActive()) { m_writeTimer->start(); } } QByteArray TwitchClient::createWebSocketFrame(const QByteArray &data, quint8 opcode) { QByteArray frame; // FIN = 1, opcode frame.append(static_cast(0x80 | opcode)); // Маска и длина if (data.size() <= 125) { frame.append(static_cast(0x80 | data.size())); } else if (data.size() <= 65535) { frame.append(static_cast(0x80 | 126)); frame.append(static_cast((data.size() >> 8) & 0xFF)); frame.append(static_cast(data.size() & 0xFF)); } else { frame.append(static_cast(0x80 | 127)); // Для больших размеров (не нужно для Twitch) } // Маскирующий ключ (4 случайных байта) QByteArray mask(4, 0); for (int i = 0; i < 4; ++i) { mask[i] = QRandomGenerator::global()->generate() & 0xFF; } frame.append(mask); // Маскированные данные QByteArray maskedData = data; for (int i = 0; i < maskedData.size(); ++i) { maskedData[i] = maskedData[i] ^ mask[i % 4]; } frame.append(maskedData); return frame; } void TwitchClient::sendIrcCommand(const QString &command, const QString ¶ms) { QString message = command; if (!params.isEmpty()) { message += " " + params; } message += "\r\n"; sendWebSocketFrame(message.toUtf8()); } void TwitchClient::parseIrcMessage(const QString &message) { if (message.startsWith("PING")) { sendWebSocketFrame("PONG :tmi.twitch.tv"); } else if (message.contains("001")) { // Добро пожаловать сообщение m_ircAuthenticated = true; emit systemMessage("Authenticated with Twitch"); // Присоединяемся к каналу sendIrcCommand("JOIN", "#" + m_channel.toLower()); } else if (message.contains("JOIN")) { if (message.contains("#" + m_channel.toLower())) { m_channelJoined = true; emit systemMessage("Joined channel: " + m_channel); } } else if (message.contains("PRIVMSG")) { // Парсим сообщение чата // Формат: :nick!nick@nick.tmi.twitch.tv PRIVMSG #channel :message QStringList parts = message.split(" ", Qt::SkipEmptyParts); if (parts.size() >= 4) { QString sender = parts[0].mid(1).split("!")[0]; QString channel = parts[2]; QString chatMessage = message.mid(message.indexOf(" :") + 2); emit newMessage(sender, channel, chatMessage); } } else if (message.contains("NOTICE")) { emit systemMessage("NOTICE: " + message); } emit systemMessage(message); } void TwitchClient::onSslErrors(const QList &errors) { QStringList errorList; for (const QSslError &error : errors) { errorList.append(error.errorString()); } // Игнорируем SSL ошибки для разработки m_socket->ignoreSslErrors(); // После игнорирования ошибок, отправляем WebSocket handshake if (m_socket->isEncrypted()) { QString handshake = QString( "GET / HTTP/1.1\r\n" "Host: irc-ws.chat.twitch.tv\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %1\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n" ).arg(QString(generateWebSocketKey())); m_socket->write(handshake.toUtf8()); } } void TwitchClient::onSocketError(QAbstractSocket::SocketError error) { QString errorMsg = QString("Socket error: %1 - %2") .arg(error) .arg(m_socket ? m_socket->errorString() : "Unknown"); } void TwitchClient::onDisconnected() { m_writeTimer->stop(); m_pingTimer->stop(); emit disconnected(); } void TwitchClient::sendQueuedData() { if (!m_writeQueue.isEmpty() && m_socket && m_socket->state() == QAbstractSocket::ConnectedState) { QByteArray data = m_writeQueue.dequeue(); m_socket->write(data); } if (m_writeQueue.isEmpty()) { m_writeTimer->stop(); } } void TwitchClient::sendPing() { if (m_webSocketHandshakeDone) { sendWebSocketFrame("PING :tmi.twitch.tv"); } } QByteArray TwitchClient::generateWebSocketKey() { QByteArray key(16, 0); for (int i = 0; i < 16; ++i) { key[i] = QRandomGenerator::global()->generate() & 0xFF; } return key.toBase64(); } void TwitchClient::sendMessage(const QString &message) { if (m_channelJoined) { sendIrcCommand("PRIVMSG", "#" + m_channel.toLower() + " :" + message); } } void TwitchClient::disconnect() { if (m_socket) { if (m_channelJoined) { sendIrcCommand("PART", "#" + m_channel.toLower()); } m_socket->disconnectFromHost(); m_writeTimer->stop(); m_pingTimer->stop(); } } bool TwitchClient::isConnected() const { return m_socket && m_socket->state() == QAbstractSocket::ConnectedState && m_webSocketHandshakeDone; }