diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index d28ea359..0cf0af12 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1242,6 +1242,14 @@ AdvSceneSwitcher.action.twitch.tags.duplicate="Duplicate Tag" AdvSceneSwitcher.action.twitch.tags.duplicate.info="This tag is already in the list." AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached" AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags." +AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels" +AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights." +AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs." +AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences." +AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic="Simulations and/or depictions of realistic violence, gore, extreme injury, or death." +AdvSceneSwitcher.action.twitch.contentClassification.gambling="Participating in or promoting online or in-person gambling, poker, or fantasy sports that involve the exchange of real money." +AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity="Prolonged and repeated use of obscenities, profanities, and vulgarities, especially as a regular part of speech." +AdvSceneSwitcher.action.twitch.contentClassification.getCurrent="Get current content classification labels" AdvSceneSwitcher.action.clipboard="Clipboard" AdvSceneSwitcher.action.clipboard.type.copy.text="Copy text" AdvSceneSwitcher.action.clipboard.type.copy.image="Copy image" diff --git a/plugins/twitch/CMakeLists.txt b/plugins/twitch/CMakeLists.txt index 1031b3b5..49cfd328 100644 --- a/plugins/twitch/CMakeLists.txt +++ b/plugins/twitch/CMakeLists.txt @@ -86,6 +86,8 @@ target_sources( chat-connection.hpp chat-message-pattern.cpp chat-message-pattern.hpp + content-classification.cpp + content-classification.hpp event-sub.cpp event-sub.hpp macro-action-twitch.cpp diff --git a/plugins/twitch/content-classification.cpp b/plugins/twitch/content-classification.cpp new file mode 100644 index 00000000..39bcae04 --- /dev/null +++ b/plugins/twitch/content-classification.cpp @@ -0,0 +1,206 @@ +#include "content-classification.hpp" +#include "channel-selection.hpp" +#include "log-helper.hpp" +#include "obs-module-helper.hpp" +#include "twitch-helpers.hpp" + +#include +#include + +namespace advss { + +void ContentClassification::Load(obs_data_t *obj) +{ + OBSDataAutoRelease data = + obs_data_get_obj(obj, "contentClassificationLabels"); + _debatedSocialIssuesAndPolitics = + obs_data_get_bool(data, "debatedSocialIssuesAndPolitics"); + _drugsIntoxication = obs_data_get_bool(data, "drugsIntoxication"); + _sexualThemes = obs_data_get_bool(data, "sexualThemes"); + _violentGraphic = obs_data_get_bool(data, "violentGraphic"); + _gambling = obs_data_get_bool(data, "gambling"); + _profanityVulgarity = obs_data_get_bool(data, "profanityVulgarity"); +} + +void ContentClassification::Save(obs_data_t *obj) const +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_bool(data, "debatedSocialIssuesAndPolitics", + _debatedSocialIssuesAndPolitics); + obs_data_set_bool(data, "drugsIntoxication", _drugsIntoxication); + obs_data_set_bool(data, "sexualThemes", _sexualThemes); + obs_data_set_bool(data, "violentGraphic", _violentGraphic); + obs_data_set_bool(data, "gambling", _gambling); + obs_data_set_bool(data, "profanityVulgarity", _profanityVulgarity); + obs_data_set_obj(obj, "contentClassificationLabels", data); +} + +void ContentClassification::SetContentClassification( + const TwitchToken &token) const +{ + OBSDataArrayAutoRelease ccls = obs_data_array_create(); + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "id", "DebatedSocialIssuesAndPolitics"); + obs_data_set_bool(data, "is_enabled", _debatedSocialIssuesAndPolitics); + obs_data_array_push_back(ccls, data); + data = obs_data_create(); + obs_data_set_string(data, "id", "DrugsIntoxication"); + obs_data_set_bool(data, "is_enabled", _drugsIntoxication); + obs_data_array_push_back(ccls, data); + data = obs_data_create(); + obs_data_set_string(data, "id", "SexualThemes"); + obs_data_set_bool(data, "is_enabled", _sexualThemes); + obs_data_array_push_back(ccls, data); + data = obs_data_create(); + obs_data_set_string(data, "id", "ViolentGraphic"); + obs_data_set_bool(data, "is_enabled", _violentGraphic); + obs_data_array_push_back(ccls, data); + data = obs_data_create(); + obs_data_set_string(data, "id", "Gambling"); + obs_data_set_bool(data, "is_enabled", _gambling); + obs_data_array_push_back(ccls, data); + data = obs_data_create(); + obs_data_set_string(data, "id", "ProfanityVulgarity"); + obs_data_set_bool(data, "is_enabled", _profanityVulgarity); + obs_data_array_push_back(ccls, data); + + data = obs_data_create(); + obs_data_set_array(data, "content_classification_labels", ccls); + + auto result = SendPatchRequest(token, "https://api.twitch.tv", + "/helix/channels", + {{"broadcaster_id", token.GetUserID()}}, + data.Get()); + + if (result.status != 204) { + blog(LOG_INFO, + "Failed to set stream content classification labels! (%d)", + result.status); + } +} + +ContentClassificationEdit::ContentClassificationEdit(QWidget *parent) + : QWidget(parent), + _debatedSocialIssuesAndPolitics(new QCheckBox( + obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics"), + this)), + _drugsIntoxication(new QCheckBox( + obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication"), + this)), + _sexualThemes(new QCheckBox( + obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes"), + this)), + _violentGraphic(new QCheckBox( + obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic"), + this)), + _gambling(new QCheckBox( + obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.gambling"), + this)), + _profanityVulgarity(new QCheckBox( + obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity"), + this)) +{ + auto getCurrent = new QPushButton(obs_module_text( + "AdvSceneSwitcher.action.twitch.contentClassification.getCurrent")); + connect(getCurrent, &QPushButton::clicked, this, + &ContentClassificationEdit::GetCurrentClicked); + + const auto emitChangeSignal = [this]() { + ContentClassification ccl; + ccl._debatedSocialIssuesAndPolitics = + _debatedSocialIssuesAndPolitics->isChecked(); + ccl._drugsIntoxication = _drugsIntoxication->isChecked(); + ccl._sexualThemes = _sexualThemes->isChecked(); + ccl._violentGraphic = _violentGraphic->isChecked(); + ccl._gambling = _gambling->isChecked(); + ccl._profanityVulgarity = _profanityVulgarity->isChecked(); + emit ContentClassificationChanged(ccl); + }; + + connect(_debatedSocialIssuesAndPolitics, &QCheckBox::stateChanged, this, + emitChangeSignal); + connect(_drugsIntoxication, &QCheckBox::stateChanged, this, + emitChangeSignal); + connect(_sexualThemes, &QCheckBox::stateChanged, this, + emitChangeSignal); + connect(_violentGraphic, &QCheckBox::stateChanged, this, + emitChangeSignal); + connect(_gambling, &QCheckBox::stateChanged, this, emitChangeSignal); + connect(_profanityVulgarity, &QCheckBox::stateChanged, this, + emitChangeSignal); + + auto layout = new QVBoxLayout; + layout->addWidget(_debatedSocialIssuesAndPolitics); + layout->addWidget(_drugsIntoxication); + layout->addWidget(_sexualThemes); + layout->addWidget(_violentGraphic); + layout->addWidget(_gambling); + layout->addWidget(_profanityVulgarity); + layout->addWidget(getCurrent); + setLayout(layout); + + // TODO: + // Figure out why the Twitch API always returns an empty CC list + // Hide for now + getCurrent->hide(); +} + +void ContentClassificationEdit::SetContentClassification( + const ContentClassification &ccl) +{ + _debatedSocialIssuesAndPolitics->setChecked( + ccl._debatedSocialIssuesAndPolitics); + _drugsIntoxication->setChecked(ccl._drugsIntoxication); + _sexualThemes->setChecked(ccl._sexualThemes); + _violentGraphic->setChecked(ccl._violentGraphic); + _gambling->setChecked(ccl._gambling); + _profanityVulgarity->setChecked(ccl._profanityVulgarity); +} + +void ContentClassificationEdit::SetToken(const std::weak_ptr &t) +{ + _token = t; +} + +void ContentClassificationEdit::GetCurrentClicked() +{ + auto token = _token.lock(); + if (!token) { + return; + } + + TwitchChannel channel; + channel.SetName(token->Name()); + const auto channelInfo = channel.GetInfo(*token); + if (!channelInfo) { + return; + } + + ContentClassification ccl; + for (const auto &label : channelInfo->content_classification_labels) { + if (label == "DebatedSocialIssuesAndPolitics") { + ccl._debatedSocialIssuesAndPolitics = true; + } else if (label == "DrugsIntoxication") { + ccl._drugsIntoxication = true; + } else if (label == "SexualThemes") { + ccl._sexualThemes = true; + } else if (label == "ViolentGraphic") { + ccl._violentGraphic = true; + } else if (label == "Gambling") { + ccl._gambling = true; + } else if (label == "ProfanityVulgarity") { + ccl._profanityVulgarity = true; + } + } + + SetContentClassification(ccl); +} + +} // namespace advss diff --git a/plugins/twitch/content-classification.hpp b/plugins/twitch/content-classification.hpp new file mode 100644 index 00000000..efc4f493 --- /dev/null +++ b/plugins/twitch/content-classification.hpp @@ -0,0 +1,50 @@ +#pragma once +#include "token.hpp" + +#include + +class QCheckBox; + +namespace advss { + +struct ContentClassification { + void Load(obs_data_t *obj); + void Save(obs_data_t *obj) const; + void SetContentClassification(const TwitchToken &token) const; + +private: + bool _debatedSocialIssuesAndPolitics = false; + bool _drugsIntoxication = false; + bool _sexualThemes = false; + bool _violentGraphic = false; + bool _gambling = false; + bool _profanityVulgarity = false; + + friend class ContentClassificationEdit; +}; + +class ContentClassificationEdit final : public QWidget { + Q_OBJECT + +public: + explicit ContentClassificationEdit(QWidget *parent = nullptr); + void SetContentClassification(const ContentClassification &ccl); + void SetToken(const std::weak_ptr &token); + +private slots: + void GetCurrentClicked(); +signals: + void ContentClassificationChanged(const ContentClassification &); + +private: + QCheckBox *_debatedSocialIssuesAndPolitics; + QCheckBox *_drugsIntoxication; + QCheckBox *_sexualThemes; + QCheckBox *_violentGraphic; + QCheckBox *_gambling; + QCheckBox *_profanityVulgarity; + + std::weak_ptr _token; +}; + +} // namespace advss diff --git a/plugins/twitch/macro-action-twitch.cpp b/plugins/twitch/macro-action-twitch.cpp index 79806c6a..f394df68 100644 --- a/plugins/twitch/macro-action-twitch.cpp +++ b/plugins/twitch/macro-action-twitch.cpp @@ -59,6 +59,8 @@ const static std::map actionTypes = { "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_CONTENT_LABELS_SET, + "AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set"}, {MacroActionTwitch::Action::RAID_START, "AdvSceneSwitcher.action.twitch.type.raid.start"}, {MacroActionTwitch::Action::COMMERCIAL_START, @@ -523,6 +525,9 @@ bool MacroActionTwitch::PerformAction() case Action::CHANNEL_INFO_TAGS_SET: _tags.SetStreamTags(*token); break; + case Action::CHANNEL_INFO_CONTENT_LABELS_SET: + _contentClassification.SetContentClassification(*token); + break; case Action::RAID_START: StartRaid(token); break; @@ -583,6 +588,7 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const _streamTitle.Save(obj, "streamTitle"); _category.Save(obj); _tags.Save(obj); + _contentClassification.Save(obj); _markerDescription.Save(obj, "markerDescription"); obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay); _duration.Save(obj); @@ -612,6 +618,7 @@ bool MacroActionTwitch::Load(obs_data_t *obj) _streamTitle.Load(obj, "streamTitle"); _category.Load(obj); _tags.Load(obj); + _contentClassification.Load(obj); _markerDescription.Load(obj, "markerDescription"); _clipHasDelay = obs_data_get_bool(obj, "clipHasDelay"); _duration.Load(obj); @@ -652,6 +659,8 @@ bool MacroActionTwitch::ActionIsSupportedByToken() {{"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, @@ -826,6 +835,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _streamTitle(new VariableLineEdit(this)), _category(new TwitchCategoryWidget(this)), _tags(new TagListWidget(this)), + _contentClassification(new ContentClassificationEdit(this)), _markerDescription(new VariableLineEdit(this)), _clipHasDelay(new QCheckBox(obs_module_text( "AdvSceneSwitcher.action.twitch.clip.hasDelay"))), @@ -852,6 +862,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( mainLayout->addWidget(_announcementMessage); mainLayout->addWidget(_chatMessage); mainLayout->addWidget(_tags); + mainLayout->addWidget(_contentClassification); mainLayout->addWidget(_tokenWarning); setLayout(mainLayout); @@ -889,6 +900,7 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token) _channel->SetToken(_entryData->_token); _pointsReward->SetToken(_entryData->_token); _tags->SetToken(_entryData->_token); + _contentClassification->SetToken(_entryData->_token); _entryData->ResetChatConnection(); SetWidgetVisibility(); @@ -949,6 +961,13 @@ void MacroActionTwitchEdit::TagsChanged(const TwitchTagList &tags) _entryData->_tags = tags; } +void MacroActionTwitchEdit::ContentClassificationChanged( + const ContentClassification &ccl) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_contentClassification = ccl; +} + void MacroActionTwitchEdit::MarkerDescriptionChanged() { GUARD_LOADING_AND_LOCK(); @@ -1031,6 +1050,12 @@ void MacroActionTwitchEdit::SetWidgetSignalConnections() SLOT(CategoryChanged(const TwitchCategory &))); QWidget::connect(_tags, SIGNAL(TagListChanged(const TwitchTagList &)), this, SLOT(TagsChanged(const TwitchTagList &))); + 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, @@ -1075,6 +1100,9 @@ void MacroActionTwitchEdit::SetWidgetVisibility() MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET); _tags->setVisible(_entryData->GetAction() == MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET); + _contentClassification->setVisible( + _entryData->GetAction() == + MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET); _channel->setVisible( _entryData->GetAction() == MacroActionTwitch::Action::RAID_START || @@ -1272,6 +1300,9 @@ void MacroActionTwitchEdit::UpdateEntryData() _category->SetCategory(_entryData->_category); _tags->SetTags(_entryData->_tags); _tags->SetToken(_entryData->_token); + _contentClassification->SetContentClassification( + _entryData->_contentClassification); + _contentClassification->SetToken(_entryData->_token); _markerDescription->setText(_entryData->_markerDescription); _clipHasDelay->setChecked(_entryData->_clipHasDelay); _duration->SetDuration(_entryData->_duration); diff --git a/plugins/twitch/macro-action-twitch.hpp b/plugins/twitch/macro-action-twitch.hpp index d2878025..46e3fb41 100644 --- a/plugins/twitch/macro-action-twitch.hpp +++ b/plugins/twitch/macro-action-twitch.hpp @@ -4,6 +4,7 @@ #include "category-selection.hpp" #include "channel-selection.hpp" #include "chat-connection.hpp" +#include "content-classification.hpp" #include "points-reward-selection.hpp" #include "tag-selection.hpp" @@ -29,7 +30,7 @@ public: CHANNEL_INFO_LANGUAGE_SET = 30, CHANNEL_INFO_DELAY_SET = 40, CHANNEL_INFO_TAGS_SET = 50, - CHANNEL_INFO_CONTENT_LABELS_SET = 60, // TODO + CHANNEL_INFO_CONTENT_LABELS_SET = 60, CHANNEL_INFO_BRANDED_CONTENT_ENABLE = 70, CHANNEL_INFO_BRANDED_CONTENT_DISABLE = 71, @@ -170,6 +171,7 @@ public: obs_module_text("AdvSceneSwitcher.action.twitch.title.title"); TwitchCategory _category; TwitchTagList _tags; + ContentClassification _contentClassification; StringVariable _markerDescription = obs_module_text( "AdvSceneSwitcher.action.twitch.marker.description"); bool _clipHasDelay = false; @@ -238,6 +240,7 @@ private slots: void StreamTitleChanged(); void CategoryChanged(const TwitchCategory &); void TagsChanged(const TwitchTagList &tags); + void ContentClassificationChanged(const ContentClassification &ccl); void MarkerDescriptionChanged(); void ClipHasDelayChanged(int state); void DurationChanged(const Duration &); @@ -273,6 +276,7 @@ private: VariableLineEdit *_streamTitle; TwitchCategoryWidget *_category; TagListWidget *_tags; + ContentClassificationEdit *_contentClassification; VariableLineEdit *_markerDescription; QCheckBox *_clipHasDelay; DurationSelection *_duration;