#include "websocketclient.h" #include #include #include #include #include #include #include "logmanager.h" #include "qregularexpression.h" WebSocketClient::WebSocketClient(QObject *parent) : QObject(parent), m_webSocket(nullptr), m_isConnected(false) { m_pingTimer = new QTimer(this); m_pingTimer->setInterval(60000); // Ping каждые 30 секунд m_channelJoined = false; // Используем новый синтаксис подключения сигналов для Qt5 connect(m_pingTimer, &QTimer::timeout, this, &WebSocketClient::onPingTimeout); } WebSocketClient::~WebSocketClient() { disconnectFromServer(); delete m_webSocket; } bool WebSocketClient::connectToServer(const QString &url) { if (m_webSocket) { disconnectFromServer(); delete m_webSocket; m_webSocket = nullptr; } m_webSocket = new QWebSocket(); // Подключаем сигналы с новым синтаксисом Qt5 connect(m_webSocket, &QWebSocket::connected, this, &WebSocketClient::onConnectedInternal); connect(m_webSocket, &QWebSocket::disconnected, this, &WebSocketClient::onDisconnectedInternal); connect(m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocketClient::onTextMessageReceived); connect(m_webSocket, QOverload::of(&QWebSocket::error), this, &WebSocketClient::onErrorInternal); connect(m_webSocket, &QWebSocket::sslErrors, this, &WebSocketClient::onSslErrors); m_webSocket->open(QUrl(url)); return true; } bool WebSocketClient::connectToTwitchChat(const QString &oauthToken, const QString &nickname, const QString &channel) { m_nickname = nickname; m_currentChannel = channel; m_oauthToken = oauthToken; // URL для подключения к Twitch IRC через WebSocket QString url = "wss://irc-ws.chat.twitch.tv:443"; return connectToServer(url); } void WebSocketClient::onConnectedInternal() { m_isConnected = true; m_pingTimer->start(); // Аутентификация в Twitch authenticate(m_oauthToken, m_nickname); emit onConnected(); } void WebSocketClient::onDisconnectedInternal() { m_isConnected = false; m_pingTimer->stop(); m_channelJoined = false; emit onDisconnected(); } void WebSocketClient::send(const QString &data) { if (m_webSocket && m_isConnected) { m_webSocket->sendTextMessage(data); } else { } } void WebSocketClient::sendMessage(const QString &message) { send(message); } void WebSocketClient::sendIrcCommand(const QString &command, const QString ¶meters) { QString message = command; if (!parameters.isEmpty()) { message += " " + parameters; } message += "\r\n"; send(message); } void WebSocketClient::authenticate(const QString &oauthToken, const QString &nickname) { // PASS команда для аутентификации if (!oauthToken.startsWith("oauth:")) { sendIrcCommand("PASS", "oauth:" + oauthToken); } else { sendIrcCommand("PASS", oauthToken); } Sleep(1000); // NICK команда sendIrcCommand("NICK", nickname); // CAP REQ для получения дополнительных возможностей sendIrcCommand("CAP REQ", ":twitch.tv/membership twitch.tv/tags twitch.tv/commands"); } void WebSocketClient::joinChannel(const QString &channel) { if (m_isConnected) { m_channelJoined = true; sendIrcCommand("JOIN", "#" + channel.toLower()); } } void WebSocketClient::sendChatMessage(const QString &channel, const QString &message) { if (m_isConnected) { sendIrcCommand("PRIVMSG", "#" + channel.toLower() + " :" + message); } } void WebSocketClient::pingServer() { if (m_isConnected) { send("PING :tmi.twitch.tv\r\n"); } } void WebSocketClient::onPingTimeout() { pingServer(); } void WebSocketClient::onTextMessageReceived(const QString &message) { QStringList lines = message.split("\r\n", Qt::SkipEmptyParts); for (const QString &line : lines) { if (line.isEmpty()) continue; qDebug() << "RAW LINE:" << line; // Определяем тип сообщения bool isPing = line.startsWith("PING"); bool isPrivmsg = false; bool isUserState = line.contains("USERSTATE"); bool isRoomState = line.contains("ROOMSTATE"); bool isUserNotice = line.contains("USERNOTICE"); bool isJoin = line.contains(" JOIN "); bool isNamesList = (line.contains("353") || line.contains("366")); bool isCapAck = line.contains("CAP * ACK"); // Проверяем PRIVMSG отдельно, чтобы избежать ложных срабатываний if (line.contains("PRIVMSG", Qt::CaseInsensitive)) { // Проверяем структуру PRIVMSG сообщения // Должно быть: @теги :префикс PRIVMSG #канал :сообщение if (!isUserState && !isRoomState && !isUserNotice) { // Проверяем наличие канала и текста сообщения if (line.contains(" #") && line.contains(" :")) { isPrivmsg = true; } } } // Обработка в зависимости от типа if (isPing) { send("PONG :tmi.twitch.tv\r\n"); } else if (isCapAck) { qDebug() << "Capabilities acknowledged"; } else if (isPrivmsg) { qDebug() << "PRIVMSG detected (final):" << line; emit onNewMessage(line); } else if (isJoin) { qDebug() << "JOIN detected:" << line; // emit onUserJoined(line); } else if (isUserState) { qDebug() << "USERSTATE detected:" << line; // emit onUserState(line); } else if (isRoomState) { qDebug() << "ROOMSTATE detected:" << line; // emit onRoomState(line); } else if (isUserNotice) { qDebug() << "USERNOTICE detected:" << line; // emit onUserNotice(line); } else if (isNamesList) { qDebug() << "NAMES list:" << line; } else if (line.contains("001") && !m_currentChannel.isEmpty()) { joinChannel(m_currentChannel); } else { qDebug() << "OTHER message:" << line; } } } void WebSocketClient::onErrorInternal(QAbstractSocket::SocketError error) { QString errorMsg = QString("WebSocket error: %1 - %2") .arg(error) .arg(m_webSocket ? m_webSocket->errorString() : "Unknown error"); emit onError(errorMsg); } void WebSocketClient::onSslErrors(const QList &errors) { QStringList errorMessages; foreach (const QSslError &error, errors) { errorMessages.append(error.errorString()); } // В разработке можно игнорировать SSL ошибки, но в продакшене это опасно if (m_webSocket) { m_webSocket->ignoreSslErrors(); } } void WebSocketClient::disconnectFromServer() { if (m_webSocket && m_isConnected) { // Отправляем команду выхода из канала для Twitch if (!m_currentChannel.isEmpty()) { sendIrcCommand("PART", "#" + m_currentChannel.toLower()); } m_webSocket->close(); m_isConnected = false; m_pingTimer->stop(); } } bool WebSocketClient::isConnected() const { return m_isConnected; }