Compare commits

..

13 Commits

Author SHA1 Message Date
WarmUpTill
20488afdd1 Fix tempvars being reset across macros for cached widgets
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2025-06-12 21:29:07 +02:00
WarmUpTill
d9d387ad47 Fix crash when deleting cached macro widgets 2025-06-12 21:29:07 +02:00
WarmUpTill
0736d673e1 Add option to check if chat was cleared / message was removed 2025-06-12 21:29:07 +02:00
WarmUpTill
34151e4bc6 Fix "Get user info" only allowing small values for user ids 2025-06-12 21:29:07 +02:00
WarmUpTill
1346c19bec Fix crash when invalid token is used to connect to Twitch chat 2025-06-12 21:29:07 +02:00
WarmUpTill
be6bc48231 Fix macro selection resetting when any macro was removed 2025-06-12 18:33:24 +02:00
WarmUpTill
79a8ad57af Rework HTTP action URL input
The URL to be accessed can now be passed as a single URL instead of it
being split into a host and path component
2025-06-12 18:33:24 +02:00
WarmUpTill
daeb9275a3 Add support for inline scripts
* Script can be defined in the macro segment or loaded from a file
* Supports both LUA and Python
* Can be used for actions and conditions
* obs_script_create and obs_script_destroy are resolved at runtime
  (Let's hope the API remains stable)
2025-06-09 14:20:44 +02:00
WarmUpTill
73b542a4db Move scripting support to seperate project 2025-06-09 14:20:44 +02:00
WarmUpTill
e3471066e9 Export symbols 2025-06-09 14:20:44 +02:00
WarmUpTill
ec41c06b4d Update feature request description 2025-06-09 14:20:44 +02:00
WarmUpTill
201e45d058 Fix macro tree not resizing properly with OBS 31.1.0 2025-06-07 18:56:42 +02:00
WarmUpTill
b17aa30432 Fix attempting to cache widgets of about to be deleted macros 2025-06-06 19:14:46 +02:00
50 changed files with 1377 additions and 182 deletions

View File

@@ -2,7 +2,7 @@
# https://github.com/obsproject/.github/tree/master/.github/ISSUE_TEMPLATE # https://github.com/obsproject/.github/tree/master/.github/ISSUE_TEMPLATE
name: Feature request name: Feature request
description: Suggest an idea for this projectReport a bug or crash description: Suggest an idea for this project
body: body:
- type: markdown - type: markdown
id: md_welcome id: md_welcome

View File

@@ -103,8 +103,6 @@ target_sources(
lib/macro/macro-action-macro.hpp lib/macro/macro-action-macro.hpp
lib/macro/macro-action-queue.cpp lib/macro/macro-action-queue.cpp
lib/macro/macro-action-queue.hpp lib/macro/macro-action-queue.hpp
lib/macro/macro-action-script.cpp
lib/macro/macro-action-script.hpp
lib/macro/macro-action-variable.cpp lib/macro/macro-action-variable.cpp
lib/macro/macro-action-variable.hpp lib/macro/macro-action-variable.hpp
lib/macro/macro-action.cpp lib/macro/macro-action.cpp
@@ -117,8 +115,6 @@ target_sources(
lib/macro/macro-condition-macro.hpp lib/macro/macro-condition-macro.hpp
lib/macro/macro-condition-queue.cpp lib/macro/macro-condition-queue.cpp
lib/macro/macro-condition-queue.hpp lib/macro/macro-condition-queue.hpp
lib/macro/macro-condition-script.cpp
lib/macro/macro-condition-script.hpp
lib/macro/macro-condition-tempvar.cpp lib/macro/macro-condition-tempvar.cpp
lib/macro/macro-condition-tempvar.hpp lib/macro/macro-condition-tempvar.hpp
lib/macro/macro-condition-variable.cpp lib/macro/macro-condition-variable.cpp
@@ -139,14 +135,10 @@ target_sources(
lib/macro/macro-ref.hpp lib/macro/macro-ref.hpp
lib/macro/macro-run-button.cpp lib/macro/macro-run-button.cpp
lib/macro/macro-run-button.hpp lib/macro/macro-run-button.hpp
lib/macro/macro-script-handler.cpp
lib/macro/macro-script-handler.hpp
lib/macro/macro-segment-copy-paste.cpp lib/macro/macro-segment-copy-paste.cpp
lib/macro/macro-segment-copy-paste.hpp lib/macro/macro-segment-copy-paste.hpp
lib/macro/macro-segment-list.cpp lib/macro/macro-segment-list.cpp
lib/macro/macro-segment-list.hpp lib/macro/macro-segment-list.hpp
lib/macro/macro-segment-script.cpp
lib/macro/macro-segment-script.hpp
lib/macro/macro-segment-selection.cpp lib/macro/macro-segment-selection.cpp
lib/macro/macro-segment-selection.hpp lib/macro/macro-segment-selection.hpp
lib/macro/macro-segment.cpp lib/macro/macro-segment.cpp
@@ -223,9 +215,6 @@ target_sources(
lib/utils/plugin-state-helpers.hpp lib/utils/plugin-state-helpers.hpp
lib/utils/priority-helper.cpp lib/utils/priority-helper.cpp
lib/utils/priority-helper.hpp lib/utils/priority-helper.hpp
lib/utils/properties-view.cpp
lib/utils/properties-view.hpp
lib/utils/properties-view.moc.hpp
lib/utils/regex-config.cpp lib/utils/regex-config.cpp
lib/utils/regex-config.hpp lib/utils/regex-config.hpp
lib/utils/resizing-text-edit.cpp lib/utils/resizing-text-edit.cpp

View File

@@ -730,6 +730,8 @@ AdvSceneSwitcher.condition.twitch.type.polling.channel.live="Stream is currently
AdvSceneSwitcher.condition.twitch.type.polling.channel.title="Current title matches" AdvSceneSwitcher.condition.twitch.type.polling.channel.title="Current title matches"
AdvSceneSwitcher.condition.twitch.type.polling.channel.category="Current category is" AdvSceneSwitcher.condition.twitch.type.polling.channel.category="Current category is"
AdvSceneSwitcher.condition.twitch.type.chat.message="Chat message received" AdvSceneSwitcher.condition.twitch.type.chat.message="Chat message received"
AdvSceneSwitcher.condition.twitch.type.chat.messageRemoved="Single chat message removed"
AdvSceneSwitcher.condition.twitch.type.chat.cleared="All messages of user or channel cleared"
AdvSceneSwitcher.condition.twitch.type.chat.properties="Chat message properties:" AdvSceneSwitcher.condition.twitch.type.chat.properties="Chat message properties:"
AdvSceneSwitcher.condition.twitch.type.chat.properties.select="Select chat message property:" AdvSceneSwitcher.condition.twitch.type.chat.properties.select="Select chat message property:"
AdvSceneSwitcher.condition.twitch.type.chat.properties.firstMessage="Is first message" AdvSceneSwitcher.condition.twitch.type.chat.properties.firstMessage="Is first message"
@@ -801,6 +803,7 @@ AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
AdvSceneSwitcher.condition.mqtt="MQTT" AdvSceneSwitcher.condition.mqtt="MQTT"
AdvSceneSwitcher.condition.mqtt.layout.match="Message was received from{{connection}} which matches{{regex}}:" AdvSceneSwitcher.condition.mqtt.layout.match="Message was received from{{connection}} which matches{{regex}}:"
AdvSceneSwitcher.condition.mqtt.layout.listen="Set message selection to incoming message:{{listenButton}}" AdvSceneSwitcher.condition.mqtt.layout.listen="Set message selection to incoming message:{{listenButton}}"
AdvSceneSwitcher.condition.script="Script"
# Macro Actions # Macro Actions
AdvSceneSwitcher.action.unknown="Unknown action" AdvSceneSwitcher.action.unknown="Unknown action"
@@ -1094,7 +1097,7 @@ AdvSceneSwitcher.action.http.type.post="POST"
AdvSceneSwitcher.action.http.type.put="PUT" AdvSceneSwitcher.action.http.type.put="PUT"
AdvSceneSwitcher.action.http.type.patch="PATCH" AdvSceneSwitcher.action.http.type.patch="PATCH"
AdvSceneSwitcher.action.http.type.delete="DELETE" AdvSceneSwitcher.action.http.type.delete="DELETE"
AdvSceneSwitcher.action.http.layout.method="Send{{method}}to URL{{url}}at path{{path}}" AdvSceneSwitcher.action.http.layout.method="Send{{method}}to URL{{url}}"
AdvSceneSwitcher.action.http.layout.contentType="Content type:{{contentType}}" AdvSceneSwitcher.action.http.layout.contentType="Content type:{{contentType}}"
AdvSceneSwitcher.action.http.layout.timeout="Timeout:{{timeout}}seconds" AdvSceneSwitcher.action.http.layout.timeout="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.http.entry.line1="Send{{method}}to{{url}}" AdvSceneSwitcher.action.http.entry.line1="Send{{method}}to{{url}}"
@@ -1250,6 +1253,7 @@ AdvSceneSwitcher.action.obsSetting.action.setOutputCanvasX="Set output resolutio
AdvSceneSwitcher.action.obsSetting.action.setOutputCanvasY="Set output resolution Y value" AdvSceneSwitcher.action.obsSetting.action.setOutputCanvasY="Set output resolution Y value"
AdvSceneSwitcher.action.obsSetting.getCurrentValue="Get current value" AdvSceneSwitcher.action.obsSetting.getCurrentValue="Get current value"
AdvSceneSwitcher.action.obsSettings.layout="{{actions}}{{fpsType}}{{fpsIntValue}}{{fpsStringValue}}{{canvasSizeValue}}{{getCurrentValue}}" AdvSceneSwitcher.action.obsSettings.layout="{{actions}}{{fpsType}}{{fpsIntValue}}{{fpsStringValue}}{{canvasSizeValue}}{{getCurrentValue}}"
AdvSceneSwitcher.action.script="Script"
# Hotkey # Hotkey
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher" AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher"
@@ -1514,6 +1518,17 @@ AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noPermission="Can't sele
AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noChannel="Can't select points reward without entering a channel first!" AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noChannel="Can't select points reward without entering a channel first!"
AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error="Can't select points reward because Twitch responded with an error! Check OBS logs for more details." AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error="Can't select points reward because Twitch responded with an error! Check OBS logs for more details."
AdvSceneSwitcher.script.type.inline="Inline"
AdvSceneSwitcher.script.type.file="File"
AdvSceneSwitcher.script.type.layout="Script type:{{scriptType}}"
AdvSceneSwitcher.script.language.python="Python"
AdvSceneSwitcher.script.language.lua="LUA"
AdvSceneSwitcher.script.language.select="--select language--"
AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
AdvSceneSwitcher.script.file.open="Open"
AdvSceneSwitcher.script.file.open.failed="Could not open script file!"
AdvSceneSwitcher.script.file.layout="Script file:{{path}}{{open}}"
AdvSceneSwitcher.tempVar.select="--select value--" AdvSceneSwitcher.tempVar.select="--select value--"
AdvSceneSwitcher.tempVar.selectionInfo.lastValues="Last values:" AdvSceneSwitcher.tempVar.selectionInfo.lastValues="Last values:"
@@ -1907,6 +1922,24 @@ AdvSceneSwitcher.tempVar.twitch.is_turbo.chatReceive.description="'true' if the
AdvSceneSwitcher.tempVar.twitch.is_vip.chatReceive="Is chatter a VIP" AdvSceneSwitcher.tempVar.twitch.is_vip.chatReceive="Is chatter a VIP"
AdvSceneSwitcher.tempVar.twitch.is_vip.chatReceive.description="'true' if the chatter is a VIP in the channel, 'false' otherwise." AdvSceneSwitcher.tempVar.twitch.is_vip.chatReceive.description="'true' if the chatter is a VIP in the channel, 'false' otherwise."
AdvSceneSwitcher.tempVar.twitch.login.chatRemove="User login"
AdvSceneSwitcher.tempVar.twitch.login.chatRemove.description="The name of the user who sent the message."
AdvSceneSwitcher.tempVar.twitch.message.chatRemove="Chat message"
AdvSceneSwitcher.tempVar.twitch.message_id.chatRemove.description="The chat message that was removed."
AdvSceneSwitcher.tempVar.twitch.message_id.chatRemove="Chat message ID"
AdvSceneSwitcher.tempVar.twitch.message_id.chatRemove.description="The ID of the message in UUID format."
AdvSceneSwitcher.tempVar.twitch.timestamp.chatRemove="Chat message timestamp"
AdvSceneSwitcher.tempVar.twitch.timestamp.chatRemove.description="The UNIX timestamp."
AdvSceneSwitcher.tempVar.twitch.login.chatClear="User login"
AdvSceneSwitcher.tempVar.twitch.login.chatClear.description="The login name of the user whose messages were removed from the chat room because they were banned or put in a timeout."
AdvSceneSwitcher.tempVar.twitch.user_id.chatClear="User ID"
AdvSceneSwitcher.tempVar.twitch.user_id.chatClear.description="Optional.\nThe User ID of the user that was banned or put in a timeout.\nEmpty in case the complete chat was cleared."
AdvSceneSwitcher.tempVar.twitch.ban_duration.chatClear="Ban duration"
AdvSceneSwitcher.tempVar.twitch.ban_duration.chatClear.description="Set to zero if the complete chat was cleared or a user was banned indefinitely.\nIs set to non-zero value, if a user was put in a timeout.\nWill contain the duration of the timeout in seconds."
AdvSceneSwitcher.tempVar.twitch.timestamp.chatClear="Timestamp"
AdvSceneSwitcher.tempVar.twitch.timestamp.chatClear.description="The UNIX timestamp."
AdvSceneSwitcher.tempVar.twitch.user_login.chatJoin="User login" AdvSceneSwitcher.tempVar.twitch.user_login.chatJoin="User login"
AdvSceneSwitcher.tempVar.twitch.user_login.chatJoin.description="The user login of the person who joined the chat room." AdvSceneSwitcher.tempVar.twitch.user_login.chatJoin.description="The user login of the person who joined the chat room."

View File

@@ -11,6 +11,7 @@ namespace advss {
class MacroActionEdit; class MacroActionEdit;
class MacroConditionEdit; class MacroConditionEdit;
class MacroSegment;
class Duration; class Duration;
class SequenceWidget; class SequenceWidget;
struct SceneGroup; struct SceneGroup;
@@ -192,7 +193,7 @@ signals:
void MacroRemoved(const QString &name); void MacroRemoved(const QString &name);
void MacroRenamed(const QString &oldName, const QString &newName); void MacroRenamed(const QString &oldName, const QString &newName);
void MacroSegmentOrderChanged(); void MacroSegmentOrderChanged();
void SegmentTempVarsChanged(); void SegmentTempVarsChanged(MacroSegment *);
void HighlightMacrosChanged(bool value); void HighlightMacrosChanged(bool value);
void ConnectionAdded(const QString &); void ConnectionAdded(const QString &);

View File

@@ -20,7 +20,7 @@ public:
MacroActionFactory() = delete; MacroActionFactory() = delete;
EXPORT static bool Register(const std::string &id, MacroActionInfo); EXPORT static bool Register(const std::string &id, MacroActionInfo);
static bool Deregister(const std::string &id); EXPORT static bool Deregister(const std::string &id);
static std::shared_ptr<MacroAction> Create(const std::string &id, static std::shared_ptr<MacroAction> Create(const std::string &id,
Macro *m); Macro *m);
static QWidget *CreateWidget(const std::string &id, QWidget *parent, static QWidget *CreateWidget(const std::string &id, QWidget *parent,

View File

@@ -20,7 +20,7 @@ class MacroConditionFactory {
public: public:
MacroConditionFactory() = delete; MacroConditionFactory() = delete;
EXPORT static bool Register(const std::string &, MacroConditionInfo); EXPORT static bool Register(const std::string &, MacroConditionInfo);
static bool Deregister(const std::string &); EXPORT static bool Deregister(const std::string &);
static std::shared_ptr<MacroCondition> Create(const std::string &, static std::shared_ptr<MacroCondition> Create(const std::string &,
Macro *m); Macro *m);
static QWidget *CreateWidget(const std::string &id, QWidget *parent, static QWidget *CreateWidget(const std::string &id, QWidget *parent,

View File

@@ -56,9 +56,7 @@ MacroSegmentList::~MacroSegmentList()
_autoScrollThread.join(); _autoScrollThread.join();
} }
for (const auto &[_, widgets] : _widgetCache) { ClearWidgetCache();
clearWidgetVector(widgets);
}
} }
static bool posIsInScrollbar(const QScrollBar *scrollbar, const QPoint &pos) static bool posIsInScrollbar(const QScrollBar *scrollbar, const QPoint &pos)
@@ -146,7 +144,6 @@ void MacroSegmentList::SetCachingEnabled(bool enable)
void MacroSegmentList::CacheCurrentWidgetsFor(const Macro *macro) void MacroSegmentList::CacheCurrentWidgetsFor(const Macro *macro)
{ {
if (!_useCache) { if (!_useCache) {
_widgetCache.clear();
return; return;
} }
@@ -168,7 +165,6 @@ void MacroSegmentList::CacheCurrentWidgetsFor(const Macro *macro)
bool MacroSegmentList::PopulateWidgetsFromCache(const Macro *macro) bool MacroSegmentList::PopulateWidgetsFromCache(const Macro *macro)
{ {
if (!_useCache) { if (!_useCache) {
_widgetCache.clear();
return false; return false;
} }
@@ -340,6 +336,13 @@ void MacroSegmentList::HideLastDropLine()
_dropLineIdx = -1; _dropLineIdx = -1;
} }
void MacroSegmentList::ClearWidgetCache()
{
for (const auto &[_, widgets] : _widgetCache) {
clearWidgetVector(widgets);
}
}
static bool isInUpperHalfOf(const QPoint &pos, const QRect &rect) static bool isInUpperHalfOf(const QPoint &pos, const QRect &rect)
{ {
return QRect(rect.topLeft(), return QRect(rect.topLeft(),

View File

@@ -61,6 +61,7 @@ private:
bool IsInListArea(const QPoint &) const; bool IsInListArea(const QPoint &) const;
QRect GetContentItemRectWithPadding(int idx) const; QRect GetContentItemRectWithPadding(int idx) const;
void HideLastDropLine(); void HideLastDropLine();
void ClearWidgetCache();
int _dragPosition = -1; int _dragPosition = -1;
int _dropLineIdx = -1; int _dropLineIdx = -1;

View File

@@ -107,7 +107,7 @@ void MacroSegment::SetupTempVars()
void MacroSegment::ClearAvailableTempvars() void MacroSegment::ClearAvailableTempvars()
{ {
_tempVariables.clear(); _tempVariables.clear();
NotifyUIAboutTempVarChange(); NotifyUIAboutTempVarChange(this);
} }
static std::shared_ptr<MacroSegment> static std::shared_ptr<MacroSegment>
@@ -149,7 +149,7 @@ void MacroSegment::AddTempvar(const std::string &id, const std::string &name,
TempVariable var(id, name, description, sharedSegment); TempVariable var(id, name, description, sharedSegment);
_tempVariables.emplace_back(std::move(var)); _tempVariables.emplace_back(std::move(var));
NotifyUIAboutTempVarChange(); NotifyUIAboutTempVarChange(this);
} }
void MacroSegment::SetTempVarValue(const std::string &id, void MacroSegment::SetTempVarValue(const std::string &id,

View File

@@ -76,12 +76,16 @@ void MacroSelection::ShowAllMacros()
void MacroSelection::MacroRemove(const QString &name) void MacroSelection::MacroRemove(const QString &name)
{ {
const bool currentSelectionWasRemoved = name == currentText();
int idx = findText(name); int idx = findText(name);
if (idx == -1) { if (idx == -1) {
return; return;
} }
removeItem(idx); removeItem(idx);
setCurrentIndex(-1); if (currentSelectionWasRemoved) {
setCurrentIndex(-1);
}
} }
void MacroSelection::MacroRename(const QString &oldName, const QString &newName) void MacroSelection::MacroRename(const QString &oldName, const QString &newName)

View File

@@ -207,13 +207,11 @@ void AdvSceneSwitcher::RemoveMacro(std::shared_ptr<Macro> &macro)
clearWidgetCache(macro.get()); clearWidgetCache(macro.get());
} }
// Currently shown macro will always be part of the macros to be // Don't cache widgets for about to be deleted macros
// removed, so, we clear the widgets as they should not be cached MacroSegmentList::SetCachingEnabled(false);
ui->conditionsList->Clear();
ui->actionsList->Clear();
ui->elseActionsList->Clear();
ui->macros->Remove(macro); ui->macros->Remove(macro);
MacroSegmentList::SetCachingEnabled(!switcher->disableMacroWidgetCache);
emit MacroRemoved(name); emit MacroRemoved(name);
} }

View File

@@ -1301,10 +1301,10 @@ QSize MacroTreeDelegate::sizeHint(const QStyleOptionViewItem &option,
QWidget *item = tree->indexWidget(index); QWidget *item = tree->indexWidget(index);
if (!item) { if (!item) {
return (QSize(0, 0)); return QStyledItemDelegate::sizeHint(option, index);
} }
return (QSize(option.widget->minimumWidth(), item->height())); return QSize(item->sizeHint());
} }
} // namespace advss } // namespace advss

View File

@@ -7,7 +7,7 @@
namespace advss { namespace advss {
class SliderIgnoreScroll : public QSlider { class ADVSS_EXPORT SliderIgnoreScroll : public QSlider {
Q_OBJECT Q_OBJECT
public: public:

View File

@@ -9,15 +9,14 @@
namespace advss { namespace advss {
class DurationSelection : public QWidget { class ADVSS_EXPORT DurationSelection : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
EXPORT DurationSelection(QWidget *parent = nullptr, DurationSelection(QWidget *parent = nullptr,
bool showUnitSelection = true, bool showUnitSelection = true, double minValue = 0.0);
double minValue = 0.0); void SetDuration(const Duration &);
EXPORT void SetDuration(const Duration &);
Duration GetDuration() const { return _current; } Duration GetDuration() const { return _current; }
EXPORT QDoubleSpinBox *SpinBox() { return _duration->SpinBox(); } QDoubleSpinBox *SpinBox() { return _duration->SpinBox(); }
private slots: private slots:
void _DurationChanged(const NumberVariable<double> &value); void _DurationChanged(const NumberVariable<double> &value);

View File

@@ -25,8 +25,8 @@ EXPORT void StopPlugin();
EXPORT void StartPlugin(); EXPORT void StartPlugin();
EXPORT bool PluginIsRunning(); EXPORT bool PluginIsRunning();
EXPORT int GetIntervalValue(); EXPORT int GetIntervalValue();
void AddStartStep(std::function<void()>); EXPORT void AddStartStep(std::function<void()>);
void AddStopStep(std::function<void()>); EXPORT void AddStopStep(std::function<void()>);
void RunStartSteps(); void RunStartSteps();
void RunStopSteps(); void RunStopSteps();
void RunIntervalResetSteps(); void RunIntervalResetSteps();

View File

@@ -5,15 +5,15 @@
namespace advss { namespace advss {
class ResizingPlainTextEdit : public QPlainTextEdit { class ADVSS_EXPORT ResizingPlainTextEdit : public QPlainTextEdit {
Q_OBJECT Q_OBJECT
public: public:
ResizingPlainTextEdit(QWidget *parent, const int scrollAt = 10, ResizingPlainTextEdit(QWidget *parent, const int scrollAt = 10,
const int minLines = 3, const int minLines = 3,
const int paddingLines = 2); const int paddingLines = 2);
virtual ~ResizingPlainTextEdit(){}; virtual ~ResizingPlainTextEdit(){};
EXPORT int maxLength(); int maxLength();
EXPORT void setMaxLength(int maxLength); void setMaxLength(int maxLength);
protected: protected:
bool eventFilter(QObject *obj, QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;

View File

@@ -274,8 +274,9 @@ TempVariableSelection::TempVariableSelection(QWidget *parent)
SLOT(HighlightChanged(int))); SLOT(HighlightChanged(int)));
QWidget::connect(window(), SIGNAL(MacroSegmentOrderChanged()), this, QWidget::connect(window(), SIGNAL(MacroSegmentOrderChanged()), this,
SLOT(MacroSegmentsChanged())); SLOT(MacroSegmentsChanged()));
QWidget::connect(window(), SIGNAL(SegmentTempVarsChanged()), this, QWidget::connect(window(),
SLOT(SegmentTempVarsChanged())); SIGNAL(SegmentTempVarsChanged(MacroSegment *)), this,
SLOT(SegmentTempVarsChanged(MacroSegment *)));
auto layout = new QHBoxLayout(); auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
@@ -314,9 +315,15 @@ void TempVariableSelection::MacroSegmentsChanged()
SetVariable(currentSelection); SetVariable(currentSelection);
} }
void TempVariableSelection::SegmentTempVarsChanged() void TempVariableSelection::SegmentTempVarsChanged(MacroSegment *segment)
{ {
MacroSegmentsChanged(); const auto currentSegment = GetSegment();
const auto currentMacro = currentSegment ? currentSegment->GetMacro()
: nullptr;
const auto changeMacro = segment ? segment->GetMacro() : nullptr;
if (currentMacro == changeMacro) {
MacroSegmentsChanged();
}
} }
void TempVariableSelection::HighlightChanged(int idx) void TempVariableSelection::HighlightChanged(int idx)
@@ -427,13 +434,11 @@ QString TempVariableSelection::SetupInfoLabel()
MacroSegment *TempVariableSelection::GetSegment() const MacroSegment *TempVariableSelection::GetSegment() const
{ {
const QWidget *widget = this; const QWidget *widget = this;
{ while (widget) {
while (widget) { if (qobject_cast<const MacroSegmentEdit *>(widget)) {
if (qobject_cast<const MacroSegmentEdit *>(widget)) { break;
break;
}
widget = widget->parentWidget();
} }
widget = widget->parentWidget();
} }
if (!widget) { if (!widget) {
return nullptr; return nullptr;
@@ -442,17 +447,18 @@ MacroSegment *TempVariableSelection::GetSegment() const
return segmentWidget->Data().get(); return segmentWidget->Data().get();
} }
void NotifyUIAboutTempVarChange() void NotifyUIAboutTempVarChange(MacroSegment *segment)
{ {
obs_queue_task( obs_queue_task(
OBS_TASK_UI, OBS_TASK_UI,
[](void *) { [](void *segment) {
if (!SettingsWindowIsOpened()) { if (!SettingsWindowIsOpened()) {
return; return;
} }
AdvSceneSwitcher::window->SegmentTempVarsChanged(); AdvSceneSwitcher::window->SegmentTempVarsChanged(
(MacroSegment *)segment);
}, },
nullptr, false); segment, false);
} }
} // namespace advss } // namespace advss

View File

@@ -88,7 +88,7 @@ public:
private slots: private slots:
void SelectionIdxChanged(int); void SelectionIdxChanged(int);
void MacroSegmentsChanged(); void MacroSegmentsChanged();
void SegmentTempVarsChanged(); void SegmentTempVarsChanged(MacroSegment *);
void HighlightChanged(int); void HighlightChanged(int);
signals: signals:
@@ -104,6 +104,6 @@ private:
AutoUpdateTooltipLabel *_info; AutoUpdateTooltipLabel *_info;
}; };
void NotifyUIAboutTempVarChange(); void NotifyUIAboutTempVarChange(MacroSegment *);
} // namespace advss } // namespace advss

View File

@@ -4,15 +4,15 @@
namespace advss { namespace advss {
class VariableTextEdit : public ResizingPlainTextEdit { class ADVSS_EXPORT VariableTextEdit : public ResizingPlainTextEdit {
Q_OBJECT Q_OBJECT
public: public:
EXPORT VariableTextEdit(QWidget *parent, const int scrollAt = 10, VariableTextEdit(QWidget *parent, const int scrollAt = 10,
const int minLines = 3, const int minLines = 3, const int paddingLines = 2);
const int paddingLines = 2); virtual ~VariableTextEdit(){};
EXPORT void setPlainText(const QString &); void setPlainText(const QString &);
EXPORT void setPlainText(const StringVariable &); void setPlainText(const StringVariable &);
EXPORT void setToolTip(const QString &string); void setToolTip(const QString &string);
private: private:
}; };

View File

@@ -32,6 +32,7 @@ add_plugin(http)
add_plugin(midi) add_plugin(midi)
add_plugin(mqtt) add_plugin(mqtt)
add_plugin(openvr) add_plugin(openvr)
add_plugin(scripting)
add_plugin(stream-deck) add_plugin(stream-deck)
add_plugin(twitch) add_plugin(twitch)
add_plugin(usb) add_plugin(usb)

View File

@@ -65,20 +65,47 @@ void MacroActionHttp::SetupTempVars()
"AdvSceneSwitcher.tempVar.http.error.description")); "AdvSceneSwitcher.tempVar.http.error.description"));
} }
struct URLInfo {
std::string host;
std::string path;
};
static URLInfo getURLInfo(const std::string &input)
{
if (input.empty()) {
return {};
}
QString urlInput = QString::fromStdString(input);
if (!urlInput.contains("://")) {
urlInput = "http://" + urlInput;
}
const QUrl url(urlInput);
auto host =
url.scheme().toStdString() + "://" + url.host().toStdString();
int port = url.port();
if (port != -1) {
host += ":" + std::to_string(port);
}
auto path = url.path().toStdString();
if (path.empty()) {
path = "/";
}
return {host, path};
}
bool MacroActionHttp::PerformAction() bool MacroActionHttp::PerformAction()
{ {
httplib::Client cli(_url); const auto [host, path] = getURLInfo(_url);
httplib::Client cli(host);
setTimeout(cli, _timeout); setTimeout(cli, _timeout);
const auto params = _setParams ? getParams(_params) : httplib::Params(); const auto params = _setParams ? getParams(_params) : httplib::Params();
const auto headers = _setHeaders ? getHeaders(_headers) const auto headers = _setHeaders ? getHeaders(_headers)
: httplib::Headers(); : httplib::Headers();
// This copy is necessary for some reason for MacOS but not Linux or
// Windows.
// Without it all requests fail with "Invalid argument" or
// "Failed to write connection" when sending out the data.
std::string path = _path;
httplib::Result response; httplib::Result response;
switch (_method) { switch (_method) {
case MacroActionHttp::Method::GET: case MacroActionHttp::Method::GET:
@@ -167,13 +194,12 @@ void MacroActionHttp::LogAction() const
ablog(LOG_INFO, ablog(LOG_INFO,
"sent HTTP request (%s) " "sent HTTP request (%s) "
"to URL \"%s\" " "to URL \"%s\" "
"to path \"%s\" "
"with content type \"%s\" " "with content type \"%s\" "
"with body \"%s\" " "with body \"%s\" "
"with headers \"%s\" " "with headers \"%s\" "
"with parameters \"%s\" " "with parameters \"%s\" "
"with timeout \"%s\"", "with timeout \"%s\"",
methodToString(_method).data(), _url.c_str(), _path.c_str(), methodToString(_method).data(), _url.c_str(),
_contentType.c_str(), _body.c_str(), _contentType.c_str(), _body.c_str(),
_setHeaders ? stringListToString(_headers).c_str() : "-", _setHeaders ? stringListToString(_headers).c_str() : "-",
_setParams ? stringListToString(_params).c_str() : "-", _setParams ? stringListToString(_params).c_str() : "-",
@@ -184,7 +210,6 @@ bool MacroActionHttp::Save(obs_data_t *obj) const
{ {
MacroAction::Save(obj); MacroAction::Save(obj);
_url.Save(obj, "url"); _url.Save(obj, "url");
_path.Save(obj, "path");
_contentType.Save(obj, "contentType"); _contentType.Save(obj, "contentType");
_body.Save(obj, "body"); _body.Save(obj, "body");
obs_data_set_bool(obj, "setHeaders", _setHeaders); obs_data_set_bool(obj, "setHeaders", _setHeaders);
@@ -200,7 +225,11 @@ bool MacroActionHttp::Load(obs_data_t *obj)
{ {
MacroAction::Load(obj); MacroAction::Load(obj);
_url.Load(obj, "url"); _url.Load(obj, "url");
_path.Load(obj, "path"); if (obs_data_has_user_value(obj, "path")) {
std::string url = _url.UnresolvedValue();
url += obs_data_get_string(obj, "path");
_url = url;
}
_contentType.Load(obj, "contentType"); _contentType.Load(obj, "contentType");
_body.Load(obj, "body"); _body.Load(obj, "body");
_setHeaders = obs_data_get_bool(obj, "setHeaders"); _setHeaders = obs_data_get_bool(obj, "setHeaders");
@@ -230,7 +259,6 @@ std::shared_ptr<MacroAction> MacroActionHttp::Copy() const
void MacroActionHttp::ResolveVariablesToFixedValues() void MacroActionHttp::ResolveVariablesToFixedValues()
{ {
_url.ResolveVariables(); _url.ResolveVariables();
_path.ResolveVariables();
_contentType.ResolveVariables(); _contentType.ResolveVariables();
_body.ResolveVariables(); _body.ResolveVariables();
_headers.ResolveVariables(); _headers.ResolveVariables();
@@ -263,7 +291,6 @@ MacroActionHttpEdit::MacroActionHttpEdit(
QWidget *parent, std::shared_ptr<MacroActionHttp> entryData) QWidget *parent, std::shared_ptr<MacroActionHttp> entryData)
: QWidget(parent), : QWidget(parent),
_url(new VariableLineEdit(this)), _url(new VariableLineEdit(this)),
_path(new VariableLineEdit(this)),
_contentType(new VariableLineEdit(this)), _contentType(new VariableLineEdit(this)),
_contentTypeLayout(new QHBoxLayout()), _contentTypeLayout(new QHBoxLayout()),
_methods(new QComboBox()), _methods(new QComboBox()),
@@ -306,7 +333,6 @@ void MacroActionHttpEdit::UpdateEntryData()
} }
_url->setText(_entryData->_url); _url->setText(_entryData->_url);
_path->setText(_entryData->_path);
_contentType->setText(_entryData->_contentType); _contentType->setText(_entryData->_contentType);
_body->setPlainText(_entryData->_body); _body->setPlainText(_entryData->_body);
_setHeaders->setChecked(_entryData->_setHeaders); _setHeaders->setChecked(_entryData->_setHeaders);
@@ -326,12 +352,6 @@ void MacroActionHttpEdit::URLChanged()
emit(HeaderInfoChanged(_url->text())); emit(HeaderInfoChanged(_url->text()));
} }
void MacroActionHttpEdit::PathChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_path = _path->text().toStdString();
}
void MacroActionHttpEdit::ContentTypeChanged() void MacroActionHttpEdit::ContentTypeChanged()
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
@@ -395,8 +415,6 @@ void MacroActionHttpEdit::SetWidgetSignalConnections()
{ {
QWidget::connect(_url, SIGNAL(editingFinished()), this, QWidget::connect(_url, SIGNAL(editingFinished()), this,
SLOT(URLChanged())); SLOT(URLChanged()));
QWidget::connect(_path, SIGNAL(editingFinished()), this,
SLOT(PathChanged()));
QWidget::connect(_contentType, SIGNAL(editingFinished()), this, QWidget::connect(_contentType, SIGNAL(editingFinished()), this,
SLOT(ContentTypeChanged())); SLOT(ContentTypeChanged()));
QWidget::connect(_body, SIGNAL(textChanged()), this, QWidget::connect(_body, SIGNAL(textChanged()), this,
@@ -420,18 +438,15 @@ void MacroActionHttpEdit::SetWidgetSignalConnections()
void MacroActionHttpEdit::SetWidgetLayout() void MacroActionHttpEdit::SetWidgetLayout()
{ {
const std::unordered_map<std::string, QWidget *> widgets = { const std::unordered_map<std::string, QWidget *> widgets = {
{"{{url}}", _url}, {"{{url}}", _url}, {"{{contentType}}", _contentType},
{"{{path}}", _path}, {"{{method}}", _methods}, {"{{body}}", _body},
{"{{contentType}}", _contentType},
{"{{method}}", _methods},
{"{{body}}", _body},
{"{{timeout}}", _timeout}, {"{{timeout}}", _timeout},
}; };
auto actionLayout = new QHBoxLayout; auto actionLayout = new QHBoxLayout;
PlaceWidgets( PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.http.layout.method"), obs_module_text("AdvSceneSwitcher.action.http.layout.method"),
actionLayout, widgets); actionLayout, widgets, false);
PlaceWidgets(obs_module_text( PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.http.layout.contentType"), "AdvSceneSwitcher.action.http.layout.contentType"),

View File

@@ -33,7 +33,6 @@ public:
}; };
StringVariable _url = "127.0.0.1:8080"; StringVariable _url = "127.0.0.1:8080";
StringVariable _path = "/";
StringVariable _body = obs_module_text("AdvSceneSwitcher.enterText"); StringVariable _body = obs_module_text("AdvSceneSwitcher.enterText");
StringVariable _contentType = "application/json"; StringVariable _contentType = "application/json";
bool _setHeaders = false; bool _setHeaders = false;
@@ -68,7 +67,6 @@ public:
private slots: private slots:
void URLChanged(); void URLChanged();
void PathChanged();
void BodyChanged(); void BodyChanged();
void ContentTypeChanged(); void ContentTypeChanged();
void MethodChanged(int); void MethodChanged(int);
@@ -86,7 +84,6 @@ private:
void SetWidgetVisibility(); void SetWidgetVisibility();
VariableLineEdit *_url; VariableLineEdit *_url;
VariableLineEdit *_path;
VariableLineEdit *_contentType; VariableLineEdit *_contentType;
QHBoxLayout *_contentTypeLayout; QHBoxLayout *_contentTypeLayout;
QComboBox *_methods; QComboBox *_methods;

View File

@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.14)
project(advanced-scene-switcher-scripting)
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
target_sources(
${PROJECT_NAME}
PRIVATE macro-action-script.cpp
macro-action-script.hpp
macro-action-script-inline.cpp
macro-action-script-inline.hpp
macro-condition-script.cpp
macro-condition-script.hpp
macro-condition-script-inline.cpp
macro-condition-script-inline.hpp
macro-script-handler.cpp
macro-script-handler.hpp
macro-segment-script.cpp
macro-segment-script.hpp
macro-segment-script-inline.cpp
macro-segment-script-inline.hpp)
target_sources(
${PROJECT_NAME}
PRIVATE utils/inline-script.cpp
utils/inline-script.hpp
utils/obs-script-helpers.cpp
utils/obs-script-helpers.hpp
utils/properties-view.cpp
utils/properties-view.hpp
utils/properties-view.moc.hpp)
target_include_directories(${PROJECT_NAME}
PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/utils")
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
install_advss_plugin(${PROJECT_NAME})

View File

@@ -0,0 +1,66 @@
#include "macro-action-script-inline.hpp"
namespace advss {
const std::string MacroActionScriptInline::_id = "script";
bool MacroActionScriptInline::_registered = MacroActionFactory::Register(
MacroActionScriptInline::_id,
{MacroActionScriptInline::Create, MacroActionScriptInlineEdit::Create,
"AdvSceneSwitcher.action.script"});
bool MacroActionScriptInline::PerformAction()
{
return _script.Run();
}
void MacroActionScriptInline::LogAction() const
{
ablog(LOG_INFO, "performing inline script action");
}
bool MacroActionScriptInline::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_script.Save(obj);
return true;
}
bool MacroActionScriptInline::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_script.Load(obj);
return true;
}
std::shared_ptr<MacroAction> MacroActionScriptInline::Create(Macro *m)
{
return std::make_shared<MacroActionScriptInline>(m);
}
std::shared_ptr<MacroAction> MacroActionScriptInline::Copy() const
{
return std::make_shared<MacroActionScriptInline>(*this);
}
void MacroActionScriptInline::ResolveVariablesToFixedValues()
{
_script.ResolveVariablesToFixedValues();
}
MacroActionScriptInlineEdit::MacroActionScriptInlineEdit(
QWidget *parent, std::shared_ptr<MacroActionScriptInline> entryData)
: MacroSegmentScriptInlineEdit(parent, entryData)
{
}
QWidget *
MacroActionScriptInlineEdit::Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionScriptInlineEdit(
parent,
std::dynamic_pointer_cast<MacroActionScriptInline>(action));
}
} // namespace advss

View File

@@ -0,0 +1,39 @@
#pragma once
#include "macro-action-edit.hpp"
#include "macro-segment-script-inline.hpp"
namespace advss {
class MacroActionScriptInline : public MacroAction,
public MacroSegmentScriptInline {
public:
MacroActionScriptInline(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return _id; };
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
private:
static bool _registered;
static const std::string _id;
};
class MacroActionScriptInlineEdit : public MacroSegmentScriptInlineEdit {
Q_OBJECT
public:
MacroActionScriptInlineEdit(
QWidget *, std::shared_ptr<MacroActionScriptInline> = nullptr);
static QWidget *Create(QWidget *, std::shared_ptr<MacroAction>);
};
} // namespace advss

View File

@@ -1,7 +1,6 @@
#include "macro-action-script.hpp" #include "macro-action-script.hpp"
#include "layout-helpers.hpp" #include "layout-helpers.hpp"
#include "macro-helpers.hpp" #include "macro-helpers.hpp"
#include "properties-view.hpp"
#include "sync-helpers.hpp" #include "sync-helpers.hpp"
namespace advss { namespace advss {

View File

@@ -0,0 +1,51 @@
#include "macro-condition-script-inline.hpp"
namespace advss {
const std::string MacroConditionScriptInline::_id = "script";
bool MacroConditionScriptInline::_registered =
MacroConditionFactory::Register(MacroConditionScriptInline::_id,
{MacroConditionScriptInline::Create,
MacroConditionScriptInlineEdit::Create,
"AdvSceneSwitcher.condition.script"});
bool MacroConditionScriptInline::CheckCondition()
{
return _script.Run();
}
bool MacroConditionScriptInline::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
_script.Save(obj);
return true;
}
bool MacroConditionScriptInline::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_script.Load(obj);
return true;
}
std::shared_ptr<MacroCondition> MacroConditionScriptInline::Create(Macro *m)
{
return std::make_shared<MacroConditionScriptInline>(m);
}
MacroConditionScriptInlineEdit::MacroConditionScriptInlineEdit(
QWidget *parent, std::shared_ptr<MacroConditionScriptInline> entryData)
: MacroSegmentScriptInlineEdit(parent, entryData)
{
}
QWidget *MacroConditionScriptInlineEdit::Create(
QWidget *parent, std::shared_ptr<MacroCondition> condition)
{
return new MacroConditionScriptInlineEdit(
parent, std::dynamic_pointer_cast<MacroConditionScriptInline>(
condition));
}
} // namespace advss

View File

@@ -0,0 +1,36 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "macro-segment-script-inline.hpp"
namespace advss {
class MacroConditionScriptInline : public MacroCondition,
public MacroSegmentScriptInline {
public:
MacroConditionScriptInline(Macro *m) : MacroCondition(m) {}
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return _id; };
static std::shared_ptr<MacroCondition> Create(Macro *m);
private:
static bool _registered;
static const std::string _id;
};
class MacroConditionScriptInlineEdit : public MacroSegmentScriptInlineEdit {
Q_OBJECT
public:
MacroConditionScriptInlineEdit(
QWidget *,
std::shared_ptr<MacroConditionScriptInline> = nullptr);
static QWidget *Create(QWidget *, std::shared_ptr<MacroCondition>);
};
} // namespace advss

View File

@@ -0,0 +1,198 @@
#include "macro-segment-script-inline.hpp"
#include "layout-helpers.hpp"
#include "obs-module-helper.hpp"
#include "sync-helpers.hpp"
#include "ui-helpers.hpp"
#include <QDesktopServices>
namespace advss {
void MacroSegmentScriptInline::SetType(InlineScript::Type type)
{
_script.SetType(type);
}
void MacroSegmentScriptInline::SetLanguage(obs_script_lang language)
{
_script.SetLanguage(language);
}
void MacroSegmentScriptInline::SetScript(const std::string &text)
{
_script.SetText(text);
}
void MacroSegmentScriptInline::SetPath(const std::string &path)
{
_script.SetPath(path);
}
static void populateLanguageSelection(QComboBox *list)
{
list->addItem(
obs_module_text("AdvSceneSwitcher.script.language.python"),
OBS_SCRIPT_LANG_PYTHON);
list->addItem(obs_module_text("AdvSceneSwitcher.script.language.lua"),
OBS_SCRIPT_LANG_LUA);
list->setPlaceholderText(
obs_module_text("AdvSceneSwitcher.script.language.select"));
}
static void populateScriptTypeSelection(QComboBox *list)
{
list->addItem(obs_module_text("AdvSceneSwitcher.script.type.inline"),
InlineScript::INLINE);
list->addItem(obs_module_text("AdvSceneSwitcher.script.type.file"),
InlineScript::FILE);
}
MacroSegmentScriptInlineEdit::MacroSegmentScriptInlineEdit(
QWidget *parent, std::shared_ptr<MacroSegmentScriptInline> entryData)
: QWidget(parent),
_scriptType(new QComboBox(this)),
_language(new QComboBox(this)),
_script(new ScriptEditor(this)),
_path(new FileSelection(FileSelection::Type::WRITE, this)),
_openFile(new QPushButton(
obs_module_text("AdvSceneSwitcher.script.file.open"), this)),
_fileLayout(new QHBoxLayout()),
_entryData(entryData)
{
SetupLayout();
SetupWidgetConnections();
PopulateWidgets();
SetWidgetVisibility();
_loading = false;
}
void MacroSegmentScriptInlineEdit::ScriptTypeChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->SetType(static_cast<InlineScript::Type>(
_scriptType->itemData(idx).toInt()));
SetWidgetVisibility();
}
void MacroSegmentScriptInlineEdit::LanguageChanged(int idx)
{
{
GUARD_LOADING_AND_LOCK();
_entryData->SetLanguage(static_cast<obs_script_lang>(
_language->itemData(idx).toInt()));
const QSignalBlocker b(_script);
_script->setPlainText(_entryData->GetScript());
}
if (_entryData->GetType() == InlineScript::Type::FILE) {
PathChanged(QString::fromStdString(_entryData->GetPath()));
}
}
void MacroSegmentScriptInlineEdit::ScriptChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->SetScript(_script->toPlainText().toStdString());
adjustSize();
updateGeometry();
}
void MacroSegmentScriptInlineEdit::PathChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
if (path.isEmpty()) {
_entryData->SetPath(path.toStdString());
return;
}
// Script language will be detected by OBS based on file extension so
// adjust it if necessary
QString pathAdjusted = path;
if (_entryData->GetLanguage() == OBS_SCRIPT_LANG_PYTHON &&
!path.endsWith(".py")) {
if (path.endsWith(".lua")) {
pathAdjusted.chop(4);
}
pathAdjusted += ".py";
}
if (_entryData->GetLanguage() == OBS_SCRIPT_LANG_LUA &&
!path.endsWith(".lua")) {
if (path.endsWith(".py")) {
pathAdjusted.chop(3);
}
pathAdjusted += ".lua";
}
const QSignalBlocker b(_path);
_path->SetPath(pathAdjusted);
_entryData->SetPath(pathAdjusted.toStdString());
}
void MacroSegmentScriptInlineEdit::PopulateWidgets()
{
populateLanguageSelection(_language);
populateScriptTypeSelection(_scriptType);
if (!_entryData) {
return;
}
_scriptType->setCurrentIndex(
_scriptType->findData(_entryData->GetType()));
_language->setCurrentIndex(
_language->findData(_entryData->GetLanguage()));
_script->setPlainText(_entryData->GetScript());
_path->SetPath(_entryData->GetPath());
}
void MacroSegmentScriptInlineEdit::SetupWidgetConnections()
{
QWidget::connect(_scriptType, SIGNAL(currentIndexChanged(int)), this,
SLOT(ScriptTypeChanged(int)));
QWidget::connect(_language, SIGNAL(currentIndexChanged(int)), this,
SLOT(LanguageChanged(int)));
QWidget::connect(_script, SIGNAL(ScriptChanged()), this,
SLOT(ScriptChanged()));
QWidget::connect(_path, SIGNAL(PathChanged(const QString &)), this,
SLOT(PathChanged(const QString &)));
QWidget::connect(_openFile, &QPushButton::clicked, this, [this] {
QUrl fileUrl = QUrl::fromLocalFile(
QString::fromStdString(_entryData->GetPath()));
if (!QDesktopServices::openUrl(fileUrl)) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.script.file.open.failed"));
}
});
}
void MacroSegmentScriptInlineEdit::SetupLayout()
{
auto languageLayout = new QHBoxLayout();
PlaceWidgets(obs_module_text("AdvSceneSwitcher.script.language.layout"),
languageLayout, {{"{{language}}", _language}});
auto typeLayout = new QHBoxLayout();
PlaceWidgets(obs_module_text("AdvSceneSwitcher.script.type.layout"),
typeLayout, {{"{{scriptType}}", _scriptType}});
PlaceWidgets(obs_module_text("AdvSceneSwitcher.script.file.layout"),
_fileLayout,
{{"{{path}}", _path}, {"{{open}}", _openFile}}, false);
auto layout = new QVBoxLayout();
layout->addLayout(typeLayout);
layout->addLayout(languageLayout);
layout->addLayout(_fileLayout);
layout->addWidget(_script);
setLayout(layout);
}
void MacroSegmentScriptInlineEdit::SetWidgetVisibility()
{
_script->setVisible(_entryData->GetType() ==
InlineScript::Type::INLINE);
SetLayoutVisible(_fileLayout,
_entryData->GetType() == InlineScript::Type::FILE);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,56 @@
#pragma once
#include "file-selection.hpp"
#include "inline-script.hpp"
#include "variable-text-edit.hpp"
#include <QLayout>
namespace advss {
class MacroSegmentScriptInline {
public:
InlineScript::Type GetType() const { return _script.GetType(); }
void SetType(InlineScript::Type);
obs_script_lang GetLanguage() const { return _script.GetLanguage(); }
void SetLanguage(obs_script_lang);
StringVariable GetScript() const { return _script.GetText(); }
void SetScript(const std::string &);
std::string GetPath() const { return _script.GetPath(); }
void SetPath(const std::string &);
protected:
InlineScript _script;
};
class MacroSegmentScriptInlineEdit : public QWidget {
Q_OBJECT
public:
MacroSegmentScriptInlineEdit(
QWidget *, std::shared_ptr<MacroSegmentScriptInline> = nullptr);
virtual ~MacroSegmentScriptInlineEdit() = default;
protected slots:
void ScriptTypeChanged(int);
void LanguageChanged(int);
void ScriptChanged();
void PathChanged(const QString &);
protected:
void PopulateWidgets();
void SetupWidgetConnections();
void SetupLayout();
void SetWidgetVisibility();
QComboBox *_scriptType;
QComboBox *_language;
ScriptEditor *_script;
FileSelection *_path;
QPushButton *_openFile;
QHBoxLayout *_fileLayout;
std::shared_ptr<MacroSegmentScriptInline> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -0,0 +1,344 @@
#include "inline-script.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include <obs-module.h>
#include <obs.hpp>
#include <QDir>
#include <QFileInfo>
namespace advss {
static constexpr std::string_view signalName = "advss_run_temp_script";
std::atomic_uint64_t InlineScript::_instanceIdCounter = 0;
const std::string_view InlineScript::_defaultPythonScript =
"import obspython as obs\n"
"\n"
"def run():\n"
" obs.script_log(obs.LOG_WARNING, \"Hello from Python!\")\n"
" return True\n";
const std::string_view InlineScript::_defaultLUAScript =
"obs = obslua\n"
"\n"
"function run()\n"
" obs.script_log(obs.LOG_WARNING, \"Hello from LUA!\")\n"
" return true\n"
"end";
;
static bool setup()
{
auto sh = obs_get_signal_handler();
auto signalDecl =
std::string("void ") + signalName.data() + "(string id)";
signal_handler_add(sh, signalDecl.c_str());
return true;
}
static bool setupDone = setup();
static void cleanupScriptFile(const std::string &path)
{
const QFileInfo fileInfo(QString::fromStdString(path));
if (!fileInfo.isFile()) {
return;
}
QFile file(fileInfo.absoluteFilePath());
if (!file.remove()) {
vblog(LOG_INFO, "failed to clean up script file %s",
fileInfo.absoluteFilePath().toStdString().c_str());
}
}
static std::optional<std::string>
getScriptTempFilePath(obs_script_lang language)
{
static int counter = 0;
++counter;
static const QString filenamePattern =
"scripting/advss-tmp-script%1.%2";
const QString filename = filenamePattern.arg(counter).arg(
language == OBS_SCRIPT_LANG_PYTHON ? "py" : "lua");
auto settingsFile =
obs_module_config_path(filename.toStdString().c_str());
if (!settingsFile) {
blog(LOG_WARNING,
"could not create temp script file! (obs_module_config_path)");
return {};
}
std::string path = settingsFile;
bfree(settingsFile);
return path;
}
static bool createScriptFile(const char *settingsFile, const char *content)
{
const QFileInfo fileInfo(settingsFile);
const QString dirPath = fileInfo.absolutePath();
const QDir dir(dirPath);
if (!dir.exists() && !dir.mkpath(dirPath)) {
blog(LOG_WARNING, "could not create script file! (mkpath)");
return false;
}
QFile file(settingsFile);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return false;
}
auto out = QTextStream(&file);
out << content;
return true;
}
InlineScript::InlineScript() : _instanceId(_instanceIdCounter++)
{
Setup();
}
InlineScript::InlineScript(const InlineScript &other)
: _language(other._language),
_textPython(other._textPython),
_textLUA(other._textLUA),
_instanceId(_instanceIdCounter++)
{
Setup();
}
void InlineScript::Save(obs_data_t *data) const
{
OBSDataAutoRelease obj = obs_data_create();
obs_data_set_int(obj, "type", _type);
obs_data_set_int(obj, "language", _language);
_textPython.Save(obj, "scriptPython");
_textLUA.Save(obj, "scriptLUA");
obs_data_set_string(obj, "file", _file.c_str());
obs_data_set_obj(data, "script", obj);
}
void InlineScript::Load(obs_data_t *data)
{
OBSDataAutoRelease obj = obs_data_get_obj(data, "script");
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
_language =
static_cast<obs_script_lang>(obs_data_get_int(obj, "language"));
_textPython.Load(obj, "scriptPython");
_textLUA.Load(obj, "scriptLUA");
_file = obs_data_get_string(obj, "file");
Setup();
}
void InlineScript::SetType(Type type)
{
_type = type;
Setup();
}
void InlineScript::SetLanguage(obs_script_lang language)
{
_language = language;
Setup();
}
void InlineScript::SetText(const std::string &text)
{
switch (_language) {
case OBS_SCRIPT_LANG_UNKNOWN:
break;
case OBS_SCRIPT_LANG_LUA:
_textLUA = text;
break;
case OBS_SCRIPT_LANG_PYTHON:
_textPython = text;
break;
default:
break;
}
Setup();
}
const StringVariable &InlineScript::GetText() const
{
static const StringVariable defaultRet;
switch (_language) {
case OBS_SCRIPT_LANG_UNKNOWN:
break;
case OBS_SCRIPT_LANG_LUA:
return _textLUA;
case OBS_SCRIPT_LANG_PYTHON:
return _textPython;
default:
break;
}
return defaultRet;
}
void InlineScript::SetPath(const std::string &path)
{
_file = path;
Setup();
}
bool InlineScript::Run()
{
static auto sh = obs_get_signal_handler();
if (_type == INLINE && _lastResolvedText != std::string(GetText())) {
Setup();
}
if (_type == FILE && _lastPath != _file) {
Setup();
}
auto cd = calldata_create();
calldata_set_string(cd, "id", GetID().c_str());
signal_handler_signal(sh, signalName.data(), cd);
bool result = calldata_bool(cd, "result");
calldata_destroy(cd);
return result;
}
void InlineScript::ResolveVariablesToFixedValues()
{
_textPython.ResolveVariables();
_textLUA.ResolveVariables();
}
static std::string preprocessScriptText(const std::string &text,
obs_script_lang language,
const std::string &id)
{
const std::string footerPython =
std::string("\n\n"
"## AUTO GENERATED ##\n"
"def script_load(settings):\n"
" def run_wrapper(data):\n"
" id = obs.calldata_string(data, \"id\")\n"
" if id == \"") +
id +
"\":\n"
" ret = run()\n"
" obs.calldata_set_bool(data, \"result\", ret)\n"
" sh = obs.obs_get_signal_handler()\n"
" obs.signal_handler_connect(sh, \"" +
signalName.data() + "\", run_wrapper)\n\n";
const std::string footerLUA =
std::string(
"\n\n"
"-- AUTO GENERATED --\n"
"function script_load(settings)\n"
" local run_wrapper = (function(data)\n"
" local id = obs.calldata_string(data, \"id\")\n"
" if id == \"") +
id +
"\" then\n"
" local ret = run()\n"
" obs.calldata_set_bool(data, \"result\", ret)\n"
" end\n"
" end)\n"
" local sh = obs.obs_get_signal_handler()\n"
" obs.signal_handler_connect(sh, \"" +
signalName.data() +
"\" , run_wrapper)\n"
"end\n";
std::string scriptText =
language == OBS_SCRIPT_LANG_PYTHON ? footerPython : footerLUA;
return text + scriptText;
}
void InlineScript::SetupFile()
{
const auto path = GetLUACompatiblePath(_file);
_fileId = path;
if (path.empty()) {
return;
}
if (!QFileInfo(QString::fromStdString(path)).exists()) {
const auto text = preprocessScriptText(
_language == OBS_SCRIPT_LANG_PYTHON
? _defaultPythonScript.data()
: _defaultLUAScript.data(),
_language, GetID());
(void)createScriptFile(_file.c_str(), text.c_str());
}
_script = std::unique_ptr<obs_script_t, ScriptDeleter>(
CreateOBSScript(path.c_str(), nullptr), {});
_lastPath = _file;
}
void InlineScript::SetupInline()
{
const StringVariable &text =
_language == OBS_SCRIPT_LANG_PYTHON ? _textPython : _textLUA;
const auto scriptText = preprocessScriptText(text, _language, GetID());
auto path_ = getScriptTempFilePath(_language);
if (!path_) {
return;
}
auto path = GetLUACompatiblePath(*path_);
if (!createScriptFile(path.c_str(), scriptText.c_str())) {
return;
}
_script = std::unique_ptr<obs_script_t, ScriptDeleter>(
CreateOBSScript(path.c_str(), nullptr), {path});
_lastResolvedText = text;
}
void InlineScript::Setup()
{
_script.reset();
_lastResolvedText = "";
_lastPath = "";
if (_type == FILE) {
SetupFile();
} else {
SetupInline();
}
}
std::string InlineScript::GetID() const
{
if (_type == FILE) {
return _fileId;
}
return std::to_string(_instanceId);
}
void InlineScript::ScriptDeleter::operator()(obs_script_t *script)
{
DestroyOBSScript(script);
if (!tempScriptPath.empty()) {
cleanupScriptFile(tempScriptPath);
}
}
ScriptEditor::ScriptEditor(QWidget *parent) : VariableTextEdit(parent, 15, 5)
{
installEventFilter(this);
}
bool ScriptEditor::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::FocusOut) {
emit ScriptChanged();
}
return QObject::eventFilter(obj, event);
}
} // namespace advss

View File

@@ -0,0 +1,76 @@
#pragma once
#include "obs-script-helpers.hpp"
#include "variable-text-edit.hpp"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <obs-data.h>
namespace advss {
class InlineScript {
public:
InlineScript();
InlineScript(const InlineScript &);
enum Type { INLINE, FILE };
void Save(obs_data_t *) const;
void Load(obs_data_t *);
void SetType(Type);
Type GetType() const { return _type; }
void SetLanguage(obs_script_lang);
obs_script_lang GetLanguage() const { return _language; }
void SetText(const std::string &);
const StringVariable &GetText() const;
void SetPath(const std::string &);
const std::string &GetPath() const { return _file; }
bool Run();
void ResolveVariablesToFixedValues();
private:
void Setup();
void SetupFile();
void SetupInline();
std::string GetID() const;
Type _type = INLINE;
obs_script_lang _language = OBS_SCRIPT_LANG_PYTHON;
std::string _file;
StringVariable _textPython = _defaultPythonScript.data();
StringVariable _textLUA = _defaultLUAScript.data();
std::string _lastResolvedText;
std::string _lastPath;
std::string _fileId;
const uint64_t _instanceId;
struct ScriptDeleter {
void operator()(obs_script_t *);
std::string tempScriptPath;
};
std::unique_ptr<obs_script_t, ScriptDeleter> _script;
static std::atomic_uint64_t _instanceIdCounter;
static const std::string_view _defaultPythonScript;
static const std::string_view _defaultLUAScript;
};
class ScriptEditor : public VariableTextEdit {
Q_OBJECT
public:
ScriptEditor(QWidget *parent);
signals:
void ScriptChanged();
private:
bool eventFilter(QObject *obj, QEvent *event) override;
};
} // namespace advss

View File

@@ -0,0 +1,72 @@
#include "obs-script-helpers.hpp"
#include "log-helper.hpp"
#include <QFileInfo>
#include <QLibrary>
namespace advss {
static const char *libName =
#if defined(WIN32)
"obs-scripting.dll";
#elif __APPLE__
"obs-scripting.dylib";
#else
"obs-scripting.so";
#endif
typedef obs_script_t *(*obs_script_create_t)(const char *, obs_data_t *);
typedef void (*obs_script_destroy_t)(obs_script_t *);
obs_script_create_t obs_script_create = nullptr;
obs_script_destroy_t obs_script_destroy = nullptr;
static bool setup()
{
QLibrary scriptingLib(libName);
obs_script_create =
(obs_script_create_t)scriptingLib.resolve("obs_script_create");
if (!obs_script_create) {
blog(LOG_WARNING,
"could not resolve obs_script_create symbol!");
}
obs_script_destroy = (obs_script_destroy_t)scriptingLib.resolve(
"obs_script_destroy");
if (!obs_script_destroy) {
blog(LOG_WARNING,
"could not resolve obs_script_destroy symbol!");
}
return true;
}
static bool setupDone = setup();
obs_script_t *CreateOBSScript(const char *path, obs_data_t *settings)
{
if (!obs_script_create) {
return nullptr;
}
return obs_script_create(path, settings);
}
void DestroyOBSScript(obs_script_t *script)
{
if (!obs_script_destroy) {
return;
}
obs_script_destroy(script);
}
std::string GetLUACompatiblePath(const std::string &path)
{
// Can't use settingsFile here as LUA will complain if Windows style
// paths (C:\some\path) are used.
// QFileInfo::absoluteFilePath will convert those paths so LUA won't
// complain. (C:/some/path)
const QFileInfo fileInfo(QString::fromStdString(path));
return fileInfo.absoluteFilePath().toStdString();
}
} // namespace advss

View File

@@ -0,0 +1,23 @@
#pragma once
#include <obs-data.h>
#include <string>
// Based on obs-scripting.h
struct obs_script;
typedef struct obs_script obs_script_t;
enum obs_script_lang {
OBS_SCRIPT_LANG_UNKNOWN,
OBS_SCRIPT_LANG_LUA,
OBS_SCRIPT_LANG_PYTHON
};
namespace advss {
obs_script_t *CreateOBSScript(const char *path, obs_data_t *settings);
void DestroyOBSScript(obs_script_t *script);
std::string GetLUACompatiblePath(const std::string &path);
} // namespace advss

View File

@@ -118,12 +118,17 @@ static void parseTags(const std::string &tags, IRCMessage &message)
message.properties.timestamp = std::stoull(tagValue); message.properties.timestamp = std::stoull(tagValue);
} else if (tagName == "turbo") { } else if (tagName == "turbo") {
message.properties.isTurbo = tagValue == "1"; message.properties.isTurbo = tagValue == "1";
} else if (tagName == "user-id") { } else if (tagName == "user-id" ||
tagName == "target-user-id" || tagName == "login") {
message.properties.userId = tagValue; message.properties.userId = tagValue;
} else if (tagName == "user-type") { } else if (tagName == "user-type") {
message.properties.userType = tagValue; message.properties.userType = tagValue;
} else if (tagName == "vip") { } else if (tagName == "vip") {
message.properties.isVIP = tagValue == "1"; message.properties.isVIP = tagValue == "1";
} else if (tagName == "ban-duration") {
message.properties.banDuration = std::stoull(tagValue);
} else if (tagName == "target-msg-id") {
message.properties.id = tagValue;
} }
} }
} }
@@ -172,34 +177,54 @@ static void parseCommand(const std::string &rawCommandComponent,
if (commandParts.size() < 3) { if (commandParts.size() < 3) {
return; return;
} }
message.type = IRCMessage::Type::OTHER;
message.command.parameters = (commandParts[2] == "ACK"); message.command.parameters = (commandParts[2] == "ACK");
} else if (message.command.command == "RECONNECT") { } else if (message.command.command == "RECONNECT") {
blog(LOG_INFO, blog(LOG_INFO,
"The Twitch IRC server is about to terminate the connection for maintenance."); "The Twitch IRC server is about to terminate the connection for maintenance.");
message.type = IRCMessage::Type::OTHER;
} else if (message.command.command == "421") { } else if (message.command.command == "421") {
// Unsupported command // Unsupported command
message.type = IRCMessage::Type::OTHER;
return; return;
} else if (message.command.command == "PING" || } else if (message.command.command == "PRIVMSG") {
message.command.command == "001" ||
message.command.command == "NOTICE" ||
message.command.command == "CLEARCHAT" ||
message.command.command == "HOSTTARGET" ||
message.command.command == "PRIVMSG") {
if (commandParts.size() < 2) { if (commandParts.size() < 2) {
return; return;
} }
message.type = IRCMessage::Type::MESSAGE_RECEIVED;
message.command.parameters = commandParts[1];
} else if (message.command.command == "CLEARCHAT") {
if (commandParts.size() < 1) {
return;
}
message.type = IRCMessage::Type::MESSAGE_CLEARED;
message.command.parameters = commandParts[1];
} else if (message.command.command == "CLEARMSG") {
if (commandParts.size() < 2) {
return;
}
message.type = IRCMessage::Type::MESSAGE_REMOVED;
message.command.parameters = commandParts[1];
} else if (message.command.command == "PING" ||
message.command.command == "001" ||
message.command.command == "NOTICE" ||
message.command.command == "HOSTTARGET") {
if (commandParts.size() < 2) {
return;
}
message.type = IRCMessage::Type::OTHER;
message.command.parameters = commandParts[1]; message.command.parameters = commandParts[1];
} else if (message.command.command == "JOIN") { } else if (message.command.command == "JOIN") {
if (commandParts.size() < 2) { if (commandParts.size() < 2) {
return; return;
} }
message.properties.joinedChannel = true; message.type = IRCMessage::Type::USER_JOIN;
message.command.parameters = commandParts[1]; message.command.parameters = commandParts[1];
} else if (message.command.command == "PART") { } else if (message.command.command == "PART") {
if (commandParts.size() < 2) { if (commandParts.size() < 2) {
return; return;
} }
message.properties.leftChannel = true; message.type = IRCMessage::Type::USER_LEAVE;
message.command.parameters = commandParts[1]; message.command.parameters = commandParts[1];
} else if (message.command.command == "GLOBALUSERSTATE" || } else if (message.command.command == "GLOBALUSERSTATE" ||
message.command.command == "USERSTATE" || message.command.command == "USERSTATE" ||
@@ -213,7 +238,9 @@ static void parseCommand(const std::string &rawCommandComponent,
message.command.command == "375" || message.command.command == "375" ||
message.command.command == "376") { message.command.command == "376") {
// Do nothing for these cases for now // Do nothing for these cases for now
message.type = IRCMessage::Type::OTHER;
} else { } else {
message.type = IRCMessage::Type::UNKNOWN;
vblog(LOG_INFO, "Unexpected IRC command: %s", vblog(LOG_INFO, "Unexpected IRC command: %s",
message.command.command.c_str()); message.command.command.c_str());
} }
@@ -501,6 +528,7 @@ void TwitchChatConnection::Authenticate()
if (!pass.has_value()) { if (!pass.has_value()) {
blog(LOG_INFO, blog(LOG_INFO,
"Joining Twitch chat failed due to invalid token!"); "Joining Twitch chat failed due to invalid token!");
return;
} }
Send("PASS oauth:" + *pass); Send("PASS oauth:" + *pass);
Send("NICK " + _token.GetName()); Send("NICK " + _token.GetName());
@@ -549,6 +577,18 @@ void TwitchChatConnection::HandleNewMessage(const IRCMessage &message)
message.message.c_str()); message.message.c_str());
} }
void TwitchChatConnection::HandleRemoveMessage(const IRCMessage &message)
{
_messageDispatcher.DispatchMessage(message);
vblog(LOG_INFO, "Chat message was removed");
}
void TwitchChatConnection::HandleClear(const IRCMessage &message)
{
_messageDispatcher.DispatchMessage(message);
vblog(LOG_INFO, "Chat was cleared");
}
void TwitchChatConnection::HandleWhisper(const IRCMessage &message) void TwitchChatConnection::HandleWhisper(const IRCMessage &message)
{ {
_whisperDispatcher.DispatchMessage(message); _whisperDispatcher.DispatchMessage(message);
@@ -609,6 +649,8 @@ void TwitchChatConnection::OnMessage(
static constexpr std::string_view reconnectCommand = "RECONNECT"; static constexpr std::string_view reconnectCommand = "RECONNECT";
static constexpr std::string_view newMessageCommand = "PRIVMSG"; static constexpr std::string_view newMessageCommand = "PRIVMSG";
static constexpr std::string_view whisperCommand = "WHISPER"; static constexpr std::string_view whisperCommand = "WHISPER";
static constexpr std::string_view clearCommand = "CLEARCHAT";
static constexpr std::string_view removeCommand = "CLEARMSG";
if (!message) { if (!message) {
return; return;
@@ -635,6 +677,10 @@ void TwitchChatConnection::OnMessage(
HandlePart(message); HandlePart(message);
} else if (message.command.command == newMessageCommand) { } else if (message.command.command == newMessageCommand) {
HandleNewMessage(message); HandleNewMessage(message);
} else if (message.command.command == clearCommand) {
HandleClear(message);
} else if (message.command.command == removeCommand) {
HandleRemoveMessage(message);
} else if (message.command.command == whisperCommand) { } else if (message.command.command == whisperCommand) {
HandleWhisper(message); HandleWhisper(message);
} else if (message.command.command == noticeCommand) { } else if (message.command.command == noticeCommand) {

View File

@@ -15,10 +15,23 @@ namespace advss {
using websocketpp::connection_hdl; using websocketpp::connection_hdl;
struct IRCMessage { struct IRCMessage {
enum class Type {
MESSAGE_RECEIVED,
MESSAGE_REMOVED,
MESSAGE_CLEARED,
USER_LEAVE,
USER_JOIN,
OTHER,
UNKNOWN,
};
Type type = Type::UNKNOWN;
struct Badge { struct Badge {
std::string name; std::string name;
bool enabled; bool enabled;
}; };
struct { struct {
std::string badgeInfoString; std::string badgeInfoString;
std::string badgesString; std::string badgesString;
@@ -39,13 +52,12 @@ struct IRCMessage {
std::string rootParentId; std::string rootParentId;
std::string rootParentUserLogin; std::string rootParentUserLogin;
bool isSubscriber = false; bool isSubscriber = false;
unsigned long long timestamp; unsigned long long timestamp = 0;
unsigned long long banDuration = 0;
bool isTurbo = false; bool isTurbo = false;
std::string userId; std::string userId;
std::string userType; std::string userType;
bool isVIP = false; bool isVIP = false;
bool joinedChannel = false;
bool leftChannel = false;
} properties; } properties;
struct { struct {
@@ -99,6 +111,8 @@ private:
void HandleJoin(const IRCMessage &); void HandleJoin(const IRCMessage &);
void HandlePart(const IRCMessage &); void HandlePart(const IRCMessage &);
void HandleNewMessage(const IRCMessage &); void HandleNewMessage(const IRCMessage &);
void HandleRemoveMessage(const IRCMessage &);
void HandleClear(const IRCMessage &);
void HandleWhisper(const IRCMessage &); void HandleWhisper(const IRCMessage &);
void HandleNotice(const IRCMessage &) const; void HandleNotice(const IRCMessage &) const;
void HandleReconnect(); void HandleReconnect();

View File

@@ -286,7 +286,7 @@ void MacroActionTwitch::GetUserInfo(const std::shared_ptr<TwitchToken> &token)
httplib::Params params; httplib::Params params;
switch (_userInfoQueryType) { switch (_userInfoQueryType) {
case UserInfoQueryType::ID: case UserInfoQueryType::ID:
params.insert({"id", std::to_string(_userId)}); params.insert({"id", std::to_string((uint64_t)_userId)});
break; break;
case UserInfoQueryType::LOGIN: case UserInfoQueryType::LOGIN:
params.insert({"login", _userLogin}); params.insert({"login", _userLogin});
@@ -825,7 +825,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_chatMessage(new VariableTextEdit(this)), _chatMessage(new VariableTextEdit(this)),
_userInfoQueryType(new QComboBox(this)), _userInfoQueryType(new QComboBox(this)),
_userLogin(new VariableLineEdit(this)), _userLogin(new VariableLineEdit(this)),
_userId(new VariableSpinBox(this)), _userId(new VariableDoubleSpinBox(this)),
_pointsReward(new TwitchPointsRewardWidget(this, false)), _pointsReward(new TwitchPointsRewardWidget(this, false)),
_rewardVariable(new VariableSelection(this)), _rewardVariable(new VariableSelection(this)),
_toggleRewardSelection(new QPushButton()) _toggleRewardSelection(new QPushButton())
@@ -984,7 +984,8 @@ void MacroActionTwitchEdit::SetWidgetProperties()
populateAnnouncementColorSelection(_announcementColor); populateAnnouncementColorSelection(_announcementColor);
populateUserQueryInfoTypeSelection(_userInfoQueryType); populateUserQueryInfoTypeSelection(_userInfoQueryType);
_userId->setMaximum(999999999); _userId->setMaximum(999999999999999);
_userId->setDecimals(0);
_toggleRewardSelection->setCheckable(true); _toggleRewardSelection->setCheckable(true);
_toggleRewardSelection->setMaximumWidth(11); _toggleRewardSelection->setMaximumWidth(11);
@@ -1030,8 +1031,8 @@ void MacroActionTwitchEdit::SetWidgetSignalConnections()
SLOT(UserLoginChanged())); SLOT(UserLoginChanged()));
QWidget::connect( QWidget::connect(
_userId, _userId,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)), SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
this, SLOT(UserIdChanged(const NumberVariable<int> &))); this, SLOT(UserIdChanged(const NumberVariable<double> &)));
QWidget::connect( QWidget::connect(
_pointsReward, _pointsReward,
SIGNAL(PointsRewardChanged(const TwitchPointsReward &)), this, SIGNAL(PointsRewardChanged(const TwitchPointsReward &)), this,
@@ -1160,7 +1161,7 @@ void MacroActionTwitchEdit::ToggleRewardSelection(bool)
SetWidgetVisibility(); SetWidgetVisibility();
} }
void MacroActionTwitchEdit::UserIdChanged(const NumberVariable<int> &value) void MacroActionTwitchEdit::UserIdChanged(const NumberVariable<double> &value)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_userId = value; _entryData->_userId = value;

View File

@@ -179,7 +179,7 @@ public:
StringVariable _chatMessage; StringVariable _chatMessage;
UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN; UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN;
StringVariable _userLogin = "user login"; StringVariable _userLogin = "user login";
IntVariable _userId = 0; DoubleVariable _userId = 0;
TwitchPointsReward _pointsReward; TwitchPointsReward _pointsReward;
std::weak_ptr<Variable> _rewardVariable; std::weak_ptr<Variable> _rewardVariable;
bool _useVariableForRewardSelection = false; bool _useVariableForRewardSelection = false;
@@ -244,7 +244,7 @@ private slots:
void ChatMessageChanged(); void ChatMessageChanged();
void UserInfoQueryTypeChanged(int); void UserInfoQueryTypeChanged(int);
void UserLoginChanged(); void UserLoginChanged();
void UserIdChanged(const NumberVariable<int> &); void UserIdChanged(const NumberVariable<double> &);
void PointsRewardChanged(const TwitchPointsReward &); void PointsRewardChanged(const TwitchPointsReward &);
void RewardVariableChanged(const QString &); void RewardVariableChanged(const QString &);
void ToggleRewardSelection(bool); void ToggleRewardSelection(bool);
@@ -278,7 +278,9 @@ private:
VariableTextEdit *_chatMessage; VariableTextEdit *_chatMessage;
QComboBox *_userInfoQueryType; QComboBox *_userInfoQueryType;
VariableLineEdit *_userLogin; VariableLineEdit *_userLogin;
VariableSpinBox *_userId; // QSpinBox uses int internally, which is too small for Twitch IDs, so
// we use QDoubleSpinBox instead
VariableDoubleSpinBox *_userId;
TwitchPointsRewardWidget *_pointsReward; TwitchPointsRewardWidget *_pointsReward;
VariableSelection *_rewardVariable; VariableSelection *_rewardVariable;
QPushButton *_toggleRewardSelection; QPushButton *_toggleRewardSelection;

View File

@@ -121,6 +121,10 @@ const static std::map<MacroConditionTwitch::Condition, std::string> conditionTyp
"AdvSceneSwitcher.condition.twitch.type.polling.channel.category"}, "AdvSceneSwitcher.condition.twitch.type.polling.channel.category"},
{MacroConditionTwitch::Condition::CHAT_MESSAGE_RECEIVED, {MacroConditionTwitch::Condition::CHAT_MESSAGE_RECEIVED,
"AdvSceneSwitcher.condition.twitch.type.chat.message"}, "AdvSceneSwitcher.condition.twitch.type.chat.message"},
{MacroConditionTwitch::Condition::CHAT_MESSAGE_REMOVED,
"AdvSceneSwitcher.condition.twitch.type.chat.messageRemoved"},
{MacroConditionTwitch::Condition::CHAT_CLEARED,
"AdvSceneSwitcher.condition.twitch.type.chat.cleared"},
{MacroConditionTwitch::Condition::CHAT_USER_JOINED, {MacroConditionTwitch::Condition::CHAT_USER_JOINED,
"AdvSceneSwitcher.condition.twitch.type.chat.userJoined"}, "AdvSceneSwitcher.condition.twitch.type.chat.userJoined"},
{MacroConditionTwitch::Condition::CHAT_USER_LEFT, {MacroConditionTwitch::Condition::CHAT_USER_LEFT,
@@ -416,87 +420,128 @@ static bool stringMatches(const RegexConfig &regex, const std::string &string,
bool MacroConditionTwitch::CheckChatMessages(TwitchToken &token) bool MacroConditionTwitch::CheckChatMessages(TwitchToken &token)
{ {
if (!_chatConnection) { if (!ChatConnectionIsSetup(token)) {
_chatConnection = TwitchChatConnection::GetChatConnection(
token, _channel);
if (!_chatConnection) {
return false;
}
_chatBuffer = _chatConnection->RegisterForMessages();
return false; return false;
} }
while (!_chatBuffer->Empty()) { return HandleChatEvents([this](const IRCMessage &message) -> bool {
auto message = _chatBuffer->ConsumeMessage(); if (message.type != IRCMessage::Type::MESSAGE_RECEIVED) {
if (!message) { return false;
continue;
} }
// Join and leave message don't have any message data if (!_chatMessagePattern.Matches(message)) {
if (message->properties.leftChannel || return false;
message->properties.joinedChannel) {
continue;
} }
if (!_chatMessagePattern.Matches(*message)) { SetTempVarValue("id", message.properties.id);
continue; SetTempVarValue("chat_message", message.message);
} SetTempVarValue("user_id", message.properties.userId);
SetTempVarValue("user_login", message.source.nick);
SetTempVarValue("id", message->properties.id); SetTempVarValue("user_name", message.properties.displayName);
SetTempVarValue("chat_message", message->message); SetTempVarValue("user_type", message.properties.userType);
SetTempVarValue("user_id", message->properties.userId);
SetTempVarValue("user_login", message->source.nick);
SetTempVarValue("user_name", message->properties.displayName);
SetTempVarValue("user_type", message->properties.userType);
SetTempVarValue("reply_parent_id", SetTempVarValue("reply_parent_id",
message->properties.replyParentId); message.properties.replyParentId);
SetTempVarValue("reply_parent_message", SetTempVarValue("reply_parent_message",
message->properties.replyParentBody); message.properties.replyParentBody);
SetTempVarValue("reply_parent_user_id", SetTempVarValue("reply_parent_user_id",
message->properties.replyParentUserId); message.properties.replyParentUserId);
SetTempVarValue("reply_parent_user_login", SetTempVarValue("reply_parent_user_login",
message->properties.replyParentUserLogin); message.properties.replyParentUserLogin);
SetTempVarValue("reply_parent_user_name", SetTempVarValue("reply_parent_user_name",
message->properties.replyParentDisplayName); message.properties.replyParentDisplayName);
SetTempVarValue("root_parent_id", SetTempVarValue("root_parent_id",
message->properties.rootParentId); message.properties.rootParentId);
SetTempVarValue("root_parent_user_login", SetTempVarValue("root_parent_user_login",
message->properties.rootParentUserLogin); message.properties.rootParentUserLogin);
SetTempVarValue("badge_info", SetTempVarValue("badge_info",
message->properties.badgeInfoString); message.properties.badgeInfoString);
SetTempVarValue("badges", message->properties.badgesString); SetTempVarValue("badges", message.properties.badgesString);
SetTempVarValue("bits", SetTempVarValue("bits",
std::to_string(message->properties.bits)); std::to_string(message.properties.bits));
SetTempVarValue("color", message->properties.color); SetTempVarValue("color", message.properties.color);
SetTempVarValue("emotes", message->properties.emotesString); SetTempVarValue("emotes", message.properties.emotesString);
SetTempVarValue("timestamp", SetTempVarValue("timestamp",
std::to_string(message->properties.timestamp)); std::to_string(message.properties.timestamp));
SetTempVarValue("is_emotes_only", SetTempVarValue("is_emotes_only",
message->properties.isUsingOnlyEmotes message.properties.isUsingOnlyEmotes ? "true"
? "true" : "false");
: "false");
SetTempVarValue("is_first_message", SetTempVarValue("is_first_message",
message->properties.isFirstMessage ? "true" message.properties.isFirstMessage ? "true"
: "false"); : "false");
SetTempVarValue("is_mod", SetTempVarValue("is_mod",
message->properties.isMod ? "true" : "false"); message.properties.isMod ? "true" : "false");
SetTempVarValue("is_subscriber", SetTempVarValue("is_subscriber", message.properties.isSubscriber
message->properties.isSubscriber ? "true" ? "true"
: "false"); : "false");
SetTempVarValue("is_turbo", SetTempVarValue("is_turbo",
message->properties.isTurbo ? "true" : "false"); message.properties.isTurbo ? "true" : "false");
SetTempVarValue("is_vip", SetTempVarValue("is_vip",
message->properties.isVIP ? "true" : "false"); message.properties.isVIP ? "true" : "false");
if (_clearBufferOnMatch) {
_chatBuffer->Clear();
}
return true; return true;
} });
return false;
} }
bool MacroConditionTwitch::CheckChatUserJoinOrLeave(TwitchToken &token) bool MacroConditionTwitch::CheckChatUserJoinOrLeave(TwitchToken &token)
{
if (!ChatConnectionIsSetup(token)) {
return false;
}
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if ((_condition == Condition::CHAT_USER_JOINED &&
message.type != IRCMessage::Type::USER_JOIN) ||
(_condition == Condition::CHAT_USER_LEFT &&
message.type != IRCMessage::Type::USER_LEAVE)) {
return false;
}
SetTempVarValue("user_login", message.source.nick);
return true;
});
}
bool MacroConditionTwitch::CheckChatClear(TwitchToken &token)
{
if (!ChatConnectionIsSetup(token)) {
return false;
}
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if (message.type != IRCMessage::Type::MESSAGE_CLEARED) {
return false;
}
SetTempVarValue("ban_duration",
std::to_string(message.properties.banDuration));
SetTempVarValue("login", message.message);
SetTempVarValue("user_id", message.properties.userId);
SetTempVarValue("timestamp",
std::to_string(message.properties.timestamp));
return true;
});
}
bool MacroConditionTwitch::CheckChatMessageRemove(TwitchToken &token)
{
if (!ChatConnectionIsSetup(token)) {
return false;
}
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if (message.type != IRCMessage::Type::MESSAGE_REMOVED) {
return false;
}
SetTempVarValue("message", message.message);
SetTempVarValue("message_id", message.properties.id);
SetTempVarValue("login", message.properties.userId);
SetTempVarValue("timestamp",
std::to_string(message.properties.timestamp));
return true;
});
}
bool MacroConditionTwitch::ChatConnectionIsSetup(TwitchToken &token)
{ {
if (!_chatConnection) { if (!_chatConnection) {
_chatConnection = TwitchChatConnection::GetChatConnection( _chatConnection = TwitchChatConnection::GetChatConnection(
@@ -507,22 +552,22 @@ bool MacroConditionTwitch::CheckChatUserJoinOrLeave(TwitchToken &token)
_chatBuffer = _chatConnection->RegisterForMessages(); _chatBuffer = _chatConnection->RegisterForMessages();
return false; return false;
} }
return true;
}
bool MacroConditionTwitch::HandleChatEvents(
const std::function<bool(const IRCMessage &)> &matchCb)
{
while (!_chatBuffer->Empty()) { while (!_chatBuffer->Empty()) {
auto message = _chatBuffer->ConsumeMessage(); auto message = _chatBuffer->ConsumeMessage();
if (!message) { if (!message) {
continue; continue;
} }
if ((_condition == Condition::CHAT_USER_JOINED && if (!matchCb(*message)) {
!message->properties.joinedChannel) ||
(_condition == Condition::CHAT_USER_LEFT &&
!message->properties.leftChannel)) {
continue; continue;
} }
SetTempVarValue("user_login", message->source.nick);
if (_clearBufferOnMatch) { if (_clearBufferOnMatch) {
_chatBuffer->Clear(); _chatBuffer->Clear();
} }
@@ -715,6 +760,20 @@ bool MacroConditionTwitch::CheckCondition()
} }
return CheckChatMessages(*token); return CheckChatMessages(*token);
} }
case Condition::CHAT_CLEARED: {
auto token = _token.lock();
if (!token) {
return false;
}
return CheckChatClear(*token);
}
case Condition::CHAT_MESSAGE_REMOVED: {
auto token = _token.lock();
if (!token) {
return false;
}
return CheckChatMessageRemove(*token);
}
case Condition::CHAT_USER_JOINED: case Condition::CHAT_USER_JOINED:
case Condition::CHAT_USER_LEFT: { case Condition::CHAT_USER_LEFT: {
auto token = _token.lock(); auto token = _token.lock();
@@ -871,6 +930,10 @@ bool MacroConditionTwitch::ConditionIsSupportedByToken()
{Condition::CATEGORY_POLLING, {}}, {Condition::CATEGORY_POLLING, {}},
{Condition::CHAT_MESSAGE_RECEIVED, {Condition::CHAT_MESSAGE_RECEIVED,
{{"chat:read"}, {"chat:edit"}}}, {{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_MESSAGE_REMOVED,
{{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_CLEARED,
{{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_USER_JOINED, {Condition::CHAT_USER_JOINED,
{{"chat:read"}, {"chat:edit"}}}, {{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_USER_LEFT, {Condition::CHAT_USER_LEFT,
@@ -1069,6 +1132,8 @@ void MacroConditionTwitch::SetupTempVars()
}; };
if (_condition != Condition::CHAT_MESSAGE_RECEIVED && if (_condition != Condition::CHAT_MESSAGE_RECEIVED &&
_condition != Condition::CHAT_MESSAGE_REMOVED &&
_condition != Condition::CHAT_CLEARED &&
_condition != Condition::CHAT_USER_JOINED && _condition != Condition::CHAT_USER_JOINED &&
_condition != Condition::CHAT_USER_LEFT && _condition != Condition::CHAT_USER_LEFT &&
_condition != Condition::RAID_INBOUND_EVENT && _condition != Condition::RAID_INBOUND_EVENT &&
@@ -1423,6 +1488,18 @@ void MacroConditionTwitch::SetupTempVars()
setupTempVarHelper("is_turbo", ".chatReceive"); setupTempVarHelper("is_turbo", ".chatReceive");
setupTempVarHelper("is_vip", ".chatReceive"); setupTempVarHelper("is_vip", ".chatReceive");
break; break;
case Condition::CHAT_MESSAGE_REMOVED:
setupTempVarHelper("message", ".chatRemove");
setupTempVarHelper("message_id", ".chatRemove");
setupTempVarHelper("login", ".chatRemove");
setupTempVarHelper("timestamp", ".chatRemove");
break;
case Condition::CHAT_CLEARED:
setupTempVarHelper("ban_duration", ".chatClear");
setupTempVarHelper("login", ".chatClear");
setupTempVarHelper("user_id", ".chatClear");
setupTempVarHelper("timestamp", ".chatClear");
break;
case Condition::CHAT_USER_JOINED: case Condition::CHAT_USER_JOINED:
setupTempVarHelper("user_login", ".chatJoin"); setupTempVarHelper("user_login", ".chatJoin");
break; break;

View File

@@ -74,6 +74,8 @@ public:
// Chat // Chat
CHAT_MESSAGE_RECEIVED = 500000, CHAT_MESSAGE_RECEIVED = 500000,
CHAT_MESSAGE_REMOVED = 500030,
CHAT_CLEARED = 500060,
CHAT_USER_JOINED = 500100, CHAT_USER_JOINED = 500100,
CHAT_USER_LEFT = 500200, CHAT_USER_LEFT = 500200,
@@ -113,10 +115,16 @@ private:
bool CheckChannelLiveEvents(); bool CheckChannelLiveEvents();
bool CheckChannelRewardChangeEvents(); bool CheckChannelRewardChangeEvents();
bool CheckChannelRewardRedemptionEvents(); bool CheckChannelRewardRedemptionEvents();
bool CheckChatMessages(TwitchToken &token);
bool HandleMatchingSubscriptionEvents( bool HandleMatchingSubscriptionEvents(
const std::function<void(const Event &)> &matchCb); const std::function<void(const Event &)> &matchCb);
bool CheckChatMessages(TwitchToken &token);
bool CheckChatUserJoinOrLeave(TwitchToken &token); bool CheckChatUserJoinOrLeave(TwitchToken &token);
bool CheckChatClear(TwitchToken &token);
bool CheckChatMessageRemove(TwitchToken &token);
bool ChatConnectionIsSetup(TwitchToken &token);
bool HandleChatEvents(
const std::function<bool(const IRCMessage &)> &matchCb);
void RegisterEventSubscription(); void RegisterEventSubscription();
void ResetSubscription(); void ResetSubscription();