470 lines
15 KiB
C++
470 lines
15 KiB
C++
// emoteprovider.cpp
|
|
#include "emoteprovider.h"
|
|
#include <QJsonDocument>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonValue>
|
|
#include <QRegularExpression>
|
|
#include <QSet>
|
|
|
|
// EmoteProvider implementation
|
|
EmoteProvider::EmoteProvider(QObject *parent)
|
|
: QObject(parent)
|
|
, m_networkManager(new QNetworkAccessManager(this)) {
|
|
}
|
|
|
|
EmoteProvider::~EmoteProvider() {
|
|
// QNetworkAccessManager будет удален автоматически как дочерний объект
|
|
}
|
|
|
|
void EmoteProvider::setLogCallback(LogCallback callback) {
|
|
m_logCallback = callback;
|
|
}
|
|
|
|
void EmoteProvider::log(const QString &method, const QString &message, LogLevelemt level) {
|
|
if (m_logCallback) {
|
|
m_logCallback(metaObject()->className(), method, message, level);
|
|
}
|
|
}
|
|
|
|
// BTTVProvider implementation
|
|
BTTVProvider::BTTVProvider(QObject *parent)
|
|
: EmoteProvider(parent) {
|
|
}
|
|
|
|
BTTVProvider::~BTTVProvider() {
|
|
m_emotes.clear();
|
|
}
|
|
|
|
void BTTVProvider::fetchGlobal() {
|
|
log("fetchGlobal", "Fetching global BTTV emotes", LOG_INFO);
|
|
|
|
QNetworkRequest request(QUrl(getBaseUrl() + "emotes/global"));
|
|
request.setHeader(QNetworkRequest::UserAgentHeader,
|
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36");
|
|
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
connect(reply, &QNetworkReply::finished,
|
|
this, [this, reply]() { onGlobalReplyFinished(reply); });
|
|
}
|
|
|
|
void BTTVProvider::fetchCustom(const QString &userId) {
|
|
if (userId.isEmpty()) {
|
|
log("fetchCustom", "User ID is empty", LOG_WARNING);
|
|
return;
|
|
}
|
|
|
|
log("fetchCustom", QString("Fetching custom BTTV emotes for user: %1").arg(userId), LOG_INFO);
|
|
m_lastUserId = userId;
|
|
|
|
QNetworkRequest request(QUrl(getBaseUrl() + "users/twitch/" + userId));
|
|
request.setHeader(QNetworkRequest::UserAgentHeader,
|
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36");
|
|
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
connect(reply, &QNetworkReply::finished,
|
|
this, [this, reply]() { onCustomReplyFinished(reply); });
|
|
}
|
|
|
|
QString BTTVProvider::getEmoteUrl(const QString &emoteName) {
|
|
for (const auto &emote : m_emotes) {
|
|
if (emote.code == emoteName) {
|
|
return emote.url();
|
|
}
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
void BTTVProvider::onGlobalReplyFinished(QNetworkReply *reply) {
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
log("onGlobalReplyFinished",
|
|
QString("Network error: %1").arg(reply->errorString()),
|
|
LOG_ERROR);
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QByteArray data = reply->readAll();
|
|
parseGlobalResponse(data);
|
|
reply->deleteLater();
|
|
|
|
emit emotesLoaded();
|
|
}
|
|
|
|
void BTTVProvider::onCustomReplyFinished(QNetworkReply *reply) {
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
log("onCustomReplyFinished",
|
|
QString("Network error: %1").arg(reply->errorString()),
|
|
LOG_ERROR);
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QByteArray data = reply->readAll();
|
|
parseCustomResponse(data, m_lastUserId);
|
|
reply->deleteLater();
|
|
|
|
emit emotesLoaded();
|
|
}
|
|
|
|
void BTTVProvider::parseGlobalResponse(const QByteArray &data) {
|
|
QJsonParseError parseError;
|
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
log("parseGlobalResponse",
|
|
QString("JSON parse error: %1").arg(parseError.errorString()),
|
|
LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
if (!doc.isArray()) {
|
|
log("parseGlobalResponse", "Expected JSON array", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonArray array = doc.array();
|
|
m_emotes.clear();
|
|
|
|
for (const QJsonValue &value : array) {
|
|
if (!value.isObject()) continue;
|
|
|
|
QJsonObject obj = value.toObject();
|
|
BTTVEmote emote;
|
|
emote.id = obj.value("id").toString();
|
|
emote.code = obj.value("code").toString();
|
|
|
|
if (!emote.id.isEmpty() && !emote.code.isEmpty()) {
|
|
m_emotes.append(emote);
|
|
}
|
|
}
|
|
|
|
log("parseGlobalResponse",
|
|
QString("Loaded %1 global emotes").arg(m_emotes.size()),
|
|
LOG_INFO);
|
|
}
|
|
|
|
void BTTVProvider::parseCustomResponse(const QByteArray &data, const QString &userId) {
|
|
QJsonParseError parseError;
|
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
log("parseCustomResponse",
|
|
QString("JSON parse error: %1").arg(parseError.errorString()),
|
|
LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
if (!doc.isObject()) {
|
|
log("parseCustomResponse", "Expected JSON object", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonObject root = doc.object();
|
|
|
|
auto parseEmotesArray = [this](const QJsonArray &array) {
|
|
for (const QJsonValue &value : array) {
|
|
if (!value.isObject()) continue;
|
|
|
|
QJsonObject obj = value.toObject();
|
|
BTTVEmote emote;
|
|
emote.id = obj.value("id").toString();
|
|
emote.code = obj.value("code").toString();
|
|
|
|
if (!emote.id.isEmpty() && !emote.code.isEmpty()) {
|
|
m_emotes.append(emote);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Очищаем только пользовательские эмодзи (можно оптимизировать)
|
|
// В реальном приложении нужно хранить отдельно глобальные и пользовательские
|
|
|
|
// Парсим channelEmotes
|
|
if (root.contains("channelEmotes") && root["channelEmotes"].isArray()) {
|
|
parseEmotesArray(root["channelEmotes"].toArray());
|
|
}
|
|
|
|
// Парсим sharedEmotes
|
|
if (root.contains("sharedEmotes") && root["sharedEmotes"].isArray()) {
|
|
parseEmotesArray(root["sharedEmotes"].toArray());
|
|
}
|
|
|
|
log("parseCustomResponse",
|
|
QString("Loaded %1 custom emotes for user %2").arg(m_emotes.size()).arg(userId),
|
|
LOG_INFO);
|
|
}
|
|
|
|
// SevenTVProvider implementation
|
|
SevenTVProvider::SevenTVProvider(QObject *parent)
|
|
: EmoteProvider(parent) {
|
|
}
|
|
|
|
SevenTVProvider::~SevenTVProvider() {
|
|
m_emotes.clear();
|
|
}
|
|
|
|
void SevenTVProvider::fetchGlobal() {
|
|
log("fetchGlobal", "Fetching global 7TV emotes", LOG_INFO);
|
|
|
|
QNetworkRequest request(QUrl(getBaseUrl() + "emote-sets/global"));
|
|
request.setHeader(QNetworkRequest::UserAgentHeader,
|
|
"Mozilla/5.0");
|
|
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
connect(reply, &QNetworkReply::finished,
|
|
this, [this, reply]() { onGlobalReplyFinished(reply); });
|
|
}
|
|
|
|
void SevenTVProvider::fetchCustom(const QString &userId) {
|
|
if (userId.isEmpty()) {
|
|
log("fetchCustom", "User ID is empty", LOG_WARNING);
|
|
return;
|
|
}
|
|
|
|
log("fetchCustom", QString("Fetching custom 7TV emotes for user: %1").arg(userId), LOG_INFO);
|
|
m_lastUserId = userId;
|
|
|
|
QNetworkRequest request(QUrl(getBaseUrl() + "users/twitch/" + userId));
|
|
request.setHeader(QNetworkRequest::UserAgentHeader,
|
|
"Mozilla/5.0");
|
|
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
connect(reply, &QNetworkReply::finished,
|
|
this, [this, reply]() { onCustomReplyFinished(reply); });
|
|
}
|
|
|
|
QString SevenTVProvider::getEmoteUrl(const QString &emoteName) {
|
|
for (const auto &emote : m_emotes) {
|
|
if (emote.code == emoteName) {
|
|
return emote.url;
|
|
}
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
void SevenTVProvider::onGlobalReplyFinished(QNetworkReply *reply) {
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
log("onGlobalReplyFinished",
|
|
QString("Network error: %1").arg(reply->errorString()),
|
|
LOG_ERROR);
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QByteArray data = reply->readAll();
|
|
parseGlobalResponse(data);
|
|
reply->deleteLater();
|
|
|
|
emit emotesLoaded();
|
|
}
|
|
|
|
void SevenTVProvider::onCustomReplyFinished(QNetworkReply *reply) {
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
log("onCustomReplyFinished",
|
|
QString("Network error: %1").arg(reply->errorString()),
|
|
LOG_ERROR);
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QByteArray data = reply->readAll();
|
|
parseCustomResponse(data, m_lastUserId);
|
|
reply->deleteLater();
|
|
|
|
emit emotesLoaded();
|
|
}
|
|
|
|
void SevenTVProvider::parseGlobalResponse(const QByteArray &data) {
|
|
QJsonParseError parseError;
|
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
log("parseGlobalResponse",
|
|
QString("JSON parse error: %1").arg(parseError.errorString()),
|
|
LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
if (!doc.isObject()) {
|
|
log("parseGlobalResponse", "Expected JSON object", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonObject root = doc.object();
|
|
m_emotes.clear();
|
|
|
|
if (!root.contains("emotes") || !root["emotes"].isArray()) {
|
|
log("parseGlobalResponse", "No emotes array found", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonArray emotesArray = root["emotes"].toArray();
|
|
|
|
for (const QJsonValue &emoteValue : emotesArray) {
|
|
if (!emoteValue.isObject()) continue;
|
|
|
|
QJsonObject emoteObj = emoteValue.toObject();
|
|
SevenTVEmote emote;
|
|
|
|
emote.id = emoteObj.value("id").toString();
|
|
emote.code = emoteObj.value("name").toString();
|
|
|
|
// Парсим URL
|
|
if (emoteObj.contains("data") && emoteObj["data"].isObject()) {
|
|
QJsonObject dataObj = emoteObj["data"].toObject();
|
|
|
|
if (dataObj.contains("host") && dataObj["host"].isObject()) {
|
|
QJsonObject hostObj = dataObj["host"].toObject();
|
|
|
|
if (hostObj.contains("url")) {
|
|
QString baseUrl = "https:" + hostObj["url"].toString();
|
|
|
|
if (hostObj.contains("files") && hostObj["files"].isArray()) {
|
|
QJsonArray filesArray = hostObj["files"].toArray();
|
|
|
|
if (!filesArray.isEmpty() && filesArray[0].isObject()) {
|
|
QJsonObject fileObj = filesArray[0].toObject();
|
|
if (fileObj.contains("name")) {
|
|
emote.url = baseUrl + "/" + fileObj["name"].toString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!emote.id.isEmpty() && !emote.code.isEmpty() && !emote.url.isEmpty()) {
|
|
m_emotes.append(emote);
|
|
}
|
|
}
|
|
|
|
log("parseGlobalResponse",
|
|
QString("Loaded %1 global emotes").arg(m_emotes.size()),
|
|
LOG_INFO);
|
|
}
|
|
|
|
void SevenTVProvider::parseCustomResponse(const QByteArray &data, const QString &userId) {
|
|
QJsonParseError parseError;
|
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
log("parseCustomResponse",
|
|
QString("JSON parse error: %1").arg(parseError.errorString()),
|
|
LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
if (!doc.isObject()) {
|
|
log("parseCustomResponse", "Expected JSON object", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonObject root = doc.object();
|
|
|
|
if (!root.contains("emote_set") || !root["emote_set"].isObject()) {
|
|
log("parseCustomResponse", "No emote_set found", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonObject emoteSet = root["emote_set"].toObject();
|
|
|
|
if (!emoteSet.contains("emotes") || !emoteSet["emotes"].isArray()) {
|
|
log("parseCustomResponse", "No emotes array in emote_set", LOG_ERROR);
|
|
return;
|
|
}
|
|
|
|
QJsonArray emotesArray = emoteSet["emotes"].toArray();
|
|
|
|
for (const QJsonValue &emoteValue : emotesArray) {
|
|
if (!emoteValue.isObject()) continue;
|
|
|
|
QJsonObject emoteObj = emoteValue.toObject();
|
|
SevenTVEmote emote;
|
|
|
|
emote.id = emoteObj.value("id").toString();
|
|
emote.code = emoteObj.value("name").toString();
|
|
|
|
// Парсим URL (аналогично parseGlobalResponse)
|
|
if (emoteObj.contains("data") && emoteObj["data"].isObject()) {
|
|
QJsonObject dataObj = emoteObj["data"].toObject();
|
|
|
|
if (dataObj.contains("host") && dataObj["host"].isObject()) {
|
|
QJsonObject hostObj = dataObj["host"].toObject();
|
|
|
|
if (hostObj.contains("url")) {
|
|
QString baseUrl = "https:" + hostObj["url"].toString();
|
|
|
|
if (hostObj.contains("files") && hostObj["files"].isArray()) {
|
|
QJsonArray filesArray = hostObj["files"].toArray();
|
|
|
|
if (!filesArray.isEmpty() && filesArray[0].isObject()) {
|
|
QJsonObject fileObj = filesArray[0].toObject();
|
|
if (fileObj.contains("name")) {
|
|
emote.url = baseUrl + "/" + fileObj["name"].toString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!emote.id.isEmpty() && !emote.code.isEmpty() && !emote.url.isEmpty()) {
|
|
m_emotes.append(emote);
|
|
}
|
|
}
|
|
|
|
log("parseCustomResponse",
|
|
QString("Loaded %1 custom emotes for user %2").arg(m_emotes.size()).arg(userId),
|
|
LOG_INFO);
|
|
}
|
|
|
|
QString BTTVProvider::cleanMessage(const QString& message) const
|
|
{
|
|
if (m_emotes.isEmpty())
|
|
return message;
|
|
|
|
// Собираем все коды эмоций в множество для быстрого поиска
|
|
QSet<QString> codes;
|
|
for (const BTTVEmote& emote : m_emotes) {
|
|
codes.insert(emote.code);
|
|
}
|
|
|
|
// Разбиваем сообщение на токены (слова и не-слова)
|
|
QRegularExpression wordSplitter("(\\w+|[^\\w]+)");
|
|
QRegularExpressionMatchIterator it = wordSplitter.globalMatch(message);
|
|
QStringList parts;
|
|
while (it.hasNext()) {
|
|
QRegularExpressionMatch match = it.next();
|
|
QString token = match.captured(0);
|
|
// Если токен состоит только из букв/цифр и является кодом эмоции – пропускаем
|
|
if (token[0].isLetterOrNumber() && codes.contains(token))
|
|
continue;
|
|
parts.append(token);
|
|
}
|
|
return parts.join("");
|
|
}
|
|
|
|
QString SevenTVProvider::cleanMessage(const QString& message) const
|
|
{
|
|
if (m_emotes.isEmpty())
|
|
return message;
|
|
|
|
QSet<QString> codes;
|
|
for (const SevenTVEmote& emote : m_emotes) {
|
|
codes.insert(emote.code);
|
|
}
|
|
|
|
QRegularExpression wordSplitter("(\\w+|[^\\w]+)");
|
|
QRegularExpressionMatchIterator it = wordSplitter.globalMatch(message);
|
|
QStringList parts;
|
|
while (it.hasNext()) {
|
|
QRegularExpressionMatch match = it.next();
|
|
QString token = match.captured(0);
|
|
if (token[0].isLetterOrNumber() && codes.contains(token))
|
|
continue;
|
|
parts.append(token);
|
|
}
|
|
return parts.join("");
|
|
}
|