diff --git a/TTW_Bot.pro b/TTW_Bot.pro index 7add8b8..dd74d90 100644 --- a/TTW_Bot.pro +++ b/TTW_Bot.pro @@ -53,6 +53,7 @@ SOURCES += \ websocketclient.cpp HEADERS += \ + action.h \ badge.h \ commandprocessor.h \ countermanager.h \ diff --git a/action.h b/action.h new file mode 100644 index 0000000..e56e2a8 --- /dev/null +++ b/action.h @@ -0,0 +1,35 @@ +// action.h +#ifndef ACTION_H +#define ACTION_H + +#include +#include + +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 diff --git a/ttw_api.cpp b/ttw_api.cpp index 2a0d2c2..8e62aaa 100644 --- a/ttw_api.cpp +++ b/ttw_api.cpp @@ -1,5 +1,6 @@ #include "ttw_api.h" #include "qeventloop.h" +#include "ttw_types.h" #include #include #include @@ -692,3 +693,115 @@ bool TTwAPI::validateTwitchToken(const QString &tokenName, return false; } } + +void TTwAPI::getCustomRewards(QVector &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); +} diff --git a/ttw_api.h b/ttw_api.h index 11010dc..ba53b3e 100644 --- a/ttw_api.h +++ b/ttw_api.h @@ -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 &rewards); + void getCustomRewards(QVector &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); diff --git a/ttw_types.h b/ttw_types.h index 90a75a0..4620e6e 100644 --- a/ttw_types.h +++ b/ttw_types.h @@ -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; diff --git a/ugeneral.cpp b/ugeneral.cpp index 4ea0ce4..2c4aeae 100644 --- a/ugeneral.cpp +++ b/ugeneral.cpp @@ -2,6 +2,7 @@ #include "fcreatenotify.h" #include "filemanager.h" #include "logmanager.h" +#include "ttw_types.h" #include "ui_ugeneral.h" #include #include @@ -187,7 +188,7 @@ void uGeneral::setupButtonIcons() { button->setIcon(tabIcons[11]); } } - else if (buttonName.contains("edt")) { + else if (buttonName.contains("edt") || (buttonName.contains("edit"))) { button->setIcon(tabIcons[10]); } else if (buttonName.contains("open")) { @@ -1805,6 +1806,12 @@ void uGeneral::handleNewMessage(const QString &message) if (m_userWidget) { m_userWidget->updateStatistics(); } + + if (ui->cbTextToSpeach->isChecked()) + { + return; + } + playNotify(msg.isMod, msg.isVIP, msg.isSubscriber); QString processedMessage = processTwitchMessage(msg); @@ -3190,3 +3197,282 @@ void uGeneral::on_btnCounterAtoText_clicked() cursor.insertText("|)" + ui->cbCounters->currentText() + "|)"); } + +void uGeneral::on_btnCRGet_clicked() +{ + // Очистка старых данных + qDeleteAll(m_rewards); + m_rewards.clear(); + + // 1. Получаем ВСЕ награды канала + QVector allRewards; + twitchAPI->getCustomRewards(allRewards, false); + + // 2. Получаем только управляемые ботом + QVector manageableRewards; + twitchAPI->getCustomRewards(manageableRewards, true); + + // 3. Формируем множество ID управляемых наград + QSet 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); +} + diff --git a/ugeneral.h b/ugeneral.h index 7522c0c..a696dc3 100644 --- a/ugeneral.h +++ b/ugeneral.h @@ -354,6 +354,18 @@ private slots: 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: // Установка статуса подключения к Twitch void setTwitchConnected(bool connected); @@ -383,7 +395,7 @@ private: QList m_timers; // Список таймеров int m_nextTimerId = 1; // Следующий ID таймера bool m_isTwitchConnected = false; // Статус подключения к Twitch - + QVector m_rewards; // Менеджеры веб-серверов QList m_notificationServers; QList m_chatServers; diff --git a/ugeneral.ui b/ugeneral.ui index a2b8716..9618795 100644 --- a/ugeneral.ui +++ b/ugeneral.ui @@ -42,7 +42,7 @@ Qt::LeftToRight - 2 + 3 false @@ -878,7 +878,7 @@ - Озвучить после !!! + Игнорировать все сообщения @@ -1087,7 +1087,158 @@ Навыки - + + + + + + + Баллы канала + + + + + + + + + 0 + + + + + Название: + + + + + + + + + + Описание: + + + + + + + + + + Цена: + + + + + + + 9999999 + + + 1000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 10 + + + + + Добавить + + + + + + + Изменить + + + + + + + Удалить + + + + + + + Обновить + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Действия + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + +