Add Twitch send chat announcement and emote-only mode actions (#890)

This commit is contained in:
Przemek Pawlas
2023-10-03 14:09:29 +02:00
committed by GitHub
parent ea5e92017e
commit 6558792e73
5 changed files with 241 additions and 61 deletions

View File

@@ -24,6 +24,35 @@ const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.twitch.type.clip"},
{MacroActionTwitch::Action::COMMERCIAL,
"AdvSceneSwitcher.action.twitch.type.commercial"},
{MacroActionTwitch::Action::ANNOUNCEMENT,
"AdvSceneSwitcher.action.twitch.type.announcement"},
{MacroActionTwitch::Action::ENABLE_EMOTE_ONLY,
"AdvSceneSwitcher.action.twitch.type.emoteOnlyEnable"},
{MacroActionTwitch::Action::DISABLE_EMOTE_ONLY,
"AdvSceneSwitcher.action.twitch.type.emoteOnlyDisable"},
};
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"},
};
void MacroActionTwitch::SetStreamTitle(
@@ -93,6 +122,7 @@ void MacroActionTwitch::CreateStreamClip(
{
OBSDataAutoRelease data = obs_data_create();
auto hasDelay = _clipHasDelay ? "true" : "false";
auto result = SendPostRequest(
"https://api.twitch.tv",
"/helix/clips?broadcaster_id=" + token->GetUserID() +
@@ -129,6 +159,47 @@ void MacroActionTwitch::StartCommercial(
}
}
void MacroActionTwitch::SendChatAnnouncement(
const std::shared_ptr<TwitchToken> &token) const
{
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 userId = token->GetUserID();
auto result =
SendPostRequest("https://api.twitch.tv",
"/helix/chat/announcements?broadcaster_id=" +
userId + "&moderator_id=" + userId,
*token, data.Get());
if (result.status != 204) {
blog(LOG_INFO, "Failed to send chat announcement! (%d)",
result.status);
}
}
void MacroActionTwitch::SetChatEmoteOnlyMode(
const std::shared_ptr<TwitchToken> &token, bool enable) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "emote_mode", enable);
auto userId = token->GetUserID();
auto result =
SendPatchRequest("https://api.twitch.tv",
"/helix/chat/settings?broadcaster_id=" +
userId + "&moderator_id=" + userId,
*token, data.Get());
if (result.status != 200) {
blog(LOG_INFO, "Failed to %s chat's emote-only mode! (%d)",
enable ? "enable" : "disable", result.status);
}
}
bool MacroActionTwitch::PerformAction()
{
auto token = _token.lock();
@@ -152,6 +223,15 @@ bool MacroActionTwitch::PerformAction()
case MacroActionTwitch::Action::COMMERCIAL:
StartCommercial(token);
break;
case MacroActionTwitch::Action::ANNOUNCEMENT:
SendChatAnnouncement(token);
break;
case MacroActionTwitch::Action::ENABLE_EMOTE_ONLY:
SetChatEmoteOnlyMode(token, true);
break;
case MacroActionTwitch::Action::DISABLE_EMOTE_ONLY:
SetChatEmoteOnlyMode(token, false);
break;
default:
break;
}
@@ -183,6 +263,9 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const
_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));
return true;
}
@@ -196,6 +279,9 @@ bool MacroActionTwitch::Load(obs_data_t *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"));
return true;
}
@@ -212,6 +298,10 @@ bool MacroActionTwitch::ActionIsSupportedByToken()
{Action::MARKER, {"channel:manage:broadcast"}},
{Action::CLIP, {"clips:edit"}},
{Action::COMMERCIAL, {"channel:edit:commercial"}},
{Action::ANNOUNCEMENT, {"moderator:manage:announcements"}},
{Action::ENABLE_EMOTE_ONLY, {"moderator:manage:chat_settings"}},
{Action::DISABLE_EMOTE_ONLY,
{"moderator:manage:chat_settings"}},
};
auto token = _token.lock();
if (!token) {
@@ -229,11 +319,21 @@ static inline void populateActionSelection(QComboBox *list)
}
}
static inline void populateAnnouncementColorSelection(QComboBox *list)
{
for (const auto &[_, name] : announcementColors) {
list->addItem(obs_module_text(name.c_str()));
}
}
MacroActionTwitchEdit::MacroActionTwitchEdit(
QWidget *parent, std::shared_ptr<MacroActionTwitch> entryData)
: QWidget(parent),
_layout(new QHBoxLayout()),
_actions(new QComboBox()),
_tokens(new TwitchConnectionSelection()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient"))),
_streamTitle(new VariableLineEdit(this)),
_category(new TwitchCategorySelection(this)),
_manualCategorySearch(new TwitchCategorySearchButton()),
@@ -241,9 +341,8 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_clipHasDelay(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.twitch.clip.hasDelay"))),
_duration(new DurationSelection(this, false, 0)),
_layout(new QHBoxLayout()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient")))
_announcementMessage(new VariableTextEdit(this)),
_announcementColor(new QComboBox(this))
{
_streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
@@ -251,16 +350,22 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_markerDescription->setMaxLength(140);
_announcementMessage->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
auto spinBox = _duration->SpinBox();
spinBox->setSuffix("s");
spinBox->setMaximum(180);
populateActionSelection(_actions);
populateAnnouncementColorSelection(_announcementColor);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(TwitchTokenChanged(const QString &)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this,
SLOT(StreamTitleChanged()));
QWidget::connect(_category,
@@ -269,26 +374,31 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
SLOT(MarkerDescriptionChanged()));
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
SLOT(HasClipDelayChanged(const Duration &)));
SLOT(HasClipDelayChanged(int)));
QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(DurationChanged(const Duration &)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
QWidget::connect(_announcementMessage, SIGNAL(textChanged()), this,
SLOT(AnnouncementMessageChanged()));
QWidget::connect(_announcementColor, SIGNAL(currentIndexChanged(int)),
this, SLOT(AnnouncementColorChanged(int)));
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.twitch.entry"),
_layout,
{{"{{account}}", _tokens},
{"{{actions}}", _actions},
{"{{streamTitle}}", _streamTitle},
{"{{category}}", _category},
{"{{manualCategorySearch}}", _manualCategorySearch},
{"{{markerDescription}}", _markerDescription},
{"{{clipHasDelay}}", _clipHasDelay},
{"{{duration}}", _duration}});
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.twitch.entry.line1"),
_layout,
{{"{{account}}", _tokens},
{"{{actions}}", _actions},
{"{{streamTitle}}", _streamTitle},
{"{{category}}", _category},
{"{{manualCategorySearch}}", _manualCategorySearch},
{"{{markerDescription}}", _markerDescription},
{"{{clipHasDelay}}", _clipHasDelay},
{"{{duration}}", _duration},
{"{{announcementColor}}", _announcementColor}});
_layout->setContentsMargins(0, 0, 0, 0);
auto mainLayout = new QVBoxLayout();
mainLayout->addLayout(_layout);
mainLayout->addWidget(_announcementMessage);
mainLayout->addWidget(_tokenPermissionWarning);
setLayout(mainLayout);
@@ -364,6 +474,31 @@ void MacroActionTwitchEdit::DurationChanged(const Duration &duration)
_entryData->_duration = duration;
}
void MacroActionTwitchEdit::AnnouncementMessageChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_announcementMessage =
_announcementMessage->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::AnnouncementColorChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_announcementColor =
static_cast<MacroActionTwitch::AnnouncementColor>(index);
}
void MacroActionTwitchEdit::CheckTokenPermissions()
{
_tokenPermissionWarning->setVisible(
@@ -386,6 +521,10 @@ void MacroActionTwitchEdit::SetupWidgetVisibility()
MacroActionTwitch::Action::CLIP);
_duration->setVisible(_entryData->_action ==
MacroActionTwitch::Action::COMMERCIAL);
_announcementMessage->setVisible(
_entryData->_action == MacroActionTwitch::Action::ANNOUNCEMENT);
_announcementColor->setVisible(_entryData->_action ==
MacroActionTwitch::Action::ANNOUNCEMENT);
if (_entryData->_action == MacroActionTwitch::Action::TITLE ||
_entryData->_action == MacroActionTwitch::Action::MARKER) {
@@ -416,6 +555,10 @@ void MacroActionTwitchEdit::UpdateEntryData()
_markerDescription->setText(_entryData->_markerDescription);
_clipHasDelay->setChecked(_entryData->_clipHasDelay);
_duration->SetDuration(_entryData->_duration);
_announcementMessage->setPlainText(_entryData->_announcementMessage);
_announcementColor->setCurrentIndex(
static_cast<int>(_entryData->_announcementColor));
SetupWidgetVisibility();
}

View File

@@ -4,6 +4,7 @@
#include "category-selection.hpp"
#include <variable-line-edit.hpp>
#include <variable-text-edit.hpp>
#include <duration-control.hpp>
namespace advss {
@@ -29,6 +30,17 @@ public:
MARKER,
CLIP,
COMMERCIAL,
ANNOUNCEMENT,
ENABLE_EMOTE_ONLY,
DISABLE_EMOTE_ONLY,
};
enum class AnnouncementColor {
PRIMARY,
BLUE,
GREEN,
ORANGE,
PURPLE,
};
Action _action = Action::TITLE;
@@ -40,6 +52,9 @@ public:
"AdvSceneSwitcher.action.twitch.marker.description");
bool _clipHasDelay = false;
Duration _duration = 60;
StringVariable _announcementMessage = obs_module_text(
"AdvSceneSwitcher.action.twitch.announcement.message");
AnnouncementColor _announcementColor = AnnouncementColor::PRIMARY;
private:
void SetStreamTitle(const std::shared_ptr<TwitchToken> &) const;
@@ -47,6 +62,9 @@ private:
void CreateStreamMarker(const std::shared_ptr<TwitchToken> &) const;
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
void SendChatAnnouncement(const std::shared_ptr<TwitchToken> &) const;
void SetChatEmoteOnlyMode(const std::shared_ptr<TwitchToken> &,
bool enable) const;
static bool _registered;
static const std::string id;
@@ -71,12 +89,14 @@ public:
private slots:
void ActionChanged(int);
void TwitchTokenChanged(const QString &);
void CheckTokenPermissions();
void StreamTitleChanged();
void CategoreyChanged(const TwitchCategory &);
void MarkerDescriptionChanged();
void ClipHasDelayChanged(int state);
void DurationChanged(const Duration &);
void CheckTokenPermissions();
void AnnouncementMessageChanged();
void AnnouncementColorChanged(int index);
signals:
void HeaderInfoChanged(const QString &);
@@ -87,17 +107,21 @@ protected:
private:
void SetupWidgetVisibility();
QHBoxLayout *_layout;
QComboBox *_actions;
TwitchConnectionSelection *_tokens;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
VariableLineEdit *_streamTitle;
TwitchCategorySelection *_category;
TwitchCategorySearchButton *_manualCategorySearch;
VariableLineEdit *_markerDescription;
QCheckBox *_clipHasDelay;
DurationSelection *_duration;
QHBoxLayout *_layout;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
VariableTextEdit *_announcementMessage;
QComboBox *_announcementColor;
bool _loading = true;
};

View File

@@ -22,10 +22,13 @@ const std::unordered_map<std::string, std::string> TokenOption::apiIdToLocale{
{"channel:manage:broadcast",
"AdvSceneSwitcher.twitchToken.channel.manageBroadcast"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.channel.createClip"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.clips.edit"},
{"channel:edit:commercial",
"AdvSceneSwitcher.twitchToken.channel.startCommercial"},
};
{"moderator:manage:announcements",
"AdvSceneSwitcher.twitchToken.moderator.manageAnnouncements"},
{"moderator:manage:chat_settings",
"AdvSceneSwitcher.twitchToken.moderator.manageChatSettings"}};
static void saveConnections(obs_data_t *obj);
static void loadConnections(obs_data_t *obj);