Add HTTP condition

This commit is contained in:
WarmUpTill 2026-03-28 22:30:00 +01:00
parent c711147d24
commit f12ef9c9f8
8 changed files with 1161 additions and 3 deletions

View File

@ -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"

View File

@ -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 "")

View File

@ -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 <QTabWidget>
#include <QTimer>
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<HttpServer>();
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<HttpServer>(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

View File

@ -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

View File

@ -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 <obs-data.h>
#include <httplib.h>
#undef DispatchMessage
Q_DECLARE_METATYPE(advss::HttpServer *);
namespace advss {
static std::deque<std::shared_ptr<Item>> 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<HttpServer>(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<httplib::Server> server;
std::thread thread;
std::mutex mutex;
std::atomic_bool listening{false};
MessageDispatcher<HttpRequest> dispatcher;
};
HttpServer::HttpServer() : _impl(std::make_unique<Impl>()) {}
HttpServer::HttpServer(const HttpServer &other)
: Item(other),
_impl(std::make_unique<Impl>())
{
_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<std::mutex> lock(_impl->mutex);
if (_impl->server) {
_impl->server->stop();
}
if (_impl->thread.joinable()) {
_impl->thread.join();
}
_impl->server = std::make_unique<httplib::Server>();
_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<std::mutex> 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<HttpServer *>(s.get());
}
}
return nullptr;
}
std::weak_ptr<HttpServer> GetWeakHttpServerByName(const std::string &name)
{
for (const auto &s : httpServers) {
if (s->Name() == name) {
return std::dynamic_pointer_cast<HttpServer>(s);
}
}
return {};
}
std::weak_ptr<HttpServer> GetWeakHttpServerByQString(const QString &name)
{
return GetWeakHttpServerByName(name.toStdString());
}
std::string GetWeakHttpServerName(const std::weak_ptr<HttpServer> &server)
{
auto s = server.lock();
if (!s) {
return obs_module_text("AdvSceneSwitcher.httpServer.invalid");
}
return s->Name();
}
std::deque<std::shared_ptr<Item>> &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<HttpServer &>(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<HttpServer> &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

View File

@ -0,0 +1,101 @@
#pragma once
#include "item-selection-helpers.hpp"
#include "message-buffer.hpp"
#include "message-dispatcher.hpp"
#include <map>
#include <memory>
#include <string>
#include <QCheckBox>
#include <QGridLayout>
#include <QSpinBox>
namespace advss {
struct HttpRequest {
std::string method;
std::string path;
std::string body;
std::map<std::string, std::string> headers;
};
using HttpRequestBuffer = std::shared_ptr<MessageBuffer<HttpRequest>>;
class HttpServerSelection;
class HttpServerSettingsDialog;
class HttpServer : public Item {
public:
HttpServer();
HttpServer(const HttpServer &);
HttpServer &operator=(const HttpServer &);
~HttpServer();
static std::shared_ptr<Item> Create()
{
return std::make_shared<HttpServer>();
}
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> _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<HttpServer> &);
};
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<HttpServer> GetWeakHttpServerByName(const std::string &);
std::weak_ptr<HttpServer> GetWeakHttpServerByQString(const QString &);
std::string GetWeakHttpServerName(const std::weak_ptr<HttpServer> &);
std::deque<std::shared_ptr<Item>> &GetHttpServers();
} // namespace advss

View File

@ -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<MacroConditionHttp::Method, std::string> 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<int>(_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<Method>(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<int>(value));
}
}
MacroConditionHttpEdit::MacroConditionHttpEdit(
QWidget *parent, std::shared_ptr<MacroConditionHttp> 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<int>(_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<MacroConditionHttp::Method>(
_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

View File

@ -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 <QCheckBox>
#include <QComboBox>
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<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionHttp>(m);
}
enum class Method {
ANY = 0,
GET,
POST,
PUT,
PATCH,
DELETE,
};
void SetServer(const std::string &name);
std::weak_ptr<HttpServer> 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<HttpServer> _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<MacroConditionHttp> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionHttpEdit(
parent,
std::dynamic_pointer_cast<MacroConditionHttp>(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<MacroConditionHttp> _entryData;
bool _loading = true;
};
} // namespace advss