#include "macro-action-twitch.hpp" #include "twitch-helpers.hpp" #include #include #include namespace advss { const std::string MacroActionTwitch::id = "twitch"; bool MacroActionTwitch::_registered = MacroActionFactory::Register( MacroActionTwitch::id, {MacroActionTwitch::Create, MacroActionTwitchEdit::Create, "AdvSceneSwitcher.action.twitch"}); void MacroActionTwitch::ResolveVariablesToFixedValues() { _streamTitle.ResolveVariables(); _tags.ResolveVariables(); _markerDescription.ResolveVariables(); _duration.ResolveVariables(); _announcementMessage.ResolveVariables(); _channel.ResolveVariables(); _chatMessage.ResolveVariables(); _userLogin.ResolveVariables(); _userId.ResolveVariables(); _banReason.ResolveVariables(); _useVariableForRewardSelection = false; auto token = _token.lock(); if (token) { ResolveVariableSelectionToRewardId(token); _pointsReward.title = _lastResolvedRewardTitle; _pointsReward.id = _lastResolvedRewardId; } else { _pointsReward.title = ""; _pointsReward.id = ""; } } std::shared_ptr MacroActionTwitch::Create(Macro *m) { return std::make_shared(m); } std::shared_ptr MacroActionTwitch::Copy() const { return std::make_shared(*this); } std::string MacroActionTwitch::GetShortDesc() const { return GetWeakTwitchTokenName(_token); } const static std::map actionTypes = { {MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.title.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.category.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.tags.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_LANGUAGE_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.language.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_BRANDED_CONTENT_ENABLE, "AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.enable"}, {MacroActionTwitch::Action::CHANNEL_INFO_BRANDED_CONTENT_DISABLE, "AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.disable"}, {MacroActionTwitch::Action::RAID_START, "AdvSceneSwitcher.action.twitch.type.raid.start"}, {MacroActionTwitch::Action::RAID_END, "AdvSceneSwitcher.action.twitch.type.raid.end"}, {MacroActionTwitch::Action::SHOUTOUT_SEND, "AdvSceneSwitcher.action.twitch.type.shoutout.send"}, {MacroActionTwitch::Action::SHIELD_MODE_START, "AdvSceneSwitcher.action.twitch.type.shieldMode.start"}, {MacroActionTwitch::Action::SHIELD_MODE_END, "AdvSceneSwitcher.action.twitch.type.shieldMode.end"}, {MacroActionTwitch::Action::COMMERCIAL_START, "AdvSceneSwitcher.action.twitch.type.commercial.start"}, {MacroActionTwitch::Action::COMMERCIAL_SNOOZE, "AdvSceneSwitcher.action.twitch.type.commercial.snooze"}, {MacroActionTwitch::Action::MARKER_CREATE, "AdvSceneSwitcher.action.twitch.type.marker.create"}, {MacroActionTwitch::Action::CLIP_CREATE, "AdvSceneSwitcher.action.twitch.type.clip.create"}, {MacroActionTwitch::Action::CHAT_ANNOUNCEMENT_SEND, "AdvSceneSwitcher.action.twitch.type.chat.announcement.send"}, {MacroActionTwitch::Action::CHAT_EMOTE_ONLY_ENABLE, "AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable"}, {MacroActionTwitch::Action::CHAT_EMOTE_ONLY_DISABLE, "AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable"}, {MacroActionTwitch::Action::CHAT_FOLLOWER_ONLY_ENABLE, "AdvSceneSwitcher.action.twitch.type.chat.followerOnly.enable"}, {MacroActionTwitch::Action::CHAT_FOLLOWER_ONLY_DISABLE, "AdvSceneSwitcher.action.twitch.type.chat.followerOnly.disable"}, {MacroActionTwitch::Action::CHAT_SUBSCRIBER_ONLY_ENABLE, "AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.enable"}, {MacroActionTwitch::Action::CHAT_SUBSCRIBER_ONLY_DISABLE, "AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.disable"}, {MacroActionTwitch::Action::CHAT_SLOW_MODE_ENABLE, "AdvSceneSwitcher.action.twitch.type.chat.slowMode.enable"}, {MacroActionTwitch::Action::CHAT_SLOW_MODE_DISABLE, "AdvSceneSwitcher.action.twitch.type.chat.slowMode.disable"}, {MacroActionTwitch::Action::CHAT_NON_MODERATOR_DELAY_ENABLE, "AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.enable"}, {MacroActionTwitch::Action::CHAT_NON_MODERATOR_DELAY_DISABLE, "AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.disable"}, {MacroActionTwitch::Action::CHAT_UNIQUE_MODE_ENABLE, "AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.enable"}, {MacroActionTwitch::Action::CHAT_UNIQUE_MODE_DISABLE, "AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.disable"}, {MacroActionTwitch::Action::SEND_CHAT_MESSAGE, "AdvSceneSwitcher.action.twitch.type.chat.sendMessage"}, {MacroActionTwitch::Action::USER_GET_INFO, "AdvSceneSwitcher.action.twitch.type.user.getInfo"}, {MacroActionTwitch::Action::USER_BAN, "AdvSceneSwitcher.action.twitch.type.user.ban"}, {MacroActionTwitch::Action::USER_UNBAN, "AdvSceneSwitcher.action.twitch.type.user.unban"}, {MacroActionTwitch::Action::USER_BLOCK, "AdvSceneSwitcher.action.twitch.type.user.block"}, {MacroActionTwitch::Action::USER_UNBLOCK, "AdvSceneSwitcher.action.twitch.type.user.unblock"}, {MacroActionTwitch::Action::USER_MODERATOR_ADD, "AdvSceneSwitcher.action.twitch.type.user.moderator.add"}, {MacroActionTwitch::Action::USER_MODERATOR_DELETE, "AdvSceneSwitcher.action.twitch.type.user.moderator.delete"}, {MacroActionTwitch::Action::USER_VIP_ADD, "AdvSceneSwitcher.action.twitch.type.user.vip.add"}, {MacroActionTwitch::Action::USER_VIP_DELETE, "AdvSceneSwitcher.action.twitch.type.user.vip.delete"}, {MacroActionTwitch::Action::POINTS_REWARD_GET_INFO, "AdvSceneSwitcher.action.twitch.type.reward.getInfo"}, {MacroActionTwitch::Action::CHANNEL_GET_INFO, "AdvSceneSwitcher.action.twitch.type.channel.getInfo"}, }; const static std::map announcementColors = { {MacroActionTwitch::AnnouncementColor::PRIMARY, "AdvSceneSwitcher.action.twitch.announcement.primary"}, {MacroActionTwitch::AnnouncementColor::BLUE, "AdvSceneSwitcher.action.twitch.announcement.blue"}, {MacroActionTwitch::AnnouncementColor::GREEN, "AdvSceneSwitcher.action.twitch.announcement.green"}, {MacroActionTwitch::AnnouncementColor::ORANGE, "AdvSceneSwitcher.action.twitch.announcement.orange"}, {MacroActionTwitch::AnnouncementColor::PURPLE, "AdvSceneSwitcher.action.twitch.announcement.purple"}, }; const static std::map announcementColorsTwitch = { {MacroActionTwitch::AnnouncementColor::PRIMARY, "primary"}, {MacroActionTwitch::AnnouncementColor::BLUE, "blue"}, {MacroActionTwitch::AnnouncementColor::GREEN, "green"}, {MacroActionTwitch::AnnouncementColor::ORANGE, "orange"}, {MacroActionTwitch::AnnouncementColor::PURPLE, "purple"}, }; const static std::map redemptionStatusesTwitch = { {MacroActionTwitch::RedemptionStatus::CANCELED, "CANCELED"}, {MacroActionTwitch::RedemptionStatus::FULFILLED, "FULFILLED"}, }; void MacroActionTwitch::SetStreamTitle( const std::shared_ptr &token) const { if (std::string(_streamTitle).empty()) { return; } const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "title", _streamTitle.c_str()); auto result = SendPatchRequest(*token, "https://api.twitch.tv", "/helix/channels", {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to set stream title! (%d)", result.status); } } void MacroActionTwitch::SetStreamCategory( const std::shared_ptr &token) const { if (_category.id == -1) { return; } const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "game_id", std::to_string(_category.id).c_str()); auto result = SendPatchRequest(*token, "https://api.twitch.tv", "/helix/channels", {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to set stream category! (%d)", result.status); } } void MacroActionTwitch::CreateStreamMarker( const std::shared_ptr &token) const { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "user_id", id->c_str()); if (!std::string(_markerDescription).empty()) { obs_data_set_string(data, "description", _markerDescription.c_str()); } auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/streams/markers", {}, data.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to create marker! (%d)", result.status); } } void MacroActionTwitch::CreateStreamClip( const std::shared_ptr &token) const { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } auto hasDelay = _clipHasDelay ? "true" : "false"; auto result = SendPostRequest( *token, "https://api.twitch.tv", "/helix/clips", {{"broadcaster_id", *id}, {"has_delay", hasDelay}}); if (result.status != 202) { blog(LOG_INFO, "Failed to create clip! (%d)", result.status); } } void MacroActionTwitch::StartCommercial( const std::shared_ptr &token) const { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "broadcaster_id", id->c_str()); obs_data_set_int(data, "length", _duration.Seconds()); auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/channels/commercial", {}, data.Get()); if (result.status == 200) { OBSDataArrayAutoRelease replyArray = obs_data_get_array(result.data, "data"); OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0); vblog(LOG_INFO, "Commercial started! (%d)\n" "length: %lld\n" "message: %s\n" "retry_after: %lld\n", result.status, obs_data_get_int(replyData, "length"), obs_data_get_string(replyData, "message"), obs_data_get_int(replyData, "retry_after")); } else { blog(LOG_INFO, "Failed to start commercial! (%d)\n" "error: %s\n" "message: %s\n", result.status, obs_data_get_string(result.data, "error"), obs_data_get_string(result.data, "message")); } } void MacroActionTwitch::SendChatAnnouncement( const std::shared_ptr &token) const { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "message", _announcementMessage.c_str()); obs_data_set_string( data, "color", announcementColorsTwitch.at(_announcementColor).c_str()); auto result = SendPostRequest( *token, "https://api.twitch.tv", "/helix/chat/announcements", {{"broadcaster_id", *id}, {"moderator_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to send chat announcement! (%d)", result.status); } } static void setChatSetting(const TwitchToken &token, const char *field, bool enable, const char *settingName) { const auto id = token.GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, field, enable); auto result = SendPatchRequest( token, "https://api.twitch.tv", "/helix/chat/settings", {{"broadcaster_id", *id}, {"moderator_id", *id}}, data.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to %s %s! (%d)", enable ? "enable" : "disable", settingName, result.status); } } void MacroActionTwitch::StartRaid(const std::shared_ptr &token) { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "from_broadcaster_id", id->c_str()); obs_data_set_string(data, "to_broadcaster_id", _channel.GetUserID(*token).c_str()); auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/raids", {}, data.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to start raid! (%d)", result.status); } } void MacroActionTwitch::SendChatMessage( const std::shared_ptr &token) { if (!_chatConnection) { _chatConnection = TwitchChatConnection::GetChatConnection( *token, _channel); return; } _chatConnection->SendChatMessage(_chatMessage); } void MacroActionTwitch::GetUserInfo(const std::shared_ptr &token) { httplib::Params params; switch (_userInfoQueryType) { case UserInfoQueryType::ID: params.insert({"id", std::to_string((uint64_t)_userId)}); break; case UserInfoQueryType::LOGIN: params.insert({"login", _userLogin}); break; default: break; } auto result = SendGetRequest(*token, "https://api.twitch.tv", "/helix/users", params, true); if (result.status != 200) { blog(LOG_INFO, "Failed get user info! (%d)", 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); }); } void MacroActionTwitch::GetChannelInfo(const std::shared_ptr &token) { auto info = _channel.GetInfo(*token); if (!info) { return; } SetTempVarValue("broadcaster_user_id", info->broadcaster_id); SetTempVarValue("broadcaster_user_login", info->broadcaster_login); SetTempVarValue("broadcaster_user_name", info->broadcaster_name); SetTempVarValue("language", info->broadcaster_language); SetTempVarValue("game_id", info->game_id); SetTempVarValue("game_name", info->game_name); SetTempVarValue("title", info->title); SetTempVarValue("delay", std::to_string(info->delay)); std::string tags; for (const auto &tag : info->tags) { tags += tag + " "; } if (!tags.empty()) { tags.pop_back(); } SetTempVarValue("tags", tags); std::string classificationLabels; for (const auto &label : info->content_classification_labels) { classificationLabels += label + " "; } if (!classificationLabels.empty()) { classificationLabels.pop_back(); } SetTempVarValue("content_classification_labels", classificationLabels); SetTempVarValue("is_branded_content", info->is_branded_content ? "true" : "false"); } std::optional MacroActionTwitch::GetTargetUserID( const std::shared_ptr &token) const { switch (_userInfoQueryType) { case UserInfoQueryType::ID: { const auto id = (uint64_t)_userId; if (id == 0) { blog(LOG_INFO, "Target user ID is not set!"); return std::nullopt; } vblog(LOG_INFO, "Resolved target user ID: %llu", (long long unsigned int)id); return std::to_string(id); } case UserInfoQueryType::LOGIN: { auto result = SendGetRequest(*token, "https://api.twitch.tv", "/helix/users", {{"login", _userLogin}}, true); if (result.status != 200) { blog(LOG_INFO, "Failed to get user id for login '%s'! (%d)", std::string(_userLogin).c_str(), result.status); return std::nullopt; } OBSDataArrayAutoRelease array = obs_data_get_array(result.data, "data"); if (obs_data_array_count(array) == 0) { blog(LOG_WARNING, "No user found for login '%s'", std::string(_userLogin).c_str()); return std::nullopt; } OBSDataAutoRelease userData = obs_data_array_item(array, 0); return obs_data_get_string(userData, "id"); } default: return std::nullopt; } } 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) { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); return; } 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; } return; } httplib::Params params = { {"broadcaster_id", *id}, {"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)", 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(); 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; case Action::CHANNEL_GET_INFO: setupTempVarHelper("language"); setupTempVarHelper("game_id"); setupTempVarHelper("game_name"); setupTempVarHelper("title"); setupTempVarHelper("delay"); setupTempVarHelper("tags"); setupTempVarHelper("content_classification_labels"); setupTempVarHelper("is_branded_content"); break; default: break; } } bool MacroActionTwitch::PerformAction() { auto token = _token.lock(); if (!token) { return true; } switch (_action) { case Action::CHANNEL_INFO_TITLE_SET: SetStreamTitle(token); break; case Action::CHANNEL_INFO_CATEGORY_SET: SetStreamCategory(token); break; case Action::CHANNEL_INFO_TAGS_SET: _tags.SetStreamTags(*token); break; case Action::CHANNEL_INFO_LANGUAGE_SET: _language.SetStreamLanguage(*token); break; case Action::CHANNEL_INFO_CONTENT_LABELS_SET: _contentClassification.SetContentClassification(*token); break; case Action::CHANNEL_INFO_BRANDED_CONTENT_ENABLE: case Action::CHANNEL_INFO_BRANDED_CONTENT_DISABLE: { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); break; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool( data, "is_branded_content", _action == Action::CHANNEL_INFO_BRANDED_CONTENT_ENABLE); auto result = SendPatchRequest(*token, "https://api.twitch.tv", "/helix/channels", {{"broadcaster_id", *id}}, data.Get()); if (result.status != 204) { blog(LOG_INFO, "Failed to set branded content flag! (%d)", result.status); } break; } case Action::RAID_START: StartRaid(token); break; case Action::RAID_END: { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); break; } auto result = SendDeleteRequest(*token, "https://api.twitch.tv", "/helix/raids", {{"broadcaster_id", *id}}); if (result.status != 204) { blog(LOG_INFO, "Failed to cancel raid! (%d)", result.status); } break; } case Action::SHOUTOUT_SEND: { const auto id = token->GetUserID(); const auto toId = _channel.GetUserID(*token); if (!id || toId.empty()) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); break; } auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/chat/shoutouts", {{"from_broadcaster_id", *id}, {"to_broadcaster_id", toId}, {"moderator_id", *id}}); if (result.status != 204) { blog(LOG_INFO, "Failed to send shoutout! (%d)", result.status); } break; } case Action::SHIELD_MODE_START: case Action::SHIELD_MODE_END: { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); break; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "is_active", _action == Action::SHIELD_MODE_START); auto result = SendPutRequest(*token, "https://api.twitch.tv", "/helix/moderation/shield_mode", {{"broadcaster_id", *id}, {"moderator_id", *id}}, data.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to set shield mode! (%d)", result.status); } break; } case Action::COMMERCIAL_START: StartCommercial(token); break; case Action::COMMERCIAL_SNOOZE: { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); break; } auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/channels/commercial/snooze", {{"broadcaster_id", *id}}); if (result.status != 200) { blog(LOG_INFO, "Failed to snooze commercial! (%d)", result.status); } break; } case Action::MARKER_CREATE: CreateStreamMarker(token); break; case Action::CLIP_CREATE: CreateStreamClip(token); break; case Action::CHAT_ANNOUNCEMENT_SEND: SendChatAnnouncement(token); break; case Action::CHAT_EMOTE_ONLY_ENABLE: setChatSetting(*token, "emote_mode", true, "chat's emote-only mode"); break; case Action::CHAT_EMOTE_ONLY_DISABLE: setChatSetting(*token, "emote_mode", false, "chat's emote-only mode"); break; case Action::CHAT_FOLLOWER_ONLY_ENABLE: setChatSetting(*token, "follower_mode", true, "chat's follower-only mode"); break; case Action::CHAT_FOLLOWER_ONLY_DISABLE: setChatSetting(*token, "follower_mode", false, "chat's follower-only mode"); break; case Action::CHAT_SUBSCRIBER_ONLY_ENABLE: setChatSetting(*token, "subscriber_mode", true, "chat's subscriber-only mode"); break; case Action::CHAT_SUBSCRIBER_ONLY_DISABLE: setChatSetting(*token, "subscriber_mode", false, "chat's subscriber-only mode"); break; case Action::CHAT_SLOW_MODE_ENABLE: setChatSetting(*token, "slow_mode", true, "chat's slow mode"); break; case Action::CHAT_SLOW_MODE_DISABLE: setChatSetting(*token, "slow_mode", false, "chat's slow mode"); break; case Action::CHAT_NON_MODERATOR_DELAY_ENABLE: { const auto id = token->GetUserID(); if (!id) { vblog(LOG_INFO, "%s skip - invalid user id", __func__); break; } OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "non_moderator_chat_delay", true); obs_data_set_int(data, "non_moderator_chat_delay_duration", _nonModDelayDuration); auto result = SendPatchRequest( *token, "https://api.twitch.tv", "/helix/chat/settings", {{"broadcaster_id", *id}, {"moderator_id", *id}}, data.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to enable chat's non-moderator message delay! (%d)", result.status); } break; } case Action::CHAT_NON_MODERATOR_DELAY_DISABLE: setChatSetting(*token, "non_moderator_chat_delay", false, "chat's non-moderator message delay"); break; case Action::CHAT_UNIQUE_MODE_ENABLE: setChatSetting(*token, "unique_chat_mode", true, "chat's unique message mode"); break; case Action::CHAT_UNIQUE_MODE_DISABLE: setChatSetting(*token, "unique_chat_mode", false, "chat's unique message mode"); break; case MacroActionTwitch::Action::SEND_CHAT_MESSAGE: SendChatMessage(token); break; case MacroActionTwitch::Action::USER_GET_INFO: GetUserInfo(token); break; case Action::USER_BAN: { const auto id = token->GetUserID(); const auto targetId = GetTargetUserID(token); if (!id || !targetId) { break; } const auto channelId = _channel.GetUserID(*token); const auto &broadcasterId = channelId.empty() ? *id : channelId; OBSDataAutoRelease innerData = obs_data_create(); obs_data_set_string(innerData, "user_id", targetId->c_str()); const auto duration = (uint64_t)_duration.Seconds(); if (duration > 0) { obs_data_set_int(innerData, "duration", (long long)duration); } const auto reason = std::string(_banReason); if (!reason.empty()) { obs_data_set_string(innerData, "reason", reason.c_str()); } OBSDataAutoRelease body = obs_data_create(); obs_data_set_obj(body, "data", innerData); auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/moderation/bans", {{"broadcaster_id", broadcasterId}, {"moderator_id", *id}}, body.Get()); if (result.status != 200) { blog(LOG_INFO, "Failed to ban user! (%d)", result.status); } break; } case Action::USER_UNBAN: { const auto id = token->GetUserID(); const auto targetId = GetTargetUserID(token); if (!id || !targetId) { break; } const auto channelId = _channel.GetUserID(*token); const auto &broadcasterId = channelId.empty() ? *id : channelId; auto result = SendDeleteRequest(*token, "https://api.twitch.tv", "/helix/moderation/bans", {{"broadcaster_id", broadcasterId}, {"moderator_id", *id}, {"user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to unban user! (%d)", result.status); } break; } case Action::USER_BLOCK: { const auto targetId = GetTargetUserID(token); if (!targetId) { break; } auto result = SendPutRequest(*token, "https://api.twitch.tv", "/helix/users/blocks", {{"target_user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to block user! (%d)", result.status); } break; } case Action::USER_UNBLOCK: { const auto targetId = GetTargetUserID(token); if (!targetId) { break; } auto result = SendDeleteRequest( *token, "https://api.twitch.tv", "/helix/users/blocks", {{"target_user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to unblock user! (%d)", result.status); } break; } case Action::USER_MODERATOR_ADD: { const auto id = token->GetUserID(); const auto targetId = GetTargetUserID(token); if (!id || !targetId) { break; } const auto channelId = _channel.GetUserID(*token); const auto &broadcasterId = channelId.empty() ? *id : channelId; auto result = SendPostRequest(*token, "https://api.twitch.tv", "/helix/moderation/moderators", {{"broadcaster_id", broadcasterId}, {"user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to add moderator! (%d)", result.status); } break; } case Action::USER_MODERATOR_DELETE: { const auto id = token->GetUserID(); const auto targetId = GetTargetUserID(token); if (!id || !targetId) { break; } const auto channelId = _channel.GetUserID(*token); const auto &broadcasterId = channelId.empty() ? *id : channelId; auto result = SendDeleteRequest(*token, "https://api.twitch.tv", "/helix/moderation/moderators", {{"broadcaster_id", broadcasterId}, {"user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to remove moderator! (%d)", result.status); } break; } case Action::USER_VIP_ADD: { const auto id = token->GetUserID(); const auto targetId = GetTargetUserID(token); if (!id || !targetId) { break; } const auto channelId = _channel.GetUserID(*token); const auto &broadcasterId = channelId.empty() ? *id : channelId; auto result = SendPostRequest( *token, "https://api.twitch.tv", "/helix/channels/vips", {{"broadcaster_id", broadcasterId}, {"user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to add VIP! (%d)", result.status); } break; } case Action::USER_VIP_DELETE: { const auto id = token->GetUserID(); const auto targetId = GetTargetUserID(token); if (!id || !targetId) { break; } const auto channelId = _channel.GetUserID(*token); const auto &broadcasterId = channelId.empty() ? *id : channelId; auto result = SendDeleteRequest( *token, "https://api.twitch.tv", "/helix/channels/vips", {{"broadcaster_id", broadcasterId}, {"user_id", *targetId}}); if (result.status != 204) { blog(LOG_INFO, "Failed to remove VIP! (%d)", result.status); } break; } case MacroActionTwitch::Action::POINTS_REWARD_GET_INFO: GetRewardInfo(token); break; case MacroActionTwitch::Action::CHANNEL_GET_INFO: GetChannelInfo(token); break; default: break; } return true; } void MacroActionTwitch::LogAction() const { auto it = actionTypes.find(_action); if (it != actionTypes.end()) { ablog(LOG_INFO, "performed action \"%s\" with token for \"%s\"", it->second.c_str(), GetWeakTwitchTokenName(_token).c_str()); } else { blog(LOG_WARNING, "ignored unknown twitch action %d", static_cast(_action)); } } bool MacroActionTwitch::Save(obs_data_t *obj) const { MacroAction::Save(obj); obs_data_set_int(obj, "action", static_cast(_action)); obs_data_set_string(obj, "token", GetWeakTwitchTokenName(_token).c_str()); _streamTitle.Save(obj, "streamTitle"); _category.Save(obj); _tags.Save(obj); _language.Save(obj); _contentClassification.Save(obj); _markerDescription.Save(obj, "markerDescription"); obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay); _duration.Save(obj); _announcementMessage.Save(obj, "announcementMessage"); obs_data_set_int(obj, "announcementColor", static_cast(_announcementColor)); obs_data_set_int(obj, "nonModDelayDuration", _nonModDelayDuration); _channel.Save(obj); _chatMessage.Save(obj, "chatMessage"); obs_data_set_int(obj, "userInfoQueryType", static_cast(_userInfoQueryType)); _userLogin.Save(obj, "userLogin"); _userId.Save(obj, "userId"); _banReason.Save(obj, "banReason"); _pointsReward.Save(obj); obs_data_set_string(obj, "rewardVariable", GetWeakVariableName(_rewardVariable).c_str()); obs_data_set_bool(obj, "useVariableForRewardSelection", _useVariableForRewardSelection); return true; } bool MacroActionTwitch::Load(obs_data_t *obj) { MacroAction::Load(obj); _token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token")); _streamTitle.Load(obj, "streamTitle"); _category.Load(obj); _tags.Load(obj); _language.Load(obj); _contentClassification.Load(obj); _markerDescription.Load(obj, "markerDescription"); _clipHasDelay = obs_data_get_bool(obj, "clipHasDelay"); _duration.Load(obj); _announcementMessage.Load(obj, "announcementMessage"); _announcementColor = static_cast( obs_data_get_int(obj, "announcementColor")); _nonModDelayDuration = (int)obs_data_get_int(obj, "nonModDelayDuration"); if (_nonModDelayDuration != 2 && _nonModDelayDuration != 4 && _nonModDelayDuration != 6) { _nonModDelayDuration = 2; } _channel.Load(obj); _chatMessage.Load(obj, "chatMessage"); _userInfoQueryType = static_cast( obs_data_get_int(obj, "userInfoQueryType")); _userLogin.Load(obj, "userLogin"); _userId.Load(obj, "userId"); _banReason.Load(obj, "banReason"); _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; } void MacroActionTwitch::SetAction(Action action) { _action = action; ResetChatConnection(); SetupTempVars(); } bool MacroActionTwitch::ActionIsSupportedByToken() { static const std::unordered_map> requiredOption = { {Action::CHANNEL_INFO_TITLE_SET, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_CATEGORY_SET, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_TAGS_SET, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_CONTENT_LABELS_SET, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_LANGUAGE_SET, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_DELAY_SET, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_BRANDED_CONTENT_ENABLE, {{"channel:manage:broadcast"}}}, {Action::CHANNEL_INFO_BRANDED_CONTENT_DISABLE, {{"channel:manage:broadcast"}}}, {Action::RAID_START, {{"channel:manage:raids"}}}, {Action::RAID_END, {{"channel:manage:raids"}}}, {Action::SHOUTOUT_SEND, {{"moderator:manage:shoutouts"}}}, {Action::POLL_END, {{"channel:manage:polls"}}}, {Action::PREDICTION_END, {{"channel:manage:predictions"}}}, {Action::SHIELD_MODE_START, {{"moderator:manage:shield_mode"}}}, {Action::SHIELD_MODE_END, {{"moderator:manage:shield_mode"}}}, {Action::POINTS_REWARD_ENABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_DISABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_PAUSE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_UNPAUSE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_TITLE_SET, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_PROMPT_SET, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_COST_SET, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_USER_INPUT_REQUIRE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_USER_INPUT_UNREQUIRE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_COOLDOWN_ENABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_COOLDOWN_DISABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_QUEUE_SKIP_ENABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_QUEUE_SKIP_DISABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_MAX_PER_STREAM_ENABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_MAX_PER_STREAM_DISABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_MAX_PER_USER_ENABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_MAX_PER_USER_DISABLE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_DELETE, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_REDEMPTION_CANCEL, {{"channel:manage:redemptions"}}}, {Action::POINTS_REWARD_REDEMPTION_FULFILL, {{"channel:manage:redemptions"}}}, {Action::USER_BAN, {{"moderator:manage:banned_users"}}}, {Action::USER_UNBAN, {{"moderator:manage:banned_users"}}}, {Action::USER_BLOCK, {{"user:manage:blocked_users"}}}, {Action::USER_UNBLOCK, {{"user:manage:blocked_users"}}}, {Action::USER_MODERATOR_ADD, {{"channel:manage:moderators"}}}, {Action::USER_MODERATOR_DELETE, {{"channel:manage:moderators"}}}, {Action::USER_VIP_ADD, {{"channel:manage:vips"}}}, {Action::USER_VIP_DELETE, {{"channel:manage:vips"}}}, {Action::COMMERCIAL_START, {{"channel:edit:commercial"}}}, {Action::COMMERCIAL_SNOOZE, {{"channel:manage:ads"}}}, {Action::MARKER_CREATE, {{"channel:manage:broadcast"}}}, {Action::CLIP_CREATE, {{"clips:edit"}}}, {Action::CHAT_ANNOUNCEMENT_SEND, {{"moderator:manage:announcements"}}}, {Action::CHAT_EMOTE_ONLY_ENABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_EMOTE_ONLY_DISABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_FOLLOWER_ONLY_ENABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_FOLLOWER_ONLY_DISABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_SUBSCRIBER_ONLY_ENABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_SUBSCRIBER_ONLY_DISABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_SLOW_MODE_ENABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_SLOW_MODE_DISABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_NON_MODERATOR_DELAY_ENABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_NON_MODERATOR_DELAY_DISABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_UNIQUE_MODE_ENABLE, {{"moderator:manage:chat_settings"}}}, {Action::CHAT_UNIQUE_MODE_DISABLE, {{"moderator:manage:chat_settings"}}}, {Action::WHISPER_SEND, {{"user:manage:whispers"}}}, {Action::SEND_CHAT_MESSAGE, {{"chat:edit"}}}, {Action::USER_GET_INFO, {}}, {Action::CHANNEL_GET_INFO, {}}, {Action::POINTS_REWARD_GET_INFO, {{"channel:read:redemptions"}, {"channel:manage:redemptions"}}}}; auto token = _token.lock(); if (!token) { return false; } auto it = requiredOption.find(_action); if (it == requiredOption.end()) { return false; } const auto &[_, options] = *it; return token->AnyOptionIsEnabled(options); } void MacroActionTwitch::ResetChatConnection() { _chatConnection.reset(); if (_action == Action::SEND_CHAT_MESSAGE) { auto token = _token.lock(); if (!token) { return; } _chatConnection = TwitchChatConnection::GetChatConnection( *token, _channel); } } static inline void populateActionSelection(QComboBox *list) { for (const auto &[action, name] : actionTypes) { list->addItem(obs_module_text(name.c_str()), static_cast(action)); } } static inline void populateAnnouncementColorSelection(QComboBox *list) { for (const auto &[_, name] : announcementColors) { list->addItem(obs_module_text(name.c_str())); } } static void populateUserQueryInfoTypeSelection(QComboBox *list) { list->addItem( obs_module_text( "AdvSceneSwitcher.action.twitch.user.getInfo.queryType.id"), static_cast(MacroActionTwitch::UserInfoQueryType::ID)); list->addItem( obs_module_text( "AdvSceneSwitcher.action.twitch.user.getInfo.queryType.login"), static_cast(MacroActionTwitch::UserInfoQueryType::LOGIN)); } MacroActionTwitchEdit::MacroActionTwitchEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _layout(new QHBoxLayout()), _actions(new FilterComboBox()), _tokens(new TwitchConnectionSelection()), _tokenWarning(new QLabel()), _streamTitle(new VariableLineEdit(this)), _category(new TwitchCategoryWidget(this)), _tags(new TagListWidget(this)), _language(new LanguageSelectionWidget(this)), _contentClassification(new ContentClassificationEdit(this)), _markerDescription(new VariableLineEdit(this)), _clipHasDelay(new QCheckBox(obs_module_text( "AdvSceneSwitcher.action.twitch.clip.hasDelay"))), _duration(new DurationSelection(this, false, 0)), _announcementMessage(new VariableTextEdit(this)), _announcementColor(new QComboBox(this)), _nonModDelayDuration(new QComboBox(this)), _channel(new TwitchChannelSelection(this)), _chatMessage(new VariableTextEdit(this)), _userInfoQueryType(new QComboBox(this)), _userLogin(new VariableLineEdit(this)), _userId(new VariableDoubleSpinBox(this)), _banReason(new VariableLineEdit(this)), _userModerationRow(new QWidget()), _layout2(new QHBoxLayout()), _pointsReward(new TwitchPointsRewardWidget(this, false)), _rewardVariable(new VariableSelection(this)), _toggleRewardSelection(new QPushButton()) { _layout2->setContentsMargins(0, 0, 0, 0); _userModerationRow->setLayout(_layout2); SetWidgetProperties(); SetWidgetSignalConnections(); _entryData = entryData; SetWidgetLayout(); auto mainLayout = new QVBoxLayout(); mainLayout->addLayout(_layout); mainLayout->addWidget(_userModerationRow); mainLayout->addWidget(_announcementMessage); mainLayout->addWidget(_chatMessage); mainLayout->addWidget(_tags); mainLayout->addWidget(_language); mainLayout->addWidget(_contentClassification); mainLayout->addWidget(_tokenWarning); setLayout(mainLayout); _tokenCheckTimer.start(1000); UpdateEntryData(); _loading = false; } void MacroActionTwitchEdit::ActionChanged(int idx) { if (_loading || !_entryData) { return; } if (idx == -1) { // Reset to previous selection const QSignalBlocker b(_actions); _actions->setCurrentIndex(_actions->findData( static_cast(_entryData->GetAction()))); return; } auto lock = LockContext(); _entryData->SetAction(static_cast( _actions->itemData(idx).toInt())); SetWidgetLayout(); SetWidgetVisibility(); } void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token) { GUARD_LOADING_AND_LOCK(); _entryData->_token = GetWeakTwitchTokenByQString(token); _category->SetToken(_entryData->_token); _channel->SetToken(_entryData->_token); _pointsReward->SetToken(_entryData->_token); _tags->SetToken(_entryData->_token); _language->SetToken(_entryData->_token); _contentClassification->SetToken(_entryData->_token); _entryData->ResetChatConnection(); SetWidgetVisibility(); emit(HeaderInfoChanged(token)); } void MacroActionTwitchEdit::SetTokenWarning(bool visible, const QString &text) { _tokenWarning->setText(text); _tokenWarning->setVisible(visible); adjustSize(); updateGeometry(); } void MacroActionTwitchEdit::CheckToken() { if (!_entryData) { return; } if (_entryData->_token.expired()) { SetTokenWarning( true, obs_module_text( "AdvSceneSwitcher.twitchToken.noSelection")); return; } if (!TokenIsValid(_entryData->_token)) { SetTokenWarning( true, obs_module_text( "AdvSceneSwitcher.twitchToken.notValid")); return; } if (!_entryData->ActionIsSupportedByToken()) { SetTokenWarning( true, obs_module_text( "AdvSceneSwitcher.twitchToken.permissionsInsufficient")); return; } SetTokenWarning(false); } void MacroActionTwitchEdit::StreamTitleChanged() { GUARD_LOADING_AND_LOCK(); _entryData->_streamTitle = _streamTitle->text().toStdString(); } void MacroActionTwitchEdit::CategoryChanged(const TwitchCategory &category) { GUARD_LOADING_AND_LOCK(); _entryData->_category = category; } void MacroActionTwitchEdit::TagsChanged(const TwitchTagList &tags) { GUARD_LOADING_AND_LOCK(); _entryData->_tags = tags; } void MacroActionTwitchEdit::LanguageChanged(const LanguageSelection &language) { GUARD_LOADING_AND_LOCK(); _entryData->_language = language; } void MacroActionTwitchEdit::ContentClassificationChanged( const ContentClassification &ccl) { GUARD_LOADING_AND_LOCK(); _entryData->_contentClassification = ccl; } void MacroActionTwitchEdit::MarkerDescriptionChanged() { GUARD_LOADING_AND_LOCK(); _entryData->_markerDescription = _markerDescription->text().toStdString(); } void MacroActionTwitchEdit::ClipHasDelayChanged(int state) { GUARD_LOADING_AND_LOCK(); _entryData->_clipHasDelay = state; } void MacroActionTwitchEdit::DurationChanged(const Duration &duration) { GUARD_LOADING_AND_LOCK(); _entryData->_duration = duration; } void MacroActionTwitchEdit::AnnouncementMessageChanged() { GUARD_LOADING_AND_LOCK(); _entryData->_announcementMessage = _announcementMessage->toPlainText().toStdString(); adjustSize(); updateGeometry(); } void MacroActionTwitchEdit::AnnouncementColorChanged(int index) { GUARD_LOADING_AND_LOCK(); _entryData->_announcementColor = static_cast(index); } void MacroActionTwitchEdit::NonModDelayDurationChanged(int index) { GUARD_LOADING_AND_LOCK(); _entryData->_nonModDelayDuration = _nonModDelayDuration->itemData(index).toInt(); } void MacroActionTwitchEdit::SetWidgetProperties() { _streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _streamTitle->setMaxLength(140); _markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _markerDescription->setMaxLength(140); _announcementMessage->setMaxLength(500); auto durationSpinBox = _duration->SpinBox(); durationSpinBox->setMaximum(180); durationSpinBox->setSuffix("s"); populateActionSelection(_actions); populateAnnouncementColorSelection(_announcementColor); populateUserQueryInfoTypeSelection(_userInfoQueryType); _userId->setMaximum(999999999999999); _userId->setDecimals(0); for (int secs : {2, 4, 6}) { _nonModDelayDuration->addItem( QString::number(secs) + " " + obs_module_text( "AdvSceneSwitcher.unit.seconds"), secs); } _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() { QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this, SLOT(ActionChanged(int))); QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)), this, SLOT(TwitchTokenChanged(const QString &))); QWidget::connect(&_tokenCheckTimer, SIGNAL(timeout()), this, SLOT(CheckToken())); QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this, SLOT(StreamTitleChanged())); QWidget::connect(_category, SIGNAL(CategoryChanged(const TwitchCategory &)), this, SLOT(CategoryChanged(const TwitchCategory &))); QWidget::connect(_tags, SIGNAL(TagListChanged(const TwitchTagList &)), this, SLOT(TagsChanged(const TwitchTagList &))); QWidget::connect(_language, SIGNAL(LanguageChanged(const LanguageSelection &)), this, SLOT(LanguageChanged(const LanguageSelection &))); QWidget::connect(_contentClassification, SIGNAL(ContentClassificationChanged( const ContentClassification &)), this, SLOT(ContentClassificationChanged( const ContentClassification &))); QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this, SLOT(MarkerDescriptionChanged())); QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this, SLOT(ClipHasDelayChanged(int))); QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)), this, SLOT(DurationChanged(const Duration &))); QWidget::connect(_announcementMessage, SIGNAL(textChanged()), this, SLOT(AnnouncementMessageChanged())); QWidget::connect(_announcementColor, SIGNAL(currentIndexChanged(int)), this, SLOT(AnnouncementColorChanged(int))); QWidget::connect(_nonModDelayDuration, SIGNAL(currentIndexChanged(int)), this, SLOT(NonModDelayDurationChanged(int))); QWidget::connect(_channel, SIGNAL(ChannelChanged(const TwitchChannel &)), this, SLOT(ChannelChanged(const TwitchChannel &))); QWidget::connect(_chatMessage, SIGNAL(textChanged()), this, SLOT(ChatMessageChanged())); QWidget::connect(_userInfoQueryType, SIGNAL(currentIndexChanged(int)), this, SLOT(UserInfoQueryTypeChanged(int))); QWidget::connect(_userLogin, SIGNAL(editingFinished()), this, SLOT(UserLoginChanged())); QWidget::connect( _userId, SIGNAL(NumberVariableChanged(const NumberVariable &)), this, SLOT(UserIdChanged(const NumberVariable &))); QWidget::connect(_banReason, SIGNAL(editingFinished()), this, SLOT(BanReasonChanged())); 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() { const auto action = _entryData->GetAction(); _streamTitle->setVisible( action == MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET); _category->setVisible( action == MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET); _tags->setVisible(action == MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET); _language->setVisible( action == MacroActionTwitch::Action::CHANNEL_INFO_LANGUAGE_SET); _contentClassification->setVisible( action == MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET); const bool isChannelModAction = action == MacroActionTwitch::Action::USER_BAN || action == MacroActionTwitch::Action::USER_UNBAN || action == MacroActionTwitch::Action::USER_MODERATOR_ADD || action == MacroActionTwitch::Action::USER_MODERATOR_DELETE || action == MacroActionTwitch::Action::USER_VIP_ADD || action == MacroActionTwitch::Action::USER_VIP_DELETE; _channel->setVisible( action == MacroActionTwitch::Action::CHANNEL_GET_INFO || action == MacroActionTwitch::Action::RAID_START || action == MacroActionTwitch::Action::RAID_END || action == MacroActionTwitch::Action::SHOUTOUT_SEND || action == MacroActionTwitch::Action::SEND_CHAT_MESSAGE || action == MacroActionTwitch::Action::POINTS_REWARD_GET_INFO || isChannelModAction); _duration->setVisible( action == MacroActionTwitch::Action::COMMERCIAL_START || action == MacroActionTwitch::Action::USER_BAN); _banReason->setVisible(action == MacroActionTwitch::Action::USER_BAN); _userModerationRow->setVisible( isChannelModAction || action == MacroActionTwitch::Action::USER_GET_INFO || action == MacroActionTwitch::Action::USER_BLOCK || action == MacroActionTwitch::Action::USER_UNBLOCK || action == MacroActionTwitch::Action::POINTS_REWARD_GET_INFO); _markerDescription->setVisible( action == MacroActionTwitch::Action::MARKER_CREATE); _clipHasDelay->setVisible(action == MacroActionTwitch::Action::CLIP_CREATE); _announcementMessage->setVisible( action == MacroActionTwitch::Action::CHAT_ANNOUNCEMENT_SEND); _announcementColor->setVisible( action == MacroActionTwitch::Action::CHAT_ANNOUNCEMENT_SEND); _nonModDelayDuration->setVisible( action == MacroActionTwitch::Action::CHAT_NON_MODERATOR_DELAY_ENABLE); _chatMessage->setVisible(action == MacroActionTwitch::Action::SEND_CHAT_MESSAGE); const bool isUserTargetAction = action == MacroActionTwitch::Action::USER_GET_INFO || action == MacroActionTwitch::Action::USER_BAN || action == MacroActionTwitch::Action::USER_UNBAN || action == MacroActionTwitch::Action::USER_BLOCK || action == MacroActionTwitch::Action::USER_UNBLOCK || action == MacroActionTwitch::Action::USER_MODERATOR_ADD || action == MacroActionTwitch::Action::USER_MODERATOR_DELETE || action == MacroActionTwitch::Action::USER_VIP_ADD || action == MacroActionTwitch::Action::USER_VIP_DELETE; _userInfoQueryType->setVisible(isUserTargetAction); _userLogin->setVisible( isUserTargetAction && _entryData->_userInfoQueryType == MacroActionTwitch::UserInfoQueryType::LOGIN); _userId->setVisible(isUserTargetAction && _entryData->_userInfoQueryType == MacroActionTwitch::UserInfoQueryType::ID); _pointsReward->setVisible( action == MacroActionTwitch::Action::POINTS_REWARD_GET_INFO && !_entryData->_useVariableForRewardSelection); _rewardVariable->setVisible( action == MacroActionTwitch::Action::POINTS_REWARD_GET_INFO && _entryData->_useVariableForRewardSelection); _toggleRewardSelection->setVisible( action == MacroActionTwitch::Action::POINTS_REWARD_GET_INFO); if (action == MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET || action == MacroActionTwitch::Action::MARKER_CREATE) { RemoveStretchIfPresent(_layout); } else { AddStretchIfNecessary(_layout); } CheckToken(); adjustSize(); updateGeometry(); } void MacroActionTwitchEdit::ChannelChanged(const TwitchChannel &channel) { GUARD_LOADING_AND_LOCK(); _entryData->_channel = channel; _pointsReward->SetChannel(channel); _entryData->ResetChatConnection(); } void MacroActionTwitchEdit::ChatMessageChanged() { GUARD_LOADING_AND_LOCK(); _entryData->_chatMessage = _chatMessage->toPlainText().toStdString(); adjustSize(); updateGeometry(); } void MacroActionTwitchEdit::UserInfoQueryTypeChanged(int idx) { GUARD_LOADING_AND_LOCK(); _entryData->_userInfoQueryType = static_cast( _userInfoQueryType->itemData(idx).toInt()); SetWidgetVisibility(); } void MacroActionTwitchEdit::UserLoginChanged() { GUARD_LOADING_AND_LOCK(); _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::BanReasonChanged() { GUARD_LOADING_AND_LOCK(); _entryData->_banReason = _banReason->text().toStdString(); } void MacroActionTwitchEdit::PointsRewardChanged(const TwitchPointsReward &reward) { GUARD_LOADING_AND_LOCK(); _entryData->_pointsReward = reward; } void MacroActionTwitchEdit::SetWidgetLayout() { const std::vector widgets{_tokens, _actions, _streamTitle, _category, _markerDescription, _clipHasDelay, _duration, _announcementColor, _nonModDelayDuration, _channel, _userInfoQueryType, _userLogin, _userId, _banReason, _pointsReward, _rewardVariable, _toggleRewardSelection}; for (auto widget : widgets) { _layout->removeWidget(widget); _layout2->removeWidget(widget); } ClearLayout(_layout); ClearLayout(_layout2); const std::unordered_map widgetsMap{ {"{{account}}", _tokens}, {"{{actions}}", _actions}, {"{{streamTitle}}", _streamTitle}, {"{{category}}", _category}, {"{{markerDescription}}", _markerDescription}, {"{{clipHasDelay}}", _clipHasDelay}, {"{{duration}}", _duration}, {"{{announcementColor}}", _announcementColor}, {"{{nonModDelayDuration}}", _nonModDelayDuration}, {"{{channel}}", _channel}, {"{{userInfoQueryType}}", _userInfoQueryType}, {"{{userLogin}}", _userLogin}, {"{{userId}}", _userId}, {"{{banReason}}", _banReason}, {"{{pointsReward}}", _pointsReward}, {"{{rewardVariable}}", _rewardVariable}, {"{{toggleRewardSelection}}", _toggleRewardSelection}}; const char *layoutText; const char *layout2Text = nullptr; switch (_entryData->GetAction()) { case MacroActionTwitch::Action::SEND_CHAT_MESSAGE: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.chat"); break; case MacroActionTwitch::Action::USER_GET_INFO: case MacroActionTwitch::Action::USER_BLOCK: case MacroActionTwitch::Action::USER_UNBLOCK: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1"); layout2Text = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2"); break; case MacroActionTwitch::Action::USER_BAN: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.user.ban.row1"); layout2Text = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.user.ban.row2"); break; case MacroActionTwitch::Action::USER_UNBAN: case MacroActionTwitch::Action::USER_MODERATOR_ADD: case MacroActionTwitch::Action::USER_MODERATOR_DELETE: case MacroActionTwitch::Action::USER_VIP_ADD: case MacroActionTwitch::Action::USER_VIP_DELETE: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.user.moderation.row1"); layout2Text = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.user.moderation.row2"); break; case MacroActionTwitch::Action::POINTS_REWARD_GET_INFO: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1"); layout2Text = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2"); break; case MacroActionTwitch::Action::CHANNEL_GET_INFO: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.channel.getInfo"); break; default: layoutText = obs_module_text( "AdvSceneSwitcher.action.twitch.layout.default"); break; } PlaceWidgets(layoutText, _layout, widgetsMap); if (layout2Text) { PlaceWidgets(layout2Text, _layout2, widgetsMap); } _layout->setContentsMargins(0, 0, 0, 0); _layout2->setContentsMargins(0, 0, 0, 0); } void MacroActionTwitchEdit::UpdateEntryData() { if (!_entryData) { return; } _actions->setCurrentIndex( _actions->findData(static_cast(_entryData->GetAction()))); _tokens->SetToken(_entryData->_token); _streamTitle->setText(_entryData->_streamTitle); _category->SetToken(_entryData->_token); _category->SetCategory(_entryData->_category); _tags->SetTags(_entryData->_tags); _tags->SetToken(_entryData->_token); _language->SetLanguageSelection(_entryData->_language); _language->SetToken(_entryData->_token); _contentClassification->SetContentClassification( _entryData->_contentClassification); _contentClassification->SetToken(_entryData->_token); _markerDescription->setText(_entryData->_markerDescription); _clipHasDelay->setChecked(_entryData->_clipHasDelay); _duration->SetDuration(_entryData->_duration); _announcementMessage->setPlainText(_entryData->_announcementMessage); _announcementColor->setCurrentIndex( static_cast(_entryData->_announcementColor)); _nonModDelayDuration->setCurrentIndex(_nonModDelayDuration->findData( _entryData->_nonModDelayDuration)); _channel->SetToken(_entryData->_token); _channel->SetChannel(_entryData->_channel); _chatMessage->setPlainText(_entryData->_chatMessage); _userInfoQueryType->setCurrentIndex(_userInfoQueryType->findData( static_cast(_entryData->_userInfoQueryType))); _userLogin->setText(_entryData->_userLogin); _userId->SetValue(_entryData->_userId); _banReason->setText(_entryData->_banReason); _pointsReward->SetToken(_entryData->_token); _pointsReward->SetChannel(_entryData->_channel); _pointsReward->SetPointsReward(_entryData->_pointsReward); _rewardVariable->SetVariable(_entryData->_rewardVariable); SetWidgetVisibility(); } } // namespace advss