From f12ef9c9f82cffc245a9112d1f82703a5873343a Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:30:00 +0100 Subject: [PATCH] Add HTTP condition --- data/locale/en-US.ini | 35 ++- plugins/http/CMakeLists.txt | 13 +- plugins/http/http-server-tab.cpp | 223 ++++++++++++++++ plugins/http/http-server-tab.hpp | 20 ++ plugins/http/http-server.cpp | 364 ++++++++++++++++++++++++++ plugins/http/http-server.hpp | 101 +++++++ plugins/http/macro-condition-http.cpp | 313 ++++++++++++++++++++++ plugins/http/macro-condition-http.hpp | 95 +++++++ 8 files changed, 1161 insertions(+), 3 deletions(-) create mode 100644 plugins/http/http-server-tab.cpp create mode 100644 plugins/http/http-server-tab.hpp create mode 100644 plugins/http/http-server.cpp create mode 100644 plugins/http/http-server.hpp create mode 100644 plugins/http/macro-condition-http.cpp create mode 100644 plugins/http/macro-condition-http.hpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index b02b8b5c..5dd25d86 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -676,6 +676,34 @@ AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event" AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions" AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:" AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:" +AdvSceneSwitcher.condition.http="HTTP" +AdvSceneSwitcher.condition.http.layout="Receive{{method}}request on{{server}}" +AdvSceneSwitcher.condition.http.layout.path="Path:{{path}}{{regex}}" +AdvSceneSwitcher.condition.http.layout.body="Body:{{body}}{{regex}}" +AdvSceneSwitcher.condition.http.method.any="any" +AdvSceneSwitcher.condition.http.method.get="GET" +AdvSceneSwitcher.condition.http.method.post="POST" +AdvSceneSwitcher.condition.http.method.put="PUT" +AdvSceneSwitcher.condition.http.method.patch="PATCH" +AdvSceneSwitcher.condition.http.method.delete="DELETE" +AdvSceneSwitcher.httpServer.select="Select HTTP server" +AdvSceneSwitcher.httpServer.add="Add HTTP server" +AdvSceneSwitcher.httpServer.configure="Configure HTTP server" +AdvSceneSwitcher.httpServer.invalid="Invalid HTTP server" +AdvSceneSwitcher.httpServer.name="Name:" +AdvSceneSwitcher.httpServer.port="Port:" +AdvSceneSwitcher.httpServer.startOnLoad="Start on load" +AdvSceneSwitcher.httpServer.status.listening="Listening" +AdvSceneSwitcher.httpServer.status.stopped="Stopped" +AdvSceneSwitcher.httpServerTab.title="HTTP Servers" +AdvSceneSwitcher.httpServerTab.help="No HTTP servers configured.\nAdd one to receive incoming HTTP requests in conditions." +AdvSceneSwitcher.httpServerTab.addButton.tooltip="Add HTTP server" +AdvSceneSwitcher.httpServerTab.removeButton.tooltip="Remove HTTP server" +AdvSceneSwitcher.httpServerTab.name.header="Name" +AdvSceneSwitcher.httpServerTab.port.header="Port" +AdvSceneSwitcher.httpServerTab.status.header="Status" +AdvSceneSwitcher.httpServerTab.removeSingle.text="Are you sure you want to remove \"%1\"?" +AdvSceneSwitcher.httpServerTab.removeMultiple.text="Are you sure you want to remove %1 HTTP servers?" AdvSceneSwitcher.condition.temporaryVariable="Macro property" AdvSceneSwitcher.condition.variable="Variable" AdvSceneSwitcher.condition.variable.type.compare="equals" @@ -2380,9 +2408,14 @@ 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.tempVar.http.method="Method" +AdvSceneSwitcher.tempVar.http.method.description="Received HTTP request method" +AdvSceneSwitcher.tempVar.http.path="Request path" +AdvSceneSwitcher.tempVar.http.path.description="Received HTTP request path" +AdvSceneSwitcher.tempVar.http.body="Message body" +AdvSceneSwitcher.tempVar.http.body.description="Received HTTP request body" AdvSceneSwitcher.tempVar.mqtt.message="Message" diff --git a/plugins/http/CMakeLists.txt b/plugins/http/CMakeLists.txt index 7b2de9ef..4deb16ba 100644 --- a/plugins/http/CMakeLists.txt +++ b/plugins/http/CMakeLists.txt @@ -48,8 +48,17 @@ if(MSVC) endif() target_sources( - ${PROJECT_NAME} PRIVATE macro-action-http.cpp macro-action-http.hpp - key-value-list.cpp key-value-list.hpp) + ${PROJECT_NAME} + PRIVATE macro-action-http.cpp + macro-action-http.hpp + macro-condition-http.cpp + macro-condition-http.hpp + http-server.cpp + http-server.hpp + http-server-tab.cpp + http-server-tab.hpp + key-value-list.cpp + key-value-list.hpp) setup_advss_plugin(${PROJECT_NAME}) set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") diff --git a/plugins/http/http-server-tab.cpp b/plugins/http/http-server-tab.cpp new file mode 100644 index 00000000..e254b722 --- /dev/null +++ b/plugins/http/http-server-tab.cpp @@ -0,0 +1,223 @@ +#include "http-server-tab.hpp" +#include "http-server.hpp" +#include "obs-module-helper.hpp" +#include "sync-helpers.hpp" +#include "tab-helpers.hpp" +#include "ui-helpers.hpp" + +#include +#include + +namespace advss { + +static bool registerTab(); +static void setupTab(QTabWidget *); +static bool registerTabDone = registerTab(); + +static HttpServersTable *tabWidget = nullptr; + +static bool registerTab() +{ + AddSetupTabCallback("httpServerTab", HttpServersTable::Create, + setupTab); + return true; +} + +static void setTabVisible(QTabWidget *tab, bool visible) +{ + SetTabVisibleByName( + tab, visible, + obs_module_text("AdvSceneSwitcher.httpServerTab.title")); +} + +HttpServersTable *HttpServersTable::Create() +{ + tabWidget = new HttpServersTable(); + return tabWidget; +} + +void HttpServersTable::Add() +{ + auto newServer = std::make_shared(); + bool accepted = HttpServerSettingsDialog::AskForSettings( + GetSettingsWindow(), *newServer); + if (!accepted) { + return; + } + + { + auto lock = LockContext(); + GetHttpServers().emplace_back(newServer); + } + + HttpServerSignalManager::Instance()->Add( + QString::fromStdString(newServer->Name())); +} + +void HttpServersTable::Remove() +{ + auto selectedRows = + tabWidget->Table()->selectionModel()->selectedRows(); + if (selectedRows.empty()) { + return; + } + + QStringList serverNames; + for (const auto &row : selectedRows) { + auto cell = tabWidget->Table()->item(row.row(), 0); + if (!cell) { + continue; + } + serverNames << cell->text(); + } + + const int count = serverNames.size(); + if (count == 1) { + const QString msg = obs_module_text( + "AdvSceneSwitcher.httpServerTab.removeSingle.text"); + if (!DisplayMessage(msg.arg(serverNames.at(0)), true)) { + return; + } + } else { + const QString msg = obs_module_text( + "AdvSceneSwitcher.httpServerTab.removeMultiple.text"); + if (!DisplayMessage(msg.arg(count), true)) { + return; + } + } + + { + auto lock = LockContext(); + RemoveItemsByName(GetHttpServers(), serverNames); + } + + for (const auto &name : serverNames) { + HttpServerSignalManager::Instance()->Remove(name); + } +} + +static QStringList getCellLabels(HttpServer *server, bool addName = true) +{ + assert(server); + QStringList result; + if (addName) { + result << QString::fromStdString(server->Name()); + } + result << QString::number(server->GetPort()) + << obs_module_text( + server->IsListening() + ? "AdvSceneSwitcher.httpServer.status.listening" + : "AdvSceneSwitcher.httpServer.status.stopped"); + return result; +} + +static void updateServerStatus(QTableWidget *table) +{ + for (int row = 0; row < table->rowCount(); ++row) { + auto item = table->item(row, 0); + if (!item) { + continue; + } + auto weakServer = GetWeakHttpServerByQString(item->text()); + auto server = weakServer.lock(); + if (!server) { + continue; + } + UpdateItemTableRow(table, row, + getCellLabels(server.get(), false)); + } +} + +static void openSettingsDialog() +{ + auto selectedRows = + tabWidget->Table()->selectionModel()->selectedRows(); + if (selectedRows.empty()) { + return; + } + + auto cell = tabWidget->Table()->item(selectedRows.last().row(), 0); + if (!cell) { + return; + } + + auto weakServer = GetWeakHttpServerByQString(cell->text()); + auto server = weakServer.lock(); + if (!server) { + return; + } + + const auto oldName = server->Name(); + bool accepted = HttpServerSettingsDialog::AskForSettings( + GetSettingsWindow(), *server); + if (accepted && oldName != server->Name()) { + HttpServerSignalManager::Instance()->Rename( + QString::fromStdString(oldName), + QString::fromStdString(server->Name())); + } + updateServerStatus(tabWidget->Table()); +} + +static const QStringList headers = + QStringList() + << obs_module_text("AdvSceneSwitcher.httpServerTab.name.header") + << obs_module_text("AdvSceneSwitcher.httpServerTab.port.header") + << obs_module_text("AdvSceneSwitcher.httpServerTab.status.header"); + +HttpServersTable::HttpServersTable(QTabWidget *parent) + : ResourceTable( + parent, + obs_module_text("AdvSceneSwitcher.httpServerTab.help"), + obs_module_text( + "AdvSceneSwitcher.httpServerTab.addButton.tooltip"), + obs_module_text( + "AdvSceneSwitcher.httpServerTab.removeButton.tooltip"), + headers, openSettingsDialog) +{ + for (const auto &s : GetHttpServers()) { + auto server = std::static_pointer_cast(s); + AddItemTableRow(Table(), getCellLabels(server.get())); + } + SetHelpVisible(GetHttpServers().empty()); +} + +static void setupTab(QTabWidget *tab) +{ + if (GetHttpServers().empty()) { + setTabVisible(tab, false); + } + + QWidget::connect(HttpServerSignalManager::Instance(), + &HttpServerSignalManager::Rename, tab, + [](const QString &oldName, const QString &newName) { + RenameItemTableRow(tabWidget->Table(), oldName, + newName); + }); + QWidget::connect( + HttpServerSignalManager::Instance(), + &HttpServerSignalManager::Add, tab, [tab](const QString &name) { + AddItemTableRow(tabWidget->Table(), + getCellLabels(GetHttpServerByName( + name.toStdString()))); + tabWidget->SetHelpVisible(false); + tabWidget->HighlightAddButton(false); + setTabVisible(tab, true); + }); + QWidget::connect(HttpServerSignalManager::Instance(), + &HttpServerSignalManager::Remove, tab, + [](const QString &name) { + RemoveItemTableRow(tabWidget->Table(), name); + if (tabWidget->Table()->rowCount() == 0) { + tabWidget->SetHelpVisible(true); + tabWidget->HighlightAddButton(true); + } + }); + + auto timer = new QTimer(tabWidget); + timer->setInterval(1000); + QWidget::connect(timer, &QTimer::timeout, + []() { updateServerStatus(tabWidget->Table()); }); + timer->start(); +} + +} // namespace advss diff --git a/plugins/http/http-server-tab.hpp b/plugins/http/http-server-tab.hpp new file mode 100644 index 00000000..8b25162d --- /dev/null +++ b/plugins/http/http-server-tab.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "resource-table.hpp" + +namespace advss { + +class HttpServersTable final : public ResourceTable { + Q_OBJECT + +public: + static HttpServersTable *Create(); + +private slots: + void Add(); + void Remove(); + +private: + HttpServersTable(QTabWidget *parent = nullptr); +}; + +} // namespace advss diff --git a/plugins/http/http-server.cpp b/plugins/http/http-server.cpp new file mode 100644 index 00000000..4117b543 --- /dev/null +++ b/plugins/http/http-server.cpp @@ -0,0 +1,364 @@ +#include "http-server.hpp" +#include "layout-helpers.hpp" +#include "log-helper.hpp" +#include "obs-module-helper.hpp" +#include "plugin-state-helpers.hpp" + +#include +#include + +#undef DispatchMessage + +Q_DECLARE_METATYPE(advss::HttpServer *); + +namespace advss { + +static std::deque> httpServers; +static void saveHttpServers(obs_data_t *obj); +static void loadHttpServers(obs_data_t *obj); +static bool setup(); +static bool setupDone = setup(); + +bool setup() +{ + AddSaveStep(saveHttpServers); + AddLoadStep(loadHttpServers); + AddPluginCleanupStep([]() { + for (auto &s : httpServers) { + auto server = std::dynamic_pointer_cast(s); + if (server) { + server->Stop(); + } + } + httpServers.clear(); + }); + return true; +} + +static void saveHttpServers(obs_data_t *obj) +{ + auto array = obs_data_array_create(); + for (const auto &s : httpServers) { + auto item = obs_data_create(); + s->Save(item); + obs_data_array_push_back(array, item); + obs_data_release(item); + } + obs_data_set_array(obj, "httpServers", array); + obs_data_array_release(array); +} + +static void loadHttpServers(obs_data_t *obj) +{ + httpServers.clear(); + auto array = obs_data_get_array(obj, "httpServers"); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; ++i) { + auto item = obs_data_array_item(array, i); + auto server = HttpServer::Create(); + httpServers.emplace_back(server); + httpServers.back()->Load(item); + obs_data_release(item); + } + obs_data_array_release(array); +} + +struct HttpServer::Impl { + std::unique_ptr server; + std::thread thread; + std::mutex mutex; + std::atomic_bool listening{false}; + MessageDispatcher dispatcher; +}; + +HttpServer::HttpServer() : _impl(std::make_unique()) {} + +HttpServer::HttpServer(const HttpServer &other) + : Item(other), + _impl(std::make_unique()) +{ + _port = other._port; + _startOnLoad = other._startOnLoad; +} + +HttpServer &HttpServer::operator=(const HttpServer &other) +{ + if (this != &other) { + Stop(); + _name = other._name; + _port = other._port; + _startOnLoad = other._startOnLoad; + } + return *this; +} + +HttpServer::~HttpServer() +{ + Stop(); +} + +bool HttpServer::IsListening() const +{ + return _impl->listening.load(); +} + +void HttpServer::Start() +{ + std::lock_guard lock(_impl->mutex); + + if (_impl->server) { + _impl->server->stop(); + } + if (_impl->thread.joinable()) { + _impl->thread.join(); + } + + _impl->server = std::make_unique(); + _impl->listening.store(false); + + auto handleRequest = [this](const httplib::Request &req, + httplib::Response &res) { + HttpRequest request; + request.method = req.method; + request.path = req.path; + request.body = req.body; + for (const auto &[name, value] : req.headers) { + request.headers.emplace(name, value); + } + _impl->dispatcher.DispatchMessage(request); + res.status = 200; + }; + + _impl->server->Get(".*", handleRequest); + _impl->server->Post(".*", handleRequest); + _impl->server->Put(".*", handleRequest); + _impl->server->Patch(".*", handleRequest); + _impl->server->Delete(".*", handleRequest); + + const int port = _port; + const std::string name = _name; + auto serverPtr = _impl->server.get(); + auto listeningFlag = &_impl->listening; + + _impl->thread = std::thread([serverPtr, listeningFlag, port, name]() { + if (!serverPtr->bind_to_port("0.0.0.0", port)) { + blog(LOG_WARNING, + "HTTP server \"%s\" failed to bind to port %d", + name.c_str(), port); + return; + } + listeningFlag->store(true); + blog(LOG_INFO, "HTTP server \"%s\" listening on port %d", + name.c_str(), port); + serverPtr->listen_after_bind(); + listeningFlag->store(false); + blog(LOG_INFO, "HTTP server \"%s\" stopped", name.c_str()); + }); +} + +void HttpServer::Stop() +{ + if (!_impl) { + return; + } + std::lock_guard lock(_impl->mutex); + if (_impl->server) { + _impl->server->stop(); + } + if (_impl->thread.joinable()) { + _impl->thread.join(); + } + _impl->server.reset(); + _impl->listening.store(false); +} + +HttpRequestBuffer HttpServer::RegisterForRequests() +{ + return _impl->dispatcher.RegisterClient(); +} + +void HttpServer::Load(obs_data_t *obj) +{ + Item::Load(obj); + _port = (int)obs_data_get_int(obj, "port"); + _startOnLoad = obs_data_get_bool(obj, "startOnLoad"); + if (_startOnLoad) { + Start(); + } +} + +void HttpServer::Save(obs_data_t *obj) const +{ + Item::Save(obj); + obs_data_set_int(obj, "port", _port); + obs_data_set_bool(obj, "startOnLoad", _startOnLoad); +} + +HttpServer *GetHttpServerByName(const std::string &name) +{ + for (auto &s : httpServers) { + if (s->Name() == name) { + return dynamic_cast(s.get()); + } + } + return nullptr; +} + +std::weak_ptr GetWeakHttpServerByName(const std::string &name) +{ + for (const auto &s : httpServers) { + if (s->Name() == name) { + return std::dynamic_pointer_cast(s); + } + } + return {}; +} + +std::weak_ptr GetWeakHttpServerByQString(const QString &name) +{ + return GetWeakHttpServerByName(name.toStdString()); +} + +std::string GetWeakHttpServerName(const std::weak_ptr &server) +{ + auto s = server.lock(); + if (!s) { + return obs_module_text("AdvSceneSwitcher.httpServer.invalid"); + } + return s->Name(); +} + +std::deque> &GetHttpServers() +{ + return httpServers; +} + +// --- HttpServerSettingsDialog --- + +static bool ServerNameAvailable(const QString &name) +{ + return !GetHttpServerByName(name.toStdString()); +} + +static bool AskForSettingsWrapper(QWidget *parent, Item &settings) +{ + HttpServer &server = dynamic_cast(settings); + return HttpServerSettingsDialog::AskForSettings(parent, server); +} + +HttpServerSettingsDialog::HttpServerSettingsDialog(QWidget *parent, + const HttpServer &settings) + : ItemSettingsDialog(settings, httpServers, + "AdvSceneSwitcher.httpServer.select", + "AdvSceneSwitcher.httpServer.add", + "AdvSceneSwitcher.item.nameNotAvailable", true, + parent), + _port(new QSpinBox()), + _startOnLoad(new QCheckBox()), + _layout(new QGridLayout()) +{ + _port->setMinimum(1); + _port->setMaximum(65535); + _port->setValue(settings._port); + _startOnLoad->setChecked(settings._startOnLoad); + + int row = 0; + _layout->addWidget( + new QLabel(obs_module_text("AdvSceneSwitcher.httpServer.name")), + row, 0); + auto nameLayout = new QHBoxLayout; + nameLayout->addWidget(_name); + nameLayout->addWidget(_nameHint); + _layout->addLayout(nameLayout, row, 1); + ++row; + _layout->addWidget( + new QLabel(obs_module_text("AdvSceneSwitcher.httpServer.port")), + row, 0); + _layout->addWidget(_port, row, 1); + ++row; + _layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.httpServer.startOnLoad")), + row, 0); + _layout->addWidget(_startOnLoad, row, 1); + ++row; + _layout->addWidget(_buttonbox, row, 0, 1, -1); + setLayout(_layout); + + MinimizeSizeOfColumn(_layout, 0); +} + +bool HttpServerSettingsDialog::AskForSettings(QWidget *parent, + HttpServer &settings) +{ + HttpServerSettingsDialog dialog(parent, settings); + dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); + if (dialog.exec() != DialogCode::Accepted) { + return false; + } + settings._name = dialog._name->text().toStdString(); + settings._port = dialog._port->value(); + settings._startOnLoad = dialog._startOnLoad->isChecked(); + settings.Start(); + return true; +} + +HttpServerSelection::HttpServerSelection(QWidget *parent) + : ItemSelection(httpServers, HttpServer::Create, AskForSettingsWrapper, + "AdvSceneSwitcher.httpServer.select", + "AdvSceneSwitcher.httpServer.add", + "AdvSceneSwitcher.item.nameNotAvailable", + "AdvSceneSwitcher.httpServer.configure", parent) +{ + QWidget::connect(HttpServerSignalManager::Instance(), + SIGNAL(Rename(const QString &, const QString &)), this, + SLOT(RenameItem(const QString &, const QString &))); + QWidget::connect(HttpServerSignalManager::Instance(), + SIGNAL(Add(const QString &)), this, + SLOT(AddItem(const QString &))); + QWidget::connect(HttpServerSignalManager::Instance(), + SIGNAL(Remove(const QString &)), this, + SLOT(RemoveItem(const QString &))); + + QWidget::connect(this, + SIGNAL(ItemRenamed(const QString &, const QString &)), + HttpServerSignalManager::Instance(), + SIGNAL(Rename(const QString &, const QString &))); + QWidget::connect(this, SIGNAL(ItemAdded(const QString &)), + HttpServerSignalManager::Instance(), + SIGNAL(Add(const QString &))); + QWidget::connect(this, SIGNAL(ItemRemoved(const QString &)), + HttpServerSignalManager::Instance(), + SIGNAL(Remove(const QString &))); +} + +void HttpServerSelection::SetServer(const std::string &name) +{ + if (GetHttpServerByName(name)) { + SetItem(name); + } else { + SetItem(""); + } +} + +void HttpServerSelection::SetServer(const std::weak_ptr &server) +{ + auto s = server.lock(); + if (s) { + SetItem(s->Name()); + } else { + SetItem(""); + } +} + +HttpServerSignalManager::HttpServerSignalManager(QObject *parent) + : QObject(parent) +{ +} + +HttpServerSignalManager *HttpServerSignalManager::Instance() +{ + static HttpServerSignalManager manager; + return &manager; +} + +} // namespace advss diff --git a/plugins/http/http-server.hpp b/plugins/http/http-server.hpp new file mode 100644 index 00000000..b910c79e --- /dev/null +++ b/plugins/http/http-server.hpp @@ -0,0 +1,101 @@ +#pragma once +#include "item-selection-helpers.hpp" +#include "message-buffer.hpp" +#include "message-dispatcher.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace advss { + +struct HttpRequest { + std::string method; + std::string path; + std::string body; + std::map headers; +}; + +using HttpRequestBuffer = std::shared_ptr>; + +class HttpServerSelection; +class HttpServerSettingsDialog; + +class HttpServer : public Item { +public: + HttpServer(); + HttpServer(const HttpServer &); + HttpServer &operator=(const HttpServer &); + ~HttpServer(); + + static std::shared_ptr Create() + { + return std::make_shared(); + } + + void Load(obs_data_t *obj); + void Save(obs_data_t *obj) const; + + HttpRequestBuffer RegisterForRequests(); + int GetPort() const { return _port; } + bool IsListening() const; + + void Start(); + void Stop(); + +private: + int _port = 16384; + bool _startOnLoad = true; + + struct Impl; + std::unique_ptr _impl; + + friend HttpServerSelection; + friend HttpServerSettingsDialog; +}; + +class HttpServerSettingsDialog : public ItemSettingsDialog { + Q_OBJECT + +public: + HttpServerSettingsDialog(QWidget *parent, const HttpServer &); + static bool AskForSettings(QWidget *parent, HttpServer &settings); + +private: + QSpinBox *_port; + QCheckBox *_startOnLoad; + QGridLayout *_layout; +}; + +class HttpServerSelection : public ItemSelection { + Q_OBJECT + +public: + HttpServerSelection(QWidget *parent = nullptr); + void SetServer(const std::string &); + void SetServer(const std::weak_ptr &); +}; + +class HttpServerSignalManager : public QObject { + Q_OBJECT +public: + HttpServerSignalManager(QObject *parent = nullptr); + static HttpServerSignalManager *Instance(); + +signals: + void Rename(const QString &, const QString &); + void Add(const QString &); + void Remove(const QString &); +}; + +HttpServer *GetHttpServerByName(const std::string &); +std::weak_ptr GetWeakHttpServerByName(const std::string &); +std::weak_ptr GetWeakHttpServerByQString(const QString &); +std::string GetWeakHttpServerName(const std::weak_ptr &); +std::deque> &GetHttpServers(); + +} // namespace advss diff --git a/plugins/http/macro-condition-http.cpp b/plugins/http/macro-condition-http.cpp new file mode 100644 index 00000000..750e7aae --- /dev/null +++ b/plugins/http/macro-condition-http.cpp @@ -0,0 +1,313 @@ +#include "macro-condition-http.hpp" +#include "layout-helpers.hpp" +#include "macro-helpers.hpp" + +#undef DELETE + +namespace advss { + +const std::string MacroConditionHttp::id = "http"; + +bool MacroConditionHttp::_registered = MacroConditionFactory::Register( + MacroConditionHttp::id, + {MacroConditionHttp::Create, MacroConditionHttpEdit::Create, + "AdvSceneSwitcher.condition.http"}); + +static const std::map methodLabels = { + {MacroConditionHttp::Method::ANY, + "AdvSceneSwitcher.condition.http.method.any"}, + {MacroConditionHttp::Method::GET, + "AdvSceneSwitcher.condition.http.method.get"}, + {MacroConditionHttp::Method::POST, + "AdvSceneSwitcher.condition.http.method.post"}, + {MacroConditionHttp::Method::PUT, + "AdvSceneSwitcher.condition.http.method.put"}, + {MacroConditionHttp::Method::PATCH, + "AdvSceneSwitcher.condition.http.method.patch"}, + {MacroConditionHttp::Method::DELETE, + "AdvSceneSwitcher.condition.http.method.delete"}, +}; + +static std::string_view methodToString(MacroConditionHttp::Method method) +{ + switch (method) { + case MacroConditionHttp::Method::GET: + return "GET"; + case MacroConditionHttp::Method::POST: + return "POST"; + case MacroConditionHttp::Method::PUT: + return "PUT"; + case MacroConditionHttp::Method::PATCH: + return "PATCH"; + case MacroConditionHttp::Method::DELETE: + return "DELETE"; + default: + break; + } + return ""; +} + +void MacroConditionHttp::SetServer(const std::string &name) +{ + _server = GetWeakHttpServerByName(name); + auto server = _server.lock(); + if (!server) { + _requestBuffer.reset(); + return; + } + _requestBuffer = server->RegisterForRequests(); +} + +bool MacroConditionHttp::CheckCondition() +{ + if (!_requestBuffer) { + return false; + } + + const bool macroWasPausedSinceLastCheck = + MacroWasPausedSince(GetMacro(), _lastCheck); + _lastCheck = std::chrono::high_resolution_clock::now(); + if (macroWasPausedSinceLastCheck) { + _requestBuffer->Clear(); + return false; + } + + while (!_requestBuffer->Empty()) { + auto request = _requestBuffer->ConsumeMessage(); + if (!request) { + continue; + } + + SetTempVarValue("method", request->method); + SetTempVarValue("path", request->path); + SetTempVarValue("body", request->body); + + if (_method != Method::ANY && + request->method != methodToString(_method)) { + continue; + } + + const std::string pathPattern = std::string(_path); + if (_pathRegex.Enabled()) { + if (!_pathRegex.Matches(request->path, _path)) { + continue; + } + } else { + if (request->path != pathPattern) { + continue; + } + } + + const std::string bodyPattern = std::string(_body); + if (_bodyRegex.Enabled()) { + if (!_bodyRegex.Matches(request->body, _body)) { + continue; + } + } else { + if (request->body != bodyPattern) { + continue; + } + } + + if (_clearBufferOnMatch) { + _requestBuffer->Clear(); + } + return true; + } + + return false; +} + +bool MacroConditionHttp::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + obs_data_set_int(obj, "method", static_cast(_method)); + _path.Save(obj, "path"); + _pathRegex.Save(obj, "pathRegex"); + _body.Save(obj, "body"); + _bodyRegex.Save(obj, "bodyRegex"); + obs_data_set_bool(obj, "clearBufferOnMatch", _clearBufferOnMatch); + auto server = _server.lock(); + obs_data_set_string(obj, "server", + server ? server->Name().c_str() : ""); + return true; +} + +bool MacroConditionHttp::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _method = static_cast(obs_data_get_int(obj, "method")); + _path.Load(obj, "path"); + _pathRegex.Load(obj, "pathRegex"); + _body.Load(obj, "body"); + _bodyRegex.Load(obj, "bodyRegex"); + _clearBufferOnMatch = obs_data_get_bool(obj, "clearBufferOnMatch"); + if (!obs_data_has_user_value(obj, "clearBufferOnMatch")) { + _clearBufferOnMatch = true; + } + SetServer(obs_data_get_string(obj, "server")); + return true; +} + +std::string MacroConditionHttp::GetShortDesc() const +{ + auto server = _server.lock(); + return server ? server->Name() : ""; +} + +void MacroConditionHttp::SetupTempVars() +{ + MacroCondition::SetupTempVars(); + AddTempvar("method", + obs_module_text("AdvSceneSwitcher.tempVar.http.method"), + obs_module_text( + "AdvSceneSwitcher.tempVar.http.method.description")); + AddTempvar("path", + obs_module_text("AdvSceneSwitcher.tempVar.http.path"), + obs_module_text( + "AdvSceneSwitcher.tempVar.http.path.description")); + AddTempvar("body", + obs_module_text("AdvSceneSwitcher.tempVar.http.body"), + obs_module_text( + "AdvSceneSwitcher.tempVar.http.body.description")); +} + +static void populateMethodSelection(QComboBox *list) +{ + for (const auto &[value, name] : methodLabels) { + list->addItem(obs_module_text(name.c_str()), + static_cast(value)); + } +} + +MacroConditionHttpEdit::MacroConditionHttpEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _method(new QComboBox(this)), + _server(new HttpServerSelection(this)), + _path(new VariableLineEdit(this)), + _pathRegex(new RegexConfigWidget(this)), + _body(new VariableTextEdit(this)), + _bodyRegex(new RegexConfigWidget(this)), + _clearBufferOnMatch(new QCheckBox( + obs_module_text("AdvSceneSwitcher.clearBufferOnMatch"))) +{ + populateMethodSelection(_method); + + QWidget::connect(_method, SIGNAL(currentIndexChanged(int)), this, + SLOT(MethodChanged(int))); + QWidget::connect(_server, SIGNAL(SelectionChanged(const QString &)), + this, SLOT(ServerChanged(const QString &))); + QWidget::connect(_path, SIGNAL(editingFinished()), this, + SLOT(PathChanged())); + QWidget::connect(_pathRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(PathRegexChanged(const RegexConfig &))); + QWidget::connect(_body, SIGNAL(textChanged()), this, + SLOT(BodyChanged())); + QWidget::connect(_bodyRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(BodyRegexChanged(const RegexConfig &))); + QWidget::connect(_clearBufferOnMatch, SIGNAL(stateChanged(int)), this, + SLOT(ClearBufferOnMatchChanged(int))); + + auto topLayout = new QHBoxLayout; + PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.http.layout"), + topLayout, + {{"{{method}}", _method}, {"{{server}}", _server}}); + + auto pathLayout = new QHBoxLayout; + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.condition.http.layout.path"), + pathLayout, {{"{{path}}", _path}, {"{{regex}}", _pathRegex}}, + false); + + auto bodyLayout = new QHBoxLayout; + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.condition.http.layout.body"), + bodyLayout, {{"{{body}}", _body}, {"{{regex}}", _bodyRegex}}, + false); + + auto mainLayout = new QVBoxLayout; + mainLayout->addLayout(topLayout); + mainLayout->addLayout(pathLayout); + mainLayout->addLayout(bodyLayout); + mainLayout->addWidget(_clearBufferOnMatch); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionHttpEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _method->setCurrentIndex( + _method->findData(static_cast(_entryData->_method))); + _server->SetServer(_entryData->GetServer()); + _path->setText(_entryData->_path); + _pathRegex->SetRegexConfig(_entryData->_pathRegex); + _body->setPlainText(_entryData->_body); + _bodyRegex->SetRegexConfig(_entryData->_bodyRegex); + _clearBufferOnMatch->setChecked(_entryData->_clearBufferOnMatch); + + adjustSize(); + updateGeometry(); +} + +void MacroConditionHttpEdit::MethodChanged(int idx) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_method = static_cast( + _method->itemData(idx).toInt()); +} + +void MacroConditionHttpEdit::ServerChanged(const QString &name) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->SetServer(name.toStdString()); + emit HeaderInfoChanged( + QString::fromStdString(_entryData->GetShortDesc())); +} + +void MacroConditionHttpEdit::PathChanged() +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_path = _path->text().toStdString(); +} + +void MacroConditionHttpEdit::PathRegexChanged(const RegexConfig &conf) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_pathRegex = conf; + adjustSize(); + updateGeometry(); +} + +void MacroConditionHttpEdit::BodyChanged() +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_body = _body->toPlainText().toUtf8().constData(); + adjustSize(); + updateGeometry(); +} + +void MacroConditionHttpEdit::BodyRegexChanged(const RegexConfig &conf) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_bodyRegex = conf; + adjustSize(); + updateGeometry(); +} + +void MacroConditionHttpEdit::ClearBufferOnMatchChanged(int value) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_clearBufferOnMatch = value; +} + +} // namespace advss diff --git a/plugins/http/macro-condition-http.hpp b/plugins/http/macro-condition-http.hpp new file mode 100644 index 00000000..be1b042a --- /dev/null +++ b/plugins/http/macro-condition-http.hpp @@ -0,0 +1,95 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "http-server.hpp" +#include "variable-line-edit.hpp" +#include "variable-text-edit.hpp" +#include "regex-config.hpp" + +#include +#include + +namespace advss { + +class MacroConditionHttp : public MacroCondition { +public: + MacroConditionHttp(Macro *m) : MacroCondition(m, true) {} + bool CheckCondition(); + 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) + { + return std::make_shared(m); + } + + enum class Method { + ANY = 0, + GET, + POST, + PUT, + PATCH, + DELETE, + }; + + void SetServer(const std::string &name); + std::weak_ptr GetServer() const { return _server; } + + StringVariable _path = ".*"; + StringVariable _body = ".*"; + RegexConfig _pathRegex = RegexConfig::PartialMatchRegexConfig(true); + RegexConfig _bodyRegex = RegexConfig::PartialMatchRegexConfig(true); + Method _method = Method::ANY; + bool _clearBufferOnMatch = true; + +private: + void SetupTempVars(); + + std::weak_ptr _server; + HttpRequestBuffer _requestBuffer; + std::chrono::high_resolution_clock::time_point _lastCheck{}; + + static bool _registered; + static const std::string id; +}; + +class MacroConditionHttpEdit : public QWidget { + Q_OBJECT +public: + MacroConditionHttpEdit( + QWidget *parent, + std::shared_ptr cond = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionHttpEdit( + parent, + std::dynamic_pointer_cast(cond)); + } + +private slots: + void MethodChanged(int); + void ServerChanged(const QString &); + void PathChanged(); + void PathRegexChanged(const RegexConfig &); + void BodyChanged(); + void BodyRegexChanged(const RegexConfig &); + void ClearBufferOnMatchChanged(int); +signals: + void HeaderInfoChanged(const QString &); + +private: + QComboBox *_method; + HttpServerSelection *_server; + VariableLineEdit *_path; + RegexConfigWidget *_pathRegex; + VariableTextEdit *_body; + RegexConfigWidget *_bodyRegex; + QCheckBox *_clearBufferOnMatch; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss