From 3115383ea854ddf7e26b7cd4ee5f000e39b9447b Mon Sep 17 00:00:00 2001 From: Zack Williamson Date: Thu, 11 Dec 2025 17:36:29 -0500 Subject: [PATCH 1/8] 5.65.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dfbc9f98a..4412d135f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebotv5", - "version": "5.65.1", + "version": "5.65.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebotv5", - "version": "5.65.1", + "version": "5.65.2", "license": "GPL-3.0", "dependencies": { "@aws-sdk/client-polly": "^3.26.0", diff --git a/package.json b/package.json index 24664792f..421470371 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebotv5", - "version": "5.65.1", + "version": "5.65.2", "description": "Powerful all-in-one bot for Twitch streamers.", "main": "build/main.js", "scripts": { From 58ec5152f0029aafcfefe92b0216cbe7d0024316 Mon Sep 17 00:00:00 2001 From: Erik Bigler Date: Thu, 11 Dec 2025 17:03:53 -0700 Subject: [PATCH 2/8] fix(effects): firebot shoutout not respecting scale option #3371 --- src/backend/effects/builtin/shoutout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/effects/builtin/shoutout.js b/src/backend/effects/builtin/shoutout.js index 1be403ad9..5bdbdbad0 100644 --- a/src/backend/effects/builtin/shoutout.js +++ b/src/backend/effects/builtin/shoutout.js @@ -357,8 +357,8 @@ const effect = { const boxArtId = `box-art-${uniqueId}`; const shoutoutElement = ` -
-
+
+
From f5ccc2607c3c6128df1a90e0a4994972189dd15e Mon Sep 17 00:00:00 2001 From: Zack Williamson Date: Fri, 12 Dec 2025 11:49:48 -0500 Subject: [PATCH 3/8] chore: add support policy --- .github/SUPPORT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index db4cdea6f..d864d0de0 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1,2 +1,10 @@ +## Support Policy + +We will always support the latest stable Firebot release. Previous stable releases will be supported for **no more than 30 days** after a newer stable update has been released. After 30 days, you must be on a supported version in order to receive support via our official channels (Discord, GitHub, etc.). + +Pre-release versions (e.g. betas) are no longer supported **immediately** upon a corresponding stable release or if superseded by a newer pre-release (e.g. `5.66.0-beta1` would immediately become unsupported upon release of either `5.66.0-beta2` or a `5.66.0` stable release). Nightly releases continue to receive limited support due to their potentially unstable nature. + + ### Need help or have a question? + Please feel free to stop by our [Discord](https://discord.gg/crowbartools-372817064034959370) or connect with us on [Bluesky](https://bsky.app/profile/firebot.app)! \ No newline at end of file From 2324925fb572de5cd84622bd2dc8f69b75f39c92 Mon Sep 17 00:00:00 2001 From: Zack Williamson Date: Fri, 12 Dec 2025 12:40:51 -0500 Subject: [PATCH 4/8] feat(settings): auto update toggle (#1930, #2769) --- .../settings/categories/advanced-settings.js | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/gui/app/directives/settings/categories/advanced-settings.js b/src/gui/app/directives/settings/categories/advanced-settings.js index 6235cc821..1bbeec10c 100644 --- a/src/gui/app/directives/settings/categories/advanced-settings.js +++ b/src/gui/app/directives/settings/categories/advanced-settings.js @@ -177,13 +177,29 @@ /> + + + + + WARNING: If you disable this option, YOU are responsible for ensuring Firebot is up to date. Outdated versions will NOT be supported via our official channels. Please see our support policy for more information. + + +

Looking for a setting that used to be located here? Try checking in the Tools app menu!

`, - controller: function ($scope, settingsService, utilityService, backendCommunicator, modalService) { + controller: function ($scope, settingsService, modalFactory, backendCommunicator, modalService) { $scope.settings = settingsService; $scope.toggleWhileLoops = () => { @@ -192,7 +208,7 @@ if (whileLoopsEnabled) { settingsService.saveSetting("WhileLoopEnabled", false); } else { - utilityService + modalFactory .showConfirmationModal({ title: "Enable While Loops", question: @@ -222,7 +238,7 @@ }; $scope.recalculateQuoteIds = () => { - utilityService + modalFactory .showConfirmationModal({ title: "Recalculate Quote IDs", question: `Are you sure you want to recalculate your quote IDs?`, @@ -235,6 +251,27 @@ } }); }; + + $scope.toggleAutoUpdates = () => { + if (settingsService.getSetting("AutoUpdateLevel") === 0) { + settingsService.saveSetting("AutoUpdateLevel", 2); + } else { + modalFactory + .showConfirmationModal({ + title: "Disable Automatic Updates?", + question: "If you disable automatic updates, you will be responsible for updating Firebot yourself and will not receive support unless you are on a supported version. Are you sure you want to disable automatic Firebot updates?", + confirmLabel: "Yes, disable", + confirmBtnType: "btn-danger", + cancelLabel: "No, keep enabled", + cancelBtnType: "btn-default" + }) + .then((confirmed) => { + if (confirmed) { + settingsService.saveSetting("AutoUpdateLevel", 0); + } + }); + } + }; } }); -})(); +})(); \ No newline at end of file From 8c5bf1117434d243a0a70c4055209d132db378da Mon Sep 17 00:00:00 2001 From: Erik Bigler Date: Fri, 12 Dec 2025 11:14:56 -0700 Subject: [PATCH 5/8] feat: add ability to quickly copy debug info from help menu or about modal --- .../electron/window-management.js | 18 +++++ src/backend/common/common-listeners.js | 3 + src/backend/common/debug-info.ts | 80 +++++++++++++++++++ .../custom-scripts/startup-scripts-manager.js | 7 +- src/gui/app/app-main.js | 59 ++++---------- .../app/directives/modals/misc/about-modal.js | 9 ++- src/gui/scss/core/_bootstrap-overrides.scss | 28 +++++++ src/server/websocket-server-manager.ts | 8 ++ 8 files changed, 165 insertions(+), 47 deletions(-) create mode 100644 src/backend/common/debug-info.ts diff --git a/src/backend/app-management/electron/window-management.js b/src/backend/app-management/electron/window-management.js index 354669237..0dc1551c0 100644 --- a/src/backend/app-management/electron/window-management.js +++ b/src/backend/app-management/electron/window-management.js @@ -15,6 +15,8 @@ const logger = require("../../logwrapper"); const EventEmitter = require("events"); +const { copyDebugInfoToClipboard } = require("../../common/debug-info"); + /** * Firebot's main window * Keeps a global reference of the window object, if you don't, the window will @@ -196,6 +198,12 @@ async function createAppMenu() { const overlayInstances = SettingsManager.getSetting("OverlayInstances"); + /** + * Steps to get new icon images: + * - Select icon from https://pictogrammers.com/library/mdi/ + * - Do an Advanced PNG Export with 48x48 size and a white foreground + */ + /** * @type {Electron.MenuItemConstructorOptions[]} */ @@ -474,6 +482,16 @@ async function createAppMenu() { { type: 'separator' }, + { + label: 'Copy Debug Info...', + click: () => { + copyDebugInfoToClipboard(); + }, + icon: await createIconImage("../../../gui/images/icons/mdi/bug-outline.png") + }, + { + type: 'separator' + }, { label: 'About Firebot...', click: () => { diff --git a/src/backend/common/common-listeners.js b/src/backend/common/common-listeners.js index a80259f8c..e7485091f 100644 --- a/src/backend/common/common-listeners.js +++ b/src/backend/common/common-listeners.js @@ -4,6 +4,7 @@ const { app, dialog, shell, autoUpdater } = require("electron"); const os = require('os'); const logger = require("../logwrapper"); const { restartApp } = require("../app-management/electron/app-helpers"); +const { copyDebugInfoToClipboard } = require("../common/debug-info"); function getLocalIpAddress() { try { @@ -136,4 +137,6 @@ exports.setupCommonListeners = () => { autoUpdater.quitAndInstall(); }); + + frontendCommunicator.on("copy-debug-info-to-clipboard", copyDebugInfoToClipboard); }; \ No newline at end of file diff --git a/src/backend/common/debug-info.ts b/src/backend/common/debug-info.ts new file mode 100644 index 000000000..5fcbfd70c --- /dev/null +++ b/src/backend/common/debug-info.ts @@ -0,0 +1,80 @@ + + +import { app } from "electron"; +import os from "os"; +import frontendCommunicator from "./frontend-communicator"; +import ConnectionManager from "./connection-manager"; +import { AccountAccess } from "./account-access"; +import HttpServerManager from "../../server/http-server-manager"; +import WebsocketServerManager from "../../server/websocket-server-manager"; +import startupScriptsManager from "../common/handlers/custom-scripts/startup-scripts-manager"; +import { isConnected } from "../integrations/builtin/obs/obs-remote"; + +function getOsName(platform: NodeJS.Platform): string { + if (platform === "darwin") { + return "macOS"; + } + + if (platform === "win32") { + return "Windows"; + } + + return "Linux"; +} + +async function getDebugInfoString(): Promise { + const appVersion = app.getVersion(); + + const electronVersion = process.versions.electron ?? "unknown"; + const nodeVersion = process.versions.node ?? process.version; + + const osName = getOsName(process.platform); + const osVersion = typeof process.getSystemVersion === "function" ? process.getSystemVersion() : os.release(); + const osArch = os.arch(); + + const { locale } = Intl.DateTimeFormat().resolvedOptions(); + + const accounts = AccountAccess.getAccounts(); + const streamerLoggedIn = accounts.streamer.loggedIn ? "Yes" : "No"; + const botLoggedIn = accounts.bot.loggedIn ? "Yes" : "No"; + + const connectedToTwitch = ConnectionManager.chatIsConnected() ? "Connected" : "Disconnected"; + const connectedToOBS = isConnected() ? "Connected" : "Disconnected"; + + const httpServerStatus = HttpServerManager.isDefaultServerStarted ? "Running" : "Stopped"; + const websocketClients = WebsocketServerManager.getNumberOfOverlayClients(); + + const startupScripts = Object.values(startupScriptsManager.getLoadedStartupScripts()); + + return [ + "Firebot Debug Info", + "------------------", + `OS: ${osName} ${osVersion} (${osArch})`, + `Firebot: ${appVersion}`, + `Electron: ${electronVersion}`, + `Node: ${nodeVersion}`, + `Locale: ${locale}\n`, + 'Accounts:', + ` - Streamer: ${streamerLoggedIn}`, + ` - Bot: ${botLoggedIn}\n`, + 'Connections:', + ` - Twitch: ${connectedToTwitch}`, + ` - OBS: ${connectedToOBS}\n`, + 'Server:', + ` - HTTP Server: ${httpServerStatus}`, + ` - Overlay Clients: ${websocketClients}\n`, + 'Plugins:', + startupScripts.length === 0 + ? " - None" + : startupScripts.map(script => ` - ${script.name}`).join("\n") + ].join("\n"); +} + +export async function copyDebugInfoToClipboard() { + const debugInfo = await getDebugInfoString(); + + frontendCommunicator.send("copy-to-clipboard", { + text: debugInfo, + toastMessage: "Debug information copied to clipboard" + }); +} \ No newline at end of file diff --git a/src/backend/common/handlers/custom-scripts/startup-scripts-manager.js b/src/backend/common/handlers/custom-scripts/startup-scripts-manager.js index 356ad5ec2..04dc335ea 100644 --- a/src/backend/common/handlers/custom-scripts/startup-scripts-manager.js +++ b/src/backend/common/handlers/custom-scripts/startup-scripts-manager.js @@ -98,6 +98,10 @@ function getStartupScriptData(startupScriptDataId) { return startupScripts[startupScriptDataId]; } +function getLoadedStartupScripts() { + return { ...startupScripts }; +} + /** * Turns startup script data into valid Custom Script effects and runs them */ @@ -123,4 +127,5 @@ frontendCommunicator.on("deleteStartupScriptData", (startupScriptDataId) => { exports.runStartupScripts = runStartupScripts; exports.loadStartupConfig = loadStartupConfig; -exports.getStartupScriptData = getStartupScriptData; \ No newline at end of file +exports.getStartupScriptData = getStartupScriptData; +exports.getLoadedStartupScripts = getLoadedStartupScripts; \ No newline at end of file diff --git a/src/gui/app/app-main.js b/src/gui/app/app-main.js index 2d0e83251..213a6cdcf 100644 --- a/src/gui/app/app-main.js +++ b/src/gui/app/app-main.js @@ -279,44 +279,20 @@ } }; - $rootScope.copyTextToClipboard = function(text) { - const textArea = document.createElement("textarea"); - // Place in top-left corner of screen regardless of scroll position. - textArea.style.position = "fixed"; - textArea.style.top = 0; - textArea.style.left = 0; - - // Ensure it has a small width and height. Setting to 1px / 1em - // doesn't work as this gives a negative w/h on some browsers. - textArea.style.width = "2em"; - textArea.style.height = "2em"; - - // We don't need padding, reducing the size if it does flash render. - textArea.style.padding = 0; - - // Clean up any borders. - textArea.style.border = "none"; - textArea.style.outline = "none"; - textArea.style.boxShadow = "none"; - - // Avoid flash of white box if rendered for any reason. - textArea.style.background = "transparent"; - - textArea.value = text; - - document.body.appendChild(textArea); - - textArea.select(); - - try { - const successful = document.execCommand("copy"); - const msg = successful ? "successful" : "unsuccessful"; - logger.info(`Copying text command was ${msg}`); - } catch { - logger.error("Oops, unable to copy text to clipboard."); - } + $rootScope.copyTextToClipboard = function(text, toastConfig = { show: false }) { + navigator.clipboard.writeText(text).then(function() { + logger.info("Text copied to clipboard"); + + if (toastConfig?.show) { + ngToast.create({ + className: 'info', + content: toastConfig.message || `Copied '${text}' to clipboard` + }); + } - document.body.removeChild(textArea); + }, function(err) { + logger.error("Could not copy text: ", err); + }); }; backendCommunicator.on("copy-to-clipboard", (data) => { @@ -324,14 +300,7 @@ return; } - $rootScope.copyTextToClipboard(data.text); - - if (!data.silent) { - ngToast.create({ - className: 'info', - content: data.toastMessage || `Copied '${data.text}' to clipboard` - }); - } + $rootScope.copyTextToClipboard(data.text, { show: !data.silent, message: data.toastMessage }); return; }); diff --git a/src/gui/app/directives/modals/misc/about-modal.js b/src/gui/app/directives/modals/misc/about-modal.js index 84c0b71e9..95054bbb9 100644 --- a/src/gui/app/directives/modals/misc/about-modal.js +++ b/src/gui/app/directives/modals/misc/about-modal.js @@ -114,6 +114,9 @@ Submit a Testimonial

+
+ +
`, bindings: { @@ -121,7 +124,7 @@ close: "&", dismiss: "&" }, - controller: function() { + controller: function(backendCommunicator) { const $ctrl = this; $ctrl.$onInit = function() { @@ -129,6 +132,10 @@ $ctrl.osType = firebotAppDetails.os.type; $ctrl.osVersion = firebotAppDetails.os.release; }; + + $ctrl.copyDebugInfoToClipboard = function() { + backendCommunicator.send("copy-debug-info-to-clipboard"); + }; } }); }()); diff --git a/src/gui/scss/core/_bootstrap-overrides.scss b/src/gui/scss/core/_bootstrap-overrides.scss index df2c2d8dd..87663d310 100644 --- a/src/gui/scss/core/_bootstrap-overrides.scss +++ b/src/gui/scss/core/_bootstrap-overrides.scss @@ -243,6 +243,34 @@ } } +.btn-default-outlined { + border: 2px solid $default-btn-bg-color; + background-color: transparent; + color: $default-btn-text-color; + &:focus { + border: 2px solid $default-btn-bg-color; + background-color: transparent; + color: $default-btn-text-color; + border-color: $default-btn-bg-color !important; + } + &:disabled { + border: 2px solid $default-btn-bg-color; + background-color: transparent; + color: $default-btn-text-color; + opacity: 0.5; + &:hover { + background-color: $default-btn-bg-color; + color: $default-btn-text-color; + opacity: 0.75; + } + } + &:hover { + background-color: $default-btn-hover-bg; + color: $default-btn-text-color; + } +} + + .btn-default.active.focus, .btn-default.active:focus, .btn-default.active:hover, diff --git a/src/server/websocket-server-manager.ts b/src/server/websocket-server-manager.ts index c941d0507..194cc4887 100644 --- a/src/server/websocket-server-manager.ts +++ b/src/server/websocket-server-manager.ts @@ -235,6 +235,14 @@ class WebSocketServerManager extends EventEmitter { } } + getNumberOfOverlayClients(): number { + if (this.server == null) { + return 0; + } + + return [...this.server.clients].filter(client => client.type === "overlay").length; + } + registerCustomWebSocketListener(pluginName: string, callback: CustomWebSocketHandler["callback"]): boolean { if (this.customHandlers.findIndex(p => p.pluginName.toLowerCase() === pluginName.toLowerCase()) === -1) { this.customHandlers.push({ From 1bbebbcc141b2dc19692201f926f40d7efd0665a Mon Sep 17 00:00:00 2001 From: ebiggz Date: Sat, 13 Dec 2025 21:42:36 -0700 Subject: [PATCH 6/8] fix: restriction manager not falling back to "all" mode if no mode is specified --- src/backend/restrictions/restriction-manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/restrictions/restriction-manager.ts b/src/backend/restrictions/restriction-manager.ts index 80056d2b2..61aaee3af 100644 --- a/src/backend/restrictions/restriction-manager.ts +++ b/src/backend/restrictions/restriction-manager.ts @@ -147,7 +147,7 @@ class RestrictionsManager extends TypedEmitter { } return Promise.resolve(); - } else if (restrictionData.mode === "all") { + } else if (restrictionData.mode === "all" || restrictionData.mode == null) { const predicatePromises = []; for (const restriction of restrictions) { const restrictionDef = this.getRestrictionById(restriction.type); @@ -165,6 +165,8 @@ class RestrictionsManager extends TypedEmitter { } }); } + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors, @typescript-eslint/restrict-template-expressions + return Promise.reject(`Invalid restriction mode '${restrictionData.mode}'`); } } From 60e461d52789498dade6696d5a0631f946c03ee0 Mon Sep 17 00:00:00 2001 From: ebiggz Date: Sat, 13 Dec 2025 22:20:36 -0700 Subject: [PATCH 7/8] fix: handle errors when saving new profile to global settings #3378 --- src/backend/common/profile-manager.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/backend/common/profile-manager.ts b/src/backend/common/profile-manager.ts index 12a21014b..7c8286908 100644 --- a/src/backend/common/profile-manager.ts +++ b/src/backend/common/profile-manager.ts @@ -1,4 +1,4 @@ -import { app } from "electron"; +import { app, dialog } from "electron"; import { JsonDB } from "node-json-db"; import fs from "fs"; import path from "path"; @@ -104,9 +104,16 @@ class ProfileManager { activeProfiles.push(profileId); // Push our new profile to settings. - globalSettingsDb.push("/profiles/activeProfiles", activeProfiles); - globalSettingsDb.push("/profiles/loggedInProfile", profileId); - logger.info(`New profile created: ${profileId}.${restart ? " Restarting." : ""}`); + try { + globalSettingsDb.push("/profiles/activeProfiles", activeProfiles); + globalSettingsDb.push("/profiles/loggedInProfile", profileId); + logger.info(`New profile created: ${profileId}.${restart ? " Restarting." : ""}`); + } catch (error) { + logger.error(`Error saving ${profileId} profile to global settings. Is the file locked or corrupted?`, error); + dialog.showErrorBox("Error Loading Profile", `An error occurred while trying to load your ${profileId} profile. Please try starting Firebot again. If this issue continues, please reach out on our Discord for support.`); + app.quit(); + return; + } // Log the new profile in and (optionally) restart app. this.logInProfile(profileId, restart); From 98187e477e9e02a2a74ee5ef638c8fd7c09ffac1 Mon Sep 17 00:00:00 2001 From: ebiggz Date: Sat, 13 Dec 2025 22:45:27 -0700 Subject: [PATCH 8/8] chore: further improved logs when global settings file is corrupted --- src/backend/common/profile-manager.ts | 3 ++- src/backend/common/settings-manager.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/common/profile-manager.ts b/src/backend/common/profile-manager.ts index 7c8286908..98b13c9b5 100644 --- a/src/backend/common/profile-manager.ts +++ b/src/backend/common/profile-manager.ts @@ -109,7 +109,8 @@ class ProfileManager { globalSettingsDb.push("/profiles/loggedInProfile", profileId); logger.info(`New profile created: ${profileId}.${restart ? " Restarting." : ""}`); } catch (error) { - logger.error(`Error saving ${profileId} profile to global settings. Is the file locked or corrupted?`, error); + const errorMessage = (error as Error).name === "DatabaseError" ? error?.inner?.message ?? error.stack : error; + logger.error(`Error saving ${profileId} profile to global settings. Is the file locked or corrupted?`, errorMessage); dialog.showErrorBox("Error Loading Profile", `An error occurred while trying to load your ${profileId} profile. Please try starting Firebot again. If this issue continues, please reach out on our Discord for support.`); app.quit(); return; diff --git a/src/backend/common/settings-manager.ts b/src/backend/common/settings-manager.ts index 5f2eda052..624b2b873 100644 --- a/src/backend/common/settings-manager.ts +++ b/src/backend/common/settings-manager.ts @@ -139,7 +139,9 @@ class SettingsManager extends EventEmitter { if (defaultValue !== undefined) { this.settingsCache[settingPath] = defaultValue; } - if ((err as Error).name !== "DataError") { + if ((err as Error).name === "DatabaseError") { + logger.error(`Failed to read "${settingPath}" in global settings file. File may be corrupt.`, err?.inner?.message ?? err.stack); + } else if ((err as Error).name !== "DataError") { logger.warn(err); } }