diff --git a/src/macro-external/twitch/CMakeLists.txt b/src/macro-external/twitch/CMakeLists.txt index 1ab9f6af..ded8df46 100644 --- a/src/macro-external/twitch/CMakeLists.txt +++ b/src/macro-external/twitch/CMakeLists.txt @@ -48,6 +48,8 @@ target_sources( macro-action-twitch.hpp macro-condition-twitch.cpp macro-condition-twitch.hpp + points-reward-selection.cpp + points-reward-selection.hpp token.cpp token.hpp twitch-helpers.cpp diff --git a/src/macro-external/twitch/points-reward-selection.cpp b/src/macro-external/twitch/points-reward-selection.cpp new file mode 100644 index 00000000..b76068e8 --- /dev/null +++ b/src/macro-external/twitch/points-reward-selection.cpp @@ -0,0 +1,219 @@ +#include "points-reward-selection.hpp" +#include "twitch-helpers.hpp" + +#include +#include + +namespace advss { + +void TwitchPointsReward::Load(obs_data_t *obj) +{ + OBSDataAutoRelease data = obs_data_get_obj(obj, "pointsReward"); + id = obs_data_get_string(data, "id"); + title = obs_data_get_string(data, "title"); +} + +void TwitchPointsReward::Save(obs_data_t *obj) const +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "id", id.c_str()); + obs_data_set_string(data, "title", title.c_str()); + obs_data_set_obj(obj, "pointsReward", data); +} + +TwitchPointsRewardSelection::TwitchPointsRewardSelection(QWidget *parent, + bool allowAny) + : FilterComboBox( + parent, + obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.placeholder")), + _allowAny(allowAny) +{ + setDuplicatesEnabled(true); + setSizeAdjustPolicy(QComboBox::AdjustToContents); + + QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this, + SLOT(SelectionChanged(int))); +} + +void TwitchPointsRewardSelection::SetPointsReward( + const TwitchPointsReward &pointsReward) +{ + int index = findData(QString::fromStdString(pointsReward.id)); + if (index != -1) { + setCurrentIndex(index); + return; + } + + setCurrentIndex(-1); +} + +void TwitchPointsRewardSelection::SetChannel(const TwitchChannel &channel) +{ + _channel = channel; + PopulateSelection(); +} + +void TwitchPointsRewardSelection::SetToken( + const std::weak_ptr &token) +{ + _token = token; + + if (token.expired()) { + DisplayErrorMessage(obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noAccount")); + return; + } + + PopulateSelection(); +} + +void TwitchPointsRewardSelection::PopulateSelection() +{ + auto token = _token.lock(); + + if (!token || !token->AnyOptionIsEnabled(SUPPORTED_TOKEN_OPTIONS)) { + DisplayErrorMessage(obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noPermission")); + return; + } + + if (!_channel || (*_channel).GetName().empty()) { + DisplayErrorMessage(obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noChannel")); + return; + } + + auto currentSelection = currentText(); + const QSignalBlocker b(this); + clear(); + + auto pointsRewards = GetPointsRewards(token, *_channel); + if (!pointsRewards) { + DisplayErrorMessage(obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error")); + return; + } + + HideErrorMessage(); + AddPredefinedItems(); + + for (const auto &pointsReward : *pointsRewards) { + addItem(QString::fromStdString(pointsReward.title), + QString::fromStdString(pointsReward.id)); + } + + setCurrentText(currentSelection); +} + +void TwitchPointsRewardSelection::AddPredefinedItems() +{ + if (_allowAny) { + addItem(obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.option.any"), + "-"); + } +} + +void TwitchPointsRewardSelection::DisplayErrorMessage(const char *errorMessage) +{ + setDisabled(true); + setToolTip(errorMessage); +} + +void TwitchPointsRewardSelection::HideErrorMessage() +{ + setDisabled(false); + setToolTip(""); +} + +std::optional> +TwitchPointsRewardSelection::GetPointsRewards( + const std::shared_ptr &token, const TwitchChannel &channel) +{ + httplib::Params params = { + {"broadcaster_id", channel.GetUserID(*token)}}; + + auto response = SendGetRequest("https://api.twitch.tv", + "/helix/channel_points/custom_rewards", + *token, params); + + if (response.status != 200) { + blog(LOG_WARNING, + "Failed to fetch points rewards for user %s and channel %s! (%d)", + token->GetName().c_str(), channel.GetName().c_str(), + response.status); + + return {}; + } + + return ParseResponse(response.data); +} + +std::vector +TwitchPointsRewardSelection::ParseResponse(obs_data_t *response) +{ + std::vector pointRewards; + OBSDataArrayAutoRelease jsonArray = + obs_data_get_array(response, "data"); + size_t count = obs_data_array_count(jsonArray); + + for (size_t i = 0; i < count; ++i) { + OBSDataAutoRelease jsonObj = obs_data_array_item(jsonArray, i); + std::string id = obs_data_get_string(jsonObj, "id"); + std::string title = obs_data_get_string(jsonObj, "title"); + pointRewards.push_back({id, title}); + } + + return pointRewards; +} + +void TwitchPointsRewardSelection::SelectionChanged(int index) +{ + TwitchPointsReward pointsReward{ + itemData(index).toString().toStdString(), + currentText().toStdString()}; + emit PointsRewardChanged(pointsReward); +} + +TwitchPointsRewardWidget::TwitchPointsRewardWidget(QWidget *parent) + : QWidget(parent), + _selection(new TwitchPointsRewardSelection(this)), + _refreshButton(new QPushButton(this)) +{ + _refreshButton->setMaximumWidth(22); + SetButtonIcon(_refreshButton, ":res/images/refresh.svg"); + _refreshButton->setToolTip(obs_module_text( + "AdvSceneSwitcher.twitch.selection.points.reward.refresh")); + + auto layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_selection); + layout->addWidget(_refreshButton); + setLayout(layout); + + QWidget::connect( + _selection, + SIGNAL(PointsRewardChanged(const TwitchPointsReward &)), this, + SIGNAL(PointsRewardChanged(const TwitchPointsReward &))); + QWidget::connect(_refreshButton, SIGNAL(clicked()), _selection, + SLOT(PopulateSelection())); +} + +void TwitchPointsRewardWidget::SetPointsReward( + const TwitchPointsReward &pointsReward) +{ + _selection->SetPointsReward(pointsReward); +} + +void TwitchPointsRewardWidget::SetChannel(const TwitchChannel &channel) +{ + _selection->SetChannel(channel); +} + +void TwitchPointsRewardWidget::SetToken(const std::weak_ptr &token) +{ + _selection->SetToken(token); +} + +} // namespace advss diff --git a/src/macro-external/twitch/points-reward-selection.hpp b/src/macro-external/twitch/points-reward-selection.hpp new file mode 100644 index 00000000..4ecd53fb --- /dev/null +++ b/src/macro-external/twitch/points-reward-selection.hpp @@ -0,0 +1,74 @@ +#pragma once +#include "channel-selection.hpp" +#include "token.hpp" + +#include +#include +#include + +namespace advss { + +struct TwitchPointsReward { + void Load(obs_data_t *obj); + void Save(obs_data_t *obj) const; + + std::string id = "-"; + std::string title = "-"; +}; + +class TwitchPointsRewardSelection : public FilterComboBox { + Q_OBJECT + +public: + TwitchPointsRewardSelection(QWidget *parent, bool allowAny = true); + + void SetPointsReward(const TwitchPointsReward &pointsReward); + void SetChannel(const TwitchChannel &channel); + void SetToken(const std::weak_ptr &token); + +protected: + void DisplayErrorMessage(const char *errorMessage); + void HideErrorMessage(); + + std::optional _channel = {}; + std::weak_ptr _token; + +private: + void AddPredefinedItems(); + std::optional> + GetPointsRewards(const std::shared_ptr &token, + const TwitchChannel &channel); + std::vector ParseResponse(obs_data_t *response); + + bool _allowAny; + const std::vector SUPPORTED_TOKEN_OPTIONS = { + {"channel:read:redemptions"}, + {"channel:manage:redemptions"}}; + +private slots: + void PopulateSelection(); + void SelectionChanged(int); + +signals: + void PointsRewardChanged(const TwitchPointsReward &); +}; + +class TwitchPointsRewardWidget : public QWidget { + Q_OBJECT + +public: + TwitchPointsRewardWidget(QWidget *parent); + + void SetPointsReward(const TwitchPointsReward &pointsReward); + void SetChannel(const TwitchChannel &channel); + void SetToken(const std::weak_ptr &token); + +signals: + void PointsRewardChanged(const TwitchPointsReward &); + +protected: + TwitchPointsRewardSelection *_selection; + QPushButton *_refreshButton; +}; + +} // namespace advss