From aa87911b7175d92bd2447caa02e9f7404e547768 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Sat, 29 Mar 2025 14:05:48 +0100 Subject: [PATCH] Use cpp-httplib based HTTP action type The goal is to remove the older, more limited version of the HTTP action at some point in the future. --- data/locale/en-US.ini | 19 + lib/utils/mouse-wheel-guard.cpp | 4 +- plugins/CMakeLists.txt | 1 + plugins/base/CMakeLists.txt | 4 +- ...-http.cpp => macro-action-http-legacy.cpp} | 4 +- ...-http.hpp => macro-action-http-legacy.hpp} | 0 plugins/http/CMakeLists.txt | 77 +++ plugins/http/key-value-list.cpp | 213 ++++++++ plugins/http/key-value-list.hpp | 53 ++ plugins/http/macro-action-http.cpp | 463 ++++++++++++++++++ plugins/http/macro-action-http.hpp | 107 ++++ plugins/twitch/CMakeLists.txt | 6 +- 12 files changed, 944 insertions(+), 7 deletions(-) rename plugins/base/{macro-action-http.cpp => macro-action-http-legacy.cpp} (98%) rename plugins/base/{macro-action-http.hpp => macro-action-http-legacy.hpp} (100%) create mode 100644 plugins/http/CMakeLists.txt create mode 100644 plugins/http/key-value-list.cpp create mode 100644 plugins/http/key-value-list.hpp create mode 100644 plugins/http/macro-action-http.cpp create mode 100644 plugins/http/macro-action-http.hpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 81e8b269..83dbe463 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1050,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" @@ -2040,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" diff --git a/lib/utils/mouse-wheel-guard.cpp b/lib/utils/mouse-wheel-guard.cpp index a002c339..7e33c01c 100644 --- a/lib/utils/mouse-wheel-guard.cpp +++ b/lib/utils/mouse-wheel-guard.cpp @@ -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); diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8df438e6..1900afd3 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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) diff --git a/plugins/base/CMakeLists.txt b/plugins/base/CMakeLists.txt index a066ba8c..536160bb 100644 --- a/plugins/base/CMakeLists.txt +++ b/plugins/base/CMakeLists.txt @@ -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 diff --git a/plugins/base/macro-action-http.cpp b/plugins/base/macro-action-http-legacy.cpp similarity index 98% rename from plugins/base/macro-action-http.cpp rename to plugins/base/macro-action-http-legacy.cpp index 8e63ab97..9b62b503 100644 --- a/plugins/base/macro-action-http.cpp +++ b/plugins/base/macro-action-http-legacy.cpp @@ -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 methods = { {MacroActionHttp::Method::GET, "AdvSceneSwitcher.action.http.type.get"}, diff --git a/plugins/base/macro-action-http.hpp b/plugins/base/macro-action-http-legacy.hpp similarity index 100% rename from plugins/base/macro-action-http.hpp rename to plugins/base/macro-action-http-legacy.hpp diff --git a/plugins/http/CMakeLists.txt b/plugins/http/CMakeLists.txt new file mode 100644 index 00000000..a0bad197 --- /dev/null +++ b/plugins/http/CMakeLists.txt @@ -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() diff --git a/plugins/http/key-value-list.cpp b/plugins/http/key-value-list.cpp new file mode 100644 index 00000000..dac6c0d2 --- /dev/null +++ b/plugins/http/key-value-list.cpp @@ -0,0 +1,213 @@ +#include "key-value-list.hpp" +#include "name-dialog.hpp" +#include "ui-helpers.hpp" + +#include +#include + +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( + _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 diff --git a/plugins/http/key-value-list.hpp b/plugins/http/key-value-list.hpp new file mode 100644 index 00000000..4800e2d0 --- /dev/null +++ b/plugins/http/key-value-list.hpp @@ -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 diff --git a/plugins/http/macro-action-http.cpp b/plugins/http/macro-action-http.cpp new file mode 100644 index 00000000..ae7d7f4c --- /dev/null +++ b/plugins/http/macro-action-http.cpp @@ -0,0 +1,463 @@ +#include "macro-action-http.hpp" +#include "layout-helpers.hpp" + +#include + +#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(_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(obs_data_get_int(obj, "method")); + _timeout.Load(obj); + return true; +} + +std::string MacroActionHttp::GetShortDesc() const +{ + return _url.UnresolvedValue(); +} + +std::shared_ptr MacroActionHttp::Create(Macro *m) +{ + return std::make_shared(m); +} + +std::shared_ptr MacroActionHttp::Copy() const +{ + return std::make_shared(*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 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(value)); + } +} + +MacroActionHttpEdit::MacroActionHttpEdit( + QWidget *parent, std::shared_ptr 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(_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( + _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 ¶ms) +{ + 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 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 diff --git a/plugins/http/macro-action-http.hpp b/plugins/http/macro-action-http.hpp new file mode 100644 index 00000000..837296f1 --- /dev/null +++ b/plugins/http/macro-action-http.hpp @@ -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 +#include +#include + +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 Create(Macro *m); + std::shared_ptr 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 entryData = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr action) + { + return new MacroActionHttpEdit( + parent, + std::dynamic_pointer_cast(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 _entryData; + bool _loading = true; +}; + +} // namespace advss diff --git a/plugins/twitch/CMakeLists.txt b/plugins/twitch/CMakeLists.txt index 125fea7d..61225d88 100644 --- a/plugins/twitch/CMakeLists.txt +++ b/plugins/twitch/CMakeLists.txt @@ -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)