From 3ae72e447f3e081da182b6c905caadb1af8d105c Mon Sep 17 00:00:00 2001 From: Nils <34674720+nils-kt@users.noreply.github.com> Date: Mon, 4 May 2026 20:26:01 +0200 Subject: [PATCH] Add OAuth login, proper API integration, ZIP extraction, UAC elevation, update notifications, CI/CD --- .github/workflows/release.yml | 254 ++++++++++++++ CMakeLists.txt | 2 + src/downloader.c | 603 ++++++++++++++++++++++++++++++---- src/downloader.h | 6 +- src/manager-dialog.cpp | 115 ++++--- src/oauth.c | 227 +++++++++++++ src/oauth.h | 13 + src/obfuscation.cpp | 5 +- src/obfuscation.h | 5 +- src/plugin-main.c | 63 ++-- 10 files changed, 1148 insertions(+), 145 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 src/oauth.c create mode 100644 src/oauth.h diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4558366 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,254 @@ +name: Build & Release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 1.0.0)' + required: true + +env: + OBS_VERSION: '32.1.0' + OBS_DEPS_VERSION: '2025-08-23' + PLUGIN_NAME: 'st-pluginmanager' + +jobs: + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Determine version + id: version + shell: bash + run: | + if [ "${{ github.event_name }}" == "push" ]; then + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + else + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Clone OBS headers + shell: bash + run: | + git clone --depth 1 --branch ${{ env.OBS_VERSION }} \ + --filter=blob:none --sparse \ + https://github.com/obsproject/obs-studio.git deps/obs-studio + cd deps/obs-studio + git sparse-checkout set libobs frontend/api deps/w32-pthreads + + - name: Create obsconfig.h + shell: powershell + run: | + @" + #pragma once + #define OBS_DATA_PATH "data" + #define OBS_PLUGIN_PATH "obs-plugins/64bit" + #define OBS_PLUGIN_DESTINATION "obs-plugins/64bit" + #define OBS_INSTALL_PREFIX "C:/Program Files/obs-studio" + #define OBS_RELEASE_CANDIDATE 0 + #define OBS_BETA 0 + "@ | Out-File -Encoding ASCII "deps/obs-studio/libobs/obsconfig.h" + + - name: Download OBS deps + shell: powershell + run: | + $url = "https://github.com/obsproject/obs-deps/releases/download/${{ env.OBS_DEPS_VERSION }}/windows-deps-${{ env.OBS_DEPS_VERSION }}-x64.zip" + Invoke-WebRequest -Uri $url -OutFile deps/obs-deps.zip -UseBasicParsing + Expand-Archive -Path deps/obs-deps.zip -DestinationPath deps/obs-deps -Force + + - name: Download Qt6 deps + shell: powershell + run: | + $url = "https://github.com/obsproject/obs-deps/releases/download/${{ env.OBS_DEPS_VERSION }}/windows-deps-qt6-${{ env.OBS_DEPS_VERSION }}-x64.zip" + Invoke-WebRequest -Uri $url -OutFile deps/qt6.zip -UseBasicParsing + Expand-Archive -Path deps/qt6.zip -DestinationPath deps/qt6 -Force + + - name: Download OBS binaries + shell: powershell + run: | + $url = "https://github.com/obsproject/obs-studio/releases/download/${{ env.OBS_VERSION }}/OBS-Studio-${{ env.OBS_VERSION }}-Windows-x64.zip" + Invoke-WebRequest -Uri $url -OutFile deps/obs-bin.zip -UseBasicParsing + Expand-Archive -Path deps/obs-bin.zip -DestinationPath deps/obs-bin -Force + + - name: Generate import libraries + shell: powershell + run: | + $outDir = "deps/obs-libs" + New-Item -ItemType Directory -Force -Path $outDir | Out-Null + foreach ($dllName in @("obs", "obs-frontend-api", "w32-pthreads")) { + $dll = Get-ChildItem -Path "deps/obs-bin" -Filter "$dllName.dll" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $dll) { throw "Could not find $dllName.dll in deps/obs-bin" } + Write-Host "Found: $($dll.FullName)" + $raw = (& dumpbin /exports $dll.FullName 2>&1) | Out-String + $lines = $raw -split "`r?`n" + $defLines = @("LIBRARY ""$dllName""", "EXPORTS") + $capture = $false + foreach ($line in $lines) { + if ($line -match "ordinal\s+hint\s+RVA\s+name") { $capture = $true; continue } + if ($capture -and $line -match "^\s*Summary") { break } + if ($capture -and $line -match "^\s+(\d+)\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+(\S+)") { + $defLines += (" " + $Matches[2]) + } + } + $exportCount = $defLines.Count - 2 + Write-Host " $dllName exports: $exportCount" + if ($exportCount -lt 1) { throw "No exports found in $dllName.dll" } + $defLines -join "`n" | Out-File -Encoding ASCII "$outDir/$dllName.def" -NoNewline + & lib /nologo /def:"$outDir/$dllName.def" /out:"$outDir/$dllName.lib" /machine:x64 + if (-not (Test-Path "$outDir/$dllName.lib")) { throw "Failed to create $dllName.lib" } + } + + - name: Build plugin + shell: powershell + run: | + cmake -S . -B build -G Ninja ` + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` + -DCMAKE_C_COMPILER=cl ` + -DCMAKE_CXX_COMPILER=cl ` + -DOBS_SOURCE_DIR="deps/obs-studio" ` + -DOBS_LIB_DIR="deps/obs-libs" ` + -DQT6_DIR="deps/qt6" ` + -DCURL_DIR="deps/obs-deps" ` + -DPLUGIN_VERSION_OVERRIDE="${{ steps.version.outputs.version }}" + cmake --build build --config RelWithDebInfo + + - name: Package + shell: powershell + run: | + $ver = "${{ steps.version.outputs.version }}" + New-Item -ItemType Directory -Force "release" | Out-Null + $dir = "staging/st-pluginmanager" + New-Item -ItemType Directory -Force "$dir/obs-plugins/64bit" | Out-Null + Copy-Item "build/st-pluginmanager.dll" "$dir/obs-plugins/64bit/" + Compress-Archive -Path "$dir/*" -DestinationPath "release/st-pluginmanager-$ver-windows-x64.zip" + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: windows-release + path: release/* + + build-linux: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + + - name: Determine version + id: version + shell: bash + run: | + if [ "${{ github.event_name }}" == "push" ]; then + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + else + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Install dependencies + run: | + sudo add-apt-repository ppa:obsproject/obs-studio -y + sudo apt-get update + sudo apt-get install -y \ + libobs-dev \ + qt6-base-dev \ + libcurl4-openssl-dev libxkbcommon-dev \ + cmake ninja-build pkg-config + + - name: Setup OBS frontend headers + id: frontend + run: | + if pkg-config --cflags obs-frontend-api 2>/dev/null; then + echo "Found obs-frontend-api via pkg-config" + echo "cmake_arg=" >> $GITHUB_OUTPUT + elif [ -f /usr/include/obs/obs-frontend-api.h ]; then + echo "Found obs-frontend-api.h in system includes" + echo "cmake_arg=" >> $GITHUB_OUTPUT + else + SYS_VER=$(pkg-config --modversion libobs | cut -d. -f1-3) + echo "Cloning OBS $SYS_VER frontend headers..." + git clone --depth 1 --branch "$SYS_VER" \ + --filter=blob:none --sparse \ + https://github.com/obsproject/obs-studio.git deps/obs-studio + cd deps/obs-studio + git sparse-checkout set frontend/api + echo "cmake_arg=-DOBS_FRONTEND_INCLUDE_DIR=deps/obs-studio/frontend/api" >> $GITHUB_OUTPUT + fi + + - name: Build plugin + run: | + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DPLUGIN_VERSION_OVERRIDE="${{ steps.version.outputs.version }}" \ + ${{ steps.frontend.outputs.cmake_arg }} + cmake --build build + + - name: Package + run: | + ver="${{ steps.version.outputs.version }}" + mkdir -p release + mkdir -p staging/obs-plugins + cp build/st-pluginmanager.so staging/obs-plugins/ + cd staging + tar czf ../release/st-pluginmanager-$ver-linux-x86_64.tar.gz * + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: linux-release + path: release/* + + release: + needs: [build-windows, build-linux] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + + - name: Determine version + id: version + shell: bash + run: | + if [ "${{ github.event_name }}" == "push" ]; then + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT + else + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + echo "tag=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Download all artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: stools Plugin Manager v${{ steps.version.outputs.version }} + draft: false + prerelease: false + generate_release_notes: true + body: | + ## Installation + + ### Windows + 1. Download **`st-pluginmanager-${{ steps.version.outputs.version }}-windows-x64.zip`** + 2. Extract into your OBS Studio folder (e.g. `C:\Program Files\obs-studio\`) + 3. Restart OBS + + ### Linux + 1. Download **`st-pluginmanager-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz`** + 2. Extract to `/usr/lib/obs-plugins/` + 3. Restart OBS + files: | + artifacts/windows-release/* + artifacts/linux-release/* diff --git a/CMakeLists.txt b/CMakeLists.txt index cba9068..2406be5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(st-pluginmanager MODULE src/plugin-main.c src/auth.c src/downloader.c + src/oauth.c src/obfuscation.cpp src/manager-dialog.cpp ) @@ -71,6 +72,7 @@ if(WIN32) obs_frontend w32_pthreads shell32 + ws2_32 Qt6::Core Qt6::Gui Qt6::Widgets diff --git a/src/downloader.c b/src/downloader.c index 5df5264..6cd734b 100644 --- a/src/downloader.c +++ b/src/downloader.c @@ -6,10 +6,12 @@ #include #include #include +#include #include #ifdef _WIN32 #include +#include #include #include #define PATH_SEP '\\' @@ -19,6 +21,23 @@ #define PATH_SEP '/' #endif +/* ---- Error reporting ---- */ + +static char s_last_error[512] = ""; + +static void set_error(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vsnprintf(s_last_error, sizeof(s_last_error), fmt, ap); + va_end(ap); +} + +const char *downloader_last_error(void) +{ + return s_last_error; +} + /* ---- cURL helpers ---- */ struct mem_buf { @@ -70,9 +89,12 @@ static char *api_get(const char *path, const char *token) 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); + if (token && token[0]) { + 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); @@ -80,7 +102,8 @@ static char *api_get(const char *path, const char *token) struct mem_buf buf = {NULL, 0}; curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + if (headers) + 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); @@ -90,7 +113,8 @@ static char *api_get(const char *path, const char *token) CURLcode res = curl_easy_perform(curl); long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - curl_slist_free_all(headers); + if (headers) + curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK || http_code != 200) { @@ -133,15 +157,51 @@ static void json_extract_string(const char *json, const char *key, out[len] = '\0'; } +static int json_extract_int(const char *json, const char *key) +{ + const char *v = json_find_key(json, key); + if (!v) return -1; + return atoi(v); +} + /* ---- OBS plugin directory ---- */ +static void ensure_dir_exists(const char *path) +{ +#ifdef _WIN32 + char tmp[512]; + snprintf(tmp, sizeof(tmp), "%s", path); + for (char *p = tmp + 3; *p; p++) { + if (*p == '\\' || *p == '/') { + *p = '\0'; + CreateDirectoryA(tmp, NULL); + *p = '\\'; + } + } + CreateDirectoryA(tmp, NULL); +#else + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", path); + system(cmd); +#endif +} + 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); + char exe_path[MAX_PATH]; + GetModuleFileNameA(NULL, exe_path, MAX_PATH); + + /* Go up from 64bit to obs-studio root */ + char *last_sep = strrchr(exe_path, '\\'); + if (last_sep) *last_sep = '\0'; /* remove obs64.exe */ + last_sep = strrchr(exe_path, '\\'); + if (last_sep) *last_sep = '\0'; /* remove 64bit */ + last_sep = strrchr(exe_path, '\\'); + if (last_sep) *last_sep = '\0'; /* remove bin */ + + snprintf(buf, sz, "%s\\obs-plugins\\64bit", exe_path); + ensure_dir_exists(buf); return true; #elif defined(__APPLE__) const char *home = getenv("HOME"); @@ -159,50 +219,145 @@ bool downloader_get_obs_plugin_dir(char *buf, size_t sz) #endif } -/* ---- Parse plugin list from API ---- */ +/* ---- Parse plugin list from /api/products ---- */ + +static const char *platform_suffix(void) +{ +#ifdef _WIN32 + return "windows"; +#elif defined(__APPLE__) + return "macos"; +#else + return "linux"; +#endif +} 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); + /* /api/products is public, but send token if available */ + char *json = api_get(obf_api_products_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"}}, ...] + * Response: array of product objects + * [{"id":"easy-irl-stream","name":"Easy IRL Stream", + * "type":"download","accessLevel":"free",...}, ...] + * + * We only show products of type "download" */ 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, '}'); + /* Find matching closing brace (simplified: first } works for + * flat objects; nested JSON could break this but products are flat) */ + int depth = 0; + const char *p = obj_start; + const char *obj_end = NULL; + while (*p) { + if (*p == '{') depth++; + else if (*p == '}') { + depth--; + if (depth == 0) { obj_end = p; break; } + } + p++; + } 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; - } + char *obj_buf = (char *)malloc(obj_len + 1); + if (!obj_buf) break; 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)); + char type[64] = ""; + json_extract_string(obj_buf, "type", type, sizeof(type)); - if (pi->slug[0] && pi->name[0]) - out->count++; + /* Only include downloadable plugins */ + if (strcmp(type, "download") == 0) { + struct plugin_info *pi = &out->items[out->count]; + json_extract_string(obj_buf, "id", pi->slug, + sizeof(pi->slug)); + json_extract_string(obj_buf, "name", pi->name, + sizeof(pi->name)); + if (pi->slug[0] && pi->name[0]) + out->count++; + } + + free(obj_buf); pos = obj_end + 1; } free(json); + + /* Now fetch latest version for each plugin from /api/releases/{slug} */ + for (int i = 0; i < out->count; i++) { + struct plugin_info *pi = &out->items[i]; + + char path[256]; + snprintf(path, sizeof(path), obf_api_releases_fmt(), pi->slug); + + char *rel_json = api_get(path, token); + if (!rel_json) continue; + + /* + * Response: {"product":{...},"releases":[{...},...]}} + * First release is latest. Has "version":"vX.Y.Z" and + * "assets":[{"id":123,"filename":"...","platform":"windows",...}] + */ + const char *releases_key = strstr(rel_json, "\"releases\""); + if (!releases_key) { free(rel_json); continue; } + + /* Find first release object */ + const char *first_rel = strchr(releases_key, '{'); + if (first_rel) { + char version[64] = ""; + json_extract_string(first_rel, "version", version, + sizeof(version)); + /* Strip leading 'v' if present */ + const char *ver = version; + if (ver[0] == 'v' || ver[0] == 'V') ver++; + snprintf(pi->latest_version, + sizeof(pi->latest_version), "%s", ver); + + /* Find matching asset for our platform */ + const char *assets_key = strstr(first_rel, "\"assets\""); + if (assets_key) { + const char *asset_pos = assets_key; + const char *plat = platform_suffix(); + + while ((asset_pos = strchr(asset_pos, '{')) != NULL) { + char asset_plat[64] = ""; + char asset_fn[256] = ""; + json_extract_string(asset_pos, + "platform", + asset_plat, + sizeof(asset_plat)); + json_extract_string(asset_pos, + "filename", + asset_fn, + sizeof(asset_fn)); + + int asset_id = json_extract_int( + asset_pos, "id"); + + if (strcmp(asset_plat, plat) == 0 && + asset_id > 0) { + pi->download_asset_id = asset_id; + break; + } + asset_pos++; + } + } + } + + free(rel_json); + } + dbg_log(LOG_INFO, "[%s] Fetched %d plugins", PLUGIN_NAME, out->count); return out->count > 0; } @@ -223,7 +378,7 @@ 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); + 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)) { @@ -257,9 +412,6 @@ void downloader_detect_installed(struct plugin_list *list, #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); @@ -285,21 +437,242 @@ void downloader_detect_installed(struct plugin_list *list, /* ---- Download and install a plugin ---- */ -bool downloader_install_plugin(const char *token, const char *slug, - const char *obs_plugin_dir) +/* ---- Archive extraction via system tools ---- */ + +static bool is_zip(const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) return false; + unsigned char sig[4] = {0}; + fread(sig, 1, 4, f); + fclose(f); + return sig[0] == 'P' && sig[1] == 'K' && + sig[2] == 0x03 && sig[3] == 0x04; +} + +static bool is_targz(const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) return false; + unsigned char sig[2] = {0}; + fread(sig, 1, 2, f); + fclose(f); + return sig[0] == 0x1f && sig[1] == 0x8b; +} + +static bool get_temp_dir(char *buf, size_t sz) { - const char *platform = #ifdef _WIN32 - "windows"; -#elif defined(__APPLE__) - "macos"; + GetTempPathA((DWORD)sz, buf); + return true; #else - "linux"; + snprintf(buf, sz, "/tmp"); + return true; +#endif +} + +/* + * Extract archive to a temp directory, then find the plugin binary. + * + * Release ZIP structure (Windows): + * easy-irl-stream/obs-plugins/64bit/easy-irl-stream.dll + * + * Release tar.gz structure (Linux): + * obs-plugins/easy-irl-stream.so + */ +static bool extract_archive(const char *archive_path, const char *extract_dir) +{ +#ifdef _WIN32 + char cmd[2048]; + snprintf(cmd, sizeof(cmd), + "powershell -NoProfile -Command \"Expand-Archive -Force " + "-Path '%s' -DestinationPath '%s'\"", + archive_path, extract_dir); + int ret = system(cmd); + return ret == 0; +#else + char cmd[2048]; + if (is_targz(archive_path)) { + snprintf(cmd, sizeof(cmd), + "mkdir -p '%s' && tar xzf '%s' -C '%s'", + extract_dir, archive_path, extract_dir); + } else { + snprintf(cmd, sizeof(cmd), + "mkdir -p '%s' && unzip -o '%s' -d '%s'", + extract_dir, archive_path, extract_dir); + } + int ret = system(cmd); + return ret == 0; +#endif +} + +/* + * Recursively search for the plugin binary (slug.dll/.so) in extract_dir. + * This handles any nested folder structure. + */ +static bool find_plugin_binary(const char *dir, const char *filename, + char *result, size_t result_sz) +{ +#ifdef _WIN32 + char search[512]; + snprintf(search, sizeof(search), "%s\\*", dir); + + WIN32_FIND_DATAA fd; + HANDLE hFind = FindFirstFileA(search, &fd); + if (hFind == INVALID_HANDLE_VALUE) return false; + + do { + if (fd.cFileName[0] == '.') continue; + + char full[512]; + snprintf(full, sizeof(full), "%s\\%s", dir, fd.cFileName); + + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (find_plugin_binary(full, filename, result, result_sz)) + { + FindClose(hFind); + return true; + } + } else if (_stricmp(fd.cFileName, filename) == 0) { + snprintf(result, result_sz, "%s", full); + FindClose(hFind); + return true; + } + } while (FindNextFileA(hFind, &fd)); + + FindClose(hFind); + return false; +#else + char cmd[1024]; + snprintf(cmd, sizeof(cmd), + "find '%s' -name '%s' -type f 2>/dev/null | head -1", + dir, filename); + FILE *p = popen(cmd, "r"); + if (!p) return false; + if (fgets(result, (int)result_sz, p)) { + size_t len = strlen(result); + while (len > 0 && (result[len-1] == '\n' || result[len-1] == '\r')) + result[--len] = '\0'; + pclose(p); + return len > 0; + } + pclose(p); + return false; +#endif +} + +static void remove_directory(const char *dir) +{ +#ifdef _WIN32 + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "rmdir /s /q \"%s\"", dir); + system(cmd); +#else + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "rm -rf '%s'", dir); + system(cmd); +#endif +} + +/* + * Copy a file with UAC elevation on Windows. + * Launches a hidden PowerShell process as Administrator. + */ +#ifdef _WIN32 +static bool copy_elevated(const char *src, const char *dst) +{ + char ps_args[2048]; + snprintf(ps_args, sizeof(ps_args), + "-NoProfile -WindowStyle Hidden -Command \"" + "Copy-Item -Force -Path '%s' -Destination '%s'\"", + src, dst); + + SHELLEXECUTEINFOA sei; + memset(&sei, 0, sizeof(sei)); + sei.cbSize = sizeof(sei); + sei.fMask = SEE_MASK_NOCLOSEPROCESS; + sei.lpVerb = "runas"; + sei.lpFile = "powershell.exe"; + sei.lpParameters = ps_args; + sei.nShow = SW_HIDE; + + if (!ShellExecuteExA(&sei)) { + dbg_log(LOG_ERROR, "[%s] UAC elevation denied or failed", + PLUGIN_NAME); + return false; + } + + HANDLE proc = sei.hProcess; + WaitForSingleObject(proc, 30000); + + DWORD exit_code = 1; + GetExitCodeProcess(proc, &exit_code); + CloseHandle(proc); + + return exit_code == 0; +} #endif +static void hide_file(const char *path) +{ +#ifdef _WIN32 + SetFileAttributesA(path, FILE_ATTRIBUTE_HIDDEN); +#else + (void)path; +#endif +} + +bool downloader_write_version_file(const char *obs_plugin_dir, + const char *slug, const char *version) +{ + char ver_path[512]; + snprintf(ver_path, sizeof(ver_path), "%s%c.%s.version", + obs_plugin_dir, PATH_SEP, slug); + + FILE *f = fopen(ver_path, "w"); + if (f) { + fputs(version, f); + fclose(f); + hide_file(ver_path); + return true; + } + +#ifdef _WIN32 + /* Try elevated write */ + char tmp_ver[512]; + char tmp_d[512]; + get_temp_dir(tmp_d, sizeof(tmp_d)); + snprintf(tmp_ver, sizeof(tmp_ver), "%s%cst_pm_%s.version", + tmp_d, PATH_SEP, slug); + + f = fopen(tmp_ver, "w"); + if (!f) return false; + fputs(version, f); + fclose(f); + + bool ok = copy_elevated(tmp_ver, ver_path); + remove(tmp_ver); + return ok; +#else + return false; +#endif +} + +/* ---- Download and install a plugin ---- */ + +bool downloader_install_plugin(const char *token, const char *slug, + int asset_id, const char *obs_plugin_dir) +{ + s_last_error[0] = '\0'; + + if (asset_id <= 0) { + set_error("No download asset found for %s", slug); + dbg_log(LOG_ERROR, "[%s] No asset ID for %s", PLUGIN_NAME, slug); + return false; + } + char path[512]; - snprintf(path, sizeof(path), obf_api_plugin_download_fmt(), - slug, platform); + snprintf(path, sizeof(path), obf_api_downloads_fmt(), slug, asset_id); char url[512]; snprintf(url, sizeof(url), "%s%s%s", @@ -312,25 +685,25 @@ bool downloader_install_plugin(const char *token, const char *slug, ".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); + /* Download to temp file */ + char tmp_dir[512]; + get_temp_dir(tmp_dir, sizeof(tmp_dir)); - FILE *f = fopen(tmp_path, "wb"); + char archive_path[512]; + snprintf(archive_path, sizeof(archive_path), + "%s%cst_pm_%s_download", tmp_dir, PATH_SEP, slug); + + FILE *f = fopen(archive_path, "wb"); if (!f) { dbg_log(LOG_ERROR, "[%s] Cannot open %s for writing", - PLUGIN_NAME, tmp_path); + PLUGIN_NAME, archive_path); return false; } CURL *curl = curl_easy_init(); if (!curl) { fclose(f); - remove(tmp_path); + remove(archive_path); return false; } @@ -359,23 +732,129 @@ bool downloader_install_plugin(const char *token, const char *slug, fclose(f); if (res != CURLE_OK || http_code != 200) { + set_error("Download failed: %s (HTTP %ld)", + res != CURLE_OK ? curl_easy_strerror(res) : "server error", + http_code); dbg_log(LOG_ERROR, "[%s] Download %s failed: %s (HTTP %ld)", PLUGIN_NAME, slug, curl_easy_strerror(res), http_code); - remove(tmp_path); + remove(archive_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; + /* Check if it's an archive or a raw binary */ + bool success = false; + char final_path[512]; + snprintf(final_path, sizeof(final_path), "%s%c%s%s", + obs_plugin_dir, PATH_SEP, slug, ext); + + if (is_zip(archive_path) || is_targz(archive_path)) { + /* Extract to temp, then find the binary */ + char extract_dir[512]; + snprintf(extract_dir, sizeof(extract_dir), + "%s%cst_pm_%s_extract", tmp_dir, PATH_SEP, slug); + + remove_directory(extract_dir); + + dbg_log(LOG_INFO, "[%s] Extracting archive for %s", + PLUGIN_NAME, slug); + + if (!extract_archive(archive_path, extract_dir)) { + set_error("Failed to extract archive for %s", slug); + dbg_log(LOG_ERROR, "[%s] Failed to extract archive for %s", + PLUGIN_NAME, slug); + remove(archive_path); + remove_directory(extract_dir); + return false; + } + + /* Find the DLL/SO inside the extracted tree */ + char dll_filename[128]; + snprintf(dll_filename, sizeof(dll_filename), "%s%s", slug, ext); + + char found_path[512] = ""; + if (!find_plugin_binary(extract_dir, dll_filename, + found_path, sizeof(found_path))) { + set_error("Could not find %s in archive", dll_filename); + dbg_log(LOG_ERROR, + "[%s] Could not find %s in extracted archive", + PLUGIN_NAME, dll_filename); + remove(archive_path); + remove_directory(extract_dir); + return false; + } + + dbg_log(LOG_INFO, "[%s] Found binary at: %s", + PLUGIN_NAME, found_path); + + /* Ensure target dir exists */ + ensure_dir_exists(obs_plugin_dir); + + /* Copy to OBS plugin dir (try normal, then elevated) */ + remove(final_path); + +#ifdef _WIN32 + success = CopyFileA(found_path, final_path, FALSE); + if (!success) { + DWORD copy_err = GetLastError(); + dbg_log(LOG_INFO, + "[%s] Normal copy failed (err %lu), trying elevated", + PLUGIN_NAME, copy_err); + success = copy_elevated(found_path, final_path); + } +#else + { + char cp_cmd[1024]; + snprintf(cp_cmd, sizeof(cp_cmd), + "cp '%s' '%s'", found_path, final_path); + success = system(cp_cmd) == 0; + if (!success) { + snprintf(cp_cmd, sizeof(cp_cmd), + "pkexec cp '%s' '%s'", + found_path, final_path); + success = system(cp_cmd) == 0; + } + } +#endif + + remove_directory(extract_dir); + } else { + /* Raw binary - just move to final location */ + remove(final_path); +#ifdef _WIN32 + success = CopyFileA(archive_path, final_path, FALSE); + if (!success) { + dbg_log(LOG_INFO, + "[%s] Normal copy failed, trying elevated", + PLUGIN_NAME); + success = copy_elevated(archive_path, final_path); + } +#else + success = rename(archive_path, final_path) == 0; + if (!success) { + char cp_cmd[1024]; + snprintf(cp_cmd, sizeof(cp_cmd), + "pkexec cp '%s' '%s'", + archive_path, final_path); + success = system(cp_cmd) == 0; + } +#endif } - /* 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; + remove(archive_path); + + if (success) { + dbg_log(LOG_INFO, "[%s] Installed %s to %s", + PLUGIN_NAME, slug, final_path); + } else { +#ifdef _WIN32 + DWORD err = GetLastError(); + set_error("Failed to copy to %s (error %lu)", final_path, err); +#else + set_error("Failed to copy to %s", final_path); +#endif + dbg_log(LOG_ERROR, "[%s] Failed to install %s to %s", + PLUGIN_NAME, slug, final_path); + } + + return success; } diff --git a/src/downloader.h b/src/downloader.h index c9bb9da..51f5d68 100644 --- a/src/downloader.h +++ b/src/downloader.h @@ -17,6 +17,7 @@ struct plugin_info { char name[MAX_NAME_LEN]; char latest_version[MAX_VER_LEN]; char installed_version[MAX_VER_LEN]; + int download_asset_id; bool installed; bool update_available; }; @@ -28,10 +29,13 @@ struct plugin_list { 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); + int asset_id, 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); +bool downloader_write_version_file(const char *obs_plugin_dir, + const char *slug, const char *version); +const char *downloader_last_error(void); #ifdef __cplusplus } diff --git a/src/manager-dialog.cpp b/src/manager-dialog.cpp index d7202fd..be83252 100644 --- a/src/manager-dialog.cpp +++ b/src/manager-dialog.cpp @@ -20,6 +20,7 @@ extern "C" { #include "auth.h" #include "downloader.h" +#include "oauth.h" } #include "manager-dialog.hpp" @@ -36,7 +37,8 @@ public: { bool de = is_de(locale); setWindowTitle("stools Plugin Manager"); - setMinimumSize(600, 450); + setMinimumSize(680, 450); + resize(700, 480); auto *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(16, 16, 16, 16); @@ -58,13 +60,8 @@ public: 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"); + m_loginBtn = new QPushButton( + de ? "Mit stools.cc anmelden" : "Login with stools.cc"); connect(m_loginBtn, &QPushButton::clicked, this, &ManagerDialog::onLogin); authLayout->addWidget(m_loginBtn); @@ -91,14 +88,19 @@ public: m_table->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch); m_table->horizontalHeader()->setSectionResizeMode( - 1, QHeaderView::ResizeToContents); + 1, QHeaderView::Fixed); m_table->horizontalHeader()->setSectionResizeMode( - 2, QHeaderView::ResizeToContents); + 2, QHeaderView::Fixed); m_table->horizontalHeader()->setSectionResizeMode( - 3, QHeaderView::ResizeToContents); + 3, QHeaderView::Fixed); + m_table->setColumnWidth(1, 120); + m_table->setColumnWidth(2, 90); + m_table->setColumnWidth(3, 110); m_table->verticalHeader()->setVisible(false); m_table->setSelectionMode(QAbstractItemView::NoSelection); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setShowGrid(false); + m_table->setAlternatingRowColors(true); mainLayout->addWidget(m_table, 1); /* ---- Refresh button ---- */ @@ -131,7 +133,6 @@ private: QString m_locale; QFrame *m_authFrame; QLabel *m_statusLabel; - QLineEdit *m_tokenInput; QPushButton *m_loginBtn; QPushButton *m_logoutBtn; QTableWidget *m_table; @@ -144,7 +145,6 @@ private: 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); @@ -166,28 +166,36 @@ private: 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"); + m_loginBtn->setEnabled(false); + m_loginBtn->setText(de ? "Browser öffnet..." : "Opening browser..."); + QApplication::processEvents(); - if (ok) { - m_tokenInput->clear(); - updateAuthUI(); - onRefresh(); + char token[512] = ""; + bool got_token = oauth_start_flow(token, sizeof(token)); + + if (got_token && token[0]) { + bool ok = auth_login(token); + if (ok) { + updateAuthUI(); + onRefresh(); + } else { + QMessageBox::warning( + this, "stools Plugin Manager", + de ? "Token-Validierung fehlgeschlagen." + : "Token validation failed."); + } } else { - QMessageBox::warning(this, "stools Plugin Manager", - de ? "Ungültiger Token." - : "Invalid token."); + QMessageBox::warning( + this, "stools Plugin Manager", + de ? "Login abgebrochen oder fehlgeschlagen." + : "Login cancelled or failed."); } + + m_loginBtn->setEnabled(true); + m_loginBtn->setText( + de ? "Mit Twitch anmelden" : "Login with Twitch"); } void onLogout() @@ -291,25 +299,22 @@ private: return; } + int asset_id = 0; + for (int i = 0; i < m_plugins.count; i++) { + if (slug == m_plugins.items[i].slug) { + asset_id = m_plugins.items[i].download_asset_id; + break; + } + } + bool ok = downloader_install_plugin( - auth_get_token(), slug.toUtf8().constData(), obs_dir); + auth_get_token(), slug.toUtf8().constData(), + asset_id, 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); - } + downloader_write_version_file( + obs_dir, slug.toUtf8().constData(), + version.toUtf8().constData()); btn->setText(de ? "Aktuell" : "Up to date"); btn->setEnabled(false); @@ -322,11 +327,19 @@ private: onRefresh(); } else { - btn->setText(de ? "Fehlgeschlagen" : "Failed"); - QTimer::singleShot(2000, [btn, de]() { - btn->setText(de ? "Erneut versuchen" : "Retry"); - btn->setEnabled(true); - }); + const char *err = downloader_last_error(); + QString detail = err && err[0] + ? QString::fromUtf8(err) + : (de ? "Unbekannter Fehler" : "Unknown error"); + + QMessageBox::critical( + this, "stools Plugin Manager", + QString(de ? "Installation von %1 fehlgeschlagen:\n%2" + : "Installation of %1 failed:\n%2") + .arg(slug, detail)); + + btn->setText(de ? "Erneut versuchen" : "Retry"); + btn->setEnabled(true); } } }; diff --git a/src/oauth.c b/src/oauth.c new file mode 100644 index 0000000..e6e9dc4 --- /dev/null +++ b/src/oauth.c @@ -0,0 +1,227 @@ +#include "oauth.h" +#include "obfuscation.h" +#include "debug-log.h" +#include "compat.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#pragma comment(lib, "ws2_32.lib") +#define OAUTH_SOCKET SOCKET +#define OAUTH_INVALID INVALID_SOCKET +#define oauth_close closesocket +#else +#include +#include +#include +#define OAUTH_SOCKET int +#define OAUTH_INVALID (-1) +#define oauth_close close +#endif + +#define LISTEN_PORT_START 19850 +#define LISTEN_PORT_END 19860 + +static const char *HTML_SUCCESS = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "Connection: close\r\n\r\n" + "" + "" + "" + "
" + "
" + "

Login erfolgreich

" + "

Du kannst dieses Fenster schließen und zu OBS zurückkehren.

" + "
"; + +static const char *HTML_ERROR = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "Connection: close\r\n\r\n" + "" + "" + "" + "
" + "
" + "

Login fehlgeschlagen

" + "

Bitte versuche es erneut.

" + "
"; + +static OAUTH_SOCKET start_server(int *port_out) +{ + for (int port = LISTEN_PORT_START; port <= LISTEN_PORT_END; port++) { + OAUTH_SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == OAUTH_INVALID) + continue; + + int reuse = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + (const char *)&reuse, sizeof(reuse)); + + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0 && + listen(sock, 1) == 0) { + *port_out = port; + return sock; + } + + oauth_close(sock); + } + return OAUTH_INVALID; +} + +static bool extract_param(const char *request, const char *key, + char *out, size_t out_sz) +{ + char search[64]; + snprintf(search, sizeof(search), "%s=", key); + const char *pos = strstr(request, search); + if (!pos) return false; + pos += strlen(search); + + size_t i = 0; + while (pos[i] && pos[i] != '&' && pos[i] != ' ' && + pos[i] != '\r' && pos[i] != '\n' && i < out_sz - 1) { + out[i] = pos[i]; + i++; + } + out[i] = '\0'; + return i > 0; +} + +bool oauth_start_flow(char *token_out, int token_out_sz) +{ +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + + int port = 0; + OAUTH_SOCKET server = start_server(&port); + if (server == OAUTH_INVALID) { + dbg_log(LOG_ERROR, "[%s] OAuth: failed to start local server", + PLUGIN_NAME); + return false; + } + + char redirect_uri[128]; + snprintf(redirect_uri, sizeof(redirect_uri), + "http://localhost:%d/callback", port); + + char url[512]; + snprintf(url, sizeof(url), + "%s%s/auth/connect?app=plugin-manager&redirect_uri=%s", + obf_https_prefix(), obf_stools_host(), redirect_uri); + +#ifdef _WIN32 + ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL); +#elif defined(__APPLE__) + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "open \"%s\"", url); + system(cmd); +#else + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "xdg-open \"%s\"", url); + system(cmd); +#endif + + dbg_log(LOG_INFO, "[%s] OAuth: waiting for callback on port %d", + PLUGIN_NAME, port); + + /* Set timeout on accept: 120 seconds */ +#ifdef _WIN32 + int timeout_ms = 120000; + setsockopt(server, SOL_SOCKET, SO_RCVTIMEO, + (const char *)&timeout_ms, sizeof(timeout_ms)); +#else + struct timeval tv = {120, 0}; + setsockopt(server, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); +#endif + + struct sockaddr_in client_addr; + int client_len = sizeof(client_addr); + OAUTH_SOCKET client = accept(server, (struct sockaddr *)&client_addr, +#ifdef _WIN32 + &client_len +#else + (socklen_t *)&client_len +#endif + ); + + bool success = false; + + if (client != OAUTH_INVALID) { + char request[4096] = {0}; + recv(client, request, sizeof(request) - 1, 0); + + char token[512] = ""; + char error[64] = ""; + + extract_param(request, "token", token, sizeof(token)); + extract_param(request, "error", error, sizeof(error)); + + if (token[0] && !error[0]) { + send(client, HTML_SUCCESS, (int)strlen(HTML_SUCCESS), 0); + snprintf(token_out, token_out_sz, "%s", token); + success = true; + dbg_log(LOG_INFO, "[%s] OAuth: token received", + PLUGIN_NAME); + } else { + send(client, HTML_ERROR, (int)strlen(HTML_ERROR), 0); + dbg_log(LOG_WARNING, + "[%s] OAuth: denied or error (%s)", + PLUGIN_NAME, error[0] ? error : "no token"); + } + + oauth_close(client); + } else { + dbg_log(LOG_WARNING, "[%s] OAuth: timeout waiting for callback", + PLUGIN_NAME); + } + + oauth_close(server); + return success; +} diff --git a/src/oauth.h b/src/oauth.h new file mode 100644 index 0000000..e432bec --- /dev/null +++ b/src/oauth.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool oauth_start_flow(char *token_out, int token_out_sz); + +#ifdef __cplusplus +} +#endif diff --git a/src/obfuscation.cpp b/src/obfuscation.cpp index ca939ba..b3914d3 100644 --- a/src/obfuscation.cpp +++ b/src/obfuscation.cpp @@ -17,5 +17,6 @@ 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") +OBF_FUNC(obf_api_products_path, "/api/products") +OBF_FUNC(obf_api_releases_fmt, "/api/releases/%s") +OBF_FUNC(obf_api_downloads_fmt, "/api/downloads/%s/%d") diff --git a/src/obfuscation.h b/src/obfuscation.h index 8a97cf9..49206f1 100644 --- a/src/obfuscation.h +++ b/src/obfuscation.h @@ -9,8 +9,9 @@ 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); +const char *obf_api_products_path(void); +const char *obf_api_releases_fmt(void); +const char *obf_api_downloads_fmt(void); #ifdef __cplusplus } diff --git a/src/plugin-main.c b/src/plugin-main.c index 92edc25..75edc39 100644 --- a/src/plugin-main.c +++ b/src/plugin-main.c @@ -22,6 +22,15 @@ static const char *g_locale = NULL; static pthread_t g_init_thread; static volatile bool g_init_done = false; +static char g_update_msg[1024] = ""; + +static void show_update_alert(void *param) +{ + (void)param; + if (g_update_msg[0]) + manager_dialog_show(g_locale); +} + static void *init_thread_func(void *arg) { (void)arg; @@ -38,41 +47,41 @@ static void *init_thread_func(void *arg) sizeof(obs_dir))) { downloader_detect_installed(&plugins, obs_dir); + int updates = 0; for (int i = 0; i < plugins.count; i++) { struct plugin_info *pi = &plugins.items[i]; - if (!pi->installed || + if (pi->installed && pi->update_available) { + updates++; dbg_log(LOG_INFO, - "[%s] Auto-installing %s v%s", - PLUGIN_NAME, pi->slug, + "[%s] Update available: %s %s -> %s", + PLUGIN_NAME, pi->name, + pi->installed_version, 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); - } - } } } + + if (updates > 0) { + bool de = g_locale && + g_locale[0] == 'd' && + g_locale[1] == 'e'; + if (updates == 1) + snprintf(g_update_msg, + sizeof(g_update_msg), + de ? "1 Plugin-Update verfügbar." + : "1 plugin update available."); + else + snprintf(g_update_msg, + sizeof(g_update_msg), + de ? "%d Plugin-Updates verfügbar." + : "%d plugin updates available.", + updates); + + obs_queue_task(OBS_TASK_UI, + show_update_alert, + NULL, false); + } } } }