+ награды за баллы

- создание
- изменение
- удаление
- отображение
This commit is contained in:
2026-02-15 11:26:19 +03:00
parent 63b7fa4ea1
commit ae4121157d
8 changed files with 608 additions and 19 deletions
+1
View File
@@ -53,6 +53,7 @@ SOURCES += \
websocketclient.cpp websocketclient.cpp
HEADERS += \ HEADERS += \
action.h \
badge.h \ badge.h \
commandprocessor.h \ commandprocessor.h \
countermanager.h \ countermanager.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
+113
View File
@@ -1,5 +1,6 @@
#include "ttw_api.h" #include "ttw_api.h"
#include "qeventloop.h" #include "qeventloop.h"
#include "ttw_types.h"
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
@@ -692,3 +693,115 @@ bool TTwAPI::validateTwitchToken(const QString &tokenName,
return false; 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 TCustomReward;
class TCustomRewardEvent; class TCustomRewardEvent;
class ChatBadge; struct ChatBadge;
class Emote; class Emote;
class TTwAPI : public QObject class TTwAPI : public QObject
@@ -53,14 +53,14 @@ public:
void delVIP(const QString &id); void delVIP(const QString &id);
// Custom Rewards // Custom Rewards
void getCustomRewards(QVector<TCustomReward*> &rewards); void getCustomRewards(QVector<TCustomReward*> &rewards, bool onlyManageable = false);
TCustomReward* createCustomReward(const QString &title, TCustomReward* createCustomReward(const QString &title,
const QString &cost, const QString &cost,
const QString &prompt = "", const QString &prompt = "",
bool isUserInput = false); bool isUserInput = false);
void updateCustomReward(TCustomReward* reward); void updateCustomReward(TCustomReward* reward);
void updateRedemptionStatus(TCustomRewardEvent* event); void updateRedemptionStatus(TCustomRewardEvent* event);
void deleteCustomReward(const QString &id); bool deleteCustomReward(const QString &id);
// Пользователи // Пользователи
User getUserByLogin(const QString &login); User getUserByLogin(const QString &login);
+2 -11
View File
@@ -22,7 +22,7 @@ struct TCustomReward {
bool isPaused; bool isPaused;
bool isInStock; bool isInStock;
bool shouldRedemptionsSkipRequestQueue; bool shouldRedemptionsSkipRequestQueue;
bool isManagedByBroadcaster;
TCustomReward() TCustomReward()
: cost(0) : cost(0)
, isEnabled(true) , isEnabled(true)
@@ -48,21 +48,12 @@ struct TCustomRewardEvent {
QString userInput; QString userInput;
QString status; QString status;
QDateTime redeemedAt; QDateTime redeemedAt;
QString redemptionId;
TCustomRewardEvent() {} TCustomRewardEvent() {}
}; };
struct ChatBadge {
QString setId;
QString versionId;
QString title;
QString description;
QString smallImageUrl;
QString mediumImageUrl;
QString largeImageUrl;
ChatBadge() {}
};
struct Emote { struct Emote {
QString id; QString id;
+287 -1
View File
@@ -2,6 +2,7 @@
#include "fcreatenotify.h" #include "fcreatenotify.h"
#include "filemanager.h" #include "filemanager.h"
#include "logmanager.h" #include "logmanager.h"
#include "ttw_types.h"
#include "ui_ugeneral.h" #include "ui_ugeneral.h"
#include <QStandardPaths> #include <QStandardPaths>
#include <QDesktopServices> #include <QDesktopServices>
@@ -187,7 +188,7 @@ void uGeneral::setupButtonIcons() {
button->setIcon(tabIcons[11]); button->setIcon(tabIcons[11]);
} }
} }
else if (buttonName.contains("edt")) { else if (buttonName.contains("edt") || (buttonName.contains("edit"))) {
button->setIcon(tabIcons[10]); button->setIcon(tabIcons[10]);
} }
else if (buttonName.contains("open")) { else if (buttonName.contains("open")) {
@@ -1805,6 +1806,12 @@ void uGeneral::handleNewMessage(const QString &message)
if (m_userWidget) { if (m_userWidget) {
m_userWidget->updateStatistics(); m_userWidget->updateStatistics();
} }
if (ui->cbTextToSpeach->isChecked())
{
return;
}
playNotify(msg.isMod, msg.isVIP, msg.isSubscriber); playNotify(msg.isMod, msg.isVIP, msg.isSubscriber);
QString processedMessage = processTwitchMessage(msg); QString processedMessage = processTwitchMessage(msg);
@@ -3190,3 +3197,282 @@ void uGeneral::on_btnCounterAtoText_clicked()
cursor.insertText("|)" + ui->cbCounters->currentText() + "|)"); cursor.insertText("|)" + ui->cbCounters->currentText() + "|)");
} }
void uGeneral::on_btnCRGet_clicked()
{
// Очистка старых данных
qDeleteAll(m_rewards);
m_rewards.clear();
// 1. Получаем ВСЕ награды канала
QVector<TCustomReward*> allRewards;
twitchAPI->getCustomRewards(allRewards, false);
// 2. Получаем только управляемые ботом
QVector<TCustomReward*> manageableRewards;
twitchAPI->getCustomRewards(manageableRewards, true);
// 3. Формируем множество ID управляемых наград
QSet<QString> manageableIds;
for (auto *r : manageableRewards) {
manageableIds.insert(r->id);
}
// 4. Освобождаем manageableRewards (они больше не нужны)
qDeleteAll(manageableRewards);
manageableRewards.clear();
// 5. Проставляем флаг для всех наград
for (auto *r : allRewards) {
r->isManagedByBroadcaster = manageableIds.contains(r->id);
}
// 6. Сохраняем в член класса
m_rewards = allRewards;
// 7. Заполняем таблицу
ui->sgCustomRewards->setRowCount(0);
ui->sgCustomRewards->setColumnCount(3);
QStringList headers = {"Название", "Цена", "Описание"};
ui->sgCustomRewards->setHorizontalHeaderLabels(headers);
ui->sgCustomRewards->setRowCount(m_rewards.size());
for (int row = 0; row < m_rewards.size(); ++row) {
TCustomReward *reward = m_rewards[row];
QTableWidgetItem *nameItem = new QTableWidgetItem(reward->title);
nameItem->setData(Qt::UserRole, reward->id);
ui->sgCustomRewards->setItem(row, 0, nameItem);
QTableWidgetItem *costItem = new QTableWidgetItem(QString::number(reward->cost));
ui->sgCustomRewards->setItem(row, 1, costItem);
QTableWidgetItem *descItem = new QTableWidgetItem(reward->prompt);
ui->sgCustomRewards->setItem(row, 2, descItem);
// Устанавливаем цвет фона
QColor bgColor = reward->isManagedByBroadcaster ? QColor(144,238,144) : QColor(255,200,200);
nameItem->setForeground(bgColor);
costItem->setForeground(bgColor);
descItem->setForeground(bgColor);
}
// Сбрасываем поля и кнопки
ui->edtCRName->clear();
ui->edtCRPrompt->clear();
ui->sbCRCost->setValue(0);
ui->btnCREdit->setEnabled(false);
ui->btnCRDelete->setEnabled(false);
}
void uGeneral::on_sgCustomRewards_cellClicked(int row, int column)
{
// Получаем ID награды из первого столбца
QTableWidgetItem *idItem = ui->sgCustomRewards->item(row, 0);
if (!idItem) return;
QString rewardId = idItem->data(Qt::UserRole).toString();
// Ищем объект награды в m_rewards по ID
TCustomReward *reward = nullptr;
for (auto *r : m_rewards) {
if (r->id == rewardId) {
reward = r;
break;
}
}
if (!reward) return; // защита от ошибок
// Заполняем поля ввода
ui->edtCRName->setText(reward->title);
ui->edtCRPrompt->setText(reward->prompt);
ui->sbCRCost->setValue(reward->cost);
// Активируем кнопки только если награда управляется ботом
bool canEdit = reward->isManagedByBroadcaster;
ui->btnCREdit->setEnabled(canEdit);
ui->btnCRDelete->setEnabled(canEdit);
}
void uGeneral::on_sgCustomRewards_cellDoubleClicked(int row, int column)
{
}
void uGeneral::on_btnCRAdd_clicked()
{
// 1. Получаем данные из интерфейса
QString title = ui->edtCRName->text().trimmed();
QString prompt = ui->edtCRPrompt->text().trimmed();
int cost = ui->sbCRCost->value();
// 2. Простейшая валидация
if (title.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Название награды не может быть пустым.");
return;
}
if (cost <= 0) {
QMessageBox::warning(this, "Ошибка", "Стоимость должна быть больше 0.");
return;
}
// 3. Вызываем API для создания награды
// Предполагаем, что чекбокс "Требуется ввод пользователя" отсутствует, ставим false
TCustomReward *newReward = twitchAPI->createCustomReward(title, QString::number(cost), prompt, false);
if (!newReward) {
QMessageBox::critical(this, "Ошибка", "Не удалось создать награду. Проверьте подключение к Twitch и права токена.");
return;
}
// 4. Уведомляем пользователя об успехе
QMessageBox::information(this, "Успех", "Награда успешно создана.");
// 5. Очищаем поля ввода (опционально)
ui->edtCRName->clear();
ui->edtCRPrompt->clear();
ui->sbCRCost->setValue(0);
// 6. Обновляем список наград, чтобы новая появилась в таблице
on_btnCRGet_clicked(); // перезагружает и обновляет таблицу
// Важно: newReward был создан в куче, но после вызова on_btnCRGet_clicked()
// старые указатели будут удалены, а новые получены. Указатель newReward
// становится недействительным, поэтому не используем его дальше.
}
void uGeneral::on_btnCREdit_clicked()
{
// 1. Проверяем, что выбрана строка в таблице
int currentRow = ui->sgCustomRewards->currentRow();
if (currentRow < 0) {
QMessageBox::warning(this, "Ошибка", "Выберите награду для редактирования.");
return;
}
// 2. Извлекаем ID награды из первого столбца (хранится в Qt::UserRole)
QTableWidgetItem *idItem = ui->sgCustomRewards->item(currentRow, 0);
if (!idItem) return;
QString rewardId = idItem->data(Qt::UserRole).toString();
// 3. Ищем объект награды в векторе m_rewards
TCustomReward *reward = nullptr;
for (auto *r : m_rewards) {
if (r->id == rewardId) {
reward = r;
break;
}
}
if (!reward) {
QMessageBox::critical(this, "Ошибка", "Не удалось найти данные награды.");
return;
}
// 4. Проверяем, что награда управляется ботом (кнопка должна быть активна только для таких)
if (!reward->isManagedByBroadcaster) {
QMessageBox::warning(this, "Ошибка", "Нельзя редактировать награду, созданную не ботом.");
return;
}
// 5. Получаем новые значения из полей ввода
QString newTitle = ui->edtCRName->text().trimmed();
QString newPrompt = ui->edtCRPrompt->text().trimmed();
int newCost = ui->sbCRCost->value();
// 6. Валидация
if (newTitle.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Название не может быть пустым.");
return;
}
if (newCost <= 0) {
QMessageBox::warning(this, "Ошибка", "Стоимость должна быть больше 0.");
return;
}
// 7. Обновляем данные в объекте
reward->title = newTitle;
reward->prompt = newPrompt;
reward->cost = newCost;
// 8. Вызываем API для обновления награды
twitchAPI->updateCustomReward(reward); // предполагается, что метод возвращает void
// 9. Обновляем список наград (гарантирует синхронизацию с Twitch)
on_btnCRGet_clicked();
// 10. Уведомляем пользователя об успехе
QMessageBox::information(this, "Успех", "Награда обновлена.");
}
void uGeneral::on_btnCRDelete_clicked()
{
// 1. Проверяем, что выбрана строка в таблице
int currentRow = ui->sgCustomRewards->currentRow();
if (currentRow < 0) {
QMessageBox::warning(this, "Ошибка", "Выберите награду для удаления.");
return;
}
// 2. Извлекаем ID награды из первого столбца
QTableWidgetItem *idItem = ui->sgCustomRewards->item(currentRow, 0);
if (!idItem) return;
QString rewardId = idItem->data(Qt::UserRole).toString();
// 3. Находим объект награды в m_rewards (для проверки управляемости)
TCustomReward *reward = nullptr;
for (auto *r : m_rewards) {
if (r->id == rewardId) {
reward = r;
break;
}
}
if (!reward) {
QMessageBox::critical(this, "Ошибка", "Не удалось найти данные награды.");
return;
}
// 4. Проверяем, что награда управляется ботом
if (!reward->isManagedByBroadcaster) {
QMessageBox::warning(this, "Ошибка", "Нельзя удалить награду, созданную не ботом.");
return;
}
// 5. Запрашиваем подтверждение
QString title = reward->title;
QMessageBox::StandardButton reply = QMessageBox::question(
this,
"Подтверждение удаления",
QString("Вы уверены, что хотите удалить награду \"%1\"?").arg(title),
QMessageBox::Yes | QMessageBox::No
);
if (reply != QMessageBox::Yes) {
return; // пользователь отменил
}
// 6. Вызываем API для удаления
bool success = twitchAPI->deleteCustomReward(rewardId); // предполагаем, что метод возвращает bool
if (!success) {
QMessageBox::critical(this, "Ошибка", "Не удалось удалить награду. Проверьте подключение к Twitch и права токена.");
return;
}
// 7. Уведомляем об успехе
QMessageBox::information(this, "Успех", "Награда успешно удалена.");
// 8. Обновляем список наград (удалённая исчезнет)
on_btnCRGet_clicked();
// 9. Сбрасываем поля ввода и отключаем кнопки (так как выбор пропал)
ui->edtCRName->clear();
ui->edtCRPrompt->clear();
ui->sbCRCost->setValue(0);
ui->btnCREdit->setEnabled(false);
ui->btnCRDelete->setEnabled(false);
}
+13 -1
View File
@@ -354,6 +354,18 @@ private slots:
void on_btnCounterAtoText_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();
public slots: public slots:
// Установка статуса подключения к Twitch // Установка статуса подключения к Twitch
void setTwitchConnected(bool connected); void setTwitchConnected(bool connected);
@@ -383,7 +395,7 @@ private:
QList<TimerInfo> m_timers; // Список таймеров QList<TimerInfo> m_timers; // Список таймеров
int m_nextTimerId = 1; // Следующий ID таймера int m_nextTimerId = 1; // Следующий ID таймера
bool m_isTwitchConnected = false; // Статус подключения к Twitch bool m_isTwitchConnected = false; // Статус подключения к Twitch
QVector<TCustomReward*> m_rewards;
// Менеджеры веб-серверов // Менеджеры веб-серверов
QList<HttpServer*> m_notificationServers; QList<HttpServer*> m_notificationServers;
QList<HttpServerChat*> m_chatServers; QList<HttpServerChat*> m_chatServers;
+154 -3
View File
@@ -42,7 +42,7 @@
<enum>Qt::LeftToRight</enum> <enum>Qt::LeftToRight</enum>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>2</number> <number>3</number>
</property> </property>
<property name="tabsClosable"> <property name="tabsClosable">
<bool>false</bool> <bool>false</bool>
@@ -878,7 +878,7 @@
<item> <item>
<widget class="QCheckBox" name="cbTextToSpeach"> <widget class="QCheckBox" name="cbTextToSpeach">
<property name="text"> <property name="text">
<string>Озвучить после !!!</string> <string>Игнорировать все сообщения</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1087,7 +1087,158 @@
<attribute name="title"> <attribute name="title">
<string>Навыки</string> <string>Навыки</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_tab_skills"/> <layout class="QVBoxLayout" name="verticalLayout_tab_skills">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_17">
<item>
<widget class="QGroupBox" name="groupBox_12">
<property name="title">
<string>Баллы канала</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_28">
<item>
<widget class="QTableWidget" name="sgCustomRewards"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_18">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_26">
<property name="text">
<string>Название:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edtCRName"/>
</item>
<item>
<widget class="QLabel" name="label_27">
<property name="text">
<string>Описание:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edtCRPrompt"/>
</item>
<item>
<widget class="QLabel" name="label_28">
<property name="text">
<string>Цена:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="sbCRCost">
<property name="maximum">
<number>9999999</number>
</property>
<property name="singleStep">
<number>1000</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_11">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_19">
<property name="topMargin">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="btnCRAdd">
<property name="text">
<string>Добавить</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnCREdit">
<property name="text">
<string>Изменить</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnCRDelete">
<property name="text">
<string>Удалить</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnCRGet">
<property name="text">
<string>Обновить</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_13">
<property name="title">
<string>Действия</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_30">
<item>
<layout class="QVBoxLayout" name="verticalLayout_29">
<item>
<widget class="QListWidget" name="listWidget_2"/>
</item>
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget> </widget>
<widget class="QWidget" name="tab_5"> <widget class="QWidget" name="tab_5">
<property name="icon" stdset="0"> <property name="icon" stdset="0">