Initial commit: stools Plugin Manager for OBS

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nils
2026-05-04 19:39:38 +02:00
commit 3673cc41a8
17 changed files with 2049 additions and 0 deletions
+283
View File
@@ -0,0 +1,283 @@
#include "auth.h"
#include "obfuscation.h"
#include "debug-log.h"
#include "compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#include <sys/stat.h>
#endif
#define MAX_TOKEN_LEN 512
#define MAX_USERNAME_LEN 128
static char g_token[MAX_TOKEN_LEN] = "";
static char g_username[MAX_USERNAME_LEN] = "";
static bool g_logged_in = false;
/* ---- Token file path ---- */
static void get_token_path(char *buf, size_t sz)
{
#ifdef _WIN32
char appdata[MAX_PATH];
SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata);
snprintf(buf, sz, "%s\\stools\\pluginmanager_token", appdata);
#else
const char *home = getenv("HOME");
if (!home) {
struct passwd *pw = getpwuid(getuid());
home = pw ? pw->pw_dir : "/tmp";
}
snprintf(buf, sz, "%s/.config/stools/pluginmanager_token", home);
#endif
}
static void ensure_dir(const char *path)
{
char dir[512];
snprintf(dir, sizeof(dir), "%s", path);
char *last_sep = strrchr(dir, '/');
#ifdef _WIN32
char *last_bs = strrchr(dir, '\\');
if (last_bs && (!last_sep || last_bs > last_sep))
last_sep = last_bs;
#endif
if (last_sep) {
*last_sep = '\0';
#ifdef _WIN32
CreateDirectoryA(dir, NULL);
#else
mkdir(dir, 0700);
#endif
}
}
static void save_token(const char *token)
{
char path[512];
get_token_path(path, sizeof(path));
ensure_dir(path);
FILE *f = fopen(path, "w");
if (f) {
fputs(token, f);
fclose(f);
}
}
static bool load_token(char *buf, size_t sz)
{
char path[512];
get_token_path(path, sizeof(path));
FILE *f = fopen(path, "r");
if (!f) return false;
if (!fgets(buf, (int)sz, f)) {
fclose(f);
return false;
}
fclose(f);
size_t len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
buf[--len] = '\0';
return len > 0;
}
static void delete_token(void)
{
char path[512];
get_token_path(path, sizeof(path));
remove(path);
}
/* ---- cURL helpers ---- */
struct mem_buf {
char *data;
size_t size;
};
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t total = size * nmemb;
struct mem_buf *buf = (struct mem_buf *)userp;
char *tmp = (char *)realloc(buf->data, buf->size + total + 1);
if (!tmp) return 0;
buf->data = tmp;
memcpy(buf->data + buf->size, contents, total);
buf->size += total;
buf->data[buf->size] = '\0';
return total;
}
static void curl_set_ssl_opts(CURL *curl)
{
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
#ifdef _WIN32
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2);
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_1_1);
curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L);
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
#endif
}
static char *api_get(const char *path, const char *token)
{
CURL *curl = curl_easy_init();
if (!curl) return NULL;
char url[512];
snprintf(url, sizeof(url), "%s%s%s",
obf_https_prefix(), obf_stools_host(), path);
struct curl_slist *headers = NULL;
char auth_header[512];
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
headers = curl_slist_append(headers, auth_header);
char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
struct mem_buf buf = {NULL, 0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
curl_set_ssl_opts(curl);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK || http_code != 200) {
free(buf.data);
return NULL;
}
return buf.data;
}
/* ---- JSON helpers ---- */
static const char *json_find_key(const char *json, const char *key)
{
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *pos = strstr(json, search);
if (!pos) return NULL;
pos += strlen(search);
while (*pos == ' ' || *pos == ':') pos++;
return pos;
}
static char *json_get_string_val(const char *json, const char *key)
{
const char *v = json_find_key(json, key);
if (!v || *v != '"') return NULL;
v++;
const char *end = strchr(v, '"');
if (!end) return NULL;
size_t len = (size_t)(end - v);
char *result = (char *)malloc(len + 1);
memcpy(result, v, len);
result[len] = '\0';
return result;
}
/* ---- Public API ---- */
bool auth_validate_token(const char *token)
{
if (!token || !token[0]) return false;
char *json = api_get(obf_api_me_path(), token);
if (!json) return false;
char *name = json_get_string_val(json, "username");
if (!name)
name = json_get_string_val(json, "name");
free(json);
if (name) {
snprintf(g_username, sizeof(g_username), "%s", name);
free(name);
return true;
}
return false;
}
bool auth_login(const char *token)
{
if (!auth_validate_token(token))
return false;
snprintf(g_token, sizeof(g_token), "%s", token);
g_logged_in = true;
save_token(token);
dbg_log(LOG_INFO, "[%s] Logged in as %s", PLUGIN_NAME, g_username);
return true;
}
void auth_logout(void)
{
g_token[0] = '\0';
g_username[0] = '\0';
g_logged_in = false;
delete_token();
dbg_log(LOG_INFO, "[%s] Logged out", PLUGIN_NAME);
}
void auth_init(void)
{
char token[MAX_TOKEN_LEN];
if (load_token(token, sizeof(token))) {
if (auth_validate_token(token)) {
snprintf(g_token, sizeof(g_token), "%s", token);
g_logged_in = true;
dbg_log(LOG_INFO, "[%s] Auto-login as %s",
PLUGIN_NAME, g_username);
} else {
dbg_log(LOG_WARNING,
"[%s] Saved token invalid, login required",
PLUGIN_NAME);
}
}
}
void auth_shutdown(void)
{
/* nothing to clean up */
}
bool auth_is_logged_in(void)
{
return g_logged_in;
}
const char *auth_get_token(void)
{
return g_token;
}
const char *auth_get_username(void)
{
return g_username;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
void auth_init(void);
void auth_shutdown(void);
bool auth_is_logged_in(void);
const char *auth_get_token(void);
const char *auth_get_username(void);
bool auth_login(const char *token);
void auth_logout(void);
bool auth_validate_token(const char *token);
#ifdef __cplusplus
}
#endif
+124
View File
@@ -0,0 +1,124 @@
#pragma once
/*
* Platform compatibility layer replacing OBS internal utilities
* (util/platform.h, util/threading.h, bmem.h) with standard C equivalents.
*/
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#endif
/* ---- Memory (replaces bmem.h) ---- */
#ifdef _MSC_VER
#define portable_strdup _strdup
#else
#define portable_strdup strdup
#endif
/* ---- Time (replaces util/platform.h os_gettime_ns / os_sleep_ms) ---- */
static inline uint64_t os_gettime_ns(void)
{
#ifdef _WIN32
static LARGE_INTEGER freq = {0};
if (freq.QuadPart == 0)
QueryPerformanceFrequency(&freq);
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
return (uint64_t)((double)counter.QuadPart / (double)freq.QuadPart *
1000000000.0);
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
#endif
}
static inline void os_sleep_ms(uint32_t ms)
{
#ifdef _WIN32
Sleep(ms);
#else
usleep((useconds_t)ms * 1000);
#endif
}
/* ---- Thread naming (replaces util/platform.h os_set_thread_name) ---- */
static inline void os_set_thread_name(const char *name)
{
#ifdef _WIN32
/* SetThreadDescription available on Windows 10 1607+ */
typedef HRESULT(WINAPI * SetThreadDescriptionFunc)(HANDLE, PCWSTR);
static SetThreadDescriptionFunc fn = NULL;
static bool resolved = false;
if (!resolved) {
HMODULE mod = GetModuleHandleW(L"kernel32.dll");
if (mod)
fn = (SetThreadDescriptionFunc)GetProcAddress(
mod, "SetThreadDescription");
resolved = true;
}
if (fn) {
wchar_t wname[64];
MultiByteToWideChar(CP_UTF8, 0, name, -1, wname, 64);
fn(GetCurrentThread(), wname);
}
#elif defined(__APPLE__)
pthread_setname_np(name);
#else
pthread_setname_np(pthread_self(), name);
#endif
}
/* ---- Atomics (replaces util/platform.h os_atomic_*) ---- */
#ifdef _WIN32
static inline long os_atomic_set_long(volatile long *ptr, long val)
{
return InterlockedExchange(ptr, val);
}
static inline long os_atomic_load_long(volatile long *ptr)
{
return InterlockedCompareExchange(ptr, 0, 0);
}
static inline long os_atomic_exchange_long(volatile long *ptr, long val)
{
return InterlockedExchange(ptr, val);
}
#else
static inline long os_atomic_set_long(volatile long *ptr, long val)
{
return __sync_lock_test_and_set(ptr, val);
}
static inline long os_atomic_load_long(volatile long *ptr)
{
return __sync_add_and_fetch(ptr, 0);
}
static inline long os_atomic_exchange_long(volatile long *ptr, long val)
{
return __sync_lock_test_and_set(ptr, val);
}
#endif
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <stdio.h>
#ifndef PLUGIN_NAME
#define PLUGIN_NAME "stools Plugin Manager"
#endif
#ifdef DEBUG_BUILD
#define dbg_log(level, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while (0)
#else
#define dbg_log(...) ((void)0)
#endif
#ifndef LOG_ERROR
#define LOG_ERROR 100
#define LOG_WARNING 200
#define LOG_INFO 300
#define LOG_DEBUG 400
#endif
+381
View File
@@ -0,0 +1,381 @@
#include "downloader.h"
#include "obfuscation.h"
#include "debug-log.h"
#include "compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#include <direct.h>
#define PATH_SEP '\\'
#else
#include <sys/stat.h>
#include <unistd.h>
#define PATH_SEP '/'
#endif
/* ---- cURL helpers ---- */
struct mem_buf {
char *data;
size_t size;
};
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t total = size * nmemb;
struct mem_buf *buf = (struct mem_buf *)userp;
char *tmp = (char *)realloc(buf->data, buf->size + total + 1);
if (!tmp) return 0;
buf->data = tmp;
memcpy(buf->data + buf->size, contents, total);
buf->size += total;
buf->data[buf->size] = '\0';
return total;
}
static size_t write_file_cb(void *contents, size_t size, size_t nmemb,
void *userp)
{
FILE *f = (FILE *)userp;
return fwrite(contents, size, nmemb, f);
}
static void curl_set_ssl_opts(CURL *curl)
{
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
#ifdef _WIN32
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2);
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_1_1);
curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L);
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
#endif
}
static char *api_get(const char *path, const char *token)
{
CURL *curl = curl_easy_init();
if (!curl) return NULL;
char url[512];
snprintf(url, sizeof(url), "%s%s%s",
obf_https_prefix(), obf_stools_host(), path);
struct curl_slist *headers = NULL;
char auth_header[512];
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
headers = curl_slist_append(headers, auth_header);
char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
struct mem_buf buf = {NULL, 0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
curl_set_ssl_opts(curl);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK || http_code != 200) {
dbg_log(LOG_WARNING, "[%s] API GET %s: %s (HTTP %ld)",
PLUGIN_NAME, path,
res != CURLE_OK ? curl_easy_strerror(res) : "error",
http_code);
free(buf.data);
return NULL;
}
return buf.data;
}
/* ---- JSON helpers ---- */
static const char *json_find_key(const char *json, const char *key)
{
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *pos = strstr(json, search);
if (!pos) return NULL;
pos += strlen(search);
while (*pos == ' ' || *pos == ':') pos++;
return pos;
}
static void json_extract_string(const char *json, const char *key,
char *out, size_t out_sz)
{
out[0] = '\0';
const char *v = json_find_key(json, key);
if (!v || *v != '"') return;
v++;
const char *end = strchr(v, '"');
if (!end) return;
size_t len = (size_t)(end - v);
if (len >= out_sz) len = out_sz - 1;
memcpy(out, v, len);
out[len] = '\0';
}
/* ---- OBS plugin directory ---- */
bool downloader_get_obs_plugin_dir(char *buf, size_t sz)
{
#ifdef _WIN32
char appdata[MAX_PATH];
SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata);
snprintf(buf, sz, "%s\\obs-studio\\obs-plugins\\64bit", appdata);
CreateDirectoryA(buf, NULL);
return true;
#elif defined(__APPLE__)
const char *home = getenv("HOME");
if (!home) return false;
snprintf(buf, sz, "%s/Library/Application Support/obs-studio/obs-plugins",
home);
mkdir(buf, 0755);
return true;
#else
const char *home = getenv("HOME");
if (!home) return false;
snprintf(buf, sz, "%s/.config/obs-studio/plugins", home);
mkdir(buf, 0755);
return true;
#endif
}
/* ---- Parse plugin list from API ---- */
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out)
{
memset(out, 0, sizeof(*out));
char *json = api_get(obf_api_plugins_path(), token);
if (!json) return false;
/*
* Expected JSON format:
* [{"slug":"easy-irl-stream","name":"Easy IRL Stream",
* "version":"1.1.4","platform":{"windows":".dll","linux":".so"}}, ...]
*/
const char *pos = json;
while (out->count < MAX_PLUGINS) {
const char *obj_start = strchr(pos, '{');
if (!obj_start) break;
const char *obj_end = strchr(obj_start, '}');
if (!obj_end) break;
size_t obj_len = (size_t)(obj_end - obj_start + 1);
char obj_buf[2048];
if (obj_len >= sizeof(obj_buf)) {
pos = obj_end + 1;
continue;
}
memcpy(obj_buf, obj_start, obj_len);
obj_buf[obj_len] = '\0';
struct plugin_info *pi = &out->items[out->count];
json_extract_string(obj_buf, "slug", pi->slug, sizeof(pi->slug));
json_extract_string(obj_buf, "name", pi->name, sizeof(pi->name));
json_extract_string(obj_buf, "version", pi->latest_version,
sizeof(pi->latest_version));
if (pi->slug[0] && pi->name[0])
out->count++;
pos = obj_end + 1;
}
free(json);
dbg_log(LOG_INFO, "[%s] Fetched %d plugins", PLUGIN_NAME, out->count);
return out->count > 0;
}
/* ---- Detect installed plugins ---- */
static bool file_exists(const char *path)
{
#ifdef _WIN32
DWORD attrs = GetFileAttributesA(path);
return attrs != INVALID_FILE_ATTRIBUTES;
#else
return access(path, F_OK) == 0;
#endif
}
static bool read_version_file(const char *dir, const char *slug,
char *ver, size_t ver_sz)
{
char path[512];
snprintf(path, sizeof(path), "%s%c%s.version", dir, PATH_SEP, slug);
FILE *f = fopen(path, "r");
if (!f) return false;
if (!fgets(ver, (int)ver_sz, f)) {
fclose(f);
return false;
}
fclose(f);
size_t len = strlen(ver);
while (len > 0 && (ver[len - 1] == '\n' || ver[len - 1] == '\r'))
ver[--len] = '\0';
return len > 0;
}
static int compare_versions(const char *a, const char *b)
{
int a1 = 0, a2 = 0, a3 = 0, b1 = 0, b2 = 0, b3 = 0;
sscanf(a, "%d.%d.%d", &a1, &a2, &a3);
sscanf(b, "%d.%d.%d", &b1, &b2, &b3);
if (a1 != b1) return a1 - b1;
if (a2 != b2) return a2 - b2;
return a3 - b3;
}
void downloader_detect_installed(struct plugin_list *list,
const char *obs_plugin_dir)
{
for (int i = 0; i < list->count; i++) {
struct plugin_info *pi = &list->items[i];
char dll_path[512];
#ifdef _WIN32
snprintf(dll_path, sizeof(dll_path), "%s\\%s.dll",
obs_plugin_dir, pi->slug);
#elif defined(__APPLE__)
snprintf(dll_path, sizeof(dll_path), "%s/%s.so",
obs_plugin_dir, pi->slug);
#else
snprintf(dll_path, sizeof(dll_path), "%s/%s.so",
obs_plugin_dir, pi->slug);
#endif
pi->installed = file_exists(dll_path);
if (pi->installed) {
if (read_version_file(obs_plugin_dir, pi->slug,
pi->installed_version,
sizeof(pi->installed_version))) {
pi->update_available =
compare_versions(pi->latest_version,
pi->installed_version) > 0;
} else {
snprintf(pi->installed_version,
sizeof(pi->installed_version), "?");
pi->update_available = true;
}
}
}
}
/* ---- Download and install a plugin ---- */
bool downloader_install_plugin(const char *token, const char *slug,
const char *obs_plugin_dir)
{
const char *platform =
#ifdef _WIN32
"windows";
#elif defined(__APPLE__)
"macos";
#else
"linux";
#endif
char path[512];
snprintf(path, sizeof(path), obf_api_plugin_download_fmt(),
slug, platform);
char url[512];
snprintf(url, sizeof(url), "%s%s%s",
obf_https_prefix(), obf_stools_host(), path);
const char *ext =
#ifdef _WIN32
".dll";
#else
".so";
#endif
char tmp_path[512], final_path[512], ver_path[512];
snprintf(tmp_path, sizeof(tmp_path), "%s%c%s%s.tmp",
obs_plugin_dir, PATH_SEP, slug, ext);
snprintf(final_path, sizeof(final_path), "%s%c%s%s",
obs_plugin_dir, PATH_SEP, slug, ext);
snprintf(ver_path, sizeof(ver_path), "%s%c%s.version",
obs_plugin_dir, PATH_SEP, slug);
FILE *f = fopen(tmp_path, "wb");
if (!f) {
dbg_log(LOG_ERROR, "[%s] Cannot open %s for writing",
PLUGIN_NAME, tmp_path);
return false;
}
CURL *curl = curl_easy_init();
if (!curl) {
fclose(f);
remove(tmp_path);
return false;
}
struct curl_slist *headers = NULL;
char auth_header[512];
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
headers = curl_slist_append(headers, auth_header);
char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, f);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_set_ssl_opts(curl);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
fclose(f);
if (res != CURLE_OK || http_code != 200) {
dbg_log(LOG_ERROR, "[%s] Download %s failed: %s (HTTP %ld)",
PLUGIN_NAME, slug, curl_easy_strerror(res), http_code);
remove(tmp_path);
return false;
}
/* Atomic replace: delete old, rename tmp */
remove(final_path);
if (rename(tmp_path, final_path) != 0) {
dbg_log(LOG_ERROR, "[%s] Failed to rename %s -> %s",
PLUGIN_NAME, tmp_path, final_path);
remove(tmp_path);
return false;
}
/* Write version file from server response header or plugin list */
dbg_log(LOG_INFO, "[%s] Installed %s to %s", PLUGIN_NAME, slug,
final_path);
return true;
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MAX_PLUGINS 32
#define MAX_NAME_LEN 128
#define MAX_VER_LEN 32
#define MAX_SLUG_LEN 64
struct plugin_info {
char slug[MAX_SLUG_LEN];
char name[MAX_NAME_LEN];
char latest_version[MAX_VER_LEN];
char installed_version[MAX_VER_LEN];
bool installed;
bool update_available;
};
struct plugin_list {
struct plugin_info items[MAX_PLUGINS];
int count;
};
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out);
bool downloader_install_plugin(const char *token, const char *slug,
const char *obs_plugin_dir);
void downloader_detect_installed(struct plugin_list *list,
const char *obs_plugin_dir);
bool downloader_get_obs_plugin_dir(char *buf, size_t sz);
#ifdef __cplusplus
}
#endif
+98
View File
@@ -0,0 +1,98 @@
#pragma once
/*
* Minimal dynamic string builder (replaces OBS util/dstr.h).
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
struct dstr {
char *array;
size_t len;
size_t capacity;
};
static inline void dstr_init(struct dstr *dst)
{
dst->array = NULL;
dst->len = 0;
dst->capacity = 0;
}
static inline void dstr_free(struct dstr *dst)
{
free(dst->array);
dst->array = NULL;
dst->len = 0;
dst->capacity = 0;
}
static inline void dstr_ensure_capacity(struct dstr *dst, size_t new_cap)
{
if (new_cap <= dst->capacity)
return;
size_t cap = dst->capacity ? dst->capacity : 64;
while (cap < new_cap)
cap *= 2;
dst->array = (char *)realloc(dst->array, cap);
dst->capacity = cap;
}
static inline void dstr_cat(struct dstr *dst, const char *str)
{
size_t slen = strlen(str);
dstr_ensure_capacity(dst, dst->len + slen + 1);
memcpy(dst->array + dst->len, str, slen + 1);
dst->len += slen;
}
static inline void dstr_ncat(struct dstr *dst, const char *str, size_t len)
{
dstr_ensure_capacity(dst, dst->len + len + 1);
memcpy(dst->array + dst->len, str, len);
dst->len += len;
dst->array[dst->len] = '\0';
}
static inline void dstr_printf(struct dstr *dst, const char *fmt, ...)
{
va_list args, args2;
va_start(args, fmt);
va_copy(args2, args);
int needed = vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (needed < 0) {
va_end(args2);
return;
}
dstr_ensure_capacity(dst, (size_t)needed + 1);
vsnprintf(dst->array, (size_t)needed + 1, fmt, args2);
dst->len = (size_t)needed;
va_end(args2);
}
static inline void dstr_catf(struct dstr *dst, const char *fmt, ...)
{
va_list args, args2;
va_start(args, fmt);
va_copy(args2, args);
int needed = vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (needed < 0) {
va_end(args2);
return;
}
dstr_ensure_capacity(dst, dst->len + (size_t)needed + 1);
vsnprintf(dst->array + dst->len, (size_t)needed + 1, fmt, args2);
dst->len += (size_t)needed;
va_end(args2);
}
+341
View File
@@ -0,0 +1,341 @@
#include <QDialog>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTableWidget>
#include <QHeaderView>
#include <QFont>
#include <QFrame>
#include <QMessageBox>
#include <QApplication>
#include <QThread>
#include <QTimer>
#include <obs-frontend-api.h>
extern "C" {
#include "auth.h"
#include "downloader.h"
}
#include "manager-dialog.hpp"
static bool is_de(const char *locale)
{
return locale && locale[0] == 'd' && locale[1] == 'e';
}
class ManagerDialog : public QDialog {
public:
ManagerDialog(QWidget *parent, const char *locale)
: QDialog(parent), m_locale(locale ? locale : "en")
{
bool de = is_de(locale);
setWindowTitle("stools Plugin Manager");
setMinimumSize(600, 450);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(16, 16, 16, 16);
mainLayout->setSpacing(12);
/* ---- Header ---- */
auto *header = new QLabel("stools Plugin Manager");
QFont headerFont = header->font();
headerFont.setPointSize(14);
headerFont.setBold(true);
header->setFont(headerFont);
mainLayout->addWidget(header);
/* ---- Auth section ---- */
m_authFrame = new QFrame();
auto *authLayout = new QHBoxLayout(m_authFrame);
authLayout->setContentsMargins(0, 0, 0, 0);
m_statusLabel = new QLabel();
authLayout->addWidget(m_statusLabel, 1);
m_tokenInput = new QLineEdit();
m_tokenInput->setEchoMode(QLineEdit::Password);
m_tokenInput->setPlaceholderText("API Token");
m_tokenInput->setMinimumWidth(200);
authLayout->addWidget(m_tokenInput);
m_loginBtn = new QPushButton(de ? "Anmelden" : "Login");
connect(m_loginBtn, &QPushButton::clicked, this,
&ManagerDialog::onLogin);
authLayout->addWidget(m_loginBtn);
m_logoutBtn = new QPushButton(de ? "Abmelden" : "Logout");
connect(m_logoutBtn, &QPushButton::clicked, this,
&ManagerDialog::onLogout);
authLayout->addWidget(m_logoutBtn);
mainLayout->addWidget(m_authFrame);
/* ---- Separator ---- */
auto *sep = new QFrame();
sep->setFrameShape(QFrame::HLine);
mainLayout->addWidget(sep);
/* ---- Plugin table ---- */
m_table = new QTableWidget(0, 4);
m_table->setHorizontalHeaderLabels(
{de ? "Plugin" : "Plugin",
de ? "Installiert" : "Installed",
de ? "Verfügbar" : "Available", ""});
m_table->horizontalHeader()->setStretchLastSection(false);
m_table->horizontalHeader()->setSectionResizeMode(
0, QHeaderView::Stretch);
m_table->horizontalHeader()->setSectionResizeMode(
1, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(
2, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(
3, QHeaderView::ResizeToContents);
m_table->verticalHeader()->setVisible(false);
m_table->setSelectionMode(QAbstractItemView::NoSelection);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
mainLayout->addWidget(m_table, 1);
/* ---- Refresh button ---- */
auto *bottomLayout = new QHBoxLayout();
m_refreshBtn = new QPushButton(
de ? "Aktualisieren" : "Refresh");
connect(m_refreshBtn, &QPushButton::clicked, this,
&ManagerDialog::onRefresh);
bottomLayout->addStretch();
bottomLayout->addWidget(m_refreshBtn);
mainLayout->addLayout(bottomLayout);
/* ---- Info label ---- */
m_infoLabel = new QLabel(
de ? "Änderungen werden nach OBS-Neustart wirksam."
: "Changes take effect after restarting OBS.");
QFont infoFont = m_infoLabel->font();
infoFont.setItalic(true);
m_infoLabel->setFont(infoFont);
m_infoLabel->setStyleSheet("color: gray;");
mainLayout->addWidget(m_infoLabel);
updateAuthUI();
if (auth_is_logged_in())
onRefresh();
}
private:
QString m_locale;
QFrame *m_authFrame;
QLabel *m_statusLabel;
QLineEdit *m_tokenInput;
QPushButton *m_loginBtn;
QPushButton *m_logoutBtn;
QTableWidget *m_table;
QPushButton *m_refreshBtn;
QLabel *m_infoLabel;
struct plugin_list m_plugins = {};
void updateAuthUI()
{
bool de = is_de(m_locale.toUtf8().constData());
bool logged_in = auth_is_logged_in();
m_tokenInput->setVisible(!logged_in);
m_loginBtn->setVisible(!logged_in);
m_logoutBtn->setVisible(logged_in);
m_refreshBtn->setEnabled(logged_in);
m_table->setEnabled(logged_in);
if (logged_in) {
m_statusLabel->setText(
QString(de ? "Angemeldet als: %1"
: "Logged in as: %1")
.arg(auth_get_username()));
m_statusLabel->setStyleSheet("color: green;");
} else {
m_statusLabel->setText(
de ? "Nicht angemeldet"
: "Not logged in");
m_statusLabel->setStyleSheet("color: red;");
}
}
void onLogin()
{
QString token = m_tokenInput->text().trimmed();
if (token.isEmpty()) return;
m_loginBtn->setEnabled(false);
m_loginBtn->setText("...");
QApplication::processEvents();
bool ok = auth_login(token.toUtf8().constData());
bool de = is_de(m_locale.toUtf8().constData());
m_loginBtn->setEnabled(true);
m_loginBtn->setText(de ? "Anmelden" : "Login");
if (ok) {
m_tokenInput->clear();
updateAuthUI();
onRefresh();
} else {
QMessageBox::warning(this, "stools Plugin Manager",
de ? "Ungültiger Token."
: "Invalid token.");
}
}
void onLogout()
{
auth_logout();
m_table->setRowCount(0);
m_plugins.count = 0;
updateAuthUI();
}
void onRefresh()
{
if (!auth_is_logged_in()) return;
m_refreshBtn->setEnabled(false);
m_refreshBtn->setText("...");
QApplication::processEvents();
bool de = is_de(m_locale.toUtf8().constData());
if (downloader_fetch_plugin_list(auth_get_token(), &m_plugins)) {
char obs_dir[512];
if (downloader_get_obs_plugin_dir(obs_dir, sizeof(obs_dir)))
downloader_detect_installed(&m_plugins, obs_dir);
populateTable();
} else {
QMessageBox::warning(
this, "stools Plugin Manager",
de ? "Plugin-Liste konnte nicht geladen werden."
: "Failed to fetch plugin list.");
}
m_refreshBtn->setEnabled(true);
m_refreshBtn->setText(de ? "Aktualisieren" : "Refresh");
}
void populateTable()
{
bool de = is_de(m_locale.toUtf8().constData());
m_table->setRowCount(m_plugins.count);
for (int i = 0; i < m_plugins.count; i++) {
struct plugin_info *pi = &m_plugins.items[i];
m_table->setItem(i, 0,
new QTableWidgetItem(pi->name));
QString installed_ver =
pi->installed
? QString(pi->installed_version)
: (de ? "Nicht installiert"
: "Not installed");
m_table->setItem(i, 1,
new QTableWidgetItem(installed_ver));
m_table->setItem(
i, 2,
new QTableWidgetItem(pi->latest_version));
QString btn_text;
if (!pi->installed)
btn_text = de ? "Installieren" : "Install";
else if (pi->update_available)
btn_text = de ? "Aktualisieren" : "Update";
else
btn_text = de ? "Aktuell" : "Up to date";
auto *btn = new QPushButton(btn_text);
btn->setEnabled(!pi->installed ||
pi->update_available);
btn->setProperty("slug",
QString(pi->slug));
btn->setProperty("version",
QString(pi->latest_version));
connect(btn, &QPushButton::clicked, this,
&ManagerDialog::onInstallClicked);
m_table->setCellWidget(i, 3, btn);
}
}
void onInstallClicked()
{
auto *btn = qobject_cast<QPushButton *>(sender());
if (!btn) return;
QString slug = btn->property("slug").toString();
QString version = btn->property("version").toString();
bool de = is_de(m_locale.toUtf8().constData());
btn->setEnabled(false);
btn->setText("...");
QApplication::processEvents();
char obs_dir[512];
if (!downloader_get_obs_plugin_dir(obs_dir, sizeof(obs_dir))) {
QMessageBox::critical(
this, "stools Plugin Manager",
de ? "OBS Plugin-Verzeichnis nicht gefunden."
: "OBS plugin directory not found.");
btn->setEnabled(true);
return;
}
bool ok = downloader_install_plugin(
auth_get_token(), slug.toUtf8().constData(), obs_dir);
if (ok) {
/* Write version file */
char ver_path[512];
snprintf(ver_path, sizeof(ver_path), "%s%c%s.version",
obs_dir,
#ifdef _WIN32
'\\',
#else
'/',
#endif
slug.toUtf8().constData());
FILE *f = fopen(ver_path, "w");
if (f) {
fputs(version.toUtf8().constData(), f);
fclose(f);
}
btn->setText(de ? "Aktuell" : "Up to date");
btn->setEnabled(false);
QMessageBox::information(
this, "stools Plugin Manager",
QString(de ? "%1 wurde installiert. Bitte OBS neu starten."
: "%1 has been installed. Please restart OBS.")
.arg(slug));
onRefresh();
} else {
btn->setText(de ? "Fehlgeschlagen" : "Failed");
QTimer::singleShot(2000, [btn, de]() {
btn->setText(de ? "Erneut versuchen" : "Retry");
btn->setEnabled(true);
});
}
}
};
void manager_dialog_show(const char *locale)
{
QMainWindow *main_window =
(QMainWindow *)obs_frontend_get_main_window();
auto *dialog = new ManagerDialog(main_window, locale);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void manager_dialog_show(const char *locale);
#ifdef __cplusplus
}
#endif
+21
View File
@@ -0,0 +1,21 @@
#include "obfuscation.h"
#include <string>
#define OBF_FUNC(name, literal) \
static const std::string &name##_storage() \
{ \
static const std::string s{literal}; \
return s; \
} \
extern "C" const char *name(void) \
{ \
return name##_storage().c_str(); \
}
OBF_FUNC(obf_https_prefix, "https://")
OBF_FUNC(obf_stools_host, "stools.cc")
OBF_FUNC(obf_auth_bearer_fmt, "Authorization: Bearer %s")
OBF_FUNC(obf_ua_prefix, "stools-pluginmanager/")
OBF_FUNC(obf_api_me_path, "/api/me")
OBF_FUNC(obf_api_plugins_path, "/api/plugins")
OBF_FUNC(obf_api_plugin_download_fmt, "/api/plugins/%s/download?platform=%s")
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
const char *obf_https_prefix(void);
const char *obf_stools_host(void);
const char *obf_auth_bearer_fmt(void);
const char *obf_ua_prefix(void);
const char *obf_api_me_path(void);
const char *obf_api_plugins_path(void);
const char *obf_api_plugin_download_fmt(void);
#ifdef __cplusplus
}
#endif
+126
View File
@@ -0,0 +1,126 @@
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <curl/curl.h>
#include "compat.h"
#include "debug-log.h"
#include "auth.h"
#include "downloader.h"
#include "manager-dialog.hpp"
OBS_DECLARE_MODULE()
#define PLUGIN_NAME "stools Plugin Manager"
static const char *g_locale = NULL;
/* ---- Background init thread ---- */
static pthread_t g_init_thread;
static volatile bool g_init_done = false;
static void *init_thread_func(void *arg)
{
(void)arg;
os_set_thread_name("stools-pm-init");
auth_init();
if (auth_is_logged_in()) {
struct plugin_list plugins = {0};
if (downloader_fetch_plugin_list(auth_get_token(), &plugins)) {
char obs_dir[512];
if (downloader_get_obs_plugin_dir(obs_dir,
sizeof(obs_dir))) {
downloader_detect_installed(&plugins, obs_dir);
for (int i = 0; i < plugins.count; i++) {
struct plugin_info *pi =
&plugins.items[i];
if (!pi->installed ||
pi->update_available) {
dbg_log(LOG_INFO,
"[%s] Auto-installing %s v%s",
PLUGIN_NAME, pi->slug,
pi->latest_version);
if (downloader_install_plugin(
auth_get_token(),
pi->slug,
obs_dir)) {
char vp[512];
snprintf(vp,
sizeof(vp),
"%s"
#ifdef _WIN32
"\\"
#else
"/"
#endif
"%s.version",
obs_dir,
pi->slug);
FILE *f = fopen(vp,
"w");
if (f) {
fputs(pi->latest_version,
f);
fclose(f);
}
}
}
}
}
}
}
g_init_done = true;
return NULL;
}
/* ---- Tools menu ---- */
static void tools_menu_cb(void *private_data)
{
(void)private_data;
manager_dialog_show(g_locale);
}
/* ---- Module lifecycle ---- */
bool obs_module_load(void)
{
curl_global_init(CURL_GLOBAL_DEFAULT);
g_locale = obs_get_locale();
if (pthread_create(&g_init_thread, NULL, init_thread_func, NULL) != 0) {
dbg_log(LOG_ERROR, "[%s] Failed to create init thread",
PLUGIN_NAME);
}
dbg_log(LOG_INFO, "[%s] Plugin loaded (v%s)", PLUGIN_NAME,
PLUGIN_VERSION);
return true;
}
void obs_module_post_load(void)
{
bool de = g_locale && g_locale[0] == 'd' && g_locale[1] == 'e';
obs_frontend_add_tools_menu_item(
de ? "stools Plugin Manager" : "stools Plugin Manager",
tools_menu_cb, NULL);
}
void obs_module_unload(void)
{
if (!g_init_done) {
pthread_join(g_init_thread, NULL);
}
auth_shutdown();
curl_global_cleanup();
dbg_log(LOG_INFO, "[%s] Plugin unloaded", PLUGIN_NAME);
}