272 lines
7.8 KiB
C++
272 lines
7.8 KiB
C++
#include "websocketclient.h"
|
||
#include <QDebug>
|
||
#include <QJsonDocument>
|
||
#include <QJsonObject>
|
||
#include <QDateTime>
|
||
#include <QCoreApplication>
|
||
#include <synchapi.h>
|
||
#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<QAbstractSocket::SocketError>::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<QSslError> &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;
|
||
}
|