Compare commits

..

16 Commits

Author SHA1 Message Date
WarmUpTill
70bbc7cdac Implement proper timestamp validation for Twitch messages
Some checks failed
debian-build / build (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-04-11 19:02:37 +02:00
WarmUpTill
d892298995 Add missing "[adv-ss]" log tag 2025-04-11 19:02:37 +02:00
WarmUpTill
0fe31432be Add "previous scene" to the "scene has (not) changed" checks 2025-04-11 18:57:58 +02:00
WarmUpTill
aaa0113ccb Ignore Xerrors 2025-04-11 18:57:25 +02:00
WarmUpTill
b908954b46 CI: Add cmake setup step to Linux build
Some checks failed
debian-build / build (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-04-02 13:48:23 +02:00
WarmUpTill
b0eede8a85 Add "disable" effect to macro conditions using "ignore" logic selection 2025-04-02 13:48:23 +02:00
WarmUpTill
aa87911b71 Use cpp-httplib based HTTP action type
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
The goal is to remove the older, more limited version of the HTTP action
at some point in the future.
2025-03-30 14:06:19 +02:00
WarmUpTill
1b05019acc Add option to hide entries from action selection
Intended to be used to hide legacy version of actions types (e.g. HTTP)
2025-03-30 14:01:41 +02:00
WarmUpTill
78a5a2629d Hide the "remote" file check option
This option will be removed at some point in the future.
The http action should be used instead.
2025-03-30 14:01:41 +02:00
WarmUpTill
634270a978 Cleanup includes 2025-03-30 14:01:41 +02:00
WarmUpTill
78ba22e1e4 Hide "get settings" button when setting macro property value 2025-03-30 14:01:41 +02:00
WarmUpTill
aba5737a60 Update and clean up locale (qwe1154323937) 2025-03-30 14:01:41 +02:00
WarmUpTill
4315f7f621 Exclude unwanted files from sources archive 2025-03-30 14:01:41 +02:00
WarmUpTill
53c535962f Use tab key to switch to dialog controls and set default focus to input 2025-03-30 14:01:41 +02:00
WarmUpTill
1e718b78c7 Fall back to project version if git tag cannot be queried 2025-03-30 14:01:41 +02:00
WarmUpTill
b1d2156228 Update libremidi to v4.3.0 2025-03-30 14:01:41 +02:00
45 changed files with 2974 additions and 544 deletions

View File

@@ -210,6 +210,11 @@ jobs:
restore-keys: |
${{ runner.os }}-ccache-x86_64-
- name: Set up CMake 🏗️
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.24.x'
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@master

3
.gitmodules vendored
View File

@@ -28,3 +28,6 @@
[submodule "deps/libusb"]
path = deps/libusb
url = https://github.com/libusb/libusb.git
[submodule "deps/date"]
path = deps/date
url = https://github.com/HowardHinnant/date.git

View File

@@ -33,6 +33,11 @@ add_library(${LIB_NAME} SHARED)
include(cmake/common/get_git_revision_description.cmake)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
git_describe(GIT_TAG)
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
set(GIT_TAG ${PROJECT_VERSION})
endif()
message(STATUS "${PROJECT_NAME} version: ${GIT_TAG}")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/version.cpp.in"
"${CMAKE_CURRENT_BINARY_DIR}/lib/version.cpp" @ONLY)
@@ -438,6 +443,9 @@ else()
endif()
target_include_directories(${LIB_NAME} PRIVATE "${PROC_INCLUDE_DIR}")
target_sources(${LIB_NAME} PRIVATE lib/linux/advanced-scene-switcher-nix.cpp)
# Don't include irrelevant folders into sources archive
list(APPEND CPACK_SOURCE_IGNORE_FILES "\\.deps/.*")
endif()
if(NOT OS_WINDOWS)

View File

@@ -526,7 +526,6 @@ AdvSceneSwitcher.action.screenshot.save.default="Standard"
AdvSceneSwitcher.action.screenshot.save.custom="Benutzerdefiniert"
AdvSceneSwitcher.action.screenshot.type.source="Quelle"
AdvSceneSwitcher.action.screenshot.type.scene="Szene"
AdvSceneSwitcher.action.screenshot.mainOutput="OBS's Haupt-Ausgabe"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Quellen oder Szenen, die nicht immer gerendert werden, können dazu führen, dass einige Teile der Screenshots leer bleiben."
AdvSceneSwitcher.action.screenshot.entry="Screenshot{{targetType}}{{sources}}{{scenes}}und speichere in{{saveType}}{{variables}}Pfad"
AdvSceneSwitcher.action.profile="Profil"

View File

@@ -1022,7 +1022,6 @@ AdvSceneSwitcher.action.screenshot.save.custom="Custom file path"
AdvSceneSwitcher.action.screenshot.save.variable="Variable (base64 encoded PNG)"
AdvSceneSwitcher.action.screenshot.type.source="Source"
AdvSceneSwitcher.action.screenshot.type.scene="Scene"
AdvSceneSwitcher.action.screenshot.mainOutput="OBS's main output"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Sources or scenes, which are not always rendered, may result in some parts of screenshots to remain blank."
AdvSceneSwitcher.action.screenshot.entry="Screenshot{{targetType}}{{sources}}{{scenes}}and save to{{saveType}}{{variables}}"
AdvSceneSwitcher.action.profile="Profile"
@@ -1051,11 +1050,25 @@ AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.request="Send{{api}}of typ
AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.event="Send{{api}}of type{{type}}to connected clients"
AdvSceneSwitcher.action.websocket.entry.generic="Send{{api}}via{{connection}}"
AdvSceneSwitcher.action.http="HTTP"
AdvSceneSwitcher.action.http.legacy="HTTP (legacy)"
AdvSceneSwitcher.action.http.setHeaders="Set headers"
AdvSceneSwitcher.action.http.headers="Headers:"
AdvSceneSwitcher.action.http.addHeader="Add header"
AdvSceneSwitcher.action.http.addHeader.name="Header name"
AdvSceneSwitcher.action.http.addHeader.value="Header value"
AdvSceneSwitcher.action.http.setParams="Set parameters"
AdvSceneSwitcher.action.http.params="Parameters:"
AdvSceneSwitcher.action.http.addParam.name="Parameter name"
AdvSceneSwitcher.action.http.addParam.value="Parameter value"
AdvSceneSwitcher.action.http.body="Message body:"
AdvSceneSwitcher.action.http.type.get="GET"
AdvSceneSwitcher.action.http.type.post="POST"
AdvSceneSwitcher.action.http.type.put="PUT"
AdvSceneSwitcher.action.http.type.patch="PATCH"
AdvSceneSwitcher.action.http.type.delete="DELETE"
AdvSceneSwitcher.action.http.layout.method="Send{{method}}to URL{{url}}at path{{path}}"
AdvSceneSwitcher.action.http.layout.contentType="Content type:{{contentType}}"
AdvSceneSwitcher.action.http.layout.timeout="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.http.entry.line1="Send{{method}}to{{url}}"
AdvSceneSwitcher.action.http.entry.line2="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.variable="Variable"
@@ -2041,6 +2054,11 @@ AdvSceneSwitcher.tempVar.gameCapture.class.description="Window class of the appl
AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"
@@ -2079,7 +2097,6 @@ AdvSceneSwitcher.settings.suffix.type.list=" (List)"
AdvSceneSwitcher.settings.suffix.type.color=" (Color)"
AdvSceneSwitcher.settings.suffix.type.button=" (Button)"
AdvSceneSwitcher.settings.suffix.type.font=" (Font)"
AdvSceneSwitcher.settings.suffix.type.font=" (Font)"
AdvSceneSwitcher.settings.suffix.type.editableList=" (Editable list)"
AdvSceneSwitcher.settings.suffix.type.frameRate=" (Frame rate)"
AdvSceneSwitcher.settings.suffix.type.group=" (Group)"

View File

@@ -438,7 +438,6 @@ AdvSceneSwitcher.action.random="Aleatorio"
AdvSceneSwitcher.action.random.entry="Ejecute aleatoriamente cualquiera de las siguientes macros (las macros en pausa se ignoran)"
AdvSceneSwitcher.action.systray="Notificación de la bandeja del sistema"
AdvSceneSwitcher.action.screenshot="Captura de pantalla"
AdvSceneSwitcher.action.screenshot.mainOutput="Salida principal de OBS"
AdvSceneSwitcher.action.profile="Perfil"
AdvSceneSwitcher.action.profile.entry="Cambiar perfil activo a {{profiles}}"
AdvSceneSwitcher.action.sceneCollection="Colección de escenas"

View File

@@ -647,7 +647,6 @@ AdvSceneSwitcher.action.screenshot.save.default="Par défaut"
AdvSceneSwitcher.action.screenshot.save.custom="Personnalisé"
AdvSceneSwitcher.action.screenshot.type.source="Source"
AdvSceneSwitcher.action.screenshot.type.scene="Scène"
AdvSceneSwitcher.action.screenshot.mainOutput="Sortie principale d'OBS"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Les sources ou les scènes qui ne sont pas toujours rendues peuvent entraîner des parties de captures d'écran vides."
AdvSceneSwitcher.action.screenshot.entry="Capturer{{targetType}}{{sources}}{{scenes}}et enregistrer à l'emplacement{{saveType}}{{variables}}"
AdvSceneSwitcher.action.profile="Profil"

View File

@@ -979,7 +979,6 @@ AdvSceneSwitcher.action.screenshot.save.default="デフォルト"
AdvSceneSwitcher.action.screenshot.save.custom="カスタム"
; AdvSceneSwitcher.action.screenshot.type.source="Source"
AdvSceneSwitcher.action.screenshot.type.scene="シーン"
AdvSceneSwitcher.action.screenshot.mainOutput="OBSの主な出力"
AdvSceneSwitcher.action.screenshot.blackscreenNote="常にレンダリングされるわけではないソースやシーンにより、スクリーンショットの一部が空白のままになる場合があります。"
AdvSceneSwitcher.action.screenshot.entry="スクリーンショット{{targetType}}{{sources}}{{scenes}}を作成し、{{saveType}}{{variables}}の場所に保存します"
AdvSceneSwitcher.action.profile="プロファイル"

View File

@@ -960,7 +960,6 @@ AdvSceneSwitcher.action.screenshot.save.default="Padrão"
AdvSceneSwitcher.action.screenshot.save.custom="Personalizado"
AdvSceneSwitcher.action.screenshot.type.source="Fonte"
AdvSceneSwitcher.action.screenshot.type.scene="Cena"
AdvSceneSwitcher.action.screenshot.mainOutput="Saída principal do OBS"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Fontes ou cenas, que nem sempre são renderizadas, podem resultar em algumas partes das capturas de tela permanecendo em branco."
AdvSceneSwitcher.action.screenshot.entry="Captura de tela{{targetType}}{{sources}}{{scenes}}e salvar em{{saveType}}{{variables}}localização"
AdvSceneSwitcher.action.profile="Perfil"

View File

@@ -356,7 +356,6 @@ AdvSceneSwitcher.action.random="Rastgele"
AdvSceneSwitcher.action.random.entry="Aşağıdaki makrolardan herhangi birini rastgele çalıştırın (duraklatılmış makrolar yoksayılır)"
AdvSceneSwitcher.action.systray="Sistem tepsisi bildirimi"
AdvSceneSwitcher.action.screenshot="Ekran görüntüsü"
AdvSceneSwitcher.action.screenshot.mainOutput="OBS'nin ana çıkışı"
AdvSceneSwitcher.action.profile="Profil"
AdvSceneSwitcher.action.profile.entry="Aktif profili şununla değiştir: {{profiles}}"
AdvSceneSwitcher.action.sceneCollection="Sahne koleksiyonu"

File diff suppressed because it is too large Load Diff

1
deps/date vendored Submodule

Submodule deps/date added at 5bdb7e6f31

2
deps/libremidi vendored

View File

@@ -1,6 +1,5 @@
#include "advanced-scene-switcher.hpp"
#include "backup.hpp"
#include "curl-helper.hpp"
#include "log-helper.hpp"
#include "macro-helpers.hpp"
#include "obs-module-helper.hpp"
@@ -19,10 +18,15 @@
#include <obs-frontend-api.h>
#include <QAction>
#include <QDirIterator>
#include <QLibrary>
#include <QMainWindow>
#include <QTextStream>
#include <regex>
#ifdef _WIN32
#include <Windows.h>
#endif
namespace advss {
AdvSceneSwitcher *AdvSceneSwitcher::window = nullptr;

View File

@@ -550,6 +550,11 @@ static void initProc2()
#endif
}
int ignoreXerror(Display *d, XErrorEvent *e)
{
return 0;
}
void PlatformInit()
{
auto display = disp();
@@ -560,6 +565,7 @@ void PlatformInit()
initXss();
initProcps();
initProc2();
XSetErrorHandler(ignoreXerror);
}
static void cleanupHelper(QLibrary *lib)
@@ -576,6 +582,7 @@ void PlatformCleanup()
cleanupHelper(libprocps);
cleanupHelper(libproc2);
cleanupDisplay();
XSetErrorHandler(NULL);
}
} // namespace advss

View File

@@ -7,8 +7,6 @@
#include "section.hpp"
#include "switch-button.hpp"
#include <QGraphicsOpacityEffect>
namespace advss {
static inline void populateActionSelection(QComboBox *list)
@@ -17,6 +15,9 @@ static inline void populateActionSelection(QComboBox *list)
QString entry(obs_module_text(action._name.c_str()));
if (list->findText(entry) == -1) {
list->addItem(entry);
qobject_cast<QListView *>(list->view())
->setRowHidden(list->count() - 1,
action._hidden);
} else {
blog(LOG_WARNING,
"did not insert duplicate action entry with name \"%s\"",
@@ -116,17 +117,6 @@ void MacroActionEdit::SetEntryData(std::shared_ptr<MacroAction> *data)
_entryData = data;
}
void MacroActionEdit::SetDisableEffect(bool value)
{
if (value) {
auto effect = new QGraphicsOpacityEffect(this);
effect->setOpacity(0.5);
_section->setGraphicsEffect(effect);
} else {
_section->setGraphicsEffect(nullptr);
}
}
void MacroActionEdit::ActionEnableChanged(bool value)
{
if (_loading || !_entryData) {
@@ -144,13 +134,9 @@ void MacroActionEdit::UpdateActionState()
return;
}
SetEnableAppearance((*_entryData)->Enabled());
}
void MacroActionEdit::SetEnableAppearance(bool value)
{
_enable->setChecked(value);
SetDisableEffect(!value);
const bool enabled = (*_entryData)->Enabled();
SetEnableAppearance(enabled);
_enable->setChecked(enabled);
}
std::shared_ptr<MacroSegment> MacroActionEdit::Data() const

View File

@@ -27,8 +27,6 @@ private slots:
private:
std::shared_ptr<MacroSegment> Data() const;
void SetDisableEffect(bool);
void SetEnableAppearance(bool);
FilterComboBox *_actionSelection;
SwitchButton *_enable;

View File

@@ -12,6 +12,7 @@ struct MacroActionInfo {
std::function<std::shared_ptr<MacroAction>(Macro *m)> _create = nullptr;
CreateActionWidget _createWidget = nullptr;
std::string _name;
bool _hidden = false;
};
class MacroActionFactory {

View File

@@ -11,15 +11,12 @@ bool MacroAction::Save(obs_data_t *obj) const
{
MacroSegment::Save(obj);
obs_data_set_string(obj, "id", GetId().c_str());
obs_data_set_bool(obj, "enabled", _enabled);
return true;
}
bool MacroAction::Load(obs_data_t *obj)
{
MacroSegment::Load(obj);
obs_data_set_default_bool(obj, "enabled", true);
_enabled = obs_data_get_bool(obj, "enabled");
return true;
}
@@ -28,16 +25,6 @@ void MacroAction::LogAction() const
ablog(LOG_INFO, "performed action %s", GetId().c_str());
}
void MacroAction::SetEnabled(bool value)
{
_enabled = value;
}
bool MacroAction::Enabled() const
{
return _enabled;
}
void MacroAction::ResolveVariablesToFixedValues() {}
std::string_view MacroAction::GetDefaultID()

View File

@@ -19,13 +19,9 @@ public:
// Used to resolve variables before actions are added to action queues
virtual void ResolveVariablesToFixedValues();
void SetEnabled(bool);
bool Enabled() const;
static std::string_view GetDefaultID();
private:
bool _enabled = true;
};
class EXPORT MacroRefAction : virtual public MacroAction {

View File

@@ -152,6 +152,8 @@ void MacroConditionEdit::LogicSelectionChanged(int idx)
const auto logic = static_cast<Logic::Type>(
_logicSelection->itemData(idx).toInt());
(*_entryData)->SetLogicType(logic);
SetEnableAppearance(logic != Logic::Type::NONE);
}
bool MacroConditionEdit::IsRootNode()
@@ -164,6 +166,7 @@ void MacroConditionEdit::SetLogicSelection()
const auto logic = (*_entryData)->GetLogicType();
_logicSelection->setCurrentIndex(
_logicSelection->findData(static_cast<int>(logic)));
SetEnableAppearance(logic != Logic::Type::NONE);
}
void MacroConditionEdit::SetRootNode(bool root)

View File

@@ -9,7 +9,6 @@
#include <QPushButton>
#include <QHBoxLayout>
#include <QTimer>
#include <QListWidget>
namespace advss {

View File

@@ -6,6 +6,7 @@
#include <QApplication>
#include <QEvent>
#include <QGraphicsOpacityEffect>
#include <QLabel>
#include <QMouseEvent>
#include <QScrollBar>
@@ -24,24 +25,24 @@ bool MacroSegment::Save(obs_data_t *obj) const
obs_data_set_bool(data, "collapsed", _collapsed);
obs_data_set_bool(data, "useCustomLabel", _useCustomLabel);
obs_data_set_string(data, "customLabel", _customLabel.c_str());
obs_data_set_bool(data, "enabled", _enabled);
obs_data_set_int(data, "version", 1);
obs_data_set_obj(obj, "segmentSettings", data);
return true;
}
bool MacroSegment::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "segmentSettings");
_collapsed = obs_data_get_bool(data, "collapsed");
_useCustomLabel = obs_data_get_bool(data, "useCustomLabel");
_customLabel = obs_data_get_string(data, "customLabel");
obs_data_set_default_bool(data, "enabled", true);
_enabled = obs_data_get_bool(data, "enabled");
// TODO: remove this fallback at some point
if (obs_data_has_user_value(obj, "segmentSettings")) {
OBSDataAutoRelease data =
obs_data_get_obj(obj, "segmentSettings");
_collapsed = obs_data_get_bool(data, "collapsed");
_useCustomLabel = obs_data_get_bool(data, "useCustomLabel");
_customLabel = obs_data_get_string(data, "customLabel");
} else {
_collapsed = obs_data_get_bool(obj, "collapsed");
_useCustomLabel = false;
_customLabel = obs_module_text(
"AdvSceneSwitcher.macroTab.segment.defaultCustomLabel");
if (!obs_data_has_user_value(data, "version")) {
_enabled = obs_data_get_bool(obj, "enabled");
}
ClearAvailableTempvars();
@@ -73,6 +74,16 @@ bool MacroSegment::GetHighlightAndReset()
return false;
}
void MacroSegment::SetEnabled(bool value)
{
_enabled = value;
}
bool MacroSegment::Enabled() const
{
return _enabled;
}
std::string MacroSegment::GetVariableValue() const
{
if (_supportsVariableValue) {
@@ -320,6 +331,22 @@ void MacroSegmentEdit::Collapsed(bool collapsed)
}
}
void MacroSegmentEdit::SetDisableEffect(bool value)
{
if (value) {
auto effect = new QGraphicsOpacityEffect(this);
effect->setOpacity(0.5);
_section->setGraphicsEffect(effect);
} else {
_section->setGraphicsEffect(nullptr);
}
}
void MacroSegmentEdit::SetEnableAppearance(bool value)
{
SetDisableEffect(!value);
}
void MacroSegmentEdit::SetFocusPolicyOfWidgets()
{
QList<QWidget *> widgets = this->findChildren<QWidget *>();

View File

@@ -39,6 +39,8 @@ public:
virtual std::string GetId() const = 0;
void EnableHighlight();
bool GetHighlightAndReset();
void SetEnabled(bool);
bool Enabled() const;
virtual std::string GetVariableValue() const;
protected:
@@ -75,6 +77,7 @@ private:
// UI helper
bool _highlight = false;
bool _collapsed = false;
bool _enabled = true;
// Custom header labels
bool _useCustomLabel = false;
@@ -118,6 +121,9 @@ signals:
void SceneGroupRenamed(const QString &oldName, const QString newName);
protected:
void SetDisableEffect(bool);
void SetEnableAppearance(bool);
bool eventFilter(QObject *obj, QEvent *ev) override;
Section *_section;

View File

@@ -32,7 +32,9 @@ void PreventMouseWheelAdjustWithoutFocus(QWidget *w)
QString(w->metaObject()->className()) ==
"advss::OSCMessageElementEdit" ||
QString(w->metaObject()->className()) ==
"advss::ChatMessagePropertyEdit") {
"advss::ChatMessagePropertyEdit" ||
QString(w->metaObject()->className()) ==
"advss::KeyValueListContainerWidget") {
return;
}
w->setFocusPolicy(Qt::StrongFocus);

View File

@@ -67,6 +67,8 @@ NonModalMessageDialog::NonModalMessageDialog(const QString &message, Type type,
}
case Type::INPUT: {
_inputEdit = new ResizingPlainTextEdit(this);
_inputEdit->setTabChangesFocus(true);
_inputEdit->setFocus();
connect(_inputEdit, &ResizingPlainTextEdit::textChanged, this,
&NonModalMessageDialog::InputChanged);
layout->addWidget(_inputEdit);

View File

@@ -28,6 +28,7 @@ install_advss_plugin_dependency(...)
... to install the plugin and its dependencies.
#]]
add_plugin(http)
add_plugin(midi)
add_plugin(openvr)
add_plugin(stream-deck)

View File

@@ -15,8 +15,8 @@ target_sources(
macro-action-filter.hpp
macro-action-hotkey.cpp
macro-action-hotkey.hpp
macro-action-http.cpp
macro-action-http.hpp
macro-action-http-legacy.cpp
macro-action-http-legacy.hpp
macro-action-log.cpp
macro-action-log.hpp
macro-action-media.cpp

View File

@@ -449,8 +449,11 @@ void MacroActionFilterEdit::SetWidgetVisibility()
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionFilter::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(_entryData->_action ==
MacroActionFilter::Action::SETTINGS);
_getSettings->setVisible(
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionFilter::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
_tempVars->setVisible(_entryData->_action ==
MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==

View File

@@ -1,4 +1,4 @@
#include "macro-action-http.hpp"
#include "macro-action-http-legacy.hpp"
#include "curl-helper.hpp"
#include "layout-helpers.hpp"
@@ -9,7 +9,7 @@ const std::string MacroActionHttp::id = "http";
bool MacroActionHttp::_registered = MacroActionFactory::Register(
MacroActionHttp::id,
{MacroActionHttp::Create, MacroActionHttpEdit::Create,
"AdvSceneSwitcher.action.http"});
"AdvSceneSwitcher.action.http.legacy", true});
const static std::map<MacroActionHttp::Method, std::string> methods = {
{MacroActionHttp::Method::GET, "AdvSceneSwitcher.action.http.type.get"},

View File

@@ -34,11 +34,11 @@ const static std::map<PluginStateAction, std::string> actionTypes = {
const static std::map<NoMatchBehavior, std::string> noMatchValues = {
{NoMatchBehavior::NO_SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.dontSwitch"},
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMatch.dontSwitch"},
{NoMatchBehavior::SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.switchTo"},
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMatch.switchTo"},
{NoMatchBehavior::RANDOM_SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.switchToRandom"},
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMatch.switchToRandom"},
};
static void stopPlugin()

View File

@@ -42,7 +42,7 @@ const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
{OBS_DEINTERLACE_MODE_DISABLE,
"AdvSceneSwitcher.action.source.deinterlaceMode.disable"},
{OBS_DEINTERLACE_MODE_DISCARD,
"AdvSceneSwitcher.action.source.deinterlaceMode.disable"},
"AdvSceneSwitcher.action.source.deinterlaceMode.discard"},
{OBS_DEINTERLACE_MODE_RETRO,
"AdvSceneSwitcher.action.source.deinterlaceMode.retro"},
{OBS_DEINTERLACE_MODE_BLEND,
@@ -526,8 +526,11 @@ void MacroActionSourceEdit::SetWidgetVisibility()
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS);
_getSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
_tempVars->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==

View File

@@ -442,6 +442,13 @@ void MacroConditionFileEdit::SetWidgetVisibility()
_entryData->_onlyMatchIfChanged &&
_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
// TODO: Remove remote file support in future version in favor of HTTP
// action.
// Hide the option for now, if it is not used already.
_fileTypes->setVisible(_entryData->_fileType ==
MacroConditionFile::FileType::REMOTE);
adjustSize();
updateGeometry();
}

View File

@@ -41,12 +41,12 @@ public:
StringVariable _file = obs_module_text("AdvSceneSwitcher.enterPath");
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
FileType _fileType = FileType::LOCAL;
RegexConfig _regex;
// TODO: Remove in future version
bool _useTime = false;
bool _onlyMatchIfChanged = false;
FileType _fileType = FileType::LOCAL;
private:
bool MatchFileContent(QString &filedata);

View File

@@ -94,11 +94,15 @@ bool MacroConditionScene::CheckCondition()
SetVariableValue(GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("current",
GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("previous",
GetWeakSourceName(GetPreviousScene()));
return sceneChanged;
case Type::NOT_CHANGED:
SetVariableValue(GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("current",
GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("previous",
GetWeakSourceName(GetPreviousScene()));
return !sceneChanged;
case Type::CURRENT_PATTERN: {
auto scene = getCurrentSceneHelper(_useTransitionTargetScene);
@@ -212,8 +216,6 @@ void MacroConditionScene::SetupTempVars()
MacroCondition::SetupTempVars();
switch (_type) {
case Type::CURRENT:
case Type::CHANGED:
case Type::NOT_CHANGED:
case Type::CURRENT_PATTERN:
AddTempvar("current",
obs_module_text(
@@ -231,6 +233,15 @@ void MacroConditionScene::SetupTempVars()
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.preview"));
break;
case Type::CHANGED:
case Type::NOT_CHANGED:
AddTempvar("current",
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.current"));
AddTempvar("previous",
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.previous"));
break;
default:
break;
}

View File

@@ -0,0 +1,77 @@
cmake_minimum_required(VERSION 3.14)
project(advanced-scene-switcher-http)
# --- Check requirements ---
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
set(CPP_HTTPLIB_DIR "${ADVSS_SOURCE_DIR}/deps/cpp-httplib")
if(NOT EXISTS "${CPP_HTTPLIB_DIR}/CMakeLists.txt")
message(WARNING "cpp-httplib directory \"${CPP_HTTPLIB_DIR}\" not found!\n"
"HTTP support will be disabled!")
return()
endif()
if(NOT TARGET httplib)
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
endif()
find_package(ZLIB)
if(NOT ZLIB_FOUND)
message(WARNING "zlib not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT=1)
if(OS_MACOS)
target_compile_definitions(
${PROJECT_NAME} PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN=1)
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreFoundation")
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework Security")
endif()
target_sources(
${PROJECT_NAME} PRIVATE macro-action-http.cpp macro-action-http.hpp
key-value-list.cpp key-value-list.hpp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
"${OPENSSL_INCLUDE_DIR}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
install_advss_plugin(${PROJECT_NAME})
if(OS_WINDOWS)
# Couldn't really find a better way to install runtime dependencies for
# Windows TODO: Clean this up at some point
function(FIND_FILES_WITH_PATTERN result pattern dir)
execute_process(
COMMAND
powershell -Command
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
"Select-Object -First 1 |"
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
OUTPUT_VARIABLE files
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(${result}
${files}
PARENT_SCOPE)
endfunction()
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
"${OPENSSL_DLL_SEARCH_DIR}")
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
"${OPENSSL_DLL_SEARCH_DIR}")
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
endif()

View File

@@ -0,0 +1,213 @@
#include "key-value-list.hpp"
#include "name-dialog.hpp"
#include "ui-helpers.hpp"
#include <QLayout>
#include <QTimer>
namespace advss {
KeyValueListEdit::KeyValueListEdit(QWidget *parent, const QString &addKeyString,
const QString &addKeyStringDescription,
const QString &addValueString,
const QString &addValueStringDescription)
: ListEditor(parent),
_addKeyString(addKeyString),
_addKeyStringDescription(addKeyStringDescription),
_addValueString(addValueString),
_addValueStringDescription(addValueStringDescription)
{
_list->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_list->setAutoScroll(false);
}
void KeyValueListEdit::SetStringList(const StringList &list)
{
_stringList = list;
_list->clear();
for (int i = 0; i < list.size(); i += 2) {
AppendListEntryWidget(
list.at(i), i + 1 >= list.size() ? "" : list.at(i + 1));
}
UpdateListSize();
}
void KeyValueListEdit::Add()
{
StringVariable key;
StringVariable value;
bool accepted = AskForKeyValue(key, value);
if (!accepted) {
return;
}
AppendListEntryWidget(key, value);
_stringList << key << value;
// Delay resizing to make sure the list viewport was already updated
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
StringListChanged(_stringList);
}
void KeyValueListEdit::Remove()
{
int idx = _list->currentRow();
if (idx == -1) {
return;
}
_stringList.removeAt(idx);
QListWidgetItem *item = _list->currentItem();
if (!item) {
return;
}
delete item;
// Delay resizing to make sure the list viewport was already updated
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
StringListChanged(_stringList);
}
void KeyValueListEdit::Up()
{
int idx = _list->currentRow();
if (idx <= 0 || idx >= _list->count()) {
return;
}
MoveStringListIdxUp(idx);
auto row = _list->itemWidget(_list->currentItem());
auto newItem = _list->currentItem()->clone();
_list->insertItem(idx - 1, newItem);
_list->setItemWidget(newItem, row);
_list->takeItem(idx + 1);
_list->setCurrentRow(idx - 1);
UpdateListSize();
StringListChanged(_stringList);
}
void KeyValueListEdit::Down()
{
int idx = _list->currentRow();
if (idx == -1 || idx == _list->count() - 1) {
return;
}
MoveStringListIdxUp(idx + 1);
auto row = _list->itemWidget(_list->currentItem());
auto newItem = _list->currentItem()->clone();
_list->insertItem(idx + 2, newItem);
_list->setItemWidget(newItem, row);
_list->takeItem(idx);
_list->setCurrentRow(idx + 1);
UpdateListSize();
StringListChanged(_stringList);
}
void KeyValueListEdit::Clicked(QListWidgetItem *item)
{
int idx = _list->currentRow();
StringVariable key = _stringList[idx * 2];
StringVariable value = _stringList[idx * 2 + 1];
bool accepted = AskForKeyValue(key, value);
if (!accepted) {
return;
}
auto container = static_cast<KeyValueListContainerWidget *>(
_list->itemWidget(item));
container->_key->setText(QString::fromStdString(key.UnresolvedValue()));
container->_value->setText(
QString::fromStdString(value.UnresolvedValue()));
container->adjustSize();
container->updateGeometry();
_stringList[idx * 2] = key;
_stringList[idx * 2 + 1] = value;
// Delay resizing to make sure the list viewport was already updated
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
StringListChanged(_stringList);
}
void KeyValueListEdit::MoveStringListIdxUp(int idx)
{
if (idx <= 0 || idx >= _list->count()) {
return;
}
_stringList.move(idx * 2, idx * 2 - 2);
_stringList.move(idx * 2 + 1, idx * 2 - 1);
}
bool KeyValueListEdit::AskForKeyValue(StringVariable &keyVariable,
StringVariable &valueVariable)
{
std::string key;
bool accepted = NameDialog::AskForName(
this, _addKeyString, _addKeyStringDescription, key,
QString::fromStdString(keyVariable.UnresolvedValue()), 4096,
false);
if (!accepted) {
return false;
}
std::string value;
accepted = NameDialog::AskForName(
this, _addValueString, _addValueStringDescription, value,
QString::fromStdString(valueVariable.UnresolvedValue()), 4096,
false);
if (!accepted) {
return false;
}
keyVariable = key;
valueVariable = value;
return true;
}
void KeyValueListEdit::AppendListEntryWidget(const StringVariable &key,
const StringVariable &value)
{
QListWidgetItem *item = new QListWidgetItem(_list);
auto container = new KeyValueListContainerWidget(this, _list->count());
container->_key->setText(QString::fromStdString(key.UnresolvedValue()));
container->_value->setText(
QString::fromStdString(value.UnresolvedValue()));
container->adjustSize();
container->updateGeometry();
_list->addItem(item);
_list->setItemWidget(item, container);
UpdateListSize();
}
KeyValueListContainerWidget::KeyValueListContainerWidget(QWidget *parent,
int index)
: QWidget(parent),
_key(new QLabel("Key", this)),
_value(new QLabel("Value", this)),
_index(index)
{
auto layout = new QHBoxLayout();
layout->addWidget(_key);
layout->addWidget(_value);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
}
} // namespace advss

View File

@@ -0,0 +1,53 @@
#pragma once
#include "string-list.hpp"
#include "variable-line-edit.hpp"
namespace advss {
class KeyValueListEdit final : public ListEditor {
Q_OBJECT
public:
KeyValueListEdit(QWidget *parent, const QString &addKeyString,
const QString &addKeyStringDescription,
const QString &addValueString,
const QString &addValueStringDescription);
void SetStringList(const StringList &);
private slots:
void Add();
void Remove();
void Up();
void Down();
void Clicked(QListWidgetItem *);
signals:
void StringListChanged(const StringList &);
private:
void MoveStringListIdxUp(int);
bool AskForKeyValue(StringVariable &key, StringVariable &value);
void AppendListEntryWidget(const StringVariable &key,
const StringVariable &value);
StringList _stringList;
QString _addKeyString;
QString _addKeyStringDescription;
QString _addValueString;
QString _addValueStringDescription;
};
class KeyValueListContainerWidget final : public QWidget {
Q_OBJECT
public:
KeyValueListContainerWidget(QWidget *parent, int index);
private:
QLabel *_key;
QLabel *_value;
int _index = -1;
friend class KeyValueListEdit;
};
} // namespace advss

View File

@@ -0,0 +1,463 @@
#include "macro-action-http.hpp"
#include "layout-helpers.hpp"
#include <httplib.h>
#undef DELETE
namespace advss {
const std::string MacroActionHttp::id = "http_v2";
bool MacroActionHttp::_registered = MacroActionFactory::Register(
MacroActionHttp::id,
{MacroActionHttp::Create, MacroActionHttpEdit::Create,
"AdvSceneSwitcher.action.http"});
static httplib::Headers getHeaders(const StringList &strings)
{
httplib::Headers headers;
for (int i = 0; i < strings.size(); i = i + 2) {
const auto pair =
i + 1 >= strings.size()
? std::make_pair(std::string(strings.at(i)), "")
: std::make_pair(
std::string(strings.at(i)),
std::string(strings.at(i + 1)));
headers.emplace(pair);
}
return headers;
}
static httplib::Params getParams(const StringList &strings)
{
httplib::Params params;
for (int i = 0; i < strings.size(); i = i + 2) {
const auto pair =
i + 1 >= strings.size()
? std::make_pair(std::string(strings.at(i)), "")
: std::make_pair(
std::string(strings.at(i)),
std::string(strings.at(i + 1)));
params.emplace(pair);
}
return params;
}
static void setTimeout(httplib::Client &client, const Duration &timeout)
{
const time_t seconds = timeout.Seconds();
const time_t usecs = timeout.Milliseconds() * 1000;
client.set_read_timeout(seconds, usecs);
client.set_write_timeout(seconds, usecs);
}
void MacroActionHttp::SetupTempVars()
{
MacroAction::SetupTempVars();
AddTempvar("status",
obs_module_text("AdvSceneSwitcher.tempVar.http.status"));
AddTempvar("body",
obs_module_text("AdvSceneSwitcher.tempVar.http.body"));
AddTempvar("error",
obs_module_text("AdvSceneSwitcher.tempVar.http.error"),
obs_module_text(
"AdvSceneSwitcher.tempVar.http.error.description"));
}
bool MacroActionHttp::PerformAction()
{
httplib::Client cli(_url);
setTimeout(cli, _timeout);
const auto params = _setParams ? getParams(_params) : httplib::Params();
const auto headers = _setHeaders ? getHeaders(_headers)
: httplib::Headers();
httplib::Result response;
switch (_method) {
case MacroActionHttp::Method::GET:
response = cli.Get(_path, params, headers);
break;
case MacroActionHttp::Method::POST: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Post(path, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PUT: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Put(path, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PATCH: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Patch(path, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::DELETE: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Delete(path, headers, _body, _contentType);
break;
}
default:
break;
}
SetTempVarValue("status",
response ? std::to_string(response->status) : "");
SetTempVarValue("body", response ? response->body : "");
SetTempVarValue("error",
response ? "" : httplib::to_string(response.error()));
return true;
}
static constexpr std::string_view methodToString(MacroActionHttp::Method method)
{
switch (method) {
case MacroActionHttp::Method::GET:
return "GET";
case MacroActionHttp::Method::POST:
return "POST";
case MacroActionHttp::Method::PUT:
return "PUT";
case MacroActionHttp::Method::PATCH:
return "PATCH";
case MacroActionHttp::Method::DELETE:
return "DELETE";
default:
break;
}
return "unknown";
}
static std::string stringListToString(const StringList &list)
{
if (list.empty()) {
return "[]";
}
std::string result = "[";
for (const auto &string : list) {
result += std::string(string) + ", ";
}
result.pop_back();
return result + "]";
}
void MacroActionHttp::LogAction() const
{
ablog(LOG_INFO,
"sent HTTP request (%s) "
"to URL \"%s\" "
"to path \"%s\" "
"with content type \"%s\" "
"with body \"%s\" "
"with headers \"%s\" "
"with parameters \"%s\" "
"with timeout \"%s\"",
methodToString(_method).data(), _url.c_str(), _path.c_str(),
_contentType.c_str(), _body.c_str(),
_setHeaders ? stringListToString(_headers).c_str() : "-",
_setParams ? stringListToString(_params).c_str() : "-",
_timeout.ToString().c_str());
}
bool MacroActionHttp::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_url.Save(obj, "url");
_path.Save(obj, "path");
_contentType.Save(obj, "contentType");
_body.Save(obj, "body");
obs_data_set_bool(obj, "setHeaders", _setHeaders);
_headers.Save(obj, "headers", "header");
obs_data_set_bool(obj, "setParams", _setParams);
_params.Save(obj, "params", "param");
obs_data_set_int(obj, "method", static_cast<int>(_method));
_timeout.Save(obj);
return true;
}
bool MacroActionHttp::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_url.Load(obj, "url");
_path.Load(obj, "path");
_contentType.Load(obj, "contentType");
_body.Load(obj, "body");
_setHeaders = obs_data_get_bool(obj, "setHeaders");
_headers.Load(obj, "headers", "header");
_setParams = obs_data_get_bool(obj, "setParams");
_params.Load(obj, "params", "param");
_method = static_cast<Method>(obs_data_get_int(obj, "method"));
_timeout.Load(obj);
return true;
}
std::string MacroActionHttp::GetShortDesc() const
{
return _url.UnresolvedValue();
}
std::shared_ptr<MacroAction> MacroActionHttp::Create(Macro *m)
{
return std::make_shared<MacroActionHttp>(m);
}
std::shared_ptr<MacroAction> MacroActionHttp::Copy() const
{
return std::make_shared<MacroActionHttp>(*this);
}
void MacroActionHttp::ResolveVariablesToFixedValues()
{
_url.ResolveVariables();
_path.ResolveVariables();
_contentType.ResolveVariables();
_body.ResolveVariables();
_headers.ResolveVariables();
_params.ResolveVariables();
_timeout.ResolveVariables();
}
static inline void populateMethodSelection(QComboBox *list)
{
const static std::map<MacroActionHttp::Method, std::string> methods = {
{MacroActionHttp::Method::GET,
"AdvSceneSwitcher.action.http.type.get"},
{MacroActionHttp::Method::POST,
"AdvSceneSwitcher.action.http.type.post"},
{MacroActionHttp::Method::PUT,
"AdvSceneSwitcher.action.http.type.put"},
{MacroActionHttp::Method::PATCH,
"AdvSceneSwitcher.action.http.type.patch"},
{MacroActionHttp::Method::DELETE,
"AdvSceneSwitcher.action.http.type.delete"},
};
for (const auto &[value, name] : methods) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(value));
}
}
MacroActionHttpEdit::MacroActionHttpEdit(
QWidget *parent, std::shared_ptr<MacroActionHttp> entryData)
: QWidget(parent),
_url(new VariableLineEdit(this)),
_path(new VariableLineEdit(this)),
_contentType(new VariableLineEdit(this)),
_contentTypeLayout(new QHBoxLayout()),
_methods(new QComboBox()),
_body(new VariableTextEdit(this)),
_bodyLayout(new QVBoxLayout()),
_setHeaders(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.http.setHeaders"))),
_headerList(new KeyValueListEdit(
this, obs_module_text("AdvSceneSwitcher.action.http.headers"),
obs_module_text("AdvSceneSwitcher.action.http.addHeader.name"),
obs_module_text("AdvSceneSwitcher.action.http.headers"),
obs_module_text(
"AdvSceneSwitcher.action.http.addHeader.value"))),
_headerListLayout(new QVBoxLayout()),
_setParams(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.http.setParams"))),
_paramList(new KeyValueListEdit(
this, obs_module_text("AdvSceneSwitcher.action.http.params"),
obs_module_text("AdvSceneSwitcher.action.http.addParam.name"),
obs_module_text("AdvSceneSwitcher.action.http.params"),
obs_module_text(
"AdvSceneSwitcher.action.http.addParam.value"))),
_paramListLayout(new QVBoxLayout()),
_timeout(new DurationSelection(this, false))
{
populateMethodSelection(_methods);
SetWidgetSignalConnections();
SetWidgetLayout();
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionHttpEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_url->setText(_entryData->_url);
_path->setText(_entryData->_path);
_contentType->setText(_entryData->_contentType);
_body->setPlainText(_entryData->_body);
_setHeaders->setChecked(_entryData->_setHeaders);
_headerList->SetStringList(_entryData->_headers);
_setParams->setChecked(_entryData->_setParams);
_paramList->SetStringList(_entryData->_params);
_methods->setCurrentIndex(
_methods->findData(static_cast<int>(_entryData->_method)));
_timeout->SetDuration(_entryData->_timeout);
SetWidgetVisibility();
}
void MacroActionHttpEdit::URLChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_url = _url->text().toStdString();
emit(HeaderInfoChanged(_url->text()));
}
void MacroActionHttpEdit::PathChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_path = _path->text().toStdString();
}
void MacroActionHttpEdit::ContentTypeChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_contentType = _contentType->text().toStdString();
}
void MacroActionHttpEdit::BodyChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_body = _body->toPlainText().toUtf8().constData();
adjustSize();
updateGeometry();
}
void MacroActionHttpEdit::MethodChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_method = static_cast<MacroActionHttp::Method>(
_methods->itemData(idx).toInt());
SetWidgetVisibility();
}
void MacroActionHttpEdit::TimeoutChanged(const Duration &dur)
{
GUARD_LOADING_AND_LOCK();
_entryData->_timeout = dur;
}
void MacroActionHttpEdit::SetHeadersChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_setHeaders = value;
SetWidgetVisibility();
}
void MacroActionHttpEdit::HeadersChanged(const StringList &headers)
{
GUARD_LOADING_AND_LOCK();
_entryData->_headers = headers;
adjustSize();
updateGeometry();
}
void MacroActionHttpEdit::SetParamsChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_setParams = value;
SetWidgetVisibility();
}
void MacroActionHttpEdit::ParamsChanged(const StringList &params)
{
GUARD_LOADING_AND_LOCK();
_entryData->_params = params;
adjustSize();
updateGeometry();
}
void MacroActionHttpEdit::SetWidgetSignalConnections()
{
QWidget::connect(_url, SIGNAL(editingFinished()), this,
SLOT(URLChanged()));
QWidget::connect(_path, SIGNAL(editingFinished()), this,
SLOT(PathChanged()));
QWidget::connect(_contentType, SIGNAL(editingFinished()), this,
SLOT(ContentTypeChanged()));
QWidget::connect(_body, SIGNAL(textChanged()), this,
SLOT(BodyChanged()));
QWidget::connect(_methods, SIGNAL(currentIndexChanged(int)), this,
SLOT(MethodChanged(int)));
QWidget::connect(_setHeaders, SIGNAL(stateChanged(int)), this,
SLOT(SetHeadersChanged(int)));
QWidget::connect(_headerList,
SIGNAL(StringListChanged(const StringList &)), this,
SLOT(HeadersChanged(const StringList &)));
QWidget::connect(_setParams, SIGNAL(stateChanged(int)), this,
SLOT(SetParamsChanged(int)));
QWidget::connect(_paramList,
SIGNAL(StringListChanged(const StringList &)), this,
SLOT(ParamsChanged(const StringList &)));
QWidget::connect(_timeout, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(TimeoutChanged(const Duration &)));
}
void MacroActionHttpEdit::SetWidgetLayout()
{
const std::unordered_map<std::string, QWidget *> widgets = {
{"{{url}}", _url},
{"{{path}}", _path},
{"{{contentType}}", _contentType},
{"{{method}}", _methods},
{"{{body}}", _body},
{"{{timeout}}", _timeout},
};
auto actionLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.http.layout.method"),
actionLayout, widgets);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.http.layout.contentType"),
_contentTypeLayout, widgets);
_bodyLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.http.body")));
_bodyLayout->addWidget(_body);
auto timeoutLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.http.layout.timeout"),
timeoutLayout, widgets);
_headerListLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.http.headers")));
_headerListLayout->addWidget(_headerList);
_paramListLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.http.params")));
_paramListLayout->addWidget(_paramList);
auto layout = new QVBoxLayout;
layout->addLayout(actionLayout);
layout->addWidget(_setHeaders);
layout->addLayout(_headerListLayout);
layout->addWidget(_setParams);
layout->addLayout(_paramListLayout);
layout->addLayout(_contentTypeLayout);
layout->addLayout(_bodyLayout);
layout->addLayout(timeoutLayout);
setLayout(layout);
}
void MacroActionHttpEdit::SetWidgetVisibility()
{
SetLayoutVisible(_headerListLayout, _entryData->_setHeaders);
SetLayoutVisible(_paramListLayout, _entryData->_setParams);
SetLayoutVisible(_contentTypeLayout,
_entryData->_method != MacroActionHttp::Method::GET);
SetLayoutVisible(_bodyLayout,
_entryData->_method != MacroActionHttp::Method::GET);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,107 @@
#pragma once
#include "macro-action-edit.hpp"
#include "key-value-list.hpp"
#include "variable-text-edit.hpp"
#include "variable-line-edit.hpp"
#include "duration-control.hpp"
#include <QLineEdit>
#include <QComboBox>
#include <QCheckBox>
namespace advss {
class MacroActionHttp final : public MacroAction {
public:
MacroActionHttp(Macro *m) : MacroAction(m, true) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
enum class Method {
GET = 0,
POST,
PUT,
PATCH,
DELETE,
};
StringVariable _url = "127.0.0.1:8080";
StringVariable _path = "/";
StringVariable _body = obs_module_text("AdvSceneSwitcher.enterText");
StringVariable _contentType = "application/json";
bool _setHeaders = false;
StringList _headers;
bool _setParams = false;
StringList _params;
Method _method = Method::GET;
Duration _timeout = Duration(1.0);
private:
void SetupTempVars();
static bool _registered;
static const std::string id;
};
class MacroActionHttpEdit final : public QWidget {
Q_OBJECT
public:
MacroActionHttpEdit(
QWidget *parent,
std::shared_ptr<MacroActionHttp> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionHttpEdit(
parent,
std::dynamic_pointer_cast<MacroActionHttp>(action));
}
private slots:
void URLChanged();
void PathChanged();
void BodyChanged();
void ContentTypeChanged();
void MethodChanged(int);
void TimeoutChanged(const Duration &seconds);
void SetHeadersChanged(int);
void HeadersChanged(const StringList &);
void SetParamsChanged(int);
void ParamsChanged(const StringList &);
signals:
void HeaderInfoChanged(const QString &);
private:
void SetWidgetSignalConnections();
void SetWidgetLayout();
void SetWidgetVisibility();
VariableLineEdit *_url;
VariableLineEdit *_path;
VariableLineEdit *_contentType;
QHBoxLayout *_contentTypeLayout;
QComboBox *_methods;
VariableTextEdit *_body;
QVBoxLayout *_bodyLayout;
QCheckBox *_setHeaders;
KeyValueListEdit *_headerList;
QVBoxLayout *_headerListLayout;
QCheckBox *_setParams;
KeyValueListEdit *_paramList;
QVBoxLayout *_paramListLayout;
DurationSelection *_timeout;
std::shared_ptr<MacroActionHttp> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -19,9 +19,22 @@ add_library(${PROJECT_NAME} MODULE)
if(OS_WINDOWS)
if(MSVC)
target_compile_options(libremidi PRIVATE /wd4251 /wd4267 /wd4275 /wd4101
/wd4244 /wd4018)
target_compile_options(${PROJECT_NAME} PRIVATE /wd4101 /wd4244 /wd4018)
target_compile_options(
libremidi
PRIVATE /wd4018
/wd4068
/wd4100
/wd4101
/wd4189
/wd4244
/wd4251
/wd4267
/wd4275
/wd4389
/wd4505
/wd4702)
target_compile_options(${PROJECT_NAME} PRIVATE /wd4017 /wd4068 /wd4100
/wd4101 /wd4244)
endif()
else()
target_compile_options(

View File

@@ -10,8 +10,10 @@ if(NOT EXISTS "${CPP_HTTPLIB_DIR}/CMakeLists.txt")
"Twitch support will be disabled!")
return()
endif()
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
if(NOT TARGET httplib)
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
@@ -28,6 +30,31 @@ if(NOT ZLIB_FOUND)
return()
endif()
set(DATE_LIB_DIR "${ADVSS_SOURCE_DIR}/deps/date")
if(EXISTS "${DATE_LIB_DIR}/CMakeLists.txt")
set(BUILD_TZ_LIB ON)
if(OS_WINDOWS)
if(CURL_FOUND AND TARGET CURL::libcurl)
get_target_property(CURL_INCLUDE_DIR CURL::libcurl
INTERFACE_INCLUDE_DIRECTORIES)
add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build"
EXCLUDE_FROM_ALL)
target_include_directories(date-tz PRIVATE "${CURL_INCLUDE_DIR}")
set(VERIFY_TWITCH_TIMESTAMPS ON)
else()
message(WARNING "CURL not found - not verifying Twitch timestamps")
endif()
else()
add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build" EXCLUDE_FROM_ALL)
target_compile_options(date-tz PUBLIC -Wno-error=conversion
-Wno-error=shadow)
set(VERIFY_TWITCH_TIMESTAMPS ON)
endif()
else()
message(WARNING "date lib not found in \"${DATE_LIB_DIR}\"!\n"
"Twitch timestamps will not be checked!")
endif()
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
@@ -74,6 +101,14 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
"${OPENSSL_INCLUDE_DIR}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
if(DEFINED VERIFY_TWITCH_TIMESTAMPS)
target_compile_definitions(${PROJECT_NAME} PRIVATE VERIFY_TIMESTAMPS=1)
target_link_libraries(${PROJECT_NAME} PRIVATE date::date-tz)
if(OS_WINDOWS)
target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl)
endif()
endif()
install_advss_plugin(${PROJECT_NAME})
if(OS_WINDOWS)
# Couldn't really find a better way to install runtime dependencies for

View File

@@ -4,6 +4,10 @@
#include <log-helper.hpp>
#ifdef VERIFY_TIMESTAMPS
#include "date/tz.h"
#endif
namespace advss {
using websocketpp::lib::placeholders::_1;
@@ -237,15 +241,43 @@ void EventSub::OnOpen(connection_hdl)
static bool isValidTimestamp(const std::string &timestamp)
{
std::tm tm = {};
std::istringstream ss(timestamp);
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S.%fZ");
auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm));
tp += std::chrono::hours(1); // UTC
std::chrono::system_clock::time_point currentTime =
std::chrono::system_clock::now();
auto diff = currentTime - tp;
return diff <= std::chrono::minutes(10);
#ifdef VERIFY_TIMESTAMPS
// Example input: 2023-07-19T14:56:51.634234626Z
try {
// Discard the nanosecond part
static constexpr size_t dotPos = 19;
std::string trimmed = timestamp.substr(0, dotPos);
auto tzStart = timestamp.find_first_of("Z+-", dotPos);
trimmed = timestamp.substr(0, dotPos);
if (tzStart != std::string::npos) {
trimmed += timestamp.substr(tzStart);
}
std::istringstream in(trimmed);
date::sys_time<std::chrono::seconds> parsedTime;
in >> date::parse("%FT%TZ", parsedTime);
if (in.fail()) {
blog(LOG_WARNING, "failed to parse timestamp %s",
timestamp.c_str());
return false;
}
auto now = date::zoned_time{date::current_zone(),
std::chrono::system_clock::now()}
.get_sys_time();
auto duration = now - parsedTime;
// Clocks might be off by a bit, so allow negative values also
return duration <= std::chrono::minutes(10) &&
duration >= std::chrono::minutes(-1);
} catch (const std::exception &e) {
blog(LOG_WARNING, "%s: %s", __func__, e.what());
return false;
}
#else
// Just assume timestamps are always valid
return true;
#endif
}
bool EventSub::IsValidMessageID(const std::string &id)
@@ -288,7 +320,8 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
obs_data_get_string(metadata, "message_timestamp");
if (!isValidTimestamp(timestamp)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid timestamp");
"Discarding Twitch EventSub with invalid timestamp %s",
timestamp.c_str());
return;
}
std::string id = obs_data_get_string(metadata, "message_id");

View File

@@ -1,6 +1,7 @@
#include "points-reward-selection.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <obs-module-helper.hpp>
#include <ui-helpers.hpp>