diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 6b90a2d4..4672802c 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1144,10 +1144,13 @@ AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Enable chat's emote-o AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Disable chat's emote-only mode" AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Send chat message" AdvSceneSwitcher.action.twitch.type.user.getInfo="Get user information" +AdvSceneSwitcher.action.twitch.type.reward.getInfo="Get channel points reward information" +AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!" -AdvSceneSwitcher.action.twitch.entry.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}" +AdvSceneSwitcher.action.twitch.entry.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}" AdvSceneSwitcher.action.twitch.entry.chat="Using account{{account}}{{actions}}on{{channel}}" AdvSceneSwitcher.action.twitch.entry.user.getInfo="Using account{{account}}{{actions}}for{{userInfoQueryType}}{{userLogin}}{{userId}}" +AdvSceneSwitcher.action.twitch.entry.reward.getInfo="Using account{{account}}{{actions}}for channel{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}" AdvSceneSwitcher.action.twitch.title.title="Enter title" AdvSceneSwitcher.action.twitch.marker.description="Describe marker" AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip" @@ -1631,22 +1634,37 @@ AdvSceneSwitcher.tempVar.twitch.title.reward="Reward title" AdvSceneSwitcher.tempVar.twitch.cost.reward="Reward cost" AdvSceneSwitcher.tempVar.twitch.prompt.reward="Reward description" AdvSceneSwitcher.tempVar.twitch.is_user_input_required.reward="Reward requires user input" -AdvSceneSwitcher.tempVar.twitch.should_redemptions_skip_request_queue.reward="Redemptions skip request queue" -AdvSceneSwitcher.tempVar.twitch.should_redemptions_skip_request_queue.reward.description="Should redemptions be set to fulfilled status immediately when redeemed and skip the request queue instead of the normal unfulfilled status." -AdvSceneSwitcher.tempVar.twitch.max_per_stream.reward="Reward maximum per stream" -AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.reward="Reward maximum per user per stream" AdvSceneSwitcher.tempVar.twitch.background_color.reward="Reward background color" -AdvSceneSwitcher.tempVar.twitch.image.reward="Reward image" +AdvSceneSwitcher.tempVar.twitch.image.reward="Reward image (JSON)" AdvSceneSwitcher.tempVar.twitch.image.reward.description="Set of custom images of 1x, 2x and 4x sizes for the reward. Can be null if no images have been uploaded." -AdvSceneSwitcher.tempVar.twitch.default_image.reward="Reward default image" +AdvSceneSwitcher.tempVar.twitch.image.url_4x.reward="Reward image URL" +AdvSceneSwitcher.tempVar.twitch.image.url_4x.reward.description="The URL to the custom image for the reward. This field is null if the broadcaster didn’t upload images." +AdvSceneSwitcher.tempVar.twitch.default_image.reward="Reward default image (JSON)" AdvSceneSwitcher.tempVar.twitch.default_image.reward.description="Set of default images of 1x, 2x and 4x sizes for the reward." -AdvSceneSwitcher.tempVar.twitch.global_cooldown.reward="Reward cooldown" -AdvSceneSwitcher.tempVar.twitch.global_cooldown.reward.description="Whether a cooldown is enabled and what the cooldown is in seconds." -AdvSceneSwitcher.tempVar.twitch.cooldown_expires_at.reward="Reward cooldown expiration" -AdvSceneSwitcher.tempVar.twitch.cooldown_expires_at.reward.description="Timestamp of the cooldown expiration. null if the reward isn’t on cooldown." +AdvSceneSwitcher.tempVar.twitch.default_image.url_4x.reward="Reward default image URL" +AdvSceneSwitcher.tempVar.twitch.default_image.url_4x.reward.description="The URL to the default image for the reward." AdvSceneSwitcher.tempVar.twitch.redemptions_redeemed_current_stream.reward="Redemption count" AdvSceneSwitcher.tempVar.twitch.redemptions_redeemed_current_stream.reward.description="The number of redemptions redeemed during the current live stream. Counts against the max_per_stream limit. null if the broadcasters stream isn’t live or max_per_stream isn’t enabled." -AdvSceneSwitcher.tempVar.twitch.redemptions_redeemed_current_stream.reward.description="The number of redemptions redeemed during the current live stream. Counts against the max_per_stream limit. null if the broadcasters stream isn’t live or max_per_stream isn’t enabled." +AdvSceneSwitcher.tempVar.twitch.should_redemptions_skip_request_queue.reward="Redemptions skip request queue" +AdvSceneSwitcher.tempVar.twitch.should_redemptions_skip_request_queue.reward.description="Should redemptions be set to fulfilled status immediately when redeemed and skip the request queue instead of the normal unfulfilled status." +AdvSceneSwitcher.tempVar.twitch.max_per_stream.reward="Maximum redemptions per stream (JSON)" +AdvSceneSwitcher.tempVar.twitch.max_per_stream.is_enabled.reward="Maximum redemptions per stream enabled" +AdvSceneSwitcher.tempVar.twitch.max_per_stream.is_enabled.reward.description="A Boolean value that determines whether the reward applies a limit on the number of redemptions allowed per live stream. Is true if the reward applies a limit." +AdvSceneSwitcher.tempVar.twitch.max_per_stream.max_per_stream.reward="Maximum redemptions per stream" +AdvSceneSwitcher.tempVar.twitch.max_per_stream.max_per_stream.reward.description="The maximum number of redemptions allowed per live stream." +AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.reward="Reward maximum per user per stream (JSON)" +AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.is_enabled.reward="Maximum redemptions per user per stream enabled" +AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.is_enabled.reward.description="A Boolean value that determines whether the reward applies a limit on the number of redemptions allowed per user per live stream. Is true if the reward applies a limit." +AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.max_per_user_per_stream.reward="Maximum redemptions per user per stream" +AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.max_per_user_per_stream.reward.description="The settings used to determine whether to apply a maximum to the number of redemptions allowed per user per live stream." +AdvSceneSwitcher.tempVar.twitch.global_cooldown.reward="Global cooldown (JSON)" +AdvSceneSwitcher.tempVar.twitch.global_cooldown.reward.description="Whether a cooldown is enabled and what the cooldown is in seconds." +AdvSceneSwitcher.tempVar.twitch.global_cooldown.is_enabled.reward="Global cooldown enabled" +AdvSceneSwitcher.tempVar.twitch.global_cooldown.is_enabled.reward.description="A Boolean value that determines whether to apply a cooldown period. Is true if a cooldown period is enabled." +AdvSceneSwitcher.tempVar.twitch.global_cooldown.global_cooldown_seconds.reward="Global cooldown" +AdvSceneSwitcher.tempVar.twitch.global_cooldown.global_cooldown_seconds.reward.description="The cooldown period, in seconds." +AdvSceneSwitcher.tempVar.twitch.cooldown_expires_at.reward="Reward cooldown expiration" +AdvSceneSwitcher.tempVar.twitch.cooldown_expires_at.reward.description="Timestamp of the cooldown expiration. null if the reward isn’t on cooldown." AdvSceneSwitcher.tempVar.twitch.charity_name="Charity name" AdvSceneSwitcher.tempVar.twitch.charity_description="Charity description" diff --git a/data/locale/pt-BR.ini b/data/locale/pt-BR.ini index 1e4b446f..ab6e01c3 100644 --- a/data/locale/pt-BR.ini +++ b/data/locale/pt-BR.ini @@ -1072,7 +1072,7 @@ AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Habilitar modo apenas AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Desabilitar modo apenas emotes no chat" AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Enviar mensagem de chat" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Não é possível selecionar categoria sem selecionar uma conta Twitch primeiro!" -AdvSceneSwitcher.action.twitch.entry.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}" +AdvSceneSwitcher.action.twitch.entry.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}" AdvSceneSwitcher.action.twitch.entry.chat="Usando conta{{account}}{{actions}}em{{channel}}" AdvSceneSwitcher.action.twitch.title.title="Digite o título" AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador" diff --git a/plugins/twitch/macro-action-twitch.cpp b/plugins/twitch/macro-action-twitch.cpp index f84d1f6f..e873f7aa 100644 --- a/plugins/twitch/macro-action-twitch.cpp +++ b/plugins/twitch/macro-action-twitch.cpp @@ -3,6 +3,7 @@ #include #include +#include namespace advss { @@ -23,6 +24,16 @@ void MacroActionTwitch::ResolveVariablesToFixedValues() _chatMessage.ResolveVariables(); _userLogin.ResolveVariables(); _userId.ResolveVariables(); + _useVariableForRewardSelection = false; + auto token = _token.lock(); + if (token) { + ResolveVariableSelectionToRewardId(token); + _pointsReward.title = _lastResolvedRewardTitle; + _pointsReward.id = _lastResolvedRewardId; + } else { + _pointsReward.id = ""; + _pointsReward.id = ""; + } } std::shared_ptr MacroActionTwitch::Create(Macro *m) @@ -63,6 +74,8 @@ const static std::map actionTypes = { "AdvSceneSwitcher.action.twitch.type.chat.sendMessage"}, {MacroActionTwitch::Action::USER_GET_INFO, "AdvSceneSwitcher.action.twitch.type.user.getInfo"}, + {MacroActionTwitch::Action::POINTS_REWARD_GET_INFO, + "AdvSceneSwitcher.action.twitch.type.reward.getInfo"}, }; const static std::map @@ -282,7 +295,7 @@ void MacroActionTwitch::GetUserInfo(const std::shared_ptr &token) break; } auto result = SendGetRequest(*token, "https://api.twitch.tv", - "/helix/users", params); + "/helix/users", params, true); if (result.status != 200) { blog(LOG_INFO, "Failed get user info! (%d)\n", result.status); @@ -297,80 +310,196 @@ void MacroActionTwitch::GetUserInfo(const std::shared_ptr &token) } OBSDataAutoRelease data = obs_data_array_item(array, 0); + SetJsonTempVars(data, [this](const char *id, const char *value) { + SetTempVarValue(id, value); + }); +} - SetTempVarValue("id", obs_data_get_string(data, "id")); - SetTempVarValue("login", obs_data_get_string(data, "login")); - SetTempVarValue("display_name", - obs_data_get_string(data, "display_name")); - SetTempVarValue("type", obs_data_get_string(data, "type")); - SetTempVarValue("broadcaster_type", - obs_data_get_string(data, "broadcaster_type")); - SetTempVarValue("description", - obs_data_get_string(data, "description")); - SetTempVarValue("profile_image_url", - obs_data_get_string(data, "profile_image_url")); - SetTempVarValue("offline_image_url", - obs_data_get_string(data, "offline_image_url")); - SetTempVarValue("created_at", obs_data_get_string(data, "created_at")); +bool MacroActionTwitch::ResolveVariableSelectionToRewardId( + const std::shared_ptr &token) +{ + if (!token) { + return false; + } + + auto variable = _rewardVariable.lock(); + if (!variable) { + return false; + } + + const auto rewardTitle = variable->Value(); + if (rewardTitle.empty()) { + return false; + } + + if (_lastResolvedRewardTitle == rewardTitle) { + return true; + } + + const auto rewards = GetPointsRewardsForChannel(token, _channel); + if (!rewards) { + return false; + } + + for (const auto &reward : *rewards) { + if (reward.title == rewardTitle) { + _lastResolvedRewardId = reward.id; + _lastResolvedRewardTitle = reward.title; + return true; + } + } + + return false; +} + +void MacroActionTwitch::GetRewardInfo(const std::shared_ptr &token) +{ + if (_useVariableForRewardSelection && + !ResolveVariableSelectionToRewardId(token)) { + if (VerboseLoggingEnabled()) { + auto variable = _rewardVariable.lock(); + if (!variable) { + blog(LOG_WARNING, + "failed to resolve variable reward name to reward id (invalid selection)"); + return; + } + blog(LOG_WARNING, + "failed to resolve variable reward name '%s' to reward id", + variable->Value().c_str()); + return; + } + } + + httplib::Params params = { + {"broadcaster_id", token->GetUserID()}, + {"id", _useVariableForRewardSelection ? _lastResolvedRewardId + : _pointsReward.id}, + }; + auto result = SendGetRequest(*token, "https://api.twitch.tv", + "/helix/channel_points/custom_rewards", + params, true); + + if (result.status != 200) { + blog(LOG_INFO, "Failed get reward info! (%d)\n", result.status); + return; + } + + OBSDataArrayAutoRelease array = obs_data_get_array(result.data, "data"); + size_t count = obs_data_array_count(array); + if (count == 0) { + blog(LOG_WARNING, "%s did not return any data!", __func__); + return; + } + + OBSDataAutoRelease data = obs_data_array_item(array, 0); + + SetJsonTempVars(data, [this](const char *id, const char *value) { + SetTempVarValue(id, value); + }); + + OBSDataAutoRelease image = obs_data_get_obj(data, "image"); + SetTempVarValue("image.url_4x", obs_data_get_string(image, "url_4x")); + + OBSDataAutoRelease default_image = + obs_data_get_obj(data, "default_image"); + SetTempVarValue("default_image.url_4x", + obs_data_get_string(default_image, "url_4x")); + + OBSDataAutoRelease max_per_stream_setting = + obs_data_get_obj(data, "max_per_stream_setting"); + SetTempVarValue("max_per_stream.is_enabled", + obs_data_get_bool(max_per_stream_setting, + "is_enabled")); + SetTempVarValue("max_per_stream.max_per_stream", + std::to_string(obs_data_get_int(max_per_stream_setting, + "max_per_stream"))); + + OBSDataAutoRelease max_per_user_per_stream_setting = + obs_data_get_obj(data, "max_per_user_per_stream_setting"); + SetTempVarValue("max_per_user_per_stream.is_enabled", + obs_data_get_bool(max_per_user_per_stream_setting, + "is_enabled")); + SetTempVarValue( + "max_per_user_per_stream.max_per_user_per_stream", + std::to_string(obs_data_get_int(max_per_user_per_stream_setting, + "max_per_user_per_stream"))); + + OBSDataAutoRelease global_cooldown_setting = + obs_data_get_obj(data, "global_cooldown_setting"); + SetTempVarValue("global_cooldown.is_enabled", + obs_data_get_bool(global_cooldown_setting, + "is_enabled")); + SetTempVarValue( + "max_per_user_per_stream.global_cooldown_seconds", + std::to_string(obs_data_get_int(max_per_user_per_stream_setting, + "global_cooldown_seconds"))); +} + +static std::string tryTranslate(const std::string &testString) +{ + auto translated = obs_module_text(testString.c_str()); + if (testString != translated) { + return translated; + } + return ""; } void MacroActionTwitch::SetupTempVars() { MacroAction::SetupTempVars(); - if (_action != Action::USER_GET_INFO) { - return; - } - AddTempvar( - "id", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.id.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.id.user.getInfo.description")); - AddTempvar( - "login", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.login.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.login.user.getInfo.description")); - AddTempvar( - "display_name", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.display_name.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.display_name.user.getInfo.description")); - AddTempvar( - "type", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.type.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.type.user.getInfo.description")); - AddTempvar( - "broadcaster_type", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.broadcaster_type.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.broadcaster_type.user.getInfo.description")); - AddTempvar( - "description", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.description.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.description.user.getInfo.description")); - AddTempvar( - "profile_image_url", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.profile_image_url.user.getInfo")); - AddTempvar( - "offline_image_url", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.offline_image_url.user.getInfo")); - AddTempvar( - "created_at", - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.created_at.user.getInfo"), - obs_module_text( - "AdvSceneSwitcher.tempVar.twitch.created_at.user.getInfo.description")); + auto setupTempVarHelper = [&](const std::string &id, + const std::string &extra = "") { + std::string name = tryTranslate( + "AdvSceneSwitcher.tempVar.twitch." + id + extra); + std::string description = + tryTranslate("AdvSceneSwitcher.tempVar.twitch." + id + + extra + ".description"); + AddTempvar(id, name.empty() ? id : name, description); + }; + + switch (_action) { + case Action::POINTS_REWARD_GET_INFO: + setupTempVarHelper("title", ".reward"); + setupTempVarHelper("prompt", ".reward"); + setupTempVarHelper("cost", ".reward"); + setupTempVarHelper("background_color", ".reward"); + setupTempVarHelper("is_enabled", ".reward"); + setupTempVarHelper("is_user_input_required", ".reward"); + setupTempVarHelper("is_paused", ".reward"); + setupTempVarHelper("is_in_stock", ".reward"); + setupTempVarHelper("should_redemptions_skip_request_queue", + ".reward"); + setupTempVarHelper("redemptions_redeemed_current_stream", + ".reward"); + setupTempVarHelper("cooldown_expires_at", ".reward"); + setupTempVarHelper("max_per_stream.is_enabled", ".reward"); + setupTempVarHelper("max_per_stream.max_per_stream", ".reward"); + setupTempVarHelper("max_per_user_per_stream.is_enabled", + ".reward"); + setupTempVarHelper( + "max_per_user_per_stream.max_per_user_per_stream", + ".reward"); + setupTempVarHelper("global_cooldown.is_enabled", ".reward"); + setupTempVarHelper("global_cooldown.global_cooldown_seconds", + ".reward"); + setupTempVarHelper("image.url_4x", ".reward"); + setupTempVarHelper("default_image.url_4x", ".reward"); + break; + case Action::USER_GET_INFO: + setupTempVarHelper("id", ".user.getInfo"); + setupTempVarHelper("login", ".user.getInfo"); + setupTempVarHelper("display_name", ".user.getInfo"); + setupTempVarHelper("type", ".user.getInfo"); + setupTempVarHelper("broadcaster_type", ".user.getInfo"); + setupTempVarHelper("description", ".user.getInfo"); + setupTempVarHelper("profile_image_url", ".user.getInfo"); + setupTempVarHelper("offline_image_url", ".user.getInfo"); + setupTempVarHelper("created_at", ".user.getInfo"); + break; + default: + break; + } } bool MacroActionTwitch::PerformAction() @@ -414,6 +543,9 @@ bool MacroActionTwitch::PerformAction() case MacroActionTwitch::Action::USER_GET_INFO: GetUserInfo(token); break; + case MacroActionTwitch::Action::POINTS_REWARD_GET_INFO: + GetRewardInfo(token); + break; default: break; } @@ -455,6 +587,11 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const static_cast(_userInfoQueryType)); _userLogin.Save(obj, "userLogin"); _userId.Save(obj, "userId"); + _pointsReward.Save(obj); + obs_data_set_string(obj, "rewardVariable", + GetWeakVariableName(_rewardVariable).c_str()); + obs_data_set_bool(obj, "useVariableForRewardSelection", + _useVariableForRewardSelection); return true; } @@ -478,6 +615,12 @@ bool MacroActionTwitch::Load(obs_data_t *obj) obs_data_get_int(obj, "userInfoQueryType")); _userLogin.Load(obj, "userLogin"); _userId.Load(obj, "userId"); + _pointsReward.Load(obj); + _rewardVariable = GetWeakVariableByName( + obs_data_get_string(obj, "rewardVariable")); + _useVariableForRewardSelection = + obs_data_get_bool(obj, "useVariableForRewardSelection"); + SetAction(static_cast(obs_data_get_int(obj, "action"))); return true; @@ -602,7 +745,9 @@ bool MacroActionTwitch::ActionIsSupportedByToken() {Action::WHISPER_SEND, {{"user:manage:whispers"}}}, {Action::SEND_CHAT_MESSAGE, {{"chat:edit"}}}, {Action::USER_GET_INFO, {}}, - }; + {Action::POINTS_REWARD_GET_INFO, + {{"channel:read:redemptions"}, + {"channel:manage:redemptions"}}}}; auto token = _token.lock(); if (!token) { @@ -679,7 +824,10 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _chatMessage(new VariableTextEdit(this)), _userInfoQueryType(new QComboBox(this)), _userLogin(new VariableLineEdit(this)), - _userId(new VariableSpinBox(this)) + _userId(new VariableSpinBox(this)), + _pointsReward(new TwitchPointsRewardWidget(this)), + _rewardVariable(new VariableSelection(this)), + _toggleRewardSelection(new QPushButton()) { SetWidgetProperties(); SetWidgetSignalConnections(); @@ -726,6 +874,7 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token) _entryData->_token = GetWeakTwitchTokenByQString(token); _category->SetToken(_entryData->_token); _channel->SetToken(_entryData->_token); + _pointsReward->SetToken(_entryData->_token); _entryData->ResetChatConnection(); SetWidgetVisibility(); @@ -835,6 +984,15 @@ void MacroActionTwitchEdit::SetWidgetProperties() populateUserQueryInfoTypeSelection(_userInfoQueryType); _userId->setMaximum(999999999); + + _toggleRewardSelection->setCheckable(true); + _toggleRewardSelection->setMaximumWidth(11); + SetButtonIcon(_toggleRewardSelection, + GetThemeTypeName() == "Light" + ? ":/res/images/dots-vert.svg" + : "theme:Dark/dots-vert.svg"); + _toggleRewardSelection->setToolTip(obs_module_text( + "AdvSceneSwitcher.action.twitch.reward.toggleControl")); } void MacroActionTwitchEdit::SetWidgetSignalConnections() @@ -873,6 +1031,15 @@ void MacroActionTwitchEdit::SetWidgetSignalConnections() _userId, SIGNAL(NumberVariableChanged(const NumberVariable &)), this, SLOT(UserIdChanged(const NumberVariable &))); + QWidget::connect( + _pointsReward, + SIGNAL(PointsRewardChanged(const TwitchPointsReward &)), this, + SLOT(PointsRewardChanged(const TwitchPointsReward &))); + QWidget::connect(_rewardVariable, + SIGNAL(SelectionChanged(const QString &)), this, + SLOT(RewardVariableChanged(const QString &))); + QWidget::connect(_toggleRewardSelection, SIGNAL(toggled(bool)), this, + SLOT(ToggleRewardSelection(bool))); } void MacroActionTwitchEdit::SetWidgetVisibility() @@ -889,7 +1056,9 @@ void MacroActionTwitchEdit::SetWidgetVisibility() _entryData->GetAction() == MacroActionTwitch::Action::RAID_END || _entryData->GetAction() == - MacroActionTwitch::Action::SEND_CHAT_MESSAGE); + MacroActionTwitch::Action::SEND_CHAT_MESSAGE || + _entryData->GetAction() == + MacroActionTwitch::Action::POINTS_REWARD_GET_INFO); _duration->setVisible(_entryData->GetAction() == MacroActionTwitch::Action::COMMERCIAL_START); _markerDescription->setVisible( @@ -917,6 +1086,17 @@ void MacroActionTwitchEdit::SetWidgetVisibility() MacroActionTwitch::Action::USER_GET_INFO && _entryData->_userInfoQueryType == MacroActionTwitch::UserInfoQueryType::ID); + _pointsReward->setVisible( + _entryData->GetAction() == + MacroActionTwitch::Action::POINTS_REWARD_GET_INFO && + !_entryData->_useVariableForRewardSelection); + _rewardVariable->setVisible( + _entryData->GetAction() == + MacroActionTwitch::Action::POINTS_REWARD_GET_INFO && + _entryData->_useVariableForRewardSelection); + _toggleRewardSelection->setVisible( + _entryData->GetAction() == + MacroActionTwitch::Action::POINTS_REWARD_GET_INFO); if (_entryData->GetAction() == MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET || @@ -937,6 +1117,7 @@ void MacroActionTwitchEdit::ChannelChanged(const TwitchChannel &channel) { GUARD_LOADING_AND_LOCK(); _entryData->_channel = channel; + _pointsReward->SetChannel(channel); _entryData->ResetChatConnection(); } @@ -964,12 +1145,32 @@ void MacroActionTwitchEdit::UserLoginChanged() _entryData->_userLogin = _userLogin->text().toStdString(); } +void MacroActionTwitchEdit::RewardVariableChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK() + _entryData->_rewardVariable = GetWeakVariableByQString(text); +} + +void MacroActionTwitchEdit::ToggleRewardSelection(bool) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_useVariableForRewardSelection = + !_entryData->_useVariableForRewardSelection; + SetWidgetVisibility(); +} + void MacroActionTwitchEdit::UserIdChanged(const NumberVariable &value) { GUARD_LOADING_AND_LOCK(); _entryData->_userId = value; } +void MacroActionTwitchEdit::PointsRewardChanged(const TwitchPointsReward &reward) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pointsReward = reward; +} + void MacroActionTwitchEdit::SetWidgetLayout() { const std::vector widgets{_tokens, @@ -983,7 +1184,10 @@ void MacroActionTwitchEdit::SetWidgetLayout() _channel, _userInfoQueryType, _userLogin, - _userId}; + _userId, + _pointsReward, + _rewardVariable, + _toggleRewardSelection}; for (auto widget : widgets) { _layout->removeWidget(widget); } @@ -999,6 +1203,10 @@ void MacroActionTwitchEdit::SetWidgetLayout() layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.entry.user.getInfo"); break; + case MacroActionTwitch::Action::POINTS_REWARD_GET_INFO: + layoutText = obs_module_text( + "AdvSceneSwitcher.action.twitch.entry.reward.getInfo"); + break; default: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.entry.default"); @@ -1017,7 +1225,10 @@ void MacroActionTwitchEdit::SetWidgetLayout() {"{{channel}}", _channel}, {"{{userInfoQueryType}}", _userInfoQueryType}, {"{{userLogin}}", _userLogin}, - {"{{userId}}", _userId}}); + {"{{userId}}", _userId}, + {"{{pointsReward}}", _pointsReward}, + {"{{rewardVariable}}", _rewardVariable}, + {"{{toggleRewardSelection}}", _toggleRewardSelection}}); _layout->setContentsMargins(0, 0, 0, 0); } @@ -1047,6 +1258,10 @@ void MacroActionTwitchEdit::UpdateEntryData() static_cast(_entryData->_userInfoQueryType))); _userLogin->setText(_entryData->_userLogin); _userId->SetValue(_entryData->_userId); + _pointsReward->SetToken(_entryData->_token); + _pointsReward->SetChannel(_entryData->_channel); + _pointsReward->SetPointsReward(_entryData->_pointsReward); + _rewardVariable->SetVariable(_entryData->_rewardVariable); SetWidgetVisibility(); } diff --git a/plugins/twitch/macro-action-twitch.hpp b/plugins/twitch/macro-action-twitch.hpp index a69e0009..6d64b1c3 100644 --- a/plugins/twitch/macro-action-twitch.hpp +++ b/plugins/twitch/macro-action-twitch.hpp @@ -54,6 +54,9 @@ public: // Reward add POINTS_REWARD_ADD = 600, // TODO + // Get reward information + POINTS_REWARD_GET_INFO = 650, + // Reward update POINTS_REWARD_ENABLE = 710, POINTS_REWARD_DISABLE = 711, @@ -177,6 +180,9 @@ public: UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN; StringVariable _userLogin = "user login"; IntVariable _userId = 0; + TwitchPointsReward _pointsReward; + std::weak_ptr _rewardVariable; + bool _useVariableForRewardSelection = false; private: void SetStreamTitle(const std::shared_ptr &) const; @@ -190,12 +196,19 @@ private: void StartRaid(const std::shared_ptr &); void SendChatMessage(const std::shared_ptr &); void GetUserInfo(const std::shared_ptr &); + void GetRewardInfo(const std::shared_ptr &); + + bool ResolveVariableSelectionToRewardId( + const std::shared_ptr &); void SetupTempVars(); Action _action = Action::CHANNEL_INFO_TITLE_SET; std::shared_ptr _chatConnection; + std::string _lastResolvedRewardTitle; + std::string _lastResolvedRewardId; + static bool _registered; static const std::string id; }; @@ -232,6 +245,9 @@ private slots: void UserInfoQueryTypeChanged(int); void UserLoginChanged(); void UserIdChanged(const NumberVariable &); + void PointsRewardChanged(const TwitchPointsReward &); + void RewardVariableChanged(const QString &); + void ToggleRewardSelection(bool); signals: void HeaderInfoChanged(const QString &); @@ -263,6 +279,9 @@ private: QComboBox *_userInfoQueryType; VariableLineEdit *_userLogin; VariableSpinBox *_userId; + TwitchPointsRewardWidget *_pointsReward; + VariableSelection *_rewardVariable; + QPushButton *_toggleRewardSelection; bool _loading = true; }; diff --git a/plugins/twitch/points-reward-selection.cpp b/plugins/twitch/points-reward-selection.cpp index 226c2db6..d8d17ebb 100644 --- a/plugins/twitch/points-reward-selection.cpp +++ b/plugins/twitch/points-reward-selection.cpp @@ -88,7 +88,7 @@ void TwitchPointsRewardSelection::PopulateSelection() const QSignalBlocker b(this); clear(); - auto pointsRewards = GetPointsRewards(token, *_channel); + auto pointsRewards = GetPointsRewardsForChannel(token, *_channel); if (!pointsRewards) { DisplayErrorMessage(obs_module_text( "AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error")); @@ -127,47 +127,6 @@ void TwitchPointsRewardSelection::HideErrorMessage() setToolTip(""); } -std::optional> -TwitchPointsRewardSelection::GetPointsRewards( - const std::shared_ptr &token, const TwitchChannel &channel) -{ - httplib::Params params = { - {"broadcaster_id", channel.GetUserID(*token)}}; - - auto response = SendGetRequest(*token, "https://api.twitch.tv", - "/helix/channel_points/custom_rewards", - params, true); - - 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{ @@ -218,4 +177,44 @@ void TwitchPointsRewardWidget::SetToken(const std::weak_ptr &token) _selection->SetToken(token); } +static std::vector parseApiResponse(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; +} + +std::optional> +GetPointsRewardsForChannel(const std::shared_ptr &token, + const TwitchChannel &channel) +{ + httplib::Params params = { + {"broadcaster_id", channel.GetUserID(*token)}}; + + auto response = SendGetRequest(*token, "https://api.twitch.tv", + "/helix/channel_points/custom_rewards", + params, true); + + 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 parseApiResponse(response.data); +} + } // namespace advss diff --git a/plugins/twitch/points-reward-selection.hpp b/plugins/twitch/points-reward-selection.hpp index 0ce41ae7..4e57671a 100644 --- a/plugins/twitch/points-reward-selection.hpp +++ b/plugins/twitch/points-reward-selection.hpp @@ -35,10 +35,6 @@ protected: 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 _supportedTokenOptions = { @@ -71,4 +67,8 @@ protected: QPushButton *_refreshButton; }; +std::optional> +GetPointsRewardsForChannel(const std::shared_ptr &token, + const TwitchChannel &channel); + } // namespace advss