SceneSwitcher/plugins/twitch/macro-action-twitch.cpp
WarmUpTill 9301ead060
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
Add chat settings toggles
2026-03-21 22:52:27 +01:00

1941 lines
62 KiB
C++

#include "macro-action-twitch.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <layout-helpers.hpp>
#include <ui-helpers.hpp>
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<MacroAction> MacroActionTwitch::Create(Macro *m)
{
return std::make_shared<MacroActionTwitch>(m);
}
std::shared_ptr<MacroAction> MacroActionTwitch::Copy() const
{
return std::make_shared<MacroActionTwitch>(*this);
}
std::string MacroActionTwitch::GetShortDesc() const
{
return GetWeakTwitchTokenName(_token);
}
const static std::map<MacroActionTwitch::Action, std::string> 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<MacroActionTwitch::AnnouncementColor, std::string>
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<MacroActionTwitch::AnnouncementColor, std::string>
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<MacroActionTwitch::RedemptionStatus, std::string>
redemptionStatusesTwitch = {
{MacroActionTwitch::RedemptionStatus::CANCELED, "CANCELED"},
{MacroActionTwitch::RedemptionStatus::FULFILLED, "FULFILLED"},
};
void MacroActionTwitch::SetStreamTitle(
const std::shared_ptr<TwitchToken> &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<TwitchToken> &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<TwitchToken> &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<TwitchToken> &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<TwitchToken> &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<TwitchToken> &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<TwitchToken> &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<TwitchToken> &token)
{
if (!_chatConnection) {
_chatConnection = TwitchChatConnection::GetChatConnection(
*token, _channel);
return;
}
_chatConnection->SendChatMessage(_chatMessage);
}
void MacroActionTwitch::GetUserInfo(const std::shared_ptr<TwitchToken> &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<TwitchToken> &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<std::string> MacroActionTwitch::GetTargetUserID(
const std::shared_ptr<TwitchToken> &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<TwitchToken> &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<TwitchToken> &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<int>(_action));
}
}
bool MacroActionTwitch::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "action", static_cast<int>(_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<int>(_announcementColor));
obs_data_set_int(obj, "nonModDelayDuration", _nonModDelayDuration);
_channel.Save(obj);
_chatMessage.Save(obj, "chatMessage");
obs_data_set_int(obj, "userInfoQueryType",
static_cast<int>(_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<AnnouncementColor>(
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<UserInfoQueryType>(
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<Action>(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<Action, std::vector<TokenOption>>
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<int>(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<int>(MacroActionTwitch::UserInfoQueryType::ID));
list->addItem(
obs_module_text(
"AdvSceneSwitcher.action.twitch.user.getInfo.queryType.login"),
static_cast<int>(MacroActionTwitch::UserInfoQueryType::LOGIN));
}
MacroActionTwitchEdit::MacroActionTwitchEdit(
QWidget *parent, std::shared_ptr<MacroActionTwitch> 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<int>(_entryData->GetAction())));
return;
}
auto lock = LockContext();
_entryData->SetAction(static_cast<MacroActionTwitch::Action>(
_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<MacroActionTwitch::AnnouncementColor>(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<double> &)),
this, SLOT(UserIdChanged(const NumberVariable<double> &)));
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<MacroActionTwitch::UserInfoQueryType>(
_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<double> &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<QWidget *> 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<std::string, QWidget *> 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<int>(_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<int>(_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<int>(_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