308 lines
9.2 KiB
C++
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 ¶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<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;
|
|
}
|