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 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": { 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/backend/common/profile-manager.ts b/src/backend/common/profile-manager.ts index 12a21014b..98b13c9b5 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,17 @@ 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) { + 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; + } // Log the new profile in and (optionally) restart app. this.logInProfile(profileId, restart); 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); } } 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 = ` -
-
+
+
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}'`); } } 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/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 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({