TTW_Bot/twitchclient.cpp

308 lines
9.2 KiB
C++

#include "twitchclient.h"
#include <QDebug>
#include <QCryptographicHash>
#include <QRandomGenerator>
#include <QCoreApplication>
#include <QDateTime>
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<const QList<QSslError>&>::of(&QSslSocket::sslErrors),
this, &TwitchClient::onSslErrors);
connect(m_socket, QOverload<QAbstractSocket::SocketError>::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<char>(0x80 | opcode));
// Маска и длина
if (data.size() <= 125) {
frame.append(static_cast<char>(0x80 | data.size()));
} else if (data.size() <= 65535) {
frame.append(static_cast<char>(0x80 | 126));
frame.append(static_cast<char>((data.size() >> 8) & 0xFF));
frame.append(static_cast<char>(data.size() & 0xFF));
} else {
frame.append(static_cast<char>(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 &params)
{
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<QSslError> &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;
}