Add Twitch condition

It supports:
* Checking if a channel is live (polling)
* Checking if a channel went live and it's stream type (event)
* Checking if a channel weng offline (event)
* Checking the stream title of a channel (polling)
* Checking the stream category of a channel (polling)
* Checking if the channel information was changed (event)
* Checking for new followers (event)
* Checking for subsciptions (new / end / gift / resub message) (event)
* Checking for cheers (event)
This commit is contained in:
WarmUpTill 2023-10-08 00:17:15 +02:00 committed by WarmUpTill
parent ce40b80d90
commit 9ea90ed61b
6 changed files with 963 additions and 2 deletions

View File

@ -490,6 +490,28 @@ AdvSceneSwitcher.condition.slideshow.condition.slideIndex="Current slide number
AdvSceneSwitcher.condition.slideshow.condition.slidePath="Current slide path is"
AdvSceneSwitcher.condition.slideshow.updateIntervalTooltip="Information about the slide show status will only be updated based on the configured time between slides"
AdvSceneSwitcher.condition.slideshow.entry="{{sources}}{{conditions}}{{index}}{{path}}"
AdvSceneSwitcher.condition.twitch="Twitch"
AdvSceneSwitcher.condition.twitch.type.channelIsLive="Is currently live"
AdvSceneSwitcher.condition.twitch.type.channelWentLive="Went live"
AdvSceneSwitcher.condition.twitch.type.channelStartedPlaylist="Started playlist"
AdvSceneSwitcher.condition.twitch.type.channelStartedWatchparty="Started watchparty"
AdvSceneSwitcher.condition.twitch.type.channelStartedPremiere="Started premiere"
AdvSceneSwitcher.condition.twitch.type.channelStartedRerun="Started rerun"
AdvSceneSwitcher.condition.twitch.type.channelWentOffline="Went offline"
AdvSceneSwitcher.condition.twitch.type.channelTitle="Title matches"
AdvSceneSwitcher.condition.twitch.type.category="Category is"
AdvSceneSwitcher.condition.twitch.type.channelUpdateEvent="Channel information was updated"
AdvSceneSwitcher.condition.twitch.type.channelFollow="Channel received a follow"
AdvSceneSwitcher.condition.twitch.type.channelSubscribe="Channel received a subscription"
AdvSceneSwitcher.condition.twitch.type.channelSubscribeEnd="Channel subscription expired"
AdvSceneSwitcher.condition.twitch.type.channelSubscribeGift="Channel received gifted subscriptions"
AdvSceneSwitcher.condition.twitch.type.channelSubscribeMessage="Channel received re-subscription chat message"
AdvSceneSwitcher.condition.twitch.type.channelCheer="Channel received a cheer chat message"
AdvSceneSwitcher.condition.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.condition.twitch.entry="Channel{{channel}}{{conditions}}{{streamTitle}}{{regex}}{{category}}"
AdvSceneSwitcher.condition.twitch.entry.account="Check using account{{account}}"
AdvSceneSwitcher.condition.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.condition.twitch.title.title="Enter title"
; Macro Actions
AdvSceneSwitcher.action.scene="Switch scene"
@ -793,7 +815,6 @@ AdvSceneSwitcher.action.twitch.type.emoteOnlyDisable="Disable chat's emote-only
AdvSceneSwitcher.action.twitch.type.raid="Raid channel"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.action.twitch.entry.line1="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
@ -954,6 +975,7 @@ AdvSceneSwitcher.twitchToken.request.success="Successfully received token!"
AdvSceneSwitcher.twitchToken.request.success.browser="Authentication successful! You can close this window now."
AdvSceneSwitcher.twitchToken.request.notSet="Account is not connected!"
AdvSceneSwitcher.twitchToken.permissions="Token permissions:"
AdvSceneSwitcher.twitchToken.permissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.twitchToken.analytics.readExtensions="View analytics data for the Twitch Extensions owned by the authenticated account."
AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the games owned by the authenticated account."
AdvSceneSwitcher.twitchToken.bits.read="View Bits information for a channel."
@ -963,6 +985,9 @@ AdvSceneSwitcher.twitchToken.clips.edit="Create clips from channel's broadcasts.
AdvSceneSwitcher.twitchToken.moderator.manageAnnouncements="Manage channel's chat announcements."
AdvSceneSwitcher.twitchToken.moderator.manageChatSettings="Manage channel's chat settings, such as emote-only or slow mode."
AdvSceneSwitcher.twitchToken.channel.raid="Manage a channel raiding another channel."
AdvSceneSwitcher.twitchToken.bits="View Bits information for a channel."
AdvSceneSwitcher.twitchToken.readSubscriptions="View a list of all subscribers to a channel and check if a user is subscribed to a channel."
AdvSceneSwitcher.twitchToken.readFollowers="Read the followers of a broadcaster."
AdvSceneSwitcher.channel.open="Show channel"

View File

@ -46,6 +46,8 @@ target_sources(
event-sub.hpp
macro-action-twitch.cpp
macro-action-twitch.hpp
macro-condition-twitch.cpp
macro-condition-twitch.hpp
token.cpp
token.hpp
twitch-helpers.cpp

View File

@ -358,7 +358,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_actions(new FilterComboBox()),
_tokens(new TwitchConnectionSelection()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient"))),
"AdvSceneSwitcher.twitchToken.permissionsInsufficient"))),
_streamTitle(new VariableLineEdit(this)),
_category(new TwitchCategoryWidget(this)),
_markerDescription(new VariableLineEdit(this)),

View File

@ -0,0 +1,800 @@
#include "macro-condition-twitch.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <utility.hpp>
namespace advss {
const std::string MacroConditionTwitch::id = "twitch";
bool MacroConditionTwitch::_registered = MacroConditionFactory::Register(
MacroConditionTwitch::id,
{MacroConditionTwitch::Create, MacroConditionTwitchEdit::Create,
"AdvSceneSwitcher.condition.twitch"});
const static std::map<MacroConditionTwitch::Condition, std::string> conditionTypes = {
{MacroConditionTwitch::Condition::LIVE_EVENT_REGULAR,
"AdvSceneSwitcher.condition.twitch.type.channelWentLive"},
{MacroConditionTwitch::Condition::LIVE_EVENT_PLAYLIST,
"AdvSceneSwitcher.condition.twitch.type.channelStartedPlaylist"},
{MacroConditionTwitch::Condition::LIVE_EVENT_WATCHPARTY,
"AdvSceneSwitcher.condition.twitch.type.channelStartedWatchparty"},
{MacroConditionTwitch::Condition::LIVE_EVENT_PREMIERE,
"AdvSceneSwitcher.condition.twitch.type.channelStartedPremiere"},
{MacroConditionTwitch::Condition::LIVE_EVENT_RERUN,
"AdvSceneSwitcher.condition.twitch.type.channelStartedRerun"},
{MacroConditionTwitch::Condition::OFFLINE_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelWentOffline"},
{MacroConditionTwitch::Condition::FOLLOW_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelFollow"},
{MacroConditionTwitch::Condition::SUBSCRIBE_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelSubscribe"},
{MacroConditionTwitch::Condition::SUBSCRIBE_END_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelSubscribeEnd"},
{MacroConditionTwitch::Condition::SUBSCRIBE_GIFT_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelSubscribeGift"},
{MacroConditionTwitch::Condition::SUBSCRIBE_MESSAGE_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelSubscribeMessage"},
{MacroConditionTwitch::Condition::CHEER_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelCheer"},
{MacroConditionTwitch::Condition::LIVE,
"AdvSceneSwitcher.condition.twitch.type.channelIsLive"},
{MacroConditionTwitch::Condition::TITLE,
"AdvSceneSwitcher.condition.twitch.type.channelTitle"},
{MacroConditionTwitch::Condition::CATEGORY,
"AdvSceneSwitcher.condition.twitch.type.category"},
{MacroConditionTwitch::Condition::CHANNEL_UPDATE_EVENT,
"AdvSceneSwitcher.condition.twitch.type.channelUpdateEvent"},
};
const static std::map<MacroConditionTwitch::Condition, std::string>
eventIdentifiers = {
{MacroConditionTwitch::Condition::LIVE_EVENT_REGULAR,
"stream.online"},
{MacroConditionTwitch::Condition::LIVE_EVENT_PLAYLIST,
"stream.online"},
{MacroConditionTwitch::Condition::LIVE_EVENT_WATCHPARTY,
"stream.online"},
{MacroConditionTwitch::Condition::LIVE_EVENT_PREMIERE,
"stream.online"},
{MacroConditionTwitch::Condition::LIVE_EVENT_RERUN,
"stream.online"},
{MacroConditionTwitch::Condition::OFFLINE_EVENT,
"stream.offline"},
{MacroConditionTwitch::Condition::CHANNEL_UPDATE_EVENT,
"channel.update"},
{MacroConditionTwitch::Condition::FOLLOW_EVENT,
"channel.follow"},
{MacroConditionTwitch::Condition::SUBSCRIBE_EVENT,
"channel.subscribe"},
{MacroConditionTwitch::Condition::SUBSCRIBE_END_EVENT,
"channel.subscription.end"},
{MacroConditionTwitch::Condition::SUBSCRIBE_GIFT_EVENT,
"channel.subscription.gift"},
{MacroConditionTwitch::Condition::SUBSCRIBE_MESSAGE_EVENT,
"channel.subscription.message"},
{MacroConditionTwitch::Condition::CHEER_EVENT, "channel.cheer"},
};
const static std::map<MacroConditionTwitch::Condition, std::string>
liveEventIDs = {
{MacroConditionTwitch::Condition::LIVE_EVENT_REGULAR, "live"},
{MacroConditionTwitch::Condition::LIVE_EVENT_PLAYLIST,
"playlist"},
{MacroConditionTwitch::Condition::LIVE_EVENT_WATCHPARTY,
"watch_party"},
{MacroConditionTwitch::Condition::LIVE_EVENT_PREMIERE,
"premiere"},
{MacroConditionTwitch::Condition::LIVE_EVENT_RERUN, "rerun"},
};
static bool titleMatches(const RegexConfig &conf, const std::string &title,
const std::string &expr)
{
if (!conf.Enabled()) {
return title == expr;
}
auto regex = conf.GetRegularExpression(expr);
if (!regex.isValid()) {
return false;
}
auto match = regex.match(QString::fromStdString(title));
return match.hasMatch();
}
bool MacroConditionTwitch::CheckChannelLiveEvents(TwitchToken &token)
{
auto eventSub = token.GetEventSub();
if (!eventSub) {
return false;
}
auto events = eventSub->Events();
for (const auto &event : events) {
if (event.type != eventIdentifiers.find(_condition)->second) {
continue;
}
auto id =
obs_data_get_string(event.data, "broadcaster_user_id");
if (id != _channel.GetUserID(token)) {
continue;
}
auto type = obs_data_get_string(event.data, "type");
auto it = liveEventIDs.find(_condition);
if (it == liveEventIDs.end()) {
continue;
}
const auto &typeId = it->second;
if (type != typeId) {
continue;
}
SetVariableValue(event.ToString());
return true;
}
return false;
}
bool MacroConditionTwitch::CheckChannelOfflineEvents(TwitchToken &token)
{
auto eventSub = token.GetEventSub();
if (!eventSub) {
return false;
}
auto events = eventSub->Events();
for (const auto &event : events) {
if (event.type != eventIdentifiers.find(_condition)->second) {
continue;
}
auto id =
obs_data_get_string(event.data, "broadcaster_user_id");
if (id != _channel.GetUserID(token)) {
continue;
}
SetVariableValue(event.ToString());
return true;
}
return false;
}
bool MacroConditionTwitch::CheckChannelUpdateEvents(TwitchToken &token)
{
auto eventSub = token.GetEventSub();
if (!eventSub) {
return false;
}
auto events = eventSub->Events();
for (const auto &event : events) {
if (event.type != eventIdentifiers.find(_condition)->second) {
continue;
}
auto id =
obs_data_get_string(event.data, "broadcaster_user_id");
if (id != _channel.GetUserID(token)) {
continue;
}
SetVariableValue(event.ToString());
return true;
}
return false;
}
bool MacroConditionTwitch::CheckChannelFollowEvents(TwitchToken &token)
{
auto eventSub = token.GetEventSub();
if (!eventSub) {
return false;
}
auto events = eventSub->Events();
for (const auto &event : events) {
if (event.type != eventIdentifiers.find(_condition)->second) {
continue;
}
auto id =
obs_data_get_string(event.data, "broadcaster_user_id");
if (id != _channel.GetUserID(token)) {
continue;
}
SetVariableValue(event.ToString());
return true;
}
return false;
}
bool MacroConditionTwitch::CheckChannelSubscribeEvents(TwitchToken &token)
{
auto eventSub = token.GetEventSub();
if (!eventSub) {
return false;
}
auto events = eventSub->Events();
for (const auto &event : events) {
if (event.type != eventIdentifiers.find(_condition)->second) {
continue;
}
auto id =
obs_data_get_string(event.data, "broadcaster_user_id");
if (id != _channel.GetUserID(token)) {
continue;
}
SetVariableValue(event.ToString());
return true;
}
return false;
}
bool MacroConditionTwitch::CheckChannelCheerEvents(TwitchToken &token)
{
auto eventSub = token.GetEventSub();
if (!eventSub) {
return false;
}
auto events = eventSub->Events();
for (const auto &event : events) {
if (event.type != eventIdentifiers.find(_condition)->second) {
continue;
}
auto id =
obs_data_get_string(event.data, "broadcaster_user_id");
if (id != _channel.GetUserID(token)) {
continue;
}
SetVariableValue(event.ToString());
return true;
}
return false;
}
bool MacroConditionTwitch::CheckCondition()
{
SetVariableValue("");
auto token = _token.lock();
if (!token) {
return false;
}
auto eventSub = token->GetEventSub();
if (IsUsingEventSubCondition()) {
if (!eventSub) {
return false;
}
CheckEventSubscription(*eventSub);
if (_subscriptionIDFuture.valid()) {
// Still waiting for the subscription to be registered
return false;
}
}
switch (_condition) {
case Condition::LIVE: {
auto info = _channel.GetLiveInfo(*token);
if (!info) {
return false;
}
return info->IsLive();
}
case Condition::LIVE_EVENT_REGULAR:
case Condition::LIVE_EVENT_PLAYLIST:
case Condition::LIVE_EVENT_WATCHPARTY:
case Condition::LIVE_EVENT_PREMIERE:
case Condition::LIVE_EVENT_RERUN:
return CheckChannelLiveEvents(*token);
case Condition::TITLE: {
auto info = _channel.GetInfo(*token);
if (!info) {
return false;
}
SetVariableValue(info->title);
return titleMatches(_regex, info->title, _streamTitle);
}
case Condition::CATEGORY: {
auto info = _channel.GetInfo(*token);
if (!info) {
return false;
}
SetVariableValue(info->game_name);
return info->game_id == std::to_string(_category.id);
}
case Condition::CHANNEL_UPDATE_EVENT:
return CheckChannelUpdateEvents(*token);
case Condition::FOLLOW_EVENT:
return CheckChannelFollowEvents(*token);
case Condition::SUBSCRIBE_EVENT:
case Condition::SUBSCRIBE_END_EVENT:
case Condition::SUBSCRIBE_GIFT_EVENT:
case Condition::SUBSCRIBE_MESSAGE_EVENT:
return CheckChannelSubscribeEvents(*token);
case Condition::CHEER_EVENT:
return CheckChannelCheerEvents(*token);
default:
break;
}
return false;
}
void MacroConditionTwitch::CheckEventSubscription(EventSub &eventSub)
{
if (_subscriptionIDFuture.valid()) {
if (_subscriptionIDFuture.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready)
return;
_subscriptionID = _subscriptionIDFuture.get();
}
if (eventSub.SubscriptionIsActive(_subscriptionID)) {
return;
}
SetupEventSubscriptions();
}
void MacroConditionTwitch::SetCondition(const Condition &cond)
{
_condition = cond;
_subscriptionID = "";
}
bool MacroConditionTwitch::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
obs_data_set_string(obj, "token",
GetWeakTwitchTokenName(_token).c_str());
_streamTitle.Save(obj, "streamTitle");
_category.Save(obj);
_channel.Save(obj);
_regex.Save(obj);
return true;
}
bool MacroConditionTwitch::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_condition = static_cast<Condition>(obs_data_get_int(obj, "condition"));
_token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token"));
_streamTitle.Load(obj, "streamTitle");
_category.Load(obj);
_channel.Load(obj);
_regex.Load(obj);
_subscriptionID = "";
return true;
}
std::string MacroConditionTwitch::GetShortDesc() const
{
return GetWeakTwitchTokenName(_token);
}
bool MacroConditionTwitch::ConditionIsSupportedByToken()
{
static const std::unordered_map<Condition, TokenOption> requiredOption = {
{Condition::LIVE_EVENT_REGULAR, {""}},
{Condition::LIVE_EVENT_PLAYLIST, {""}},
{Condition::LIVE_EVENT_WATCHPARTY, {""}},
{Condition::LIVE_EVENT_PREMIERE, {""}},
{Condition::LIVE_EVENT_RERUN, {""}},
{Condition::OFFLINE_EVENT, {""}},
{Condition::CHANNEL_UPDATE_EVENT, {""}},
{Condition::FOLLOW_EVENT, {"moderator:read:followers"}},
{Condition::SUBSCRIBE_EVENT, {"channel:read:subscriptions"}},
{Condition::SUBSCRIBE_END_EVENT,
{"channel:read:subscriptions"}},
{Condition::SUBSCRIBE_GIFT_EVENT,
{"channel:read:subscriptions"}},
{Condition::SUBSCRIBE_MESSAGE_EVENT,
{"channel:read:subscriptions"}},
{Condition::CHEER_EVENT, {"bits:read"}},
{Condition::LIVE, {""}},
{Condition::TITLE, {""}},
{Condition::CATEGORY, {""}},
};
auto token = _token.lock();
if (!token) {
return false;
}
auto option = requiredOption.find(_condition);
assert(option != requiredOption.end());
if (option == requiredOption.end()) {
return false;
}
return option->second.apiId.empty() ||
token->OptionIsEnabled(option->second);
}
void MacroConditionTwitch::SetupEventSubscriptions()
{
if (!IsUsingEventSubCondition()) {
return;
}
switch (_condition) {
case MacroConditionTwitch::Condition::LIVE_EVENT_REGULAR:
case MacroConditionTwitch::Condition::LIVE_EVENT_PLAYLIST:
case MacroConditionTwitch::Condition::LIVE_EVENT_WATCHPARTY:
case MacroConditionTwitch::Condition::LIVE_EVENT_PREMIERE:
case MacroConditionTwitch::Condition::LIVE_EVENT_RERUN:
AddChannelLiveEventSubscription();
break;
case MacroConditionTwitch::Condition::OFFLINE_EVENT:
AddChannelOfflineEventSubscription();
break;
case MacroConditionTwitch::Condition::CHANNEL_UPDATE_EVENT:
AddChannelUpdateEventSubscription();
break;
case MacroConditionTwitch::Condition::FOLLOW_EVENT:
AddChannelFollowEventSubscription();
break;
case MacroConditionTwitch::Condition::SUBSCRIBE_EVENT:
case MacroConditionTwitch::Condition::SUBSCRIBE_END_EVENT:
case MacroConditionTwitch::Condition::SUBSCRIBE_GIFT_EVENT:
case MacroConditionTwitch::Condition::SUBSCRIBE_MESSAGE_EVENT:
AddChannelSubscribeEventSubscription();
break;
default:
break;
}
}
bool MacroConditionTwitch::IsUsingEventSubCondition()
{
const static std::set<Condition> eventConditions{
Condition::LIVE_EVENT_REGULAR,
Condition::LIVE_EVENT_PLAYLIST,
Condition::LIVE_EVENT_WATCHPARTY,
Condition::LIVE_EVENT_PREMIERE,
Condition::LIVE_EVENT_RERUN,
Condition::CHANNEL_UPDATE_EVENT,
Condition::FOLLOW_EVENT,
Condition::SUBSCRIBE_EVENT,
Condition::SUBSCRIBE_END_EVENT,
Condition::SUBSCRIBE_GIFT_EVENT,
Condition::SUBSCRIBE_MESSAGE_EVENT,
Condition::CHEER_EVENT,
};
return eventConditions.find(_condition) != eventConditions.end();
}
std::future<std::string>
waitForSubscription(const std::shared_ptr<TwitchToken> &token,
const Subscription &subscription)
{
return std::async(std::launch::async, [token, subscription]() {
auto id = EventSub::AddEventSubscribtion(token, subscription);
return id;
});
}
void MacroConditionTwitch::AddChannelLiveEventSubscription()
{
if (!IsUsingEventSubCondition()) {
return;
}
auto token = _token.lock();
if (!token) {
return;
}
OBSDataAutoRelease temp = obs_data_create();
Subscription subscription{temp.Get()};
obs_data_set_string(subscription.data, "type",
eventIdentifiers.find(_condition)->second.c_str());
obs_data_set_string(subscription.data, "version", "1");
OBSDataAutoRelease condition = obs_data_create();
obs_data_set_string(condition, "broadcaster_user_id",
_channel.GetUserID(*token).c_str());
obs_data_set_obj(subscription.data, "condition", condition);
_subscriptionIDFuture = waitForSubscription(token, subscription);
}
void MacroConditionTwitch::AddChannelOfflineEventSubscription()
{
if (!IsUsingEventSubCondition()) {
return;
}
auto token = _token.lock();
if (!token) {
return;
}
OBSDataAutoRelease temp = obs_data_create();
Subscription subscription{temp.Get()};
obs_data_set_string(subscription.data, "type",
eventIdentifiers.find(_condition)->second.c_str());
obs_data_set_string(subscription.data, "version", "1");
OBSDataAutoRelease condition = obs_data_create();
obs_data_set_string(condition, "broadcaster_user_id",
_channel.GetUserID(*token).c_str());
obs_data_set_obj(subscription.data, "condition", condition);
_subscriptionIDFuture = waitForSubscription(token, subscription);
}
void MacroConditionTwitch::AddChannelUpdateEventSubscription()
{
if (!IsUsingEventSubCondition()) {
return;
}
auto token = _token.lock();
if (!token) {
return;
}
OBSDataAutoRelease temp = obs_data_create();
Subscription subscription{temp.Get()};
obs_data_set_string(subscription.data, "type",
eventIdentifiers.find(_condition)->second.c_str());
obs_data_set_string(subscription.data, "version", "2");
OBSDataAutoRelease condition = obs_data_create();
obs_data_set_string(condition, "broadcaster_user_id",
_channel.GetUserID(*token).c_str());
obs_data_set_obj(subscription.data, "condition", condition);
_subscriptionIDFuture = waitForSubscription(token, subscription);
}
void MacroConditionTwitch::AddChannelFollowEventSubscription()
{
if (!IsUsingEventSubCondition()) {
return;
}
auto token = _token.lock();
if (!token) {
return;
}
OBSDataAutoRelease temp = obs_data_create();
Subscription subscription{temp.Get()};
obs_data_set_string(subscription.data, "type",
eventIdentifiers.find(_condition)->second.c_str());
obs_data_set_string(subscription.data, "version", "2");
OBSDataAutoRelease condition = obs_data_create();
obs_data_set_string(condition, "broadcaster_user_id",
_channel.GetUserID(*token).c_str());
obs_data_set_string(condition, "moderator_user_id",
token->GetUserID().c_str());
obs_data_set_obj(subscription.data, "condition", condition);
_subscriptionIDFuture = waitForSubscription(token, subscription);
}
void MacroConditionTwitch::AddChannelSubscribeEventSubscription()
{
if (!IsUsingEventSubCondition()) {
return;
}
auto token = _token.lock();
if (!token) {
return;
}
OBSDataAutoRelease temp = obs_data_create();
Subscription subscription{temp.Get()};
obs_data_set_string(subscription.data, "type",
eventIdentifiers.find(_condition)->second.c_str());
obs_data_set_string(subscription.data, "version", "1");
OBSDataAutoRelease condition = obs_data_create();
obs_data_set_string(condition, "broadcaster_user_id",
_channel.GetUserID(*token).c_str());
obs_data_set_obj(subscription.data, "condition", condition);
_subscriptionIDFuture = waitForSubscription(token, subscription);
}
void MacroConditionTwitch::AddChannelCheerEventSubscription()
{
if (!IsUsingEventSubCondition()) {
return;
}
auto token = _token.lock();
if (!token) {
return;
}
OBSDataAutoRelease temp = obs_data_create();
Subscription subscription{temp.Get()};
obs_data_set_string(subscription.data, "type",
eventIdentifiers.find(_condition)->second.c_str());
obs_data_set_string(subscription.data, "version", "1");
OBSDataAutoRelease condition = obs_data_create();
obs_data_set_string(condition, "broadcaster_user_id",
_channel.GetUserID(*token).c_str());
obs_data_set_obj(subscription.data, "condition", condition);
_subscriptionIDFuture = waitForSubscription(token, subscription);
}
static inline void populateConditionSelection(QComboBox *list)
{
for (const auto &[condition, name] : conditionTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(condition));
}
}
MacroConditionTwitchEdit::MacroConditionTwitchEdit(
QWidget *parent, std::shared_ptr<MacroConditionTwitch> entryData)
: QWidget(parent),
_conditions(new FilterComboBox()),
_tokens(new TwitchConnectionSelection()),
_streamTitle(new VariableLineEdit(this)),
_category(new TwitchCategoryWidget(this)),
_channel(new TwitchChannelSelection(this)),
_regex(new RegexConfigWidget(parent)),
_layout(new QHBoxLayout()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.twitchToken.permissionsInsufficient")))
{
_streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_streamTitle->setMaxLength(140);
populateConditionSelection(_conditions);
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(TwitchTokenChanged(const QString &)));
QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this,
SLOT(StreamTitleChanged()));
QWidget::connect(_category,
SIGNAL(CategoreyChanged(const TwitchCategory &)), this,
SLOT(CategoreyChanged(const TwitchCategory &)));
QWidget::connect(_channel,
SIGNAL(ChannelChanged(const TwitchChannel &)), this,
SLOT(ChannelChanged(const TwitchChannel &)));
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
SLOT(RegexChanged(RegexConfig)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.twitch.entry"),
_layout,
{{"{{conditions}}", _conditions},
{"{{streamTitle}}", _streamTitle},
{"{{category}}", _category},
{"{{regex}}", _regex},
{"{{channel}}", _channel}});
_layout->setContentsMargins(0, 0, 0, 0);
auto accountLayout = new QHBoxLayout();
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.twitch.entry.account"),
accountLayout, {{"{{account}}", _tokens}});
accountLayout->setContentsMargins(0, 0, 0, 0);
auto mainLayout = new QVBoxLayout();
mainLayout->addLayout(_layout);
mainLayout->addLayout(accountLayout);
mainLayout->addWidget(_tokenPermissionWarning);
setLayout(mainLayout);
_tokenPermissionCheckTimer.start(1000);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionTwitchEdit::TwitchTokenChanged(const QString &token)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_token = GetWeakTwitchTokenByQString(token);
_category->SetToken(_entryData->_token);
SetupWidgetVisibility();
emit(HeaderInfoChanged(token));
}
void MacroConditionTwitchEdit::StreamTitleChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_streamTitle = _streamTitle->text().toStdString();
}
void MacroConditionTwitchEdit::CategoreyChanged(const TwitchCategory &category)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_category = category;
}
void MacroConditionTwitchEdit::CheckTokenPermissions()
{
_tokenPermissionWarning->setVisible(
_entryData && !_entryData->ConditionIsSupportedByToken());
adjustSize();
updateGeometry();
}
void MacroConditionTwitchEdit::SetupWidgetVisibility()
{
auto condition = _entryData->GetCondition();
_streamTitle->setVisible(condition ==
MacroConditionTwitch::Condition::TITLE);
_regex->setVisible(condition == MacroConditionTwitch::Condition::TITLE);
_category->setVisible(condition ==
MacroConditionTwitch::Condition::CATEGORY);
if (condition == MacroConditionTwitch::Condition::TITLE) {
RemoveStretchIfPresent(_layout);
} else {
AddStretchIfNecessary(_layout);
}
_tokenPermissionWarning->setVisible(
!_entryData->ConditionIsSupportedByToken());
adjustSize();
updateGeometry();
}
void MacroConditionTwitchEdit::ChannelChanged(const TwitchChannel &channel)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_channel = channel;
}
void MacroConditionTwitchEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_conditions->setCurrentIndex(_conditions->findData(
static_cast<int>(_entryData->GetCondition())));
_tokens->SetToken(_entryData->_token);
_streamTitle->setText(_entryData->_streamTitle);
_category->SetToken(_entryData->_token);
_category->SetCategory(_entryData->_category);
_channel->SetChannel(_entryData->_channel);
_regex->SetRegexConfig(_entryData->_regex);
SetupWidgetVisibility();
}
void MacroConditionTwitchEdit::ConditionChanged(int idx)
{
if (_loading || !_entryData) {
return;
}
if (idx == -1) { // Reset to previous selection
_conditions->setCurrentIndex(_conditions->findData(
static_cast<int>(_entryData->GetCondition())));
return;
}
auto lock = LockContext();
_entryData->SetCondition(static_cast<MacroConditionTwitch::Condition>(
_conditions->itemData(idx).toInt()));
SetupWidgetVisibility();
}
void MacroConditionTwitchEdit::RegexChanged(RegexConfig conf)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_regex = conf;
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@ -0,0 +1,129 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "token.hpp"
#include "category-selection.hpp"
#include "channel-selection.hpp"
#include <variable-line-edit.hpp>
#include <regex-config.hpp>
namespace advss {
class MacroConditionTwitch : public MacroCondition {
public:
MacroConditionTwitch(Macro *m) : MacroCondition(m, true) {}
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionTwitch>(m);
}
bool ConditionIsSupportedByToken();
enum class Condition {
// Event based
LIVE_EVENT_REGULAR = 10,
LIVE_EVENT_PLAYLIST = 20,
LIVE_EVENT_WATCHPARTY = 30,
LIVE_EVENT_PREMIERE = 40,
LIVE_EVENT_RERUN = 50,
OFFLINE_EVENT = 60,
CHANNEL_UPDATE_EVENT = 70,
FOLLOW_EVENT = 80,
SUBSCRIBE_EVENT = 90,
SUBSCRIBE_END_EVENT = 100,
SUBSCRIBE_GIFT_EVENT = 110,
SUBSCRIBE_MESSAGE_EVENT = 120,
CHEER_EVENT = 130,
// Polling
LIVE = 1000,
TITLE = 1010,
CATEGORY = 1020,
};
void SetCondition(const Condition &);
Condition GetCondition() { return _condition; }
std::weak_ptr<TwitchToken> _token;
TwitchChannel _channel;
StringVariable _streamTitle = obs_module_text(
"AdvSceneSwitcher.condition.twitch.title.title");
RegexConfig _regex = RegexConfig::PartialMatchRegexConfig();
TwitchCategory _category;
private:
bool CheckChannelLiveEvents(TwitchToken &);
bool CheckChannelOfflineEvents(TwitchToken &);
bool CheckChannelUpdateEvents(TwitchToken &);
bool CheckChannelFollowEvents(TwitchToken &);
bool CheckChannelSubscribeEvents(TwitchToken &);
bool CheckChannelCheerEvents(TwitchToken &);
bool IsUsingEventSubCondition();
void SetupEventSubscriptions();
void CheckEventSubscription(EventSub &);
void AddChannelLiveEventSubscription();
void AddChannelOfflineEventSubscription();
void AddChannelUpdateEventSubscription();
void AddChannelFollowEventSubscription();
void AddChannelSubscribeEventSubscription();
void AddChannelCheerEventSubscription();
Condition _condition = Condition::LIVE;
std::future<std::string> _subscriptionIDFuture;
std::string _subscriptionID;
static bool _registered;
static const std::string id;
};
class MacroConditionTwitchEdit : public QWidget {
Q_OBJECT
public:
MacroConditionTwitchEdit(
QWidget *parent,
std::shared_ptr<MacroConditionTwitch> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionTwitchEdit(
parent,
std::dynamic_pointer_cast<MacroConditionTwitch>(cond));
}
private slots:
void ConditionChanged(int);
void TwitchTokenChanged(const QString &);
void StreamTitleChanged();
void CategoreyChanged(const TwitchCategory &);
void ChannelChanged(const TwitchChannel &);
void RegexChanged(RegexConfig);
void CheckTokenPermissions();
signals:
void HeaderInfoChanged(const QString &);
protected:
std::shared_ptr<MacroConditionTwitch> _entryData;
private:
void SetupWidgetVisibility();
FilterComboBox *_conditions;
TwitchConnectionSelection *_tokens;
VariableLineEdit *_streamTitle;
TwitchCategoryWidget *_category;
TwitchChannelSelection *_channel;
RegexConfigWidget *_regex;
QHBoxLayout *_layout;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
bool _loading = true;
};
} // namespace advss

View File

@ -21,6 +21,11 @@ const std::unordered_map<std::string, std::string> TokenOption::_apiIdToLocale{
{"moderator:manage:chat_settings",
"AdvSceneSwitcher.twitchToken.moderator.manageChatSettings"},
{"channel:manage:raids", "AdvSceneSwitcher.twitchToken.channel.raid"},
{"moderator:read:followers",
"AdvSceneSwitcher.twitchToken.readSubscriptions"},
{"channel:read:subscriptions",
"AdvSceneSwitcher.twitchToken.readFollowers"},
{"bits:read", "AdvSceneSwitcher.twitchToken.bits"},
};
static void saveConnections(obs_data_t *obj);