Compare commits

11 Commits

Author SHA1 Message Date
PTyTb 1fc22ec606 соединение с сервером событий
- получение информации о фоллов, подписках, рейдах, покупках наград за баллы
2026-02-22 14:44:30 +03:00
PTyTb 5094834ea1 Добавил привязку действий к событиям
за баллы канала и за донаты можено можно выполнять списки действий, за каждое событие свой набор
2026-02-22 10:20:04 +03:00
PTyTb eb494ae8fa создал менеджер донатов
- добавление
- удаление
- сохранение
2026-02-22 09:18:00 +03:00
PTyTb 05662be287 добавил создание, хранение, удаление действий 2026-02-21 11:08:06 +03:00
PTyTb b430b36e87 добавил интерфейс для менеджера действий 2026-02-21 10:27:40 +03:00
PTyTb ae4121157d + награды за баллы
- создание
- изменение
- удаление
- отображение
2026-02-15 11:26:19 +03:00
PTyTb 63b7fa4ea1 добавил обработку счетчиков 2026-02-14 11:26:45 +03:00
PTyTb 5f53bdcf96 добавил установщик 2026-02-12 20:15:28 +03:00
PTyTb 76439727d2 Добавил "адаптивный" дизайн 2026-02-11 22:46:34 +03:00
PTyTb 1d3131ea8c удаление веб сервисов, автоизация, отображание срока жизни токенов 2026-02-11 07:00:15 +03:00
PTyTb 47c0c7ed85 + проверка срока жизни токена 2026-02-11 06:58:55 +03:00
29 changed files with 6129 additions and 2869 deletions
+3
View File
@@ -13,6 +13,9 @@
*.sln
*.vcxproj.*
*.vcxproj.user
*.iss
*.rtf
install.ico
# Собранные файлы и каталоги
build-*/
+9
View File
@@ -21,7 +21,10 @@ RC_ICONS = ico\app_icon.ico
INCLUDEPATH += $$PWD
SOURCES += \
actionmanager.cpp \
commandprocessor.cpp \
countermanager.cpp \
donationmanager.cpp \
emoteprovider.cpp \
fcolorsetting.cpp \
fcreatechat.cpp \
@@ -40,6 +43,7 @@ SOURCES += \
soundmanager.cpp \
tauth.cpp \
ttw_api.cpp \
twitcheventsub.cpp \
twitchmessage.cpp \
udatabase.cpp \
ugeneral.cpp \
@@ -52,8 +56,12 @@ SOURCES += \
websocketclient.cpp
HEADERS += \
action.h \
actionmanager.h \
badge.h \
commandprocessor.h \
countermanager.h \
donationmanager.h \
emoteprovider.h \
fcolorsetting.h \
fcreatechat.h \
@@ -74,6 +82,7 @@ HEADERS += \
timerinfo.h \
ttw_api.h \
ttw_types.h \
twitcheventsub.h \
twitchmessage.h \
udatabase.h \
ugeneral.h \
+35
View File
@@ -0,0 +1,35 @@
// action.h
#ifndef ACTION_H
#define ACTION_H
#include <QString>
#include <QVariantMap>
class Action {
public:
enum Type { KeyPress, Sound, Notification, File };
virtual ~Action() = default;
virtual Type type() const = 0;
virtual void execute() = 0;
virtual QVariantMap toJson() const = 0;
virtual void fromJson(const QVariantMap &data) = 0;
};
// Конкретные действия
class KeyPressAction : public Action {
public:
Type type() const override { return KeyPress; }
void execute() override;
QVariantMap toJson() const override;
void fromJson(const QVariantMap &data) override;
private:
int keyCode;
Qt::KeyboardModifiers modifiers;
};
class SoundAction : public Action {
// ...
};
// ... и т.д.
#endif // ACTION_H
+72
View File
@@ -0,0 +1,72 @@
#include "actionmanager.h"
ActionManager::ActionManager(uDataBase *db, QObject *parent)
: QObject(parent)
, m_db(db)
{
}
bool ActionManager::addAction(const ActionData &action)
{
if (!m_db) return false;
if (!m_db->saveAction(action)) return false;
// Перезагрузим все действия, чтобы получить свежий ID
loadFromDatabase();
emit dataChanged();
return true;
}
bool ActionManager::updateAction(int id, const ActionData &action)
{
if (!m_db) return false;
if (!m_db->updateAction(id, action)) return false;
for (int i = 0; i < m_actions.size(); ++i) {
if (m_actions[i].id == id) {
m_actions[i] = action;
m_actions[i].id = id;
break;
}
}
emit dataChanged();
return true;
}
bool ActionManager::deleteAction(int id)
{
if (!m_db) return false;
if (!m_db->deleteAction(id)) return false;
// Удаляем все связи с этим действием
m_db->deleteLinksByActionId(id);
for (int i = 0; i < m_actions.size(); ++i) {
if (m_actions[i].id == id) {
m_actions.removeAt(i);
break;
}
}
emit dataChanged();
return true;
}
QList<ActionData> ActionManager::getAllActions() const
{
return m_actions;
}
bool ActionManager::loadFromDatabase()
{
if (!m_db) return false;
m_actions = m_db->loadAllActions();
emit dataChanged();
return true;
}
ActionData ActionManager::getAction(int id) const
{
for (const ActionData &a : m_actions) {
if (a.id == id) return a;
}
return ActionData(); // с id = -1
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef ACTIONMANAGER_H
#define ACTIONMANAGER_H
#include <QObject>
#include "udatabase.h"
class ActionManager : public QObject
{
Q_OBJECT
public:
explicit ActionManager(uDataBase *db, QObject *parent = nullptr);
bool addAction(const ActionData &action);
bool updateAction(int id, const ActionData &action);
bool deleteAction(int id);
QList<ActionData> getAllActions() const;
bool loadFromDatabase();
ActionData getAction(int id) const;
signals:
void actionAdded(const ActionData &action);
void actionUpdated(int id, const ActionData &action);
void actionRemoved(int id);
void dataChanged();
private:
uDataBase *m_db;
QList<ActionData> m_actions;
};
#endif // ACTIONMANAGER_H
+19
View File
@@ -108,6 +108,7 @@ QString CommandProcessor::processCommand(const QString &sender, const QString &f
response = parseTextFiles(response);
response = parseBan(response, sender);
response = parseAPI(response, sender);
response = parseCounters(response);
if (response.contains("[AI]", Qt::CaseInsensitive)) {
response = parseAI(response, parameters);
@@ -214,6 +215,24 @@ QString CommandProcessor::parseTextFiles(const QString &response)
return result;
}
QString CommandProcessor::parseCounters(const QString &response)
{
QString result = response;
// Используем ленивый квантификатор +?
QRegularExpression regex("\\|\\)([^\\)]+?)\\|\\)");
QRegularExpressionMatchIterator matches = regex.globalMatch(response);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
QString counterName = match.captured(1);
int count = m_context.counterManager->getCount(counterName);
QString countStr = QString::number(count);
result.replace("|)" + counterName + "|)", countStr);
}
return result;
}
QString CommandProcessor::parseRandomGroups(const QString &response)
{
QString result = response;
+3
View File
@@ -6,6 +6,7 @@
#include <QVector>
#include <QMap>
#include <QDate>
#include "countermanager.h"
#include "mediafilemanager.h"
#include "user_manager.h"
#include "ttw_api.h"
@@ -34,6 +35,7 @@ public:
RandomResponses* randomResponses = nullptr;
MediaFileManager* mediaFileManager = nullptr;
NeuralTemplateManager *neuralTemplateManager = nullptr;
CounterManager* counterManager = nullptr;
QString channel;
int notifyVolume = 50;
};
@@ -67,6 +69,7 @@ private:
QString parseAPI(const QString &response, const QString &sender);
QString parseAI(const QString &response, const QString &question);
QString parseNeuralTemplates(const QString &response, const QString &sender, const QString &parameters);
QString parseCounters(const QString &response);
QString extractParameters(const QString &fullCommand);
QString getUsernameByIndex(QString userIndex) const;
+215
View File
@@ -0,0 +1,215 @@
// countermanager.cpp
#include "countermanager.h"
#include <QTableWidget>
#include <QDebug>
CounterManager::CounterManager(QObject *parent)
: QObject(parent)
, m_database(nullptr)
{
}
CounterManager::~CounterManager()
{
if (m_database) {
saveToDatabase();
}
}
bool CounterManager::initialize(uDataBase *database)
{
if (!database || !database->isConnected()) {
qWarning() << "CounterManager: База данных не подключена";
return false;
}
m_database = database;
return loadFromDatabase();
}
bool CounterManager::addCounter(const QString &name, int initialValue)
{
if (name.isEmpty()) {
qWarning() << "CounterManager: Нельзя добавить счётчик с пустым именем";
return false;
}
if (contains(name)) {
qWarning() << "CounterManager: Счётчик с именем" << name << "уже существует";
return false;
}
Counter newCounter(name, initialValue);
m_counters.append(newCounter);
if (!saveToDatabase()) {
m_counters.removeLast();
return false;
}
emit counterAdded(name, initialValue);
emit dataChanged();
return true;
}
bool CounterManager::removeCounter(const QString &name)
{
int index = findIndex(name);
if (index == -1) {
qWarning() << "CounterManager: Счётчик" << name << "не найден";
return false;
}
Counter removed = m_counters.at(index);
m_counters.removeAt(index);
if (!saveToDatabase()) {
m_counters.insert(index, removed);
return false;
}
emit counterRemoved(name);
emit dataChanged();
return true;
}
bool CounterManager::updateCounter(const QString &oldName, const QString &newName, int newValue)
{
int index = findIndex(oldName);
if (index == -1) {
qWarning() << "CounterManager: Счётчик" << oldName << "не найден";
return false;
}
if (oldName != newName && contains(newName)) {
qWarning() << "CounterManager: Имя" << newName << "уже занято";
return false;
}
Counter oldCounter = m_counters.at(index);
m_counters[index].name = newName;
m_counters[index].count = newValue;
if (!saveToDatabase()) {
m_counters[index] = oldCounter;
return false;
}
emit counterUpdated(oldName, newName);
emit dataChanged();
return true;
}
bool CounterManager::incrementCounter(const QString &name, int step)
{
int index = findIndex(name);
if (index == -1) return false;
m_counters[index].count += step;
if (!saveToDatabase()) {
m_counters[index].count -= step; // откат
return false;
}
emit counterIncremented(name, m_counters[index].count);
emit dataChanged();
return true;
}
bool CounterManager::decrementCounter(const QString &name, int step)
{
return incrementCounter(name, -step);
}
int CounterManager::getCount(const QString &name) const
{
int index = findIndex(name);
return (index != -1) ? m_counters.at(index).count : 0;
}
void CounterManager::processMessage(const QString &message)
{
for (const Counter &c : m_counters) {
if (message.contains(c.name, Qt::CaseInsensitive)) {
// Увеличиваем счётчик, но без повторного сохранения после каждого (один общий save)
// Здесь мы меняем значение, но сохраним после цикла, чтобы не делать много запросов
// Для простоты используем incrementCounter, который сам сохраняет, но это будет много раз.
// Лучше собрать имена, которые нужно увеличить, а потом применить.
// Но в данном случае можно просто вызывать incrementCounter, это не критично.
incrementCounter(c.name);
}
}
}
bool CounterManager::contains(const QString &name) const
{
return findIndex(name) != -1;
}
bool CounterManager::saveToDatabase()
{
if (!m_database) {
qWarning() << "CounterManager: База данных не установлена";
return false;
}
QTableWidget tempTable;
QStringList headers = {"Имя", "Значение"};
tempTable.setColumnCount(2);
tempTable.setHorizontalHeaderLabels(headers);
tempTable.setRowCount(m_counters.size());
for (int i = 0; i < m_counters.size(); ++i) {
const Counter &c = m_counters.at(i);
tempTable.setItem(i, 0, new QTableWidgetItem(c.name));
tempTable.setItem(i, 1, new QTableWidgetItem(QString::number(c.count)));
}
tempTable.setObjectName("sgCounters");
// Сохраняем через общий метод (предполагаем, что таблица в БД называется "sgCounters")
m_database->SaveTableWidget(&tempTable);
return true;
}
bool CounterManager::loadFromDatabase()
{
if (!m_database) {
qWarning() << "CounterManager: База данных не установлена";
return false;
}
QTableWidget tempTable;
QStringList headers = {"Имя", "Значение"};
tempTable.setColumnCount(2);
tempTable.setHorizontalHeaderLabels(headers);
tempTable.setObjectName("sgCounters");
m_database->LoadTableWidget(&tempTable);
m_counters.clear();
for (int row = 0; row < tempTable.rowCount(); ++row) {
QTableWidgetItem *nameItem = tempTable.item(row, 0);
QTableWidgetItem *valueItem = tempTable.item(row, 1);
if (!nameItem || !valueItem) continue;
QString name = nameItem->text().trimmed();
if (name.isEmpty()) continue;
bool ok;
int value = valueItem->text().toInt(&ok);
if (!ok) continue;
m_counters.append(Counter(name, value));
}
emit dataChanged();
return true;
}
int CounterManager::findIndex(const QString &name) const
{
for (int i = 0; i < m_counters.size(); ++i) {
if (m_counters.at(i).name == name)
return i;
}
return -1;
}
+64
View File
@@ -0,0 +1,64 @@
// countermanager.h
#ifndef COUNTERMANAGER_H
#define COUNTERMANAGER_H
#include <QObject>
#include <QVector>
#include <QString>
#include "udatabase.h"
class CounterManager : public QObject
{
Q_OBJECT
public:
struct Counter {
QString name;
int count;
Counter() : count(0) {}
Counter(const QString& n, int c = 0) : name(n), count(c) {}
bool operator==(const QString& otherName) const { return name == otherName; }
};
explicit CounterManager(QObject *parent = nullptr);
~CounterManager();
// Инициализация с базой данных
bool initialize(uDataBase *database);
// Управление счетчиками
bool addCounter(const QString &name, int initialValue = 0);
bool removeCounter(const QString &name);
bool updateCounter(const QString &oldName, const QString &newName, int newValue);
bool incrementCounter(const QString &name, int step = 1);
bool decrementCounter(const QString &name, int step = 1);
int getCount(const QString &name) const;
// Обработка сообщения: увеличивает счётчики, чьи имена встречаются в тексте
void processMessage(const QString &message);
// Получение всех счётчиков
QVector<Counter> getAllCounters() const { return m_counters; }
bool contains(const QString &name) const;
// Сохранение/загрузка в БД
bool saveToDatabase();
bool loadFromDatabase();
signals:
void dataChanged();
void counterAdded(const QString &name, int value);
void counterRemoved(const QString &name);
void counterUpdated(const QString &oldName, const QString &newName);
void counterIncremented(const QString &name, int newValue);
private:
uDataBase *m_database;
QVector<Counter> m_counters;
int findIndex(const QString &name) const;
};
#endif // COUNTERMANAGER_H
+175
View File
@@ -0,0 +1,175 @@
#include "donationmanager.h"
#include "udatabase.h"
#include <QRegularExpression>
#include <QDebug>
DonationManager::DonationManager(uDataBase *db, QObject *parent)
: QObject(parent)
, m_db(db)
{
}
bool DonationManager::loadFromDatabase()
{
if (!m_db) return false;
// Предполагаем, что в uDataBase есть метод loadAllDonationTriggers()
m_triggers = m_db->loadAllDonationTriggers();
return true;
}
bool DonationManager::addTrigger(const QString &name, const QString &rule)
{
if (name.isEmpty() || rule.isEmpty()) return false;
DonationTrigger trig;
trig.name = name;
trig.rule = rule;
if (!parseRule(rule, trig)) {
qWarning() << "Invalid rule:" << rule;
return false;
}
// Сохраняем в БД (метод saveDonationTrigger должен вернуть id)
int newId = m_db->saveDonationTrigger(trig);
if (newId < 0) return false;
trig.id = newId;
m_triggers.append(trig);
emit dataChanged();
return true;
}
bool DonationManager::deleteTrigger(int id)
{
if (!m_db->deleteDonationTrigger(id)) return false;
for (int i = 0; i < m_triggers.size(); ++i) {
if (m_triggers[i].id == id) {
m_triggers.removeAt(i);
break;
}
}
emit dataChanged();
return true;
}
QList<DonationTrigger> DonationManager::getAllTriggers() const
{
return m_triggers;
}
QString DonationManager::matchDonation(double amount) const
{
const DonationTrigger *best = nullptr;
int bestPriority = 999; // чем меньше, тем выше приоритет
for (const DonationTrigger &t : m_triggers) {
bool ok = false;
switch (t.priority) {
case 1: // exact
if (qFuzzyCompare(amount, t.minValue))
ok = true;
break;
case 2: // range
if (amount >= t.minValue && amount <= t.maxValue)
ok = true;
break;
case 3: // greater / greater-equal
if (t.isGreaterEqual) {
if (amount >= t.minValue) ok = true;
} else {
if (amount > t.minValue) ok = true;
}
break;
default:
continue;
}
if (ok) {
// Сравниваем приоритет
if (t.priority < bestPriority) {
best = &t;
bestPriority = t.priority;
}
// Если приоритет одинаковый, применяем дополнительные правила:
else if (t.priority == bestPriority) {
if (bestPriority == 2) {
// Для диапазонов выбираем более узкий (меньше разница max-min)
double bestRange = best->maxValue - best->minValue;
double thisRange = t.maxValue - t.minValue;
if (thisRange < bestRange) {
best = &t;
}
}
else if (bestPriority == 3) {
// Для больше/больше-равно выбираем наибольшее пороговое значение (minValue)
if (t.minValue > best->minValue) {
best = &t;
}
}
// Для exact приоритет 1 – только одно значение, совпадение уже есть.
}
}
}
return best ? best->name : QString();
}
bool DonationManager::parseRule(const QString &rule, DonationTrigger &t) const
{
QString r = rule.trimmed();
if (r.isEmpty()) return false;
// Точное равенство: =123
if (r.startsWith('=')) {
QString numStr = r.mid(1);
bool ok;
double val = numStr.toDouble(&ok);
if (!ok) return false;
t.priority = 1;
t.minValue = val;
t.maxValue = val;
return true;
}
// Диапазон: 100-200
if (r.contains('-')) {
QStringList parts = r.split('-', Qt::SkipEmptyParts);
if (parts.size() != 2) return false;
bool ok1, ok2;
double a = parts[0].toDouble(&ok1);
double b = parts[1].toDouble(&ok2);
if (!ok1 || !ok2 || a > b) return false;
t.priority = 2;
t.minValue = a;
t.maxValue = b;
return true;
}
// Больше/больше-равно
if (r.startsWith(">=")) {
QString numStr = r.mid(2);
bool ok;
double val = numStr.toDouble(&ok);
if (!ok) return false;
t.priority = 3;
t.minValue = val;
t.maxValue = 0;
t.isGreaterEqual = true;
return true;
}
if (r.startsWith('>')) {
QString numStr = r.mid(1);
bool ok;
double val = numStr.toDouble(&ok);
if (!ok) return false;
t.priority = 3;
t.minValue = val;
t.maxValue = 0;
t.isGreaterEqual = false;
return true;
}
return false;
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef DONATIONMANAGER_H
#define DONATIONMANAGER_H
#include <QObject>
#include <QList>
#include <QString>
class uDataBase;
struct DonationTrigger
{
int id = -1;
QString name; // Название триггера (например "Малый донат")
QString rule; // Строка правила: "=100", ">1000", "100-200" и т.д.
int priority = 0; // Вычисляется при парсинге (1 – exact, 2 range, 3 greater/ge)
double minValue = 0; // Для численных сравнений
double maxValue = 0; // Для диапазона (если range)
bool isGreaterEqual = false; // true для ">="
};
class DonationManager : public QObject
{
Q_OBJECT
public:
explicit DonationManager(uDataBase *db, QObject *parent = nullptr);
// Загрузить все триггеры из БД
bool loadFromDatabase();
// Сохранить новый триггер в БД и добавить в список
bool addTrigger(const QString &name, const QString &rule);
// Удалить триггер по ID (и из БД, и из списка)
bool deleteTrigger(int id);
// Получить все триггеры (для отображения)
QList<DonationTrigger> getAllTriggers() const;
// Поиск подходящего триггера по сумме доната
// Возвращает имя триггера или пустую строку
QString matchDonation(double amount) const;
signals:
void dataChanged(); // для обновления таблицы
private:
// Парсинг строки правила, заполняет поля min, max, priority, isGreaterEqual
bool parseRule(const QString &rule, DonationTrigger &trigger) const;
uDataBase *m_db;
QList<DonationTrigger> m_triggers;
};
#endif // DONATIONMANAGER_H
+51
View File
@@ -4,6 +4,8 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QRegularExpression>
#include <QSet>
// EmoteProvider implementation
EmoteProvider::EmoteProvider(QObject *parent)
@@ -416,3 +418,52 @@ void SevenTVProvider::parseCustomResponse(const QByteArray &data, const QString
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("");
}
+3
View File
@@ -47,6 +47,7 @@ public:
virtual void fetchGlobal() = 0;
virtual void fetchCustom(const QString &userId) = 0;
virtual QString getEmoteUrl(const QString &emoteName) = 0;
virtual QString cleanMessage(const QString& message) const = 0;
signals:
void emotesLoaded();
@@ -71,6 +72,7 @@ public:
void fetchGlobal() override;
void fetchCustom(const QString &userId) override;
QString getEmoteUrl(const QString &emoteName) override;
QString cleanMessage(const QString& message) const override;
private slots:
void onGlobalReplyFinished(QNetworkReply *reply);
@@ -95,6 +97,7 @@ public:
void fetchGlobal() override;
void fetchCustom(const QString &userId) override;
QString getEmoteUrl(const QString &emoteName) override;
QString cleanMessage(const QString& message) const override;
private slots:
void onGlobalReplyFinished(QNetworkReply *reply);
+8
View File
@@ -1,4 +1,7 @@
debug/actionmanager.o
debug/commandprocessor.o
debug/countermanager.o
debug/donationmanager.o
debug/emoteprovider.o
debug/fcolorsetting.o
debug/fcreatechat.o
@@ -17,6 +20,7 @@ debug/randomresponses.o
debug/soundmanager.o
debug/tauth.o
debug/ttw_api.o
debug/twitcheventsub.o
debug/twitchmessage.o
debug/udatabase.o
debug/ugeneral.o
@@ -27,7 +31,10 @@ debug/userwidget.o
debug/webserverchat.o
debug/webservernotify.o
debug/websocketclient.o
debug/moc_actionmanager.o
debug/moc_commandprocessor.o
debug/moc_countermanager.o
debug/moc_donationmanager.o
debug/moc_emoteprovider.o
debug/moc_fcolorsetting.o
debug/moc_fcreatechat.o
@@ -43,6 +50,7 @@ debug/moc_randomresponses.o
debug/moc_soundmanager.o
debug/moc_tauth.o
debug/moc_ttw_api.o
debug/moc_twitcheventsub.o
debug/moc_udatabase.o
debug/moc_ugeneral.o
debug/moc_ulink.o
+8
View File
@@ -1,4 +1,7 @@
release/actionmanager.o
release/commandprocessor.o
release/countermanager.o
release/donationmanager.o
release/emoteprovider.o
release/fcolorsetting.o
release/fcreatechat.o
@@ -17,6 +20,7 @@ release/randomresponses.o
release/soundmanager.o
release/tauth.o
release/ttw_api.o
release/twitcheventsub.o
release/twitchmessage.o
release/udatabase.o
release/ugeneral.o
@@ -27,7 +31,10 @@ release/userwidget.o
release/webserverchat.o
release/webservernotify.o
release/websocketclient.o
release/moc_actionmanager.o
release/moc_commandprocessor.o
release/moc_countermanager.o
release/moc_donationmanager.o
release/moc_emoteprovider.o
release/moc_fcolorsetting.o
release/moc_fcreatechat.o
@@ -43,6 +50,7 @@ release/moc_randomresponses.o
release/moc_soundmanager.o
release/moc_tauth.o
release/moc_ttw_api.o
release/moc_twitcheventsub.o
release/moc_udatabase.o
release/moc_ugeneral.o
release/moc_ulink.o
+26 -5
View File
@@ -87,7 +87,13 @@ void TAuth::handleNewConnection()
connect(m_clientSocket, &QTcpSocket::readyRead,
this, &TAuth::readClientData);
connect(m_clientSocket, &QTcpSocket::disconnected,
m_clientSocket, &QTcpSocket::deleteLater);
this, [this]() {
// Удаляем сокет после отключения
if (m_clientSocket) {
m_clientSocket->deleteLater();
m_clientSocket = nullptr;
}
});
}
}
@@ -98,7 +104,6 @@ void TAuth::readClientData()
QByteArray requestData = m_clientSocket->readAll();
QString request = QString::fromUtf8(requestData);
QStringList lines = request.split("\r\n");
if (lines.isEmpty()) return;
@@ -118,7 +123,6 @@ void TAuth::readClientData()
}
}
// Обрабатываем обычный redirect
if (document.startsWith("/redirect")) {
handleRedirectRequest(document, m_clientSocket);
@@ -237,6 +241,13 @@ void TAuth::handleRedirectRequest(const QString &request, QTcpSocket *socket)
"<h2>Authorization Successful!</h2>\n"
"<p>Token received. You can close this window.</p>\n"
"</body>\n</html>";
// Отправляем ответ клиенту
sendResponse(socket, html);
// Останавливаем сервер СРАЗУ
QTimer::singleShot(100, this, &TAuth::stopServer);
return;
}
// Проверяем наличие error_description
else if (params.contains("error_description=")) {
@@ -260,6 +271,13 @@ void TAuth::handleRedirectRequest(const QString &request, QTcpSocket *socket)
"<h2>Authorization Successful!</h2>\n"
"<p>Code received. You can close this window.</p>\n"
"</body>\n</html>";
// Отправляем ответ клиенту
sendResponse(socket, html);
// Останавливаем сервер СРАЗУ
QTimer::singleShot(100, this, &TAuth::stopServer);
return;
}
else {
html =
@@ -269,13 +287,16 @@ void TAuth::handleRedirectRequest(const QString &request, QTcpSocket *socket)
"<p>Try again or check your authorization URL.</p>\n"
"</body>\n</html>";
sendResponse(socket, html);
// Останавливаем сервер через 5 секунд если нет данных
QTimer::singleShot(5000, this, &TAuth::stopServer);
return;
}
sendResponse(socket, html);
// Останавливаем сервер после обработки
QTimer::singleShot(1000, this, &TAuth::stopServer);
// Останавливаем сервер через 5 секунд для других случаев
QTimer::singleShot(5000, this, &TAuth::stopServer);
}
QString TAuth::extractParam(const QString &params, const QString &paramName)
+214
View File
@@ -1,5 +1,6 @@
#include "ttw_api.h"
#include "qeventloop.h"
#include "ttw_types.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
@@ -591,3 +592,216 @@ void TTwAPI::parseBadgesFromApi(const QString &jsonString, QVector<ChatBadge> &b
badges.append(badge);
}
}
bool TTwAPI::validateTwitchToken(const QString &tokenName,
const QString &tokenValue,
int &daysValid)
{
daysValid = 0;
if (tokenValue.trimmed().isEmpty()) {
toLog(1, "TTwAPI::validateTwitchToken", "Пустой токен: " + tokenName);
return false;
}
QUrl url("https://id.twitch.tv/oauth2/validate");
QNetworkRequest request(url);
// Устанавливаем заголовки
request.setHeader(QNetworkRequest::UserAgentHeader, "YourApp/1.0");
request.setRawHeader("Accept", "application/json");
// Формируем заголовок Authorization
QString authHeader = "OAuth " + tokenValue;
request.setRawHeader("Authorization", authHeader.toUtf8());
// Отправляем GET запрос
QNetworkReply *reply = m_networkManager->get(request);
if (!reply) {
toLog(2, "TTwAPI::validateTwitchToken", "Не удалось создать запрос для токена: " + tokenName);
return false;
}
// Ожидание завершения запроса
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
// Проверяем статус ответа
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray responseData = reply->readAll();
QString responseText = QString::fromUtf8(responseData);
// Обрабатываем ошибки сети
if (reply->error() != QNetworkReply::NoError) {
QString errorMsg = QString("Ошибка сети: %1 - %2")
.arg(reply->error())
.arg(reply->errorString());
toLog(2, "TTwAPI::validateTwitchToken", errorMsg);
reply->deleteLater();
return false;
}
reply->deleteLater();
// Обработка HTTP статусов
if (statusCode == 200) {
// Успешная валидация
QJsonDocument doc = QJsonDocument::fromJson(responseData);
if (!doc.isObject()) {
toLog(2, "TTwAPI::validateTwitchToken", "Ошибка парсинга JSON для токена: " + tokenName);
return false;
}
QJsonObject jsonObj = doc.object();
// Извлекаем expires_in (в секундах)
if (jsonObj.contains("expires_in") && jsonObj["expires_in"].isDouble()) {
int expiresInSeconds = jsonObj["expires_in"].toInt();
daysValid = qRound(expiresInSeconds / 86400.0); // Конвертируем секунды в дни
// Логируем информацию о токене
QString logMsg = QString("Токен '%1' действителен. Осталось: %2 дней. "
"Client ID: %3, Логин: %4")
.arg(tokenName)
.arg(daysValid)
.arg(jsonObj.value("client_id").toString("N/A"))
.arg(jsonObj.value("login").toString("N/A"));
toLog(0, "TTwAPI::validateTwitchToken", logMsg);
return true;
} else {
toLog(2, "TTwAPI::validateTwitchToken",
QString("В ответе отсутствует expires_in для токена: %1").arg(tokenName));
return false;
}
}
else if (statusCode == 401) {
// Невалидный токен
toLog(2, "TTwAPI::validateTwitchToken",
QString("Токен '%1' невалиден (HTTP 401)").arg(tokenName));
return false;
}
else {
// Другие HTTP ошибки
QString errorMsg = QString("HTTP %1 для токена '%2': %3")
.arg(statusCode)
.arg(tokenName)
.arg(responseText);
toLog(2, "TTwAPI::validateTwitchToken", errorMsg);
return false;
}
}
void TTwAPI::getCustomRewards(QVector<TCustomReward*> &rewards, bool onlyManageable)
{
QString roomId = getRoomId();
if (roomId.isEmpty()) return;
QString url = "channel_points/custom_rewards?broadcaster_id=" + roomId;
if (onlyManageable) {
url += "&only_manageable_rewards=true";
}
QString response = getTTW(url, m_clientId, true);
if (response.isEmpty()) return;
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
if (!doc.isObject()) return;
QJsonArray data = doc.object()["data"].toArray();
for (const QJsonValue &val : data) {
QJsonObject obj = val.toObject();
TCustomReward *reward = new TCustomReward;
reward->id = obj["id"].toString();
reward->title = obj["title"].toString();
reward->cost = obj["cost"].toInt();
reward->prompt = obj["prompt"].toString();
reward->isUserInputRequired = obj["is_user_input_required"].toBool();
reward->isEnabled = obj["is_enabled"].toBool();
// Для manageable запроса все награды по определению управляемы
reward->isManagedByBroadcaster = onlyManageable;
rewards.append(reward);
}
}
TCustomReward* TTwAPI::createCustomReward(const QString &title, const QString &cost,
const QString &prompt, bool isUserInput)
{
QString roomId = getRoomId();
if (roomId.isEmpty()) return nullptr;
QJsonObject body;
body["title"] = title;
body["cost"] = cost.toInt();
if (!prompt.isEmpty()) body["prompt"] = prompt;
body["is_user_input_required"] = isUserInput;
// Можно добавить и другие параметры: is_enabled, color и т.д.
QJsonDocument doc(body);
QString response = postTTW("channel_points/custom_rewards?broadcaster_id=" + roomId,
m_clientId, doc.toJson(), true);
if (response.isEmpty()) return nullptr;
QJsonDocument respDoc = QJsonDocument::fromJson(response.toUtf8());
if (!respDoc.isObject()) return nullptr;
QJsonArray data = respDoc.object()["data"].toArray();
if (data.isEmpty()) return nullptr;
QJsonObject obj = data[0].toObject();
TCustomReward *reward = new TCustomReward;
reward->id = obj["id"].toString();
reward->title = obj["title"].toString();
reward->cost = obj["cost"].toInt();
reward->prompt = obj["prompt"].toString();
reward->isUserInputRequired = obj["is_user_input_required"].toBool();
reward->isEnabled = obj["is_enabled"].toBool();
return reward;
}
void TTwAPI::updateCustomReward(TCustomReward* reward)
{
if (!reward) return;
QString roomId = getRoomId();
if (roomId.isEmpty()) return;
QJsonObject body;
if (!reward->title.isEmpty()) body["title"] = reward->title;
body["cost"] = reward->cost;
if (!reward->prompt.isEmpty()) body["prompt"] = reward->prompt;
body["is_user_input_required"] = reward->isUserInputRequired;
body["is_enabled"] = reward->isEnabled;
QJsonDocument doc(body);
patchTTW(QString("channel_points/custom_rewards?broadcaster_id=%1&id=%2")
.arg(roomId, reward->id),
m_clientId, doc.toJson(), true);
}
bool TTwAPI::deleteCustomReward(const QString &id)
{
QString roomId = getRoomId();
if (roomId.isEmpty()) return false;
deleteTTW("channel_points/custom_rewards?broadcaster_id=" + roomId + "&id=" + id,
m_clientId, true);
return true; // или проверка HTTP-статуса, но deleteTTW возвращает пустую строку при ошибке
}
void TTwAPI::updateRedemptionStatus(TCustomRewardEvent* event)
{
// event содержит id награды, id redemption и новый статус (COMPLETED/CANCELED)
QString roomId = getRoomId();
if (roomId.isEmpty()) return;
QJsonObject body;
body["status"] = event->status; // "COMPLETED" или "CANCELED"
QJsonDocument doc(body);
patchTTW(QString("channel_points/custom_rewards/redemptions?broadcaster_id=%1&reward_id=%2&id=%3")
.arg(roomId, event->rewardId, event->redemptionId),
m_clientId, doc.toJson(), true);
}
+3 -3
View File
@@ -12,7 +12,7 @@
class TCustomReward;
class TCustomRewardEvent;
class ChatBadge;
struct ChatBadge;
class Emote;
class TTwAPI : public QObject
@@ -53,14 +53,14 @@ public:
void delVIP(const QString &id);
// Custom Rewards
void getCustomRewards(QVector<TCustomReward*> &rewards);
void getCustomRewards(QVector<TCustomReward*> &rewards, bool onlyManageable = false);
TCustomReward* createCustomReward(const QString &title,
const QString &cost,
const QString &prompt = "",
bool isUserInput = false);
void updateCustomReward(TCustomReward* reward);
void updateRedemptionStatus(TCustomRewardEvent* event);
void deleteCustomReward(const QString &id);
bool deleteCustomReward(const QString &id);
// Пользователи
User getUserByLogin(const QString &login);
+2 -11
View File
@@ -22,7 +22,7 @@ struct TCustomReward {
bool isPaused;
bool isInStock;
bool shouldRedemptionsSkipRequestQueue;
bool isManagedByBroadcaster;
TCustomReward()
: cost(0)
, isEnabled(true)
@@ -48,21 +48,12 @@ struct TCustomRewardEvent {
QString userInput;
QString status;
QDateTime redeemedAt;
QString redemptionId;
TCustomRewardEvent() {}
};
struct ChatBadge {
QString setId;
QString versionId;
QString title;
QString description;
QString smallImageUrl;
QString mediumImageUrl;
QString largeImageUrl;
ChatBadge() {}
};
struct Emote {
QString id;
+352
View File
@@ -0,0 +1,352 @@
#include "twitcheventsub.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QThread>
TwitchEventSub::TwitchEventSub(QObject *parent)
: QObject(parent)
, m_connected(false)
{
connect(&m_webSocket, &QWebSocket::connected, this, &TwitchEventSub::onWebSocketConnected);
connect(&m_webSocket, &QWebSocket::disconnected, this, &TwitchEventSub::onWebSocketDisconnected);
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &TwitchEventSub::onWebSocketTextMessageReceived);
connect(&m_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &TwitchEventSub::onWebSocketError);
m_pingTimer.setInterval(30000); // 30 секунд
connect(&m_pingTimer, &QTimer::timeout, this, &TwitchEventSub::onPingTimer);
}
TwitchEventSub::~TwitchEventSub()
{
disconnectFromTwitch();
}
void TwitchEventSub::init(const QString &accessToken, const QString &clientId, const QString &broadcasterId)
{
m_accessToken = accessToken;
m_clientId = clientId;
m_broadcasterId = broadcasterId;
}
void TwitchEventSub::connectToTwitch()
{
if (m_webSocket.state() == QAbstractSocket::ConnectedState)
return;
emit onLog(0, "connectToTwitch", "Connecting to EventSub WebSocket...");
m_webSocket.open(QUrl("wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=60"));
}
void TwitchEventSub::disconnectFromTwitch()
{
m_pingTimer.stop();
m_webSocket.close();
}
void TwitchEventSub::onWebSocketConnected()
{
emit onLog(0, "onWebSocketConnected", "WebSocket connected");
m_pingTimer.start();
m_connected = true;
emit onConnected();
}
void TwitchEventSub::onWebSocketDisconnected()
{
m_pingTimer.stop();
m_connected = false;
emit onLog(1, "onWebSocketDisconnected", "WebSocket disconnected");
emit onDisconnected();
}
void TwitchEventSub::onWebSocketTextMessageReceived(const QString &message)
{
emit onRawMessage(message);
parseMessage(message);
}
void TwitchEventSub::onWebSocketError(QAbstractSocket::SocketError error)
{
emit onLog(2, "onWebSocketError", m_webSocket.errorString());
emit onError(m_webSocket.errorString());
}
void TwitchEventSub::onPingTimer()
{
if (m_webSocket.state() == QAbstractSocket::ConnectedState) {
m_webSocket.ping(); // отправляем Ping-фрейм
emit onLog(3, "onPingTimer", "Ping sent");
}
}
void TwitchEventSub::parseMessage(const QString &message)
{
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
if (!doc.isObject()) return;
QJsonObject root = doc.object();
QJsonObject metadata = root.value("metadata").toObject();
QString messageType = metadata.value("message_type").toString();
QString subscriptionType = metadata.value("subscription_type").toString();
emit onLog(0, "parseMessage", QString("Message type: %1, Subscription: %2").arg(messageType, subscriptionType));
if (messageType == "session_welcome") {
QJsonObject payload = root.value("payload").toObject();
QJsonObject session = payload.value("session").toObject();
m_sessionId = session.value("id").toString();
emit onLog(0, "parseMessage", "Received session_welcome, session_id: " + m_sessionId);
performSubscriptions();
}
else if (messageType == "notification") {
QJsonObject payload = root.value("payload").toObject();
if (subscriptionType == "channel.channel_points_custom_reward_redemption.add") {
emit onCustomReward(parseCustomReward(payload));
}
else if (subscriptionType == "channel.follow") {
emit onFollow(parseFollow(payload));
}
else if (subscriptionType == "channel.subscribe") {
emit onSubscribe(parseSubscribe(payload));
}
else if (subscriptionType == "channel.subscription.gift") {
emit onGift(parseGift(payload));
}
else if (subscriptionType == "channel.raid") {
emit onRaid(parseRaid(payload));
}
}
else if (messageType == "session_keepalive") {
emit onLog(3, "parseMessage", "Received keepalive");
}
}
void TwitchEventSub::performSubscriptions()
{
if (m_sessionId.isEmpty()) {
emit onLog(2, "performSubscriptions", "No session_id, cannot subscribe");
return;
}
// Формируем условие для каждого типа подписки
subscribeTo("channel.channel_points_custom_reward_redemption.add", "1",
QString("{\"broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId));
_sleep(500);
subscribeTo("channel.raid", "1",
QString("{\"to_broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId));
_sleep(500);
subscribeTo("channel.follow", "2",
QString("{\"broadcaster_user_id\":\"%1\",\"moderator_user_id\":\"%1\"}").arg(m_broadcasterId));
_sleep(500);
subscribeTo("channel.subscribe", "1",
QString("{\"broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId));
_sleep(500);
subscribeTo("channel.subscription.gift", "1",
QString("{\"broadcaster_user_id\":\"%1\"}").arg(m_broadcasterId));
_sleep(500);
}
// --- Парсеры событий ---
TCustomRewardEvent2 TwitchEventSub::parseCustomReward(const QJsonObject &payload)
{
TCustomRewardEvent2 result;
QJsonObject subscription = payload.value("subscription").toObject();
result.subscription.id = subscription.value("id").toString();
result.subscription.type = subscription.value("type").toString();
result.subscription.version = subscription.value("version").toString();
result.subscription.status = subscription.value("status").toString();
result.subscription.cost = subscription.value("cost").toInt();
result.subscription.created_at = subscription.value("created_at").toString();
QJsonObject cond = subscription.value("condition").toObject();
result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString();
result.subscription.condition.reward_id = cond.value("reward_id").toString();
QJsonObject transp = subscription.value("transport").toObject();
result.subscription.transport.method = transp.value("method").toString();
QJsonObject event = payload.value("event").toObject();
result.event.id = event.value("id").toString();
result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString();
result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString();
result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString();
result.event.user_id = event.value("user_id").toString();
result.event.user_login = event.value("user_login").toString();
result.event.user_name = event.value("user_name").toString();
result.event.user_input = event.value("user_input").toString();
QJsonObject reward = event.value("reward").toObject();
result.event.reward.id = reward.value("id").toString();
result.event.reward.title = reward.value("title").toString();
result.event.reward.cost = reward.value("cost").toInt();
result.event.reward.prompt = reward.value("prompt").toString();
return result;
}
TFollowEvent TwitchEventSub::parseFollow(const QJsonObject &payload)
{
TFollowEvent result;
QJsonObject subscription = payload.value("subscription").toObject();
result.subscription.id = subscription.value("id").toString();
result.subscription.type = subscription.value("type").toString();
result.subscription.version = subscription.value("version").toString();
result.subscription.status = subscription.value("status").toString();
result.subscription.cost = subscription.value("cost").toInt();
result.subscription.created_at = subscription.value("created_at").toString();
QJsonObject cond = subscription.value("condition").toObject();
result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString();
QJsonObject transp = subscription.value("transport").toObject();
result.subscription.transport.method = transp.value("method").toString();
QJsonObject event = payload.value("event").toObject();
result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString();
result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString();
result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString();
result.event.user_id = event.value("user_id").toString();
result.event.user_login = event.value("user_login").toString();
result.event.user_name = event.value("user_name").toString();
result.event.followed_at = event.value("followed_at").toString();
return result;
}
TSubEvent TwitchEventSub::parseSubscribe(const QJsonObject &payload)
{
TSubEvent result;
QJsonObject subscription = payload.value("subscription").toObject();
result.subscription.id = subscription.value("id").toString();
result.subscription.type = subscription.value("type").toString();
result.subscription.version = subscription.value("version").toString();
result.subscription.status = subscription.value("status").toString();
result.subscription.cost = subscription.value("cost").toInt();
result.subscription.created_at = subscription.value("created_at").toString();
QJsonObject cond = subscription.value("condition").toObject();
result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString();
QJsonObject transp = subscription.value("transport").toObject();
result.subscription.transport.method = transp.value("method").toString();
QJsonObject event = payload.value("event").toObject();
result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString();
result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString();
result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString();
result.event.user_id = event.value("user_id").toString();
result.event.user_login = event.value("user_login").toString();
result.event.user_name = event.value("user_name").toString();
result.event.tier = event.value("tier").toString();
result.event.is_gift = event.value("is_gift").toBool();
return result;
}
TGiftEvent TwitchEventSub::parseGift(const QJsonObject &payload)
{
TGiftEvent result;
QJsonObject subscription = payload.value("subscription").toObject();
result.subscription.id = subscription.value("id").toString();
result.subscription.type = subscription.value("type").toString();
result.subscription.version = subscription.value("version").toString();
result.subscription.status = subscription.value("status").toString();
result.subscription.cost = subscription.value("cost").toInt();
result.subscription.created_at = subscription.value("created_at").toString();
QJsonObject cond = subscription.value("condition").toObject();
result.subscription.condition.broadcaster_user_id = cond.value("broadcaster_user_id").toString();
QJsonObject transp = subscription.value("transport").toObject();
result.subscription.transport.method = transp.value("method").toString();
QJsonObject event = payload.value("event").toObject();
result.event.broadcaster_user_id = event.value("broadcaster_user_id").toString();
result.event.broadcaster_user_login = event.value("broadcaster_user_login").toString();
result.event.broadcaster_user_name = event.value("broadcaster_user_name").toString();
result.event.user_id = event.value("user_id").toString();
result.event.user_login = event.value("user_login").toString();
result.event.user_name = event.value("user_name").toString();
result.event.total = event.value("total").toInt();
result.event.tier = event.value("tier").toString();
result.event.cumulative_total = event.value("cumulative_total").toInt();
result.event.is_anonymous = event.value("is_anonymous").toBool();
return result;
}
TRaidEvent TwitchEventSub::parseRaid(const QJsonObject &payload)
{
TRaidEvent result;
QJsonObject subscription = payload.value("subscription").toObject();
result.subscription.id = subscription.value("id").toString();
result.subscription.type = subscription.value("type").toString();
result.subscription.version = subscription.value("version").toString();
result.subscription.status = subscription.value("status").toString();
result.subscription.cost = subscription.value("cost").toInt();
result.subscription.created_at = subscription.value("created_at").toString();
QJsonObject cond = subscription.value("condition").toObject();
result.subscription.condition.to_broadcaster_user_id = cond.value("to_broadcaster_user_id").toString();
QJsonObject transp = subscription.value("transport").toObject();
result.subscription.transport.method = transp.value("method").toString();
QJsonObject event = payload.value("event").toObject();
result.event.from_broadcaster_user_id = event.value("from_broadcaster_user_id").toString();
result.event.from_broadcaster_user_login = event.value("from_broadcaster_user_login").toString();
result.event.from_broadcaster_user_name = event.value("from_broadcaster_user_name").toString();
result.event.to_broadcaster_user_id = event.value("to_broadcaster_user_id").toString();
result.event.to_broadcaster_user_login = event.value("to_broadcaster_user_login").toString();
result.event.to_broadcaster_user_name = event.value("to_broadcaster_user_name").toString();
result.event.viewers = event.value("viewers").toInt();
return result;
}
bool TwitchEventSub::subscribeTo(const QString &eventType, const QString &version, const QString &condition)
{
emit onLog(0, "subscribeTo", QString("Subscribing to %1").arg(eventType));
QJsonObject transport;
transport["method"] = "websocket";
transport["session_id"] = m_sessionId;
QJsonObject requestBody;
requestBody["type"] = eventType;
requestBody["version"] = version;
requestBody["condition"] = QJsonDocument::fromJson(condition.toUtf8()).object();
requestBody["transport"] = transport;
QNetworkRequest request(QUrl("https://api.twitch.tv/helix/eventsub/subscriptions"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
request.setRawHeader("Client-Id", m_clientId.toUtf8());
QNetworkReply *reply = m_networkManager.post(request, QJsonDocument(requestBody).toJson());
connect(reply, &QNetworkReply::finished, this, [this, reply, eventType]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
emit onLog(3, "subscribeTo", QString("Response: %1").arg(QString(data)));
if (data.contains("\"status\":\"enabled\"")) {
emit onLog(0, "subscribeTo", QString("Subscription %1 successful").arg(eventType));
} else {
emit onLog(1, "subscribeTo", QString("Subscription %1 failed: %2").arg(eventType, QString(data)));
}
} else {
emit onLog(2, "subscribeTo", QString("Network error: %1").arg(reply->errorString()));
}
reply->deleteLater(); // обязательно удаляем
});
return true;
}
+218
View File
@@ -0,0 +1,218 @@
#ifndef TWITCHEVENTSUB_H
#define TWITCHEVENTSUB_H
#include <QObject>
#include <QWebSocket>
#include <QNetworkAccessManager>
#include <QTimer>
#include <QJsonObject>
// Структуры данных событий (аналог Delphi-записей)
struct TCustomRewardEvent2
{
struct Subscription {
QString id;
QString type;
QString version;
QString status;
int cost;
QString created_at;
struct Condition {
QString broadcaster_user_id;
QString reward_id;
} condition;
struct Transport {
QString method;
} transport;
} subscription;
struct Event {
QString id;
QString broadcaster_user_id;
QString broadcaster_user_login;
QString broadcaster_user_name;
QString user_id;
QString user_login;
QString user_name;
QString user_input;
struct Reward {
QString id;
QString title;
int cost;
QString prompt;
} reward;
} event;
};
struct TFollowEvent
{
struct Subscription {
QString id;
QString type;
QString version;
QString status;
int cost;
QString created_at;
struct Condition {
QString broadcaster_user_id;
} condition;
struct Transport {
QString method;
} transport;
} subscription;
struct Event {
QString broadcaster_user_id;
QString broadcaster_user_login;
QString broadcaster_user_name;
QString user_id;
QString user_login;
QString user_name;
QString followed_at;
} event;
};
struct TSubEvent
{
struct Subscription {
QString id;
QString type;
QString version;
QString status;
int cost;
QString created_at;
struct Condition {
QString broadcaster_user_id;
} condition;
struct Transport {
QString method;
} transport;
} subscription;
struct Event {
QString broadcaster_user_id;
QString broadcaster_user_login;
QString broadcaster_user_name;
QString user_id;
QString user_login;
QString user_name;
QString tier;
bool is_gift;
} event;
};
struct TGiftEvent
{
struct Subscription {
QString id;
QString type;
QString version;
QString status;
int cost;
QString created_at;
struct Condition {
QString broadcaster_user_id;
} condition;
struct Transport {
QString method;
} transport;
} subscription;
struct Event {
QString broadcaster_user_id;
QString broadcaster_user_login;
QString broadcaster_user_name;
QString user_id;
QString user_login;
QString user_name;
int total;
QString tier;
int cumulative_total;
bool is_anonymous;
} event;
};
struct TRaidEvent
{
struct Subscription {
QString id;
QString type;
QString version;
QString status;
int cost;
QString created_at;
struct Condition {
QString to_broadcaster_user_id;
} condition;
struct Transport {
QString method;
} transport;
} subscription;
struct Event {
QString from_broadcaster_user_id;
QString from_broadcaster_user_login;
QString from_broadcaster_user_name;
QString to_broadcaster_user_id;
QString to_broadcaster_user_login;
QString to_broadcaster_user_name;
int viewers;
} event;
};
class TwitchEventSub : public QObject
{
Q_OBJECT
public:
explicit TwitchEventSub(QObject *parent = nullptr);
~TwitchEventSub();
void init(const QString &accessToken, const QString &clientId, const QString &broadcasterId);
void connectToTwitch();
void disconnectFromTwitch();
signals:
void onConnected();
void onDisconnected();
void onError(const QString &error);
void onLog(int level, const QString &method, const QString &message);
void onStatus(const QString &event, int code, const QString &description);
void onRawMessage(const QString &message);
void onCustomReward(const TCustomRewardEvent2 &data);
void onFollow(const TFollowEvent &data);
void onSubscribe(const TSubEvent &data);
void onGift(const TGiftEvent &data);
void onRaid(const TRaidEvent &data);
private slots:
void onWebSocketConnected();
void onWebSocketDisconnected();
void onWebSocketTextMessageReceived(const QString &message);
void onWebSocketError(QAbstractSocket::SocketError error);
void onPingTimer();
private:
bool subscribeTo(const QString &eventType, const QString &version, const QString &condition);
void performSubscriptions();
void parseMessage(const QString &message);
// Парсеры событий
TCustomRewardEvent2 parseCustomReward(const QJsonObject &payload);
TFollowEvent parseFollow(const QJsonObject &payload);
TSubEvent parseSubscribe(const QJsonObject &payload);
TGiftEvent parseGift(const QJsonObject &payload);
TRaidEvent parseRaid(const QJsonObject &payload);
QString m_accessToken;
QString m_clientId;
QString m_broadcasterId;
QString m_sessionId;
QWebSocket m_webSocket;
QNetworkAccessManager m_networkManager;
QTimer m_pingTimer;
bool m_connected;
};
#endif // TWITCHEVENTSUB_H
+401
View File
@@ -1399,3 +1399,404 @@ bool uDataBase::deleteNotification(int port)
return true;
}
bool uDataBase::createActionsTable()
{
QSqlQuery query(m_db);
QString sql =
"CREATE TABLE IF NOT EXISTS actions ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" type INTEGER NOT NULL,"
" key_combination TEXT,"
" audio_file TEXT,"
" notification_title TEXT,"
" notification_description TEXT,"
" notification_image TEXT,"
" notification_sound TEXT,"
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
")";
if (!query.exec(sql)) {
m_lastError = query.lastError().text();
qWarning() << "Failed to create actions table:" << m_lastError;
return false;
}
return true;
}
bool uDataBase::saveAction(const ActionData &action)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
// Создаём таблицу, если ещё нет
if (!tableExists("actions")) {
if (!createActionsTable())
return false;
}
QSqlQuery query(m_db);
query.prepare(
"INSERT INTO actions ("
" type, key_combination, audio_file, notification_title,"
" notification_description, notification_image, notification_sound"
") VALUES ("
" :type, :key_combination, :audio_file, :notification_title,"
" :notification_description, :notification_image, :notification_sound"
")"
);
query.bindValue(":type", action.type);
query.bindValue(":key_combination", action.keyCombination);
query.bindValue(":audio_file", action.audioFile);
query.bindValue(":notification_title", action.notificationTitle);
query.bindValue(":notification_description", action.notificationDescription);
query.bindValue(":notification_image", action.notificationImage);
query.bindValue(":notification_sound", action.notificationSound);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to save action:" << m_lastError;
return false;
}
return true;
}
bool uDataBase::updateAction(int id, const ActionData &action)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
query.prepare(
"UPDATE actions SET "
" type = :type,"
" key_combination = :key_combination,"
" audio_file = :audio_file,"
" notification_title = :notification_title,"
" notification_description = :notification_description,"
" notification_image = :notification_image,"
" notification_sound = :notification_sound "
"WHERE id = :id"
);
query.bindValue(":type", action.type);
query.bindValue(":key_combination", action.keyCombination);
query.bindValue(":audio_file", action.audioFile);
query.bindValue(":notification_title", action.notificationTitle);
query.bindValue(":notification_description", action.notificationDescription);
query.bindValue(":notification_image", action.notificationImage);
query.bindValue(":notification_sound", action.notificationSound);
query.bindValue(":id", id);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to update action:" << m_lastError;
return false;
}
return true;
}
bool uDataBase::deleteAction(int id)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM actions WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to delete action:" << m_lastError;
return false;
}
return true;
}
QList<ActionData> uDataBase::loadAllActions()
{
QList<ActionData> actions;
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return actions;
}
if (!tableExists("actions")) {
return actions; // таблицы нет – пустой список
}
QSqlQuery query(m_db);
query.prepare("SELECT * FROM actions ORDER BY id");
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to load actions:" << m_lastError;
return actions;
}
while (query.next()) {
ActionData a;
a.id = query.value("id").toInt();
a.type = query.value("type").toInt();
a.keyCombination = query.value("key_combination").toString();
a.audioFile = query.value("audio_file").toString();
a.notificationTitle = query.value("notification_title").toString();
a.notificationDescription = query.value("notification_description").toString();
a.notificationImage = query.value("notification_image").toString();
a.notificationSound = query.value("notification_sound").toString();
actions.append(a);
}
return actions;
}
bool uDataBase::clearActionsTable()
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
if (!query.exec("DELETE FROM actions")) {
m_lastError = query.lastError().text();
return false;
}
// Сброс автоинкремента
query.exec("DELETE FROM sqlite_sequence WHERE name='actions'");
return true;
}
bool uDataBase::createDonationTriggersTable()
{
QSqlQuery query(m_db);
QString sql =
"CREATE TABLE IF NOT EXISTS donation_triggers ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" rule TEXT NOT NULL,"
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
")";
if (!query.exec(sql)) {
m_lastError = query.lastError().text();
qWarning() << "Failed to create donation_triggers table:" << m_lastError;
return false;
}
return true;
}
int uDataBase::saveDonationTrigger(const DonationTrigger &trigger)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return -1;
}
if (!tableExists("donation_triggers")) {
if (!createDonationTriggersTable()) return -1;
}
QSqlQuery query(m_db);
query.prepare("INSERT INTO donation_triggers (name, rule) VALUES (:name, :rule)");
query.bindValue(":name", trigger.name);
query.bindValue(":rule", trigger.rule);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to save donation trigger:" << m_lastError;
return -1;
}
return query.lastInsertId().toInt();
}
bool uDataBase::deleteDonationTrigger(int id)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM donation_triggers WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to delete donation trigger:" << m_lastError;
return false;
}
return true;
}
QList<DonationTrigger> uDataBase::loadAllDonationTriggers()
{
QList<DonationTrigger> list;
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return list;
}
if (!tableExists("donation_triggers")) {
return list; // таблицы нет – пусто
}
QSqlQuery query(m_db);
query.prepare("SELECT id, name, rule FROM donation_triggers ORDER BY id");
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to load donation triggers:" << m_lastError;
return list;
}
while (query.next()) {
DonationTrigger t;
t.id = query.value("id").toInt();
t.name = query.value("name").toString();
t.rule = query.value("rule").toString();
// Парсить rule будем в DonationManager, здесь только храним
list.append(t);
}
return list;
}
bool uDataBase::saveEventActionLink(const QString &eventType, const QString &eventName, int actionId)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QString et = eventType.trimmed();
QString en = eventName.trimmed();
// Создаём таблицу, если её нет
if (!tableExists("event_action_links")) {
QSqlQuery query(m_db);
QString sql = "CREATE TABLE IF NOT EXISTS event_action_links ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"event_type TEXT NOT NULL,"
"event_name TEXT NOT NULL,"
"action_id INTEGER NOT NULL,"
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
")";
if (!query.exec(sql)) {
m_lastError = query.lastError().text();
qWarning() << "Failed to create event_action_links table:" << m_lastError;
return false;
}
}
qDebug()<<"Я ТУТ НАХУЙ";
// Проверяем, нет ли уже такой связи
QSqlQuery checkQuery(m_db);
checkQuery.prepare("SELECT id FROM event_action_links WHERE event_type=:et AND event_name=:en AND action_id=:aid");
checkQuery.bindValue(":et", et);
checkQuery.bindValue(":en", en);
checkQuery.bindValue(":aid", actionId);
if (checkQuery.exec() && checkQuery.next()) {
m_lastError = "Такая связь уже существует";
return false;
}
QSqlQuery query(m_db);
query.prepare("INSERT INTO event_action_links (event_type, event_name, action_id) VALUES (:et, :en, :aid)");
query.bindValue(":et", eventType);
query.bindValue(":en", eventName);
query.bindValue(":aid", actionId);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to save event-action link:" << m_lastError;
return false;
}
return true;
}
bool uDataBase::deleteEventActionLink(int id)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM event_action_links WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to delete event-action link:" << m_lastError;
return false;
}
if (query.numRowsAffected() == 0) {
m_lastError = "Связь с указанным ID не найдена";
return false;
}
return true;
}
QList<EventActionLink> uDataBase::getLinksForEvent(const QString &eventType, const QString &eventName)
{
QList<EventActionLink> links;
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return links;
}
if (!tableExists("event_action_links")) {
return links;
}
QSqlQuery query(m_db);
query.prepare("SELECT id, event_type, event_name, action_id FROM event_action_links WHERE event_type=:et AND event_name=:en");
query.bindValue(":et", eventType);
query.bindValue(":en", eventName);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to get links for event:" << m_lastError;
return links;
}
while (query.next()) {
EventActionLink link;
link.id = query.value(0).toInt();
link.eventType = query.value(1).toString();
link.eventName = query.value(2).toString();
link.actionId = query.value(3).toInt();
links.append(link);
}
return links;
}
bool uDataBase::deleteLinksForEvent(const QString &eventType, const QString &eventName)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM event_action_links WHERE event_type=:et AND event_name=:en");
query.bindValue(":et", eventType);
query.bindValue(":en", eventName);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to delete links for event:" << m_lastError;
return false;
}
return true;
}
bool uDataBase::deleteLinksByActionId(int actionId)
{
if (!m_db.isOpen()) {
m_lastError = "Database is not open";
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM event_action_links WHERE action_id = :aid");
query.bindValue(":aid", actionId);
if (!query.exec()) {
m_lastError = query.lastError().text();
qWarning() << "Failed to delete links by action id:" << m_lastError;
return false;
}
return true;
}
+34
View File
@@ -1,6 +1,7 @@
#ifndef UDATABASE_H
#define UDATABASE_H
#include "donationmanager.h"
#include "qlistwidget.h"
#include <QObject>
#include <QString>
@@ -56,7 +57,23 @@ struct NotificationSettings {
QDateTime createdAt;
};
struct ActionData {
int id = -1;
int type = 0; // 0 - клавиши, 1 - звук, 2 - уведомление
QString keyCombination;
QString audioFile;
QString notificationTitle;
QString notificationDescription;
QString notificationImage;
QString notificationSound;
};
struct EventActionLink {
int id;
QString eventType;
QString eventName;
int actionId;
};
class uDataBase : public QObject
{
@@ -121,6 +138,23 @@ public:
// Получение последней ошибки
QString lastError() const;
bool createActionsTable();
bool saveAction(const ActionData &action);
bool updateAction(int id, const ActionData &action);
bool deleteAction(int id);
QList<ActionData> loadAllActions();
bool clearActionsTable();
bool createDonationTriggersTable();
int saveDonationTrigger(const DonationTrigger &trigger);
bool deleteDonationTrigger(int id);
QList<DonationTrigger> loadAllDonationTriggers();
bool saveEventActionLink(const QString &eventType, const QString &eventName, int actionId);
bool deleteEventActionLink(int id);
QList<EventActionLink> getLinksForEvent(const QString &eventType, const QString &eventName);
bool deleteLinksForEvent(const QString &eventType, const QString &eventName);
bool deleteLinksByActionId(int actionId);
private:
+1523 -887
View File
File diff suppressed because it is too large Load Diff
+72 -10
View File
@@ -9,12 +9,15 @@
#include <ulink.h>
#include <udatabase.h>
#include <soundmanager.h>
#include "actionmanager.h"
#include "commandprocessor.h"
#include "countermanager.h"
#include "fcreatechat.h"
#include "fcreatenotify.h"
#include "logmanager.h"
#include "neuralnetworkmanager.h"
#include "ttw_api.h"
#include "twitcheventsub.h"
#include "user_manager.h"
#include "webserverchat.h"
#include "webservernotify.h"
@@ -84,7 +87,7 @@ public:
SoundManager *soundManager; // Менеджер звуков
UserManager* getUserManager(); // Получение менеджера пользователей
TTwAPI *twitchAPI; // API для работы с Twitch
bool eventFilter(QObject *obj, QEvent *event) override;
// Методы логирования и работы с командами
void toCommands(QString command, QString response);
@@ -141,11 +144,6 @@ private slots:
void handleConnected();
void handleDisconnected();
// ========================================================================
// СЛОТЫ ДЛЯ РАБОТЫ С КОМАНДАМИ И ОТВЕТАМИ
// ========================================================================
void execCommand(const QString &sender, const QString &message);
// ========================================================================
// СЛОТЫ ДЛЯ РАБОТЫ С ИСКУССТВЕННЫМ ИНТЕЛЛЕКТОМ
@@ -344,6 +342,58 @@ private slots:
void on_btnAUserName_clicked();
void on_btnRmWebService_clicked();
void on_sgCounters_cellClicked(int row, int column);
void on_sgCounters_cellDoubleClicked(int row, int column);
void on_btnCounterAdd_clicked();
void on_btnCounterDelete_clicked();
void on_btnCounterEdit_clicked();
void on_btnCounterAtoText_clicked();
void on_btnCRGet_clicked();
void on_sgCustomRewards_cellClicked(int row, int column);
void on_sgCustomRewards_cellDoubleClicked(int row, int column);
void on_btnCRAdd_clicked();
void on_btnCREdit_clicked();
void on_btnCRDelete_clicked();
void on_cbActions_currentIndexChanged(int index);
void on_btnOpenAudioFile_clicked();
void on_btnActionPicOpen_clicked();
void on_btnActionAudioOpen_clicked();
void on_btnAddAction_clicked();
void on_btnDelAction_clicked();
void on_sgActions_cellClicked(int row, int column);
void on_sgActions_cellDoubleClicked(int row, int column);
void updateActionsTable();
void clearActionInputs();
void on_btnDonateAdd_clicked();
void on_btnDonateDel_clicked();
void on_sgDotateTriggers_cellDoubleClicked(int row, int column);
void updateDonationTriggersTable();
void on_cbDonateList_currentIndexChanged(int index);
void on_btnLinksAdd_clicked();
void on_btnLinksDel_clicked();
public slots:
// Установка статуса подключения к Twitch
void setTwitchConnected(bool connected);
@@ -361,6 +411,7 @@ private:
uLink *fLinkForm; // Форма ссылок
TTTVAuth *TTVAuth; // Данные авторизации Twitch
UserManager *m_userManager; // Менеджер пользователей
CounterManager *m_counterManager;
CommandProcessor* m_commandProcessor; // Процессор команд
WebSocketClient *m_twitchClient; // WebSocket клиент для Twitch
UserWidget* m_userWidget; // Виджет пользователя
@@ -369,10 +420,15 @@ private:
MediaFileManager *m_SoundFiles;
MediaFileManager *m_TextFiles;
NeuralTemplateManager *m_neuralTemplateManager;
DonationManager *m_donationManager;
TwitchEventSub *m_twitchEventSub = nullptr;
QList<TimerInfo> m_timers; // Список таймеров
int m_nextTimerId = 1; // Следующий ID таймера
bool m_isTwitchConnected = false; // Статус подключения к Twitch
QVector<TCustomReward*> m_rewards;
ActionManager *m_actionManager;
// возможно, сохраняем текущее редактируемое действие
int m_currentActionId = -1;
// Менеджеры веб-серверов
QList<HttpServer*> m_notificationServers;
QList<HttpServerChat*> m_chatServers;
@@ -403,8 +459,8 @@ private:
int findNotificationServerRow(HttpServer *server);
int findChatServerRow(HttpServerChat *server);
QString generateServerId() const;
void setupCountersTable();
void updateCountersTable();
// Текущие настройки для формы
QVariantMap m_currentSettings;
@@ -464,7 +520,8 @@ private:
// Загрузка эмодзи
void loadEmoties();
void loadChatBadges();
void disconnectFromTwitch();
void connectToTwitch();
/**
* @brief Инициализирует звуковые уведомления
*/
@@ -478,6 +535,11 @@ private:
void loadNeuralTemplatesFromTableWidget();
void processUserCommand(const QString &username, const QString &commandText);
void sendChatResponse(const QString &response);
QString cleanMessageFromAllEmotes(const QString& message) const;
void updateDonateList();
void updateActionsList();
void updateLinksList();
};
#endif // UGENERAL_H
+2446 -1810
View File
File diff suppressed because it is too large Load Diff
+84 -140
View File
@@ -17,16 +17,15 @@ UserWidget::UserWidget(UserManager* userManager, QWidget *parent)
connect(m_tableWidget, &QTableWidget::customContextMenuRequested,
this, &UserWidget::showContextMenu);
setupContextMenu(); // Добавляем инициализацию контекстного меню
// Подключаем сигналы если менеджер уже есть
setupContextMenu();
if (m_userManager) {
connectSignals();
}
// Таймер для периодического обновления
m_refreshTimer = new QTimer(this);
connect(m_refreshTimer, &QTimer::timeout, this, &UserWidget::onRefreshTimer);
m_refreshTimer->start(30000); // 30 секунд
m_refreshTimer->start(30000);
refreshData();
}
@@ -40,7 +39,6 @@ UserWidget::~UserWidget()
void UserWidget::setUserManager(UserManager* userManager)
{
// Отключаем старые соединения если были
if (m_userManager) {
disconnectSignals();
}
@@ -76,14 +74,26 @@ void UserWidget::disconnectSignals()
void UserWidget::setupUI()
{
// ------------------------------------------------------------
// 1. Глобальные настройки самого виджета
// ------------------------------------------------------------
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setMinimumSize(400, 300); // гарантия, что виджет не сожмётся до нуля
// ------------------------------------------------------------
// 2. Главный вертикальный layout
// ------------------------------------------------------------
QVBoxLayout* mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(5, 5, 5, 5);
mainLayout->setSpacing(5);
mainLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
// Панель инструментов
// ------------------------------------------------------------
// 3. Панель инструментов (фильтр, поиск, кнопка обновления)
// ------------------------------------------------------------
QHBoxLayout* toolbarLayout = new QHBoxLayout();
toolbarLayout->setSpacing(5);
// Фильтр
QLabel* filterLabel = new QLabel("Фильтр:", this);
m_filterCombo = new QComboBox(this);
m_filterCombo->addItem("Все зрители", FilterAll);
@@ -91,70 +101,86 @@ void UserWidget::setupUI()
m_filterCombo->addItem("VIP", FilterVIPs);
m_filterCombo->addItem("Подписчики", FilterSubscribers);
m_filterCombo->addItem("Активные (10 мин)", FilterActive);
m_filterCombo->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
connect(m_filterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &UserWidget::onFilterChanged);
// Поиск
QLabel* searchLabel = new QLabel("Поиск:", this);
m_searchEdit = new QLineEdit(this);
m_searchEdit->setPlaceholderText("Введите имя зрителя...");
m_searchEdit->setPlaceholderText("Имя зрителя...");
m_searchEdit->setClearButtonEnabled(true);
m_searchEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
connect(m_searchEdit, &QLineEdit::textChanged,
this, &UserWidget::onSearchTextChanged);
// Кнопка обновления
QPushButton* refreshBtn = new QPushButton("Обновить", this);
refreshBtn->setFixedWidth(100);
refreshBtn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
refreshBtn->setMaximumWidth(100);
connect(refreshBtn, &QPushButton::clicked,
this, &UserWidget::onRefreshButtonClicked);
toolbarLayout->addWidget(filterLabel);
toolbarLayout->addWidget(m_filterCombo);
toolbarLayout->addWidget(searchLabel);
toolbarLayout->addWidget(m_searchEdit);
toolbarLayout->addWidget(m_searchEdit, 1);
toolbarLayout->addStretch();
toolbarLayout->addWidget(refreshBtn);
// Таблица
// ------------------------------------------------------------
// 4. Таблица пользователей
// ------------------------------------------------------------
m_tableWidget = new QTableWidget(this);
m_tableWidget->setColumnCount(6);
m_tableWidget->setHorizontalHeaderLabels(
QStringList() << "Имя" << "Сообщений" << "Статус"
<< "VIP" << "Подписчик" << "Последняя активность");
// Настройка таблицы
m_tableWidget->setSortingEnabled(true);
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableWidget->setAlternatingRowColors(true);
m_tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
m_tableWidget->horizontalHeader()->setStretchLastSection(true);
m_tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
m_tableWidget->verticalHeader()->setVisible(false);
// Настройка ширины колонок
m_tableWidget->setColumnWidth(0, 250); // Имя
m_tableWidget->setColumnWidth(1, 80); // Сообщений
m_tableWidget->setColumnWidth(2, 100); // Статус
m_tableWidget->setColumnWidth(3, 50); // VIP
m_tableWidget->setColumnWidth(4, 80); // Подписчик
// Последняя колонка растягивается
// Политика размеров таблицы – активное расширение
m_tableWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_tableWidget->setMinimumHeight(200); // минимальная высота для отображения хоть чего-то
// Настройка ширины колонок (интерактивно, последняя растягивается)
QHeaderView* header = m_tableWidget->horizontalHeader();
header->setStretchLastSection(true);
header->setSectionResizeMode(QHeaderView::Interactive);
// Начальные предпочтительные ширины
m_tableWidget->setColumnWidth(0, 200);
m_tableWidget->setColumnWidth(1, 80);
m_tableWidget->setColumnWidth(2, 100);
m_tableWidget->setColumnWidth(3, 50);
m_tableWidget->setColumnWidth(4, 80);
connect(m_tableWidget, &QTableWidget::cellDoubleClicked,
this, &UserWidget::onUserDoubleClicked);
// Статистика внизу
// ------------------------------------------------------------
// 5. Панель статистики
// ------------------------------------------------------------
QHBoxLayout* statsLayout = new QHBoxLayout();
m_statsLabel = new QLabel(this);
m_statsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
statsLayout->addWidget(m_statsLabel);
statsLayout->addStretch();
// Добавляем всё на форму
// ------------------------------------------------------------
// 6. Сборка интерфейса
// ------------------------------------------------------------
mainLayout->addLayout(toolbarLayout);
mainLayout->addWidget(m_tableWidget);
mainLayout->addWidget(m_tableWidget, 1); // растягиваем таблицу с коэффициентом 1
mainLayout->addLayout(statsLayout);
setLayout(mainLayout);
// Принудительно обновляем геометрию после установки layout
updateGeometry();
}
void UserWidget::refreshData()
@@ -176,7 +202,6 @@ void UserWidget::populateTable()
QVector<User*> users;
// Получаем пользователей в зависимости от фильтра
switch (m_currentFilter) {
case FilterAll:
for (const User& user : m_userManager->getAllUsers()) {
@@ -197,11 +222,9 @@ void UserWidget::populateTable()
break;
}
// Применяем поисковый фильтр
if (!m_searchText.isEmpty()) {
QString searchLower = m_searchText.toLower();
QVector<User*> filteredUsers;
for (User* user : users) {
if (user->displayName.toLower().contains(searchLower) ||
user->login.toLower().contains(searchLower)) {
@@ -211,22 +234,18 @@ void UserWidget::populateTable()
users = filteredUsers;
}
// Заполняем таблицу
m_tableWidget->setRowCount(users.size());
for (int i = 0; i < users.size(); ++i) {
User* user = users[i];
// Имя
QTableWidgetItem* nameItem = new QTableWidgetItem(user->displayName);
nameItem->setData(Qt::UserRole, user->id);
m_tableWidget->setItem(i, 0, nameItem);
// Количество сообщений
m_tableWidget->setItem(i, 1,
new QTableWidgetItem(QString::number(user->messageCount)));
// Статус
QString status;
if (user->isModerator) status = "👑 Модератор";
else if (user->isVIP) status = "⭐ VIP";
@@ -234,43 +253,35 @@ void UserWidget::populateTable()
else status = "👤 Зритель";
m_tableWidget->setItem(i, 2, new QTableWidgetItem(status));
// VIP
QTableWidgetItem* vipItem = new QTableWidgetItem(user->isVIP ? "Да" : "Нет");
if (user->isVIP) {
vipItem->setForeground(Qt::darkGreen);
}
if (user->isVIP) vipItem->setForeground(Qt::darkGreen);
m_tableWidget->setItem(i, 3, vipItem);
// Подписчик
QTableWidgetItem* subItem = new QTableWidgetItem(user->isSubscriber ? "Да" : "Нет");
if (user->isSubscriber) {
subItem->setForeground(Qt::darkBlue);
}
if (user->isSubscriber) subItem->setForeground(Qt::darkBlue);
m_tableWidget->setItem(i, 4, subItem);
// Последняя активность
QString lastActive;
if (user->lastMessageTime.isValid()) {
QDateTime now = QDateTime::currentDateTime();
qint64 secs = user->lastMessageTime.secsTo(now);
if (secs < 60) {
lastActive = "Только что";
} else if (secs < 3600) {
lastActive = QString("%1 мин назад").arg(secs / 60);
} else if (secs < 86400) {
lastActive = QString("%1 ч назад").arg(secs / 3600);
} else {
lastActive = user->lastMessageTime.toString("dd.MM.yyyy hh:mm");
}
if (secs < 60) lastActive = "Только что";
else if (secs < 3600) lastActive = QString("%1 мин назад").arg(secs / 60);
else if (secs < 86400) lastActive = QString("%1 ч назад").arg(secs / 3600);
else lastActive = user->lastMessageTime.toString("dd.MM.yyyy hh:mm");
} else {
lastActive = "Никогда";
}
m_tableWidget->setItem(i, 5, new QTableWidgetItem(lastActive));
}
// Автоподгон ширины колонок после заполнения
// Автоподгон ширины колонок с ограничением
m_tableWidget->resizeColumnsToContents();
for (int col = 0; col < m_tableWidget->columnCount() - 1; ++col) {
if (m_tableWidget->columnWidth(col) > 300) {
m_tableWidget->setColumnWidth(col, 300);
}
}
}
void UserWidget::updateStatistics()
@@ -292,16 +303,13 @@ void UserWidget::addUserToTable(const User &user)
int row = m_tableWidget->rowCount();
m_tableWidget->insertRow(row);
// Имя
QTableWidgetItem* nameItem = new QTableWidgetItem(user.displayName);
nameItem->setData(Qt::UserRole, user.id);
m_tableWidget->setItem(row, 0, nameItem);
// Количество сообщений
m_tableWidget->setItem(row, 1,
new QTableWidgetItem(QString::number(user.messageCount)));
// Статус
QString status;
if (user.isModerator) status = "👑 Модератор";
else if (user.isVIP) status = "⭐ VIP";
@@ -309,23 +317,15 @@ void UserWidget::addUserToTable(const User &user)
else status = "👤 Зритель";
m_tableWidget->setItem(row, 2, new QTableWidgetItem(status));
// VIP
QTableWidgetItem* vipItem = new QTableWidgetItem(user.isVIP ? "Да" : "Нет");
if (user.isVIP) {
vipItem->setForeground(Qt::darkGreen);
}
if (user.isVIP) vipItem->setForeground(Qt::darkGreen);
m_tableWidget->setItem(row, 3, vipItem);
// Подписчик
QTableWidgetItem* subItem = new QTableWidgetItem(user.isSubscriber ? "Да" : "Нет");
if (user.isSubscriber) {
subItem->setForeground(Qt::darkBlue);
}
if (user.isSubscriber) subItem->setForeground(Qt::darkBlue);
m_tableWidget->setItem(row, 4, subItem);
// Последняя активность
QString lastActive = "Только что";
m_tableWidget->setItem(row, 5, new QTableWidgetItem(lastActive));
m_tableWidget->setItem(row, 5, new QTableWidgetItem("Только что"));
updateStatistics();
}
@@ -335,11 +335,9 @@ void UserWidget::updateUserInTable(const User &user)
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
QTableWidgetItem* item = m_tableWidget->item(row, 0);
if (item && item->data(Qt::UserRole).toString() == user.id) {
// Обновляем данные
item->setText(user.displayName);
m_tableWidget->item(row, 1)->setText(
QString::number(user.messageCount));
m_tableWidget->item(row, 1)->setText(QString::number(user.messageCount));
QString status;
if (user.isModerator) status = "👑 Модератор";
@@ -349,34 +347,19 @@ void UserWidget::updateUserInTable(const User &user)
m_tableWidget->item(row, 2)->setText(status);
m_tableWidget->item(row, 3)->setText(user.isVIP ? "Да" : "Нет");
if (user.isVIP) {
m_tableWidget->item(row, 3)->setForeground(Qt::darkGreen);
} else {
m_tableWidget->item(row, 3)->setForeground(Qt::black);
}
m_tableWidget->item(row, 3)->setForeground(user.isVIP ? Qt::darkGreen : Qt::black);
m_tableWidget->item(row, 4)->setText(user.isSubscriber ? "Да" : "Нет");
if (user.isSubscriber) {
m_tableWidget->item(row, 4)->setForeground(Qt::darkBlue);
} else {
m_tableWidget->item(row, 4)->setForeground(Qt::black);
}
m_tableWidget->item(row, 4)->setForeground(user.isSubscriber ? Qt::darkBlue : Qt::black);
// Обновляем время активности
QString lastActive;
if (user.lastMessageTime.isValid()) {
QDateTime now = QDateTime::currentDateTime();
qint64 secs = user.lastMessageTime.secsTo(now);
if (secs < 60) {
lastActive = "Только что";
} else if (secs < 3600) {
lastActive = QString("%1 мин назад").arg(secs / 60);
} else if (secs < 86400) {
lastActive = QString("%1 ч назад").arg(secs / 3600);
} else {
lastActive = user.lastMessageTime.toString("dd.MM hh:mm");
}
if (secs < 60) lastActive = "Только что";
else if (secs < 3600) lastActive = QString("%1 мин назад").arg(secs / 60);
else if (secs < 86400) lastActive = QString("%1 ч назад").arg(secs / 3600);
else lastActive = user.lastMessageTime.toString("dd.MM hh:mm");
}
m_tableWidget->item(row, 5)->setText(lastActive);
@@ -388,7 +371,6 @@ void UserWidget::updateUserInTable(const User &user)
void UserWidget::removeUserFromTable(const QString &displayName)
{
// Ищем пользователя по displayName
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
QTableWidgetItem* item = m_tableWidget->item(row, 0);
if (item && item->text() == displayName) {
@@ -417,19 +399,12 @@ void UserWidget::onRefreshTimer()
User* user = m_userManager->findUserById(userId);
if (user && user->lastMessageTime.isValid()) {
qint64 secs = user->lastMessageTime.secsTo(now);
QString lastActive;
if (secs < 60) {
lastActive = "Только что";
} else if (secs < 3600) {
lastActive = QString("%1 мин назад").arg(secs / 60);
} else if (secs < 86400) {
lastActive = QString("%1 ч назад").arg(secs / 3600);
} else {
lastActive = user->lastMessageTime.toString("dd.MM hh:mm");
}
if (secs < 60) lastActive = "Только что";
else if (secs < 3600) lastActive = QString("%1 мин назад").arg(secs / 60);
else if (secs < 86400) lastActive = QString("%1 ч назад").arg(secs / 3600);
else lastActive = user->lastMessageTime.toString("dd.MM hh:mm");
// Проверяем, существует ли ячейка
if (m_tableWidget->item(row, 5)) {
m_tableWidget->item(row, 5)->setText(lastActive);
}
@@ -442,27 +417,17 @@ bool UserWidget::userMatchesFilter(const User* user) const
{
if (!user) return false;
// Проверка фильтра
switch (m_currentFilter) {
case FilterAll:
break;
case FilterModerators:
if (!user->isModerator) return false;
break;
case FilterVIPs:
if (!user->isVIP) return false;
break;
case FilterSubscribers:
if (!user->isSubscriber) return false;
break;
case FilterAll: break;
case FilterModerators: if (!user->isModerator) return false; break;
case FilterVIPs: if (!user->isVIP) return false; break;
case FilterSubscribers: if (!user->isSubscriber) return false; break;
case FilterActive:
// Активные за последние 10 минут
if (user->lastMessageTime.secsTo(QDateTime::currentDateTime()) > 10*60)
if (user->lastMessageTime.secsTo(QDateTime::currentDateTime()) > 10 * 60)
return false;
break;
}
// Проверка поиска
if (!m_searchText.isEmpty()) {
QString searchLower = m_searchText.toLower();
if (!user->displayName.toLower().contains(searchLower) &&
@@ -476,7 +441,6 @@ bool UserWidget::userMatchesFilter(const User* user) const
void UserWidget::onUserAdded(const User &user)
{
// Проверяем, проходит ли пользователь текущий фильтр и поиск
if (userMatchesFilter(&user)) {
addUserToTable(user);
}
@@ -484,22 +448,16 @@ void UserWidget::onUserAdded(const User &user)
void UserWidget::onUserUpdated(const User &user)
{
// Ищем пользователя в таблице
int row = findRowByUserId(user.id);
if (row >= 0) {
// Пользователь уже в таблице
if (userMatchesFilter(&user)) {
// Обновляем существующую запись
updateUserInTable(user);
} else {
// Удаляем, так как пользователь больше не проходит фильтр
removeUserFromTableByUserId(user.id);
}
} else {
// Пользователя нет в таблице
if (userMatchesFilter(&user)) {
// Добавляем, так как пользователь теперь проходит фильтр
addUserToTable(user);
}
}
@@ -534,7 +492,6 @@ void UserWidget::onUserRemoved(const QString &userId, const QString &displayName
void UserWidget::onUserDoubleClicked(int row, int column)
{
Q_UNUSED(column);
if (!m_userManager) return;
QTableWidgetItem* item = m_tableWidget->item(row, 0);
@@ -551,9 +508,6 @@ void UserWidget::onUserDoubleClicked(int row, int column)
<< "\nVIP:" << user->isVIP
<< "\nПодписчик:" << user->isSubscriber
<< "\nПоследнее сообщение:" << user->lastMessageTime.toString();
// Здесь можно открыть диалог с детальной информацией
// или выполнить другие действия
}
}
}
@@ -610,38 +564,29 @@ void UserWidget::setupContextMenu()
connect(m_infoAction, &QAction::triggered, this, &UserWidget::onShowUserInfo);
}
// Добавляем обработчик контекстного меню:
void UserWidget::showContextMenu(const QPoint &pos)
{
QTableWidgetItem* item = m_tableWidget->itemAt(pos);
if (!item) {
return; // Клик был не по ячейке (например, по заголовку или пустой области)
}
if (!item) return;
int row = item->row();
QTableWidgetItem* nameItem = m_tableWidget->item(row, 0);
if (nameItem) {
m_selectedUserId = nameItem->data(Qt::UserRole).toString();
m_selectedUserName = nameItem->text();
// Получаем данные пользователя
User* user = m_userManager->findUserById(m_selectedUserId);
if (user) {
// Обновляем состояние пунктов меню
m_setModAction->setEnabled(!user->isModerator);
m_removeModAction->setEnabled(user->isModerator);
m_setVIPAction->setEnabled(!user->isVIP);
m_removeVIPAction->setEnabled(user->isVIP);
// Показываем меню в позиции клика
m_contextMenu->exec(m_tableWidget->viewport()->mapToGlobal(pos));
}
}
}
// Добавляем вспомогательный метод:
User* UserWidget::getSelectedUser()
{
if (!m_selectedUserId.isEmpty()) {
@@ -650,7 +595,6 @@ User* UserWidget::getSelectedUser()
return nullptr;
}
// Добавляем слоты для обработки действий меню:
void UserWidget::onBanUser()
{
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
+2 -2
View File
@@ -54,7 +54,7 @@ bool WebSocketClient::connectToServer(const QString &url)
return true;
}
void WebSocketClient::connectToTwitchChat(const QString &oauthToken,
bool WebSocketClient::connectToTwitchChat(const QString &oauthToken,
const QString &nickname,
const QString &channel)
{
@@ -65,7 +65,7 @@ void WebSocketClient::connectToTwitchChat(const QString &oauthToken,
// URL для подключения к Twitch IRC через WebSocket
QString url = "wss://irc-ws.chat.twitch.tv:443";
connectToServer(url);
return connectToServer(url);
}
void WebSocketClient::onConnectedInternal()
+1 -1
View File
@@ -23,7 +23,7 @@ public:
Q_INVOKABLE bool isConnected() const;
// Методы для Twitch чата
Q_INVOKABLE void connectToTwitchChat(const QString &oauthToken, const QString &nickname, const QString &channel);
Q_INVOKABLE bool connectToTwitchChat(const QString &oauthToken, const QString &nickname, const QString &channel);
Q_INVOKABLE void joinChannel(const QString &channel);
Q_INVOKABLE void sendChatMessage(const QString &channel, const QString &message);