From e8762b329c746c7436d2f9f344267f1df5dd67a0 Mon Sep 17 00:00:00 2001 From: CKY- Date: Thu, 29 Aug 2024 12:22:44 -0600 Subject: [PATCH 01/17] fix: remove rotation height --- .../app/directives/effect-option-settings/eosOverlayRotation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/app/directives/effect-option-settings/eosOverlayRotation.js b/src/gui/app/directives/effect-option-settings/eosOverlayRotation.js index cea171234..5cdcce7f3 100644 --- a/src/gui/app/directives/effect-option-settings/eosOverlayRotation.js +++ b/src/gui/app/directives/effect-option-settings/eosOverlayRotation.js @@ -10,7 +10,7 @@ }, template: ` -
+
Date: Tue, 27 Aug 2024 19:04:58 -0500 Subject: [PATCH 02/17] feat: Add Custom Role filtering to $activeChatUserCount - adds `$activeChatUserCount[customRole]` for filtering the count of active chat users by the custom role. - Implements #2750 --- .../builtin/misc/active-chat-user-count.ts | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/backend/variables/builtin/misc/active-chat-user-count.ts b/src/backend/variables/builtin/misc/active-chat-user-count.ts index 187ee0a8b..e7434bdc9 100644 --- a/src/backend/variables/builtin/misc/active-chat-user-count.ts +++ b/src/backend/variables/builtin/misc/active-chat-user-count.ts @@ -1,20 +1,43 @@ import { ReplaceVariable } from "../../../../types/variables"; import { OutputDataType, VariableCategory } from "../../../../shared/variable-constants"; - -const logger = require("../../../logwrapper"); -const activeViewerHandler = require("../../../chat/chat-listeners/active-user-handler"); +import { getActiveUserCount, getAllActiveUsers } from "../../../chat/chat-listeners/active-user-handler"; +import customRolesManager from "../../../roles/custom-roles-manager"; +import logger from "../../../logwrapper"; const model : ReplaceVariable = { definition: { handle: "activeChatUserCount", description: "Get the number of active viewers in chat.", categories: [VariableCategory.NUMBERS], - possibleDataOutput: [OutputDataType.NUMBER] + possibleDataOutput: [OutputDataType.NUMBER], + examples: [ + { + usage: "activeChatUserCount[CustomRole]", + description: "Gets the number of active viewers in the specified custom role." + } + ] }, - evaluator: async () => { + evaluator: async (_, ...args: unknown[]) => { logger.debug("Getting number of active viewers in chat."); - return activeViewerHandler.getActiveUserCount() || 0; + if (args && args.length >= 1 && args[0] && args[0] !== "" && `${args[0]}`.toLowerCase() !== "null") { + const customRole = customRolesManager.getRoleByName(`${args[0]}`); + if (customRole == null) { + logger.warn(`Unable to get custom role from name ${args[0]}`); + return 0; + } + + const customRoleUsers = customRole.viewers.map(crv => crv.username); + if (customRoleUsers.length === 0) { + logger.warn(`Custom role named ${customRole.name} appears to be empty`); + return 0; + } + + const activeCustomRoleUsers = getAllActiveUsers().filter(user => customRoleUsers.includes(user.username)); + return activeCustomRoleUsers.length; + } + + return getActiveUserCount() || 0; } }; From c8b5d576ed12819f8c3b79184eb00123672db6f5 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Sun, 1 Sep 2024 14:22:26 -0500 Subject: [PATCH 03/17] Some more work on anchor transform update --- package-lock.json | 4 +- .../obs/effects/transform-obs-source-scale.ts | 206 ++++++++++-------- .../builtin/obs/obs-integration.ts | 4 +- .../integrations/builtin/obs/obs-remote.ts | 37 +++- 4 files changed, 153 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dbfb9bc8..197f53685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebotv5", - "version": "5.63.0-beta3", + "version": "5.63.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebotv5", - "version": "5.63.0-beta3", + "version": "5.63.1", "license": "GPL-3.0", "dependencies": { "@aws-sdk/client-polly": "^3.26.0", diff --git a/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts b/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts index b1a02ab1f..2170261c1 100644 --- a/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts +++ b/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts @@ -1,7 +1,7 @@ import { EffectType } from "../../../../../types/effects"; import { OBSSceneItem, OBSSourceTransformKeys, transformSceneItem } from "../obs-remote"; -export const TransformSourceScaleEffectType: EffectType<{ +export const TransformSourceEffectType: EffectType<{ sceneName?: string; sceneItem?: OBSSceneItem; duration: string | number; @@ -10,6 +10,7 @@ export const TransformSourceScaleEffectType: EffectType<{ isTransformingPosition: boolean; isTransformingScale: boolean; isTransformingRotation: boolean; + positionalAlignment: number; startTransform: Record; endTransform: Record; }> = { @@ -52,110 +53,129 @@ export const TransformSourceScaleEffectType: EffectType<{ No transformable sources found. {{ isObsConfigured ? "Is OBS running?" : "Have you configured the OBS integration?" }}
- - -
- - -
-
- - -
-
- + + +
+ -
-
- - -
-
- -
-
- - + + + + + + +
+
+ + +
+
+ + +
-
- - + +
+
+ + +
+
+ + +
-
- -
-
- - + +
+
+ + +
-
- + +
`, optionsController: ($scope: any, backendCommunicator: any) => { $scope.isObsConfigured = false; $scope.scenes = []; $scope.sceneItems = []; + $scope.positionalAlignmentOptions = Object.freeze({ + [5]: "Top Left", + [4]: "Top", + [6]: "Top Right", + [1]: "Center Left", + [0]: "Center", + [2]: "Center Right", + [8]: "Bottom", + [9]: "Bottom Left", + [10]: "Bottom Right" + }); $scope.selectScene = (sceneName: string) => { $scope.effect.sceneItem = undefined; diff --git a/src/backend/integrations/builtin/obs/obs-integration.ts b/src/backend/integrations/builtin/obs/obs-integration.ts index 3bc4350f1..5887533e7 100644 --- a/src/backend/integrations/builtin/obs/obs-integration.ts +++ b/src/backend/integrations/builtin/obs/obs-integration.ts @@ -23,7 +23,7 @@ import { CreateRecordChapter } from "./effects/create-recording-chapter"; import { ToggleSourceVisibilityEffectType } from "./effects/toggle-obs-source-visibility"; import { ToggleSourceFilterEffectType } from "./effects/toggle-obs-source-filter"; import { ToggleSourceMutedEffectType } from "./effects/toggle-obs-source-muted"; -import { TransformSourceScaleEffectType } from "./effects/transform-obs-source-scale"; +import { TransformSourceEffectType } from "./effects/transform-obs-source-scale"; import { StartStreamEffectType } from "./effects/start-stream"; import { StopStreamEffectType } from "./effects/stop-stream"; import { StartVirtualCamEffectType } from "./effects/start-virtual-cam"; @@ -146,7 +146,7 @@ class ObsIntegration effectManager.registerEffect(ToggleSourceVisibilityEffectType); effectManager.registerEffect(ToggleSourceFilterEffectType); effectManager.registerEffect(ToggleSourceMutedEffectType); - effectManager.registerEffect(TransformSourceScaleEffectType); + effectManager.registerEffect(TransformSourceEffectType); effectManager.registerEffect(StartStreamEffectType); effectManager.registerEffect(StopStreamEffectType); effectManager.registerEffect(StartVirtualCamEffectType); diff --git a/src/backend/integrations/builtin/obs/obs-remote.ts b/src/backend/integrations/builtin/obs/obs-remote.ts index 8b97c31d0..84fbd6899 100644 --- a/src/backend/integrations/builtin/obs/obs-remote.ts +++ b/src/backend/integrations/builtin/obs/obs-remote.ts @@ -1108,6 +1108,17 @@ function getLerpedCallsArray( return calls; } +export function getOffsetMultipliersFromAlignment(alignment: number) { + const offsets = [ + alignment % 4, // X position, 0 if center, 1 if left, 2 if right + Math.floor(alignment / 4) // Y position, 0 if center, 1 if top, 2 if bottom + ].map(offset => + // Convert to usable offset multiplier + [0.5, 0, 1][offset] + ); + return offsets; +} + export async function transformSceneItem( sceneName: string, sceneItemId: number, @@ -1115,7 +1126,8 @@ export async function transformSceneItem( transformStart: Record, transformEnd: Record, easeIn: boolean, - easeOut: boolean + easeOut: boolean, + positionalAlignment?: number ) { try { const currentTransform = (await obs.call("GetSceneItemTransform", { @@ -1123,6 +1135,28 @@ export async function transformSceneItem( sceneItemId })).sceneItemTransform; + // If anchor change, update transformStart to account + if (positionalAlignment && positionalAlignment !== currentTransform.alignment) { + const [currentXOffset, currentYOffset] = getOffsetMultipliersFromAlignment(Number(currentTransform.alignment)); + const [endXOffset, endYOffset] = getOffsetMultipliersFromAlignment(Number(positionalAlignment)); + + transformStart.alignment = positionalAlignment; + if (!transformStart.hasOwnProperty("positionX")) { + const posX = Number(currentTransform.positionX); + transformStart.positionX = posX + posX * (currentXOffset - endXOffset); + } + if (!transformEnd.hasOwnProperty("positionX")) { + transformEnd.positionX = transformStart.positionX; + } + if (!transformStart.hasOwnProperty("positionY")) { + const posY = Number(currentTransform.positionY); + transformStart.positionY = posY + posY * (currentYOffset - endYOffset); + } + if (!transformEnd.hasOwnProperty("positionY")) { + transformEnd.positionY = transformStart.positionY; + } + } + Object.keys(transformEnd).forEach((key) => { if (!transformStart.hasOwnProperty(key)) { transformStart[key] = Number(currentTransform[key]); @@ -1133,6 +1167,7 @@ export async function transformSceneItem( }); const calls = getLerpedCallsArray(sceneName, sceneItemId, transformStart, transformEnd, duration, easeIn, easeOut); + await obs.callBatch(calls); } catch (error) { logger.error("Failed to transform scene item", error); From ea4df84fb1eef9c1021e6eee863d4548e8d5d958 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Sun, 1 Sep 2024 14:22:26 -0500 Subject: [PATCH 04/17] Some more work on anchor transform update --- package-lock.json | 4 +- .../obs/effects/transform-obs-source-scale.ts | 206 ++++++++++-------- .../builtin/obs/obs-integration.ts | 4 +- .../integrations/builtin/obs/obs-remote.ts | 37 +++- 4 files changed, 153 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dbfb9bc8..197f53685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebotv5", - "version": "5.63.0-beta3", + "version": "5.63.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebotv5", - "version": "5.63.0-beta3", + "version": "5.63.1", "license": "GPL-3.0", "dependencies": { "@aws-sdk/client-polly": "^3.26.0", diff --git a/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts b/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts index b1a02ab1f..2170261c1 100644 --- a/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts +++ b/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts @@ -1,7 +1,7 @@ import { EffectType } from "../../../../../types/effects"; import { OBSSceneItem, OBSSourceTransformKeys, transformSceneItem } from "../obs-remote"; -export const TransformSourceScaleEffectType: EffectType<{ +export const TransformSourceEffectType: EffectType<{ sceneName?: string; sceneItem?: OBSSceneItem; duration: string | number; @@ -10,6 +10,7 @@ export const TransformSourceScaleEffectType: EffectType<{ isTransformingPosition: boolean; isTransformingScale: boolean; isTransformingRotation: boolean; + positionalAlignment: number; startTransform: Record; endTransform: Record; }> = { @@ -52,110 +53,129 @@ export const TransformSourceScaleEffectType: EffectType<{ No transformable sources found. {{ isObsConfigured ? "Is OBS running?" : "Have you configured the OBS integration?" }}
- - -
- - -
-
- - -
-
- + + +
+ -
-
- - -
-
- -
-
- - + + + + + + +
+
+ + +
+
+ + +
-
- - + +
+
+ + +
+
+ + +
-
- -
-
- - + +
+
+ + +
-
- + +
`, optionsController: ($scope: any, backendCommunicator: any) => { $scope.isObsConfigured = false; $scope.scenes = []; $scope.sceneItems = []; + $scope.positionalAlignmentOptions = Object.freeze({ + [5]: "Top Left", + [4]: "Top", + [6]: "Top Right", + [1]: "Center Left", + [0]: "Center", + [2]: "Center Right", + [8]: "Bottom", + [9]: "Bottom Left", + [10]: "Bottom Right" + }); $scope.selectScene = (sceneName: string) => { $scope.effect.sceneItem = undefined; diff --git a/src/backend/integrations/builtin/obs/obs-integration.ts b/src/backend/integrations/builtin/obs/obs-integration.ts index 3bc4350f1..5887533e7 100644 --- a/src/backend/integrations/builtin/obs/obs-integration.ts +++ b/src/backend/integrations/builtin/obs/obs-integration.ts @@ -23,7 +23,7 @@ import { CreateRecordChapter } from "./effects/create-recording-chapter"; import { ToggleSourceVisibilityEffectType } from "./effects/toggle-obs-source-visibility"; import { ToggleSourceFilterEffectType } from "./effects/toggle-obs-source-filter"; import { ToggleSourceMutedEffectType } from "./effects/toggle-obs-source-muted"; -import { TransformSourceScaleEffectType } from "./effects/transform-obs-source-scale"; +import { TransformSourceEffectType } from "./effects/transform-obs-source-scale"; import { StartStreamEffectType } from "./effects/start-stream"; import { StopStreamEffectType } from "./effects/stop-stream"; import { StartVirtualCamEffectType } from "./effects/start-virtual-cam"; @@ -146,7 +146,7 @@ class ObsIntegration effectManager.registerEffect(ToggleSourceVisibilityEffectType); effectManager.registerEffect(ToggleSourceFilterEffectType); effectManager.registerEffect(ToggleSourceMutedEffectType); - effectManager.registerEffect(TransformSourceScaleEffectType); + effectManager.registerEffect(TransformSourceEffectType); effectManager.registerEffect(StartStreamEffectType); effectManager.registerEffect(StopStreamEffectType); effectManager.registerEffect(StartVirtualCamEffectType); diff --git a/src/backend/integrations/builtin/obs/obs-remote.ts b/src/backend/integrations/builtin/obs/obs-remote.ts index 69f33fc51..726b221c5 100644 --- a/src/backend/integrations/builtin/obs/obs-remote.ts +++ b/src/backend/integrations/builtin/obs/obs-remote.ts @@ -1099,6 +1099,17 @@ function getLerpedCallsArray( return calls; } +export function getOffsetMultipliersFromAlignment(alignment: number) { + const offsets = [ + alignment % 4, // X position, 0 if center, 1 if left, 2 if right + Math.floor(alignment / 4) // Y position, 0 if center, 1 if top, 2 if bottom + ].map(offset => + // Convert to usable offset multiplier + [0.5, 0, 1][offset] + ); + return offsets; +} + export async function transformSceneItem( sceneName: string, sceneItemId: number, @@ -1106,7 +1117,8 @@ export async function transformSceneItem( transformStart: Record, transformEnd: Record, easeIn: boolean, - easeOut: boolean + easeOut: boolean, + positionalAlignment?: number ) { try { const currentTransform = (await obs.call("GetSceneItemTransform", { @@ -1114,6 +1126,28 @@ export async function transformSceneItem( sceneItemId })).sceneItemTransform; + // If anchor change, update transformStart to account + if (positionalAlignment && positionalAlignment !== currentTransform.alignment) { + const [currentXOffset, currentYOffset] = getOffsetMultipliersFromAlignment(Number(currentTransform.alignment)); + const [endXOffset, endYOffset] = getOffsetMultipliersFromAlignment(Number(positionalAlignment)); + + transformStart.alignment = positionalAlignment; + if (!transformStart.hasOwnProperty("positionX")) { + const posX = Number(currentTransform.positionX); + transformStart.positionX = posX + posX * (currentXOffset - endXOffset); + } + if (!transformEnd.hasOwnProperty("positionX")) { + transformEnd.positionX = transformStart.positionX; + } + if (!transformStart.hasOwnProperty("positionY")) { + const posY = Number(currentTransform.positionY); + transformStart.positionY = posY + posY * (currentYOffset - endYOffset); + } + if (!transformEnd.hasOwnProperty("positionY")) { + transformEnd.positionY = transformStart.positionY; + } + } + Object.keys(transformEnd).forEach((key) => { if (!transformStart.hasOwnProperty(key)) { transformStart[key] = Number(currentTransform[key]); @@ -1124,6 +1158,7 @@ export async function transformSceneItem( }); const calls = getLerpedCallsArray(sceneName, sceneItemId, transformStart, transformEnd, duration, easeIn, easeOut); + await obs.callBatch(calls); } catch (error) { logger.error("Failed to transform scene item", error); From 99c74091c23b4cb5fa363be15c8db25b2f91421c Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Sun, 1 Sep 2024 16:21:52 -0500 Subject: [PATCH 05/17] Finished effect --- ...ource-scale.ts => transform-obs-source.ts} | 12 +++++---- .../builtin/obs/obs-integration.ts | 2 +- .../integrations/builtin/obs/obs-remote.ts | 26 ++++++++++--------- 3 files changed, 22 insertions(+), 18 deletions(-) rename src/backend/integrations/builtin/obs/effects/{transform-obs-source-scale.ts => transform-obs-source.ts} (97%) diff --git a/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts b/src/backend/integrations/builtin/obs/effects/transform-obs-source.ts similarity index 97% rename from src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts rename to src/backend/integrations/builtin/obs/effects/transform-obs-source.ts index 2170261c1..dc74ec7c2 100644 --- a/src/backend/integrations/builtin/obs/effects/transform-obs-source-scale.ts +++ b/src/backend/integrations/builtin/obs/effects/transform-obs-source.ts @@ -10,7 +10,7 @@ export const TransformSourceEffectType: EffectType<{ isTransformingPosition: boolean; isTransformingScale: boolean; isTransformingRotation: boolean; - positionalAlignment: number; + alignment: number; startTransform: Record; endTransform: Record; }> = { @@ -76,8 +76,8 @@ export const TransformSourceEffectType: EffectType<{
@@ -165,7 +165,7 @@ export const TransformSourceEffectType: EffectType<{ $scope.scenes = []; $scope.sceneItems = []; - $scope.positionalAlignmentOptions = Object.freeze({ + $scope.alignmentOptions = Object.freeze({ [5]: "Top Left", [4]: "Top", [6]: "Top Right", @@ -228,6 +228,7 @@ export const TransformSourceEffectType: EffectType<{ if (isNaN(Number(effect.duration))) { effect.duration = 0; } + const alignment = effect.alignment ? Number(effect.alignment) : undefined; const parsedStart: Record = {}; const parsedEnd: Record = {}; const transformKeys: Array = []; @@ -263,7 +264,8 @@ export const TransformSourceEffectType: EffectType<{ parsedStart, parsedEnd, effect.easeIn, - effect.easeOut + effect.easeOut, + alignment ); return true; diff --git a/src/backend/integrations/builtin/obs/obs-integration.ts b/src/backend/integrations/builtin/obs/obs-integration.ts index 5887533e7..671c43834 100644 --- a/src/backend/integrations/builtin/obs/obs-integration.ts +++ b/src/backend/integrations/builtin/obs/obs-integration.ts @@ -23,7 +23,7 @@ import { CreateRecordChapter } from "./effects/create-recording-chapter"; import { ToggleSourceVisibilityEffectType } from "./effects/toggle-obs-source-visibility"; import { ToggleSourceFilterEffectType } from "./effects/toggle-obs-source-filter"; import { ToggleSourceMutedEffectType } from "./effects/toggle-obs-source-muted"; -import { TransformSourceEffectType } from "./effects/transform-obs-source-scale"; +import { TransformSourceEffectType } from "./effects/transform-obs-source"; import { StartStreamEffectType } from "./effects/start-stream"; import { StopStreamEffectType } from "./effects/stop-stream"; import { StartVirtualCamEffectType } from "./effects/start-virtual-cam"; diff --git a/src/backend/integrations/builtin/obs/obs-remote.ts b/src/backend/integrations/builtin/obs/obs-remote.ts index 726b221c5..0596997fa 100644 --- a/src/backend/integrations/builtin/obs/obs-remote.ts +++ b/src/backend/integrations/builtin/obs/obs-remote.ts @@ -1099,16 +1099,15 @@ function getLerpedCallsArray( return calls; } -export function getOffsetMultipliersFromAlignment(alignment: number) { - const offsets = [ +export const getOffsetMultipliersFromAlignment = (alignment: number) => ( + [ alignment % 4, // X position, 0 if center, 1 if left, 2 if right Math.floor(alignment / 4) // Y position, 0 if center, 1 if top, 2 if bottom ].map(offset => // Convert to usable offset multiplier [0.5, 0, 1][offset] - ); - return offsets; -} + ) +); export async function transformSceneItem( sceneName: string, @@ -1118,7 +1117,7 @@ export async function transformSceneItem( transformEnd: Record, easeIn: boolean, easeOut: boolean, - positionalAlignment?: number + alignment?: number ) { try { const currentTransform = (await obs.call("GetSceneItemTransform", { @@ -1127,21 +1126,24 @@ export async function transformSceneItem( })).sceneItemTransform; // If anchor change, update transformStart to account - if (positionalAlignment && positionalAlignment !== currentTransform.alignment) { - const [currentXOffset, currentYOffset] = getOffsetMultipliersFromAlignment(Number(currentTransform.alignment)); - const [endXOffset, endYOffset] = getOffsetMultipliersFromAlignment(Number(positionalAlignment)); + const currentAlignment = Number(currentTransform.alignment); + if (!isNaN(alignment) && alignment !== currentAlignment) { + const [currentXOffset, currentYOffset] = getOffsetMultipliersFromAlignment(currentAlignment); + const [endXOffset, endYOffset] = getOffsetMultipliersFromAlignment(alignment); - transformStart.alignment = positionalAlignment; + transformStart.alignment = alignment; if (!transformStart.hasOwnProperty("positionX")) { const posX = Number(currentTransform.positionX); - transformStart.positionX = posX + posX * (currentXOffset - endXOffset); + const width = Number(currentTransform.width); + transformStart.positionX = posX + width * (endXOffset - currentXOffset); } if (!transformEnd.hasOwnProperty("positionX")) { transformEnd.positionX = transformStart.positionX; } if (!transformStart.hasOwnProperty("positionY")) { const posY = Number(currentTransform.positionY); - transformStart.positionY = posY + posY * (currentYOffset - endYOffset); + const height = Number(currentTransform.height); + transformStart.positionY = posY + height * (endYOffset - currentYOffset); } if (!transformEnd.hasOwnProperty("positionY")) { transformEnd.positionY = transformStart.positionY; From 5a38e84bea6b5119110718bd77c9cfed48912e11 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Sun, 1 Sep 2024 16:50:08 -0500 Subject: [PATCH 06/17] Undid changes to package-lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 197f53685..2dbfb9bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebotv5", - "version": "5.63.1", + "version": "5.63.0-beta3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebotv5", - "version": "5.63.1", + "version": "5.63.0-beta3", "license": "GPL-3.0", "dependencies": { "@aws-sdk/client-polly": "^3.26.0", From 4cfac66b10120bcc63a32816846fe414c8fe0885 Mon Sep 17 00:00:00 2001 From: Izzy Deane Date: Sun, 1 Sep 2024 17:10:01 -0500 Subject: [PATCH 07/17] Removed unnecessary newline --- src/backend/integrations/builtin/obs/obs-remote.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/integrations/builtin/obs/obs-remote.ts b/src/backend/integrations/builtin/obs/obs-remote.ts index 0596997fa..187a1549f 100644 --- a/src/backend/integrations/builtin/obs/obs-remote.ts +++ b/src/backend/integrations/builtin/obs/obs-remote.ts @@ -1160,7 +1160,6 @@ export async function transformSceneItem( }); const calls = getLerpedCallsArray(sceneName, sceneItemId, transformStart, transformEnd, duration, easeIn, easeOut); - await obs.callBatch(calls); } catch (error) { logger.error("Failed to transform scene item", error); From fde907a90286481c4cea535cd2a983d08a175b5e Mon Sep 17 00:00:00 2001 From: Oceanity Date: Wed, 11 Sep 2024 18:41:38 -0500 Subject: [PATCH 08/17] Feat: `arraySlice` replace variable (#2776) * Array slice variable * Added some safety measures to the start and end parsing --------- Co-authored-by: Erik Bigler --- .../variables/builtin/array/array-slice.ts | 43 +++++++++++++++++++ src/backend/variables/builtin/array/index.ts | 6 ++- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/backend/variables/builtin/array/array-slice.ts diff --git a/src/backend/variables/builtin/array/array-slice.ts b/src/backend/variables/builtin/array/array-slice.ts new file mode 100644 index 000000000..16c0915b9 --- /dev/null +++ b/src/backend/variables/builtin/array/array-slice.ts @@ -0,0 +1,43 @@ +import { ReplaceVariable, Trigger } from "../../../../types/variables"; +import { OutputDataType, VariableCategory } from "../../../../shared/variable-constants"; + +const model : ReplaceVariable = { + definition: { + handle: "arraySlice", + description: "Returns a slice of an array", + usage: "arraySlice[array, start, end]", + categories: [VariableCategory.ADVANCED], + possibleDataOutput: [OutputDataType.TEXT] + }, + evaluator: ( + trigger: Trigger, + subject: string | unknown[], + start?: string | number, + end?: string | number + ) : unknown[] => { + if (typeof subject === 'string' || subject instanceof String) { + try { + subject = JSON.parse(`${subject}`); + } catch (ignore) { + return []; + } + } + if (!Array.isArray(subject)) { + return []; + } + + start = start ? Number(start) : 0; + if (Number.isNaN(start)) { + start = 0; + } + + end = end ? Number(end) : subject.length; + if (Number.isNaN(end)) { + end = subject.length; + } + + return [...subject].slice(start, end); + } +}; + +export default model; \ No newline at end of file diff --git a/src/backend/variables/builtin/array/index.ts b/src/backend/variables/builtin/array/index.ts index 6841c0838..9ae88a571 100644 --- a/src/backend/variables/builtin/array/index.ts +++ b/src/backend/variables/builtin/array/index.ts @@ -1,5 +1,5 @@ import arrayAdd from './array-add'; -import arrayElemenet from './array-element'; +import arrayElement from './array-element'; import arrayFilter from './array-filter'; import arrayFindIndex from './array-find-index'; import arrayFind from './array-find'; @@ -11,6 +11,7 @@ import arrayRandomItem from './array-random-item'; import arrayRemove from './array-remove'; import arrayReverse from './array-reverse'; import arrayShuffle from './array-shuffle'; +import arraySlice from './array-slice'; // Deprecated import rawArrayAdd from './raw-array-add'; @@ -27,7 +28,7 @@ import rawArrayShuffle from './raw-array-shuffle'; export default [ arrayAdd, - arrayElemenet, + arrayElement, arrayFilter, arrayFindIndex, arrayFind, @@ -39,6 +40,7 @@ export default [ arrayRemove, arrayReverse, arrayShuffle, + arraySlice, // Deprecated rawArrayAdd, From 9ac36a0f9b6cf89a22c8e6625f89d9a9f54daae9 Mon Sep 17 00:00:00 2001 From: CKY Date: Wed, 11 Sep 2024 17:43:39 -0600 Subject: [PATCH 09/17] fix(events): complex manualMetadata being malformed on test event button (#2784) * fix(events): complex manualMetadata being malformed on test event button * deepClone metadata --- src/backend/events/EventManager.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backend/events/EventManager.js b/src/backend/events/EventManager.js index dd350465d..b9594d604 100644 --- a/src/backend/events/EventManager.js +++ b/src/backend/events/EventManager.js @@ -124,7 +124,13 @@ ipcMain.on("triggerManualEvent", function(_, data) { return; } - const meta = event.manualMetadata || {}; + const meta = structuredClone(event.manualMetadata || {}); + for (const [key, value] of Object.entries(meta)) { + if (typeof value !== 'object' || value == null || Array.isArray(value) || value.type == null || value.value == null) { + continue; + } + meta[key] = value.value; + } if (meta.username == null) { const accountAccess = require("../common/account-access"); meta.username = accountAccess.getAccounts().streamer.username; From 3a25b0c325bf7a403d401b8bf56854c2f13f1ed2 Mon Sep 17 00:00:00 2001 From: CKY Date: Wed, 11 Sep 2024 17:46:07 -0600 Subject: [PATCH 10/17] fix: Move clips to iframe (#2778) * fix: Move clips to iframe * fix: clipSlug to embedUrl --- src/backend/effects/builtin/clips.js | 23 ++++++++++++----------- src/backend/effects/builtin/play-video.js | 5 +++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/backend/effects/builtin/clips.js b/src/backend/effects/builtin/clips.js index 5f4c42cc3..6adb4150d 100644 --- a/src/backend/effects/builtin/clips.js +++ b/src/backend/effects/builtin/clips.js @@ -211,7 +211,7 @@ const clip = { } webServer.sendToOverlay("playTwitchClip", { - clipSlug: clip.id, + clipVideoUrl: clip.embedUrl, width: effect.width, height: effect.height, duration: clipDuration, @@ -277,18 +277,19 @@ const clip = { rotation } = event; - const styles = (width ? `width: ${width}px;` : '') + - (height ? `height: ${height}px;` : '') + - (rotation ? `transform: rotate(${rotation});` : ''); + // eslint-disable-next-line prefer-template + const styles = `width: ${width || screen.width}px; + height: ${height || screen.height}px; + transform: rotate(${rotation || 0});`; const videoElement = ` -
- +
@@ -486,7 +486,8 @@ const playVideo = { } } - const clipVideoUrl = `${clip.thumbnailUrl.split("-preview-")[0]}.mp4`; + //const clipVideoUrl = `${clip.thumbnailUrl.split("-preview-")[0]}.mp4`; + const clipVideoUrl = clip.embedUrl; const clipDuration = clip.duration; const volume = parseInt(effect.volume) / 10; From 469a15ba29e7b44c2508eb7dcaf46c35c503e0e0 Mon Sep 17 00:00:00 2001 From: Dennis Rijsdijk <70665154+dennisrijsdijk@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:48:14 +0200 Subject: [PATCH 11/17] fix: frontend error when saving quick action (#2779) --- src/backend/quick-actions/quick-action-manager.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backend/quick-actions/quick-action-manager.js b/src/backend/quick-actions/quick-action-manager.js index aef22d0b6..fd8c93827 100644 --- a/src/backend/quick-actions/quick-action-manager.js +++ b/src/backend/quick-actions/quick-action-manager.js @@ -51,7 +51,8 @@ class QuickActionManager extends JsonDbManager { } saveQuickAction(quickAction, notify = true) { - if (!super.saveItem(quickAction)) { + const savedQuickAction = super.saveItem(quickAction); + if (!savedQuickAction) { return; } const quickActionSettings = settings.getQuickActionSettings(); @@ -62,7 +63,7 @@ class QuickActionManager extends JsonDbManager { if (notify) { this.triggerUiRefresh(); } - return true; + return savedQuickAction; } deleteQuickAction(customQuickActionId) { From a30784c9d72655bc4249eaece41bf87f0c11c65b Mon Sep 17 00:00:00 2001 From: Alf <2608282+alfw@users.noreply.github.com> Date: Thu, 12 Sep 2024 04:05:44 +0200 Subject: [PATCH 12/17] feat: add more events to Effect Queue (#2785) * feat: added new events for queue status and new entry * feat: added filter option for effect queue name is or is not * fix: clean up debug code and error message * fix(effect-queue-status): fixed return type --- .../effects/queues/effect-queue-runner.js | 14 ++++++ .../events/builtin/firebot-event-source.js | 19 ++++++++ .../events/filters/builtin-filter-loader.js | 1 + .../events/filters/builtin/effect-queue.js | 46 +++++++++++++++++++ .../builtin/metadata/effect-queue-id.ts | 2 +- .../builtin/metadata/effect-queue-name.ts | 2 +- .../builtin/metadata/effect-queue-status.ts | 26 +++++++++++ .../variables/builtin/metadata/index.ts | 2 + 8 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/backend/events/filters/builtin/effect-queue.js create mode 100644 src/backend/variables/builtin/metadata/effect-queue-status.ts diff --git a/src/backend/effects/queues/effect-queue-runner.js b/src/backend/effects/queues/effect-queue-runner.js index b274ab262..6c7c0d95d 100644 --- a/src/backend/effects/queues/effect-queue-runner.js +++ b/src/backend/effects/queues/effect-queue-runner.js @@ -142,6 +142,10 @@ class EffectQueue { logger.debug(`Added more effects to queue ${this.id}. Current length=${this._queue.length}`); + eventManager.triggerEvent("firebot", "effect-queue-added", { + effectQueueId: this.id + }); + this.sendQueueLengthUpdate(); this.processEffectQueue(); @@ -169,11 +173,21 @@ class EffectQueue { pauseQueue() { logger.debug(`Pausing queue ${this.id}...`); + + eventManager.triggerEvent("firebot", "effect-queue-status", { + effectQueueId: this.id + }); + this._paused = true; } resumeQueue() { logger.debug(`Resuming queue ${this.id}...`); + + eventManager.triggerEvent("firebot", "effect-queue-status", { + effectQueueId: this.id + }); + this._paused = false; this.processEffectQueue(); } diff --git a/src/backend/events/builtin/firebot-event-source.js b/src/backend/events/builtin/firebot-event-source.js index 3487885fb..41603ab88 100644 --- a/src/backend/events/builtin/firebot-event-source.js +++ b/src/backend/events/builtin/firebot-event-source.js @@ -113,6 +113,25 @@ const firebotEventSource = { queueName: "Just Chatting" } }, + { + id: "effect-queue-added", + name: "Effect Queue Added", + description: "When an new entry added to effect queue.", + cached: false, + manualMetadata: { + queueName: "Just Chatting" + } + }, + { + id: "effect-queue-status", + name: "Effect Queue Status", + description: "When an effect queue status changes.", + cached: false, + manualMetadata: { + queueName: "Just Chatting", + status: "paused" + } + }, { id: "before-firebot-closed", name: "Before Firebot Closed", diff --git a/src/backend/events/filters/builtin-filter-loader.js b/src/backend/events/filters/builtin-filter-loader.js index aa13ef3be..5cfea24cd 100644 --- a/src/backend/events/filters/builtin-filter-loader.js +++ b/src/backend/events/filters/builtin-filter-loader.js @@ -13,6 +13,7 @@ exports.loadFilters = () => { 'custom-variable-name', 'donation-amount', 'donation-from', + 'effect-queue', 'gift-count', 'gift-duration', 'is-anonymous', diff --git a/src/backend/events/filters/builtin/effect-queue.js b/src/backend/events/filters/builtin/effect-queue.js new file mode 100644 index 000000000..b17d9c77a --- /dev/null +++ b/src/backend/events/filters/builtin/effect-queue.js @@ -0,0 +1,46 @@ +"use strict"; + +module.exports = { + id: "firebot:effect-queue", + name: "Effect Queue", + description: "Filter to a Effect Queue", + events: [ + { eventSourceId: "firebot", eventId: "effect-queue-added" }, + { eventSourceId: "firebot", eventId: "effect-queue-cleared" }, + { eventSourceId: "firebot", eventId: "effect-queue-status" } + ], + comparisonTypes: ["is", "is not"], + valueType: "preset", + presetValues: (effectQueuesService) => { + return effectQueuesService.getEffectQueues().map((c) => ({ value: c.id, display: c.name })); + }, + valueIsStillValid: (filterSettings, effectQueuesService) => { + return new Promise((resolve) => { + resolve(effectQueuesService.getEffectQueues().some((c) => c.id === filterSettings.value)); + }); + }, + getSelectedValueDisplay: (filterSettings, effectQueuesService) => { + return new Promise((resolve) => { + resolve( + effectQueuesService.getEffectQueues().find((c) => c.id === filterSettings.value)?.name ?? + "Unknown Effect Queue" + ); + }); + }, + predicate: (filterSettings, eventData) => { + const { comparisonType, value } = filterSettings; + const { eventMeta } = eventData; + + const actual = eventMeta.effectQueueId; + const expected = value; + + switch (comparisonType) { + case "is": + return actual === expected; + case "is not": + return actual !== expected; + default: + return false; + } + } +}; diff --git a/src/backend/variables/builtin/metadata/effect-queue-id.ts b/src/backend/variables/builtin/metadata/effect-queue-id.ts index 9b049ec22..c5d0328d4 100644 --- a/src/backend/variables/builtin/metadata/effect-queue-id.ts +++ b/src/backend/variables/builtin/metadata/effect-queue-id.ts @@ -3,7 +3,7 @@ import { EffectTrigger } from "../../../../shared/effect-constants"; import { OutputDataType, VariableCategory } from "../../../../shared/variable-constants"; const triggers = {}; -triggers[EffectTrigger.EVENT] = ["firebot:effect-queue-cleared"]; +triggers[EffectTrigger.EVENT] = ["firebot:effect-queue-cleared", "firebot:effect-queue-added", "firebot:effect-queue-status"]; triggers[EffectTrigger.MANUAL] = true; const model: ReplaceVariable = { diff --git a/src/backend/variables/builtin/metadata/effect-queue-name.ts b/src/backend/variables/builtin/metadata/effect-queue-name.ts index f1aa50cf0..ebe4e5aff 100644 --- a/src/backend/variables/builtin/metadata/effect-queue-name.ts +++ b/src/backend/variables/builtin/metadata/effect-queue-name.ts @@ -4,7 +4,7 @@ import { OutputDataType, VariableCategory } from "../../../../shared/variable-co import effectQueueManager from "../../../effects/queues/effect-queue-manager"; const triggers = {}; -triggers[EffectTrigger.EVENT] = ["firebot:effect-queue-cleared"]; +triggers[EffectTrigger.EVENT] = ["firebot:effect-queue-cleared", "firebot:effect-queue-added", "firebot:effect-queue-status"]; triggers[EffectTrigger.MANUAL] = true; const model: ReplaceVariable = { diff --git a/src/backend/variables/builtin/metadata/effect-queue-status.ts b/src/backend/variables/builtin/metadata/effect-queue-status.ts new file mode 100644 index 000000000..34c1637dd --- /dev/null +++ b/src/backend/variables/builtin/metadata/effect-queue-status.ts @@ -0,0 +1,26 @@ +import { ReplaceVariable } from "../../../../types/variables"; +import { EffectTrigger } from "../../../../shared/effect-constants"; +import { OutputDataType, VariableCategory } from "../../../../shared/variable-constants"; +import effectQueueManager from "../../../effects/queues/effect-queue-manager"; + +const triggers = {}; +triggers[EffectTrigger.EVENT] = ["firebot:effect-queue-cleared", "firebot:effect-queue-added", "firebot:effect-queue-status"]; +triggers[EffectTrigger.MANUAL] = true; + +const model: ReplaceVariable = { + definition: { + handle: "effectQueueStatus", + description: "The status of the effect queue.", + triggers: triggers, + categories: [VariableCategory.TRIGGER], + possibleDataOutput: [OutputDataType.BOOLEAN, OutputDataType.NULL] + }, + evaluator: (trigger) => { + const queueId = trigger?.metadata?.eventData?.effectQueueId; + const effectQueue = effectQueueManager.getItem(queueId); + + return effectQueue?.active ?? null; + } +}; + +export default model; \ No newline at end of file diff --git a/src/backend/variables/builtin/metadata/index.ts b/src/backend/variables/builtin/metadata/index.ts index a60a71f86..c04face94 100644 --- a/src/backend/variables/builtin/metadata/index.ts +++ b/src/backend/variables/builtin/metadata/index.ts @@ -7,6 +7,7 @@ import count from './count'; import effectOutput from './effect-output'; import effectQueueId from './effect-queue-id'; import effectQueueName from './effect-queue-name'; +import effectQueueStatus from './effect-queue-status'; import overlayInstance from './overlay-instance'; import presetListArg from './preset-list-arg'; import user from './user'; @@ -26,6 +27,7 @@ export default [ effectOutput, effectQueueId, effectQueueName, + effectQueueStatus, overlayInstance, presetListArg, user, From d1cf9c111a4f2a5c94bd00932b6857a8cb1e98fe Mon Sep 17 00:00:00 2001 From: Troy Date: Fri, 13 Sep 2024 10:42:31 +0800 Subject: [PATCH 13/17] feat: option to default quotes to streamer name if no @ is provided #2336 * Addressing #2336 to default quotes to streamer name if no @ is provided * PR#2753 - prefer string templates over string concatenation * PR#2753 - Added quote management setting making this change opt-in * PR#2753 - Check arguments after applying streamer default * PR#2753 - Simplify to use logical AND as suggested * PR#2753 - use shouldInsertStreamerUsername constant suggested * PR#2753 - Confirm command in syntactically valid before API calls --------- Co-authored-by: Erik Bigler --- src/backend/chat/commands/builtin/quotes.ts | 29 ++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/backend/chat/commands/builtin/quotes.ts b/src/backend/chat/commands/builtin/quotes.ts index cea9edece..65d0af3af 100644 --- a/src/backend/chat/commands/builtin/quotes.ts +++ b/src/backend/chat/commands/builtin/quotes.ts @@ -12,6 +12,7 @@ export const QuotesManagementSystemCommand: SystemCommand<{ quoteDisplayTemplate: string; quoteDateFormat: string; useTTS: boolean; + defaultStreamerAttribution: boolean; }> = { definition: { id: "firebot:quotesmanagement", @@ -50,6 +51,12 @@ export const QuotesManagementSystemCommand: SystemCommand<{ title: "Read Quotes via TTS", description: "Have quotes read by TTS whenever one is created or looked up.", default: false + }, + defaultStreamerAttribution: { + type: "boolean", + title: "Attribute new quote to streamer if nobody is explicitly tagged with @", + description: "If @username is not included when adding a quote, it is attributed to the streamer.", + default: false } }, subCommands: [ @@ -270,14 +277,24 @@ export const QuotesManagementSystemCommand: SystemCommand<{ switch (triggeredArg) { case "add": { - if (args.length < 3) { - await twitchChat.sendChatMessage(`Please provide some quote text!`); - return resolve(); + const shouldInsertStreamerUsername = (commandOptions.defaultStreamerAttribution && args.length === 1) + || (commandOptions.defaultStreamerAttribution && !args[1].includes("@")); + const expectedArgs = shouldInsertStreamerUsername + ? 2 + : 3; + + if (args.length < expectedArgs) { + await twitchChat.sendChatMessage(`Please provide some quote text!`); + return resolve(); } - + // Once we've evaluated that the syntax is correct we make our API calls const channelData = await TwitchApi.channels.getChannelInformation(); - const currentGameName = channelData && channelData.gameName ? channelData.gameName : "Unknown game"; + + // If shouldInsertStreamerUsername and no @ is included in the originator arg, set originator @streamerName and treat the rest as the quote + if (shouldInsertStreamerUsername) { + args.splice(1,0,`@${channelData.displayName}`) + } const newQuote = { text: args.slice(2, args.length).join(" "), @@ -597,4 +614,4 @@ export const QuotesManagementSystemCommand: SystemCommand<{ } }); } -}; \ No newline at end of file +}; From aec4eedb47073c56e4143f3492302981c58eb757 Mon Sep 17 00:00:00 2001 From: Karlan Bousquet <1155877+karrbs@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:47:19 -0400 Subject: [PATCH 14/17] feat: Raid Sent Off Event (#2767) * Added Raid Sent Off Event * Changed name of incoming and outgoing raid Changed description of raid outgoing completed Changed icon for raids in activity manager Changed filter description for raid viewer count * Adjusting names to and icons of event. --------- Co-authored-by: Erik Bigler --- .../events/builtin/twitch-event-source.js | 197 +++++++++++++----- .../filters/builtin/raid-viewer-count.js | 8 +- src/backend/events/twitch-events/raid.ts | 21 +- .../twitch-api/eventsub/eventsub-client.ts | 25 ++- src/backend/variables/builtin/twitch/index.ts | 4 +- .../variables/builtin/twitch/raid/index.ts | 11 + .../raid/raid-target-user-display-name.ts | 22 ++ .../twitch/raid/raid-target-user-id.ts | 22 ++ .../twitch/raid/raid-target-username.ts | 23 ++ .../twitch/{ => raid}/raid-viewer-count.ts | 10 +- src/gui/app/services/settings.service.js | 1 + 11 files changed, 279 insertions(+), 65 deletions(-) create mode 100644 src/backend/variables/builtin/twitch/raid/index.ts create mode 100644 src/backend/variables/builtin/twitch/raid/raid-target-user-display-name.ts create mode 100644 src/backend/variables/builtin/twitch/raid/raid-target-user-id.ts create mode 100644 src/backend/variables/builtin/twitch/raid/raid-target-username.ts rename src/backend/variables/builtin/twitch/{ => raid}/raid-viewer-count.ts (55%) diff --git a/src/backend/events/builtin/twitch-event-source.js b/src/backend/events/builtin/twitch-event-source.js index 0754c0767..2a258a3ff 100644 --- a/src/backend/events/builtin/twitch-event-source.js +++ b/src/backend/events/builtin/twitch-event-source.js @@ -7,7 +7,7 @@ module.exports = { events: [ { id: "raid", - name: "Raid", + name: "Incoming Raid", description: "When someone raids your channel.", cached: true, cacheMetaKey: "username", @@ -18,10 +18,39 @@ module.exports = { viewerCount: 5 }, activityFeed: { - icon: "fad fa-siren-on", + icon: "fad fa-inbox-in", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** raided with **${eventData.viewerCount}** viewer(s)`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** raided with **${eventData.viewerCount}** viewer(s)`; + } + } + }, + { + id: "raid-sent-off", + name: "Outgoing Raid", + description: "When your outgoing raid is completed.", + cached: false, + cacheMetaKey: "fromUsername", + manualMetadata: { + username: "firebot", + userId: "", + userDisplayName: "Firebot", + raidTargetUsername: "user", + raidTargetUserId: "", + raidTargetUserDisplayName: "User", + viewerCount: 5 + }, + activityFeed: { + icon: "fad fa-inbox-out", + getMessage: (eventData) => { + const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** raiding user **${eventData.raidTargetUserDisplayName}** with **${ + eventData.viewerCount + }** viewer(s)`; } } }, @@ -40,7 +69,9 @@ module.exports = { icon: "fas fa-heart", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** followed`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** followed`; } } }, @@ -73,8 +104,13 @@ module.exports = { icon: "fas fa-star", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** ${eventData.isResub ? 'resubscribed' : 'subscribed'} for **${eventData.totalMonths} month(s)** ${eventData.subPlan === 'Prime' ? - "with **Twitch Prime**" : `at **Tier ${eventData.subPlan.replace("000", "")}**`}`; + return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** ${ + eventData.isResub ? "resubscribed" : "subscribed" + } for **${eventData.totalMonths} month(s)** ${ + eventData.subPlan === "Prime" + ? "with **Twitch Prime**" + : `at **Tier ${eventData.subPlan.replace("000", "")}**` + }`; } } }, @@ -101,7 +137,9 @@ module.exports = { icon: "fas fa-star", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** upgraded their Prime sub at **Tier ${eventData.subPlan.replace("000", "")}!**`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** upgraded their Prime sub at **Tier ${eventData.subPlan.replace("000", "")}!**`; } } }, @@ -129,7 +167,11 @@ module.exports = { activityFeed: { icon: "fad fa-gift", getMessage: (eventData) => { - return `**${eventData.isAnonymous ? "An Anonymous Gifter" : eventData.gifterUsername}** gifted a ${eventData.giftDuration > 1 ? ` **${eventData.giftDuration} month** ` : ''} **Tier ${eventData.subPlan.replace("000", "")}** sub to **${eventData.gifteeUsername}** (Subbed for ${eventData.giftSubMonths} month${eventData.giftSubMonths > 1 ? 's' : ''} total)`; + return `**${eventData.isAnonymous ? "An Anonymous Gifter" : eventData.gifterUsername}** gifted a ${ + eventData.giftDuration > 1 ? ` **${eventData.giftDuration} month** ` : "" + } **Tier ${eventData.subPlan.replace("000", "")}** sub to **${ + eventData.gifteeUsername + }** (Subbed for ${eventData.giftSubMonths} month${eventData.giftSubMonths > 1 ? "s" : ""} total)`; } } }, @@ -165,7 +207,11 @@ module.exports = { activityFeed: { icon: "fad fa-gifts", getMessage: (eventData) => { - return `**${eventData.isAnonymous ? "An Anonymous Gifter" : eventData.gifterUsername}** gifted **${eventData.subCount} Tier ${eventData.subPlan.replace("000", "")}** sub${eventData.subCount > 1 ? 's' : ''} to the community`; + return `**${eventData.isAnonymous ? "An Anonymous Gifter" : eventData.gifterUsername}** gifted **${ + eventData.subCount + } Tier ${eventData.subPlan.replace("000", "")}** sub${ + eventData.subCount > 1 ? "s" : "" + } to the community`; } } }, @@ -194,7 +240,9 @@ module.exports = { icon: "fas fa-star", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** upgraded their gift sub at **Tier ${eventData.subPlan.replace("000", "")}!**`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** upgraded their gift sub at **Tier ${eventData.subPlan.replace("000", "")}!**`; } } }, @@ -216,7 +264,11 @@ module.exports = { icon: "fad fa-diamond", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** cheered **${eventData.bits}** bits. They have cheered a total of **${eventData.totalBits}** in the channel.`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** cheered **${eventData.bits}** bits. They have cheered a total of **${ + eventData.totalBits + }** in the channel.`; } } }, @@ -269,7 +321,9 @@ module.exports = { icon: "fad fa-diamond", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** unlocked the **${eventData.badgeTier}** bits badge in your channel!`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** unlocked the **${eventData.badgeTier}** bits badge in your channel!`; } } }, @@ -288,7 +342,9 @@ module.exports = { icon: "fad fa-house-return", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** arrived`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** arrived`; } } }, @@ -343,7 +399,9 @@ module.exports = { icon: "fad fa-sparkles", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** has chatted in your channel for the very first time`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** has chatted in your channel for the very first time`; } } }, @@ -377,7 +435,9 @@ module.exports = { icon: "fad fa-gavel", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - let message = `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** was banned by **${eventData.moderator}**.`; + let message = `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** was banned by **${eventData.moderator}**.`; if (eventData.modReason) { message = `${message} Reason: **${eventData.modReason}**`; @@ -402,7 +462,9 @@ module.exports = { icon: "fad fa-gavel", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** was unbanned by **${eventData.moderator}**.`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** was unbanned by **${eventData.moderator}**.`; } } }, @@ -424,7 +486,9 @@ module.exports = { icon: "fad fa-stopwatch", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - let message = `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** was timed out for **${eventData.timeoutDuration} sec(s)** by ${eventData.moderator}.`; + let message = `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** was timed out for **${eventData.timeoutDuration} sec(s)** by ${eventData.moderator}.`; if (eventData.modReason) { message = `${message} Reason: **${eventData.modReason}**`; @@ -454,7 +518,11 @@ module.exports = { icon: "fad fa-circle", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** redeemed **${eventData.rewardName}**${eventData.messageText && !!eventData.messageText.length ? `: *${eventData.messageText}*` : ''}`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** redeemed **${eventData.rewardName}**${ + eventData.messageText && !!eventData.messageText.length ? `: *${eventData.messageText}*` : "" + }`; } } }, @@ -479,7 +547,11 @@ module.exports = { icon: "fad fa-circle", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}**'s redemption of **${eventData.rewardName}** was approved. ${eventData.messageText && !!eventData.messageText.length ? `*${eventData.messageText}*` : ''}`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }**'s redemption of **${eventData.rewardName}** was approved. ${ + eventData.messageText && !!eventData.messageText.length ? `*${eventData.messageText}*` : "" + }`; } } }, @@ -504,7 +576,11 @@ module.exports = { icon: "fad fa-circle", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}**'s redemption of **${eventData.rewardName}** was rejected. ${eventData.messageText && !!eventData.messageText.length ? `*${eventData.messageText}*` : ''}`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }**'s redemption of **${eventData.rewardName}** was rejected. ${ + eventData.messageText && !!eventData.messageText.length ? `*${eventData.messageText}*` : "" + }`; } } }, @@ -531,7 +607,9 @@ module.exports = { icon: "fad fa-comment-alt", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** sent your **${eventData.sentTo}** account the following whisper: ${eventData.message}`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** sent your **${eventData.sentTo}** account the following whisper: ${eventData.message}`; } } }, @@ -545,19 +623,19 @@ module.exports = { chatMode: { type: "enum", options: { - "emoteonly": "Emote Only", - "subscribers": "Subscribers Only", - "followers": "Followers", - "slow": "Slow", - "r9kbeta": "Unique Chat" + emoteonly: "Emote Only", + subscribers: "Subscribers Only", + followers: "Followers", + slow: "Slow", + r9kbeta: "Unique Chat" }, value: "emoteonly" }, chatModeState: { type: "enum", options: { - "enabled": "Enabled", - "disabled": "Disabled" + enabled: "Enabled", + disabled: "Disabled" }, value: "enabled" }, @@ -681,9 +759,13 @@ module.exports = { getMessage: (eventData) => { let message; if (eventData.description) { - message = `Channel ${eventData.type} goal **${eventData.description}** has ended. Goal **${eventData.isAchieved ? "was" : "was not"}** achieved. (**${eventData.currentAmount}**/**${eventData.targetAmount}**).`; + message = `Channel ${eventData.type} goal **${eventData.description}** has ended. Goal **${ + eventData.isAchieved ? "was" : "was not" + }** achieved. (**${eventData.currentAmount}**/**${eventData.targetAmount}**).`; } else { - message = `Channel ${eventData.type} goal has ended. Goal **${eventData.isAchieved ? "was" : "was not"}** achieved. (**${eventData.currentAmount}**/**${eventData.targetAmount}**).`; + message = `Channel ${eventData.type} goal has ended. Goal **${ + eventData.isAchieved ? "was" : "was not" + }** achieved. (**${eventData.currentAmount}**/**${eventData.targetAmount}**).`; } return message; } @@ -787,7 +869,9 @@ module.exports = { activityFeed: { icon: "fad fa-train", getMessage: (eventData) => { - return `Level **${eventData.level}** hype train currently at **${Math.floor((eventData.progress / eventData.goal) * 100)}%**.`; + return `Level **${eventData.level}** hype train currently at **${Math.floor( + (eventData.progress / eventData.goal) * 100 + )}%**.`; } } }, @@ -814,7 +898,7 @@ module.exports = { description: "When your stream starts.", cached: false, queued: false, - manualMetadata: { }, + manualMetadata: {}, activityFeed: { icon: "fad fa-play-circle", getMessage: () => { @@ -828,7 +912,7 @@ module.exports = { description: "When your stream ends.", cached: false, queued: false, - manualMetadata: { }, + manualMetadata: {}, activityFeed: { icon: "fad fa-stop-circle", getMessage: () => { @@ -923,7 +1007,9 @@ module.exports = { activityFeed: { icon: "fad fa-ribbon", getMessage: (eventData) => { - return `Charity campaign has ended. Goal reached: **${eventData.goalReached ? "Yes" : "No"}**. Total raised: **${eventData.currentTotalAmount} ${eventData.currentTotalCurrency}**.`; + return `Charity campaign has ended. Goal reached: **${ + eventData.goalReached ? "Yes" : "No" + }**. Total raised: **${eventData.currentTotalAmount} ${eventData.currentTotalCurrency}**.`; } } }, @@ -944,7 +1030,9 @@ module.exports = { icon: "fad fa-bullhorn", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.moderator}** sent a shoutout to **${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}**`; + return `**${eventData.moderator}** sent a shoutout to **${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }**`; } } }, @@ -964,7 +1052,9 @@ module.exports = { icon: "fad fa-bullhorn", getMessage: (eventData) => { const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); - return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}** shouted out your channel to ${eventData.viewerCount} viewers`; + return `**${eventData.userDisplayName}${ + showUserIdName ? ` (${eventData.username})` : "" + }** shouted out your channel to ${eventData.viewerCount} viewers`; } } }, @@ -1013,13 +1103,16 @@ module.exports = { const mins = Math.floor(eventData.adBreakDuration / 60); const remainingSecs = eventData.adBreakDuration % 60; - const friendlyDuration = mins > 0 - ? `${mins}m${remainingSecs > 0 ? ` ${remainingSecs}s` : ""}` - : `${eventData.adBreakDuration}s`; + const friendlyDuration = + mins > 0 + ? `${mins}m${remainingSecs > 0 ? ` ${remainingSecs}s` : ""}` + : `${eventData.adBreakDuration}s`; const minutesUntilNextAdBreak = Math.round(eventData.secondsUntilNextAdBreak / 60); - return `**${friendlyDuration}** scheduled ad break starting in about **${minutesUntilNextAdBreak}** minute${minutesUntilNextAdBreak !== 1 ? "s" : ""}`; + return `**${friendlyDuration}** scheduled ad break starting in about **${minutesUntilNextAdBreak}** minute${ + minutesUntilNextAdBreak !== 1 ? "s" : "" + }`; } } }, @@ -1038,11 +1131,14 @@ module.exports = { const mins = Math.floor(eventData.adBreakDuration / 60); const remainingSecs = eventData.adBreakDuration % 60; - const friendlyDuration = mins > 0 - ? `${mins}m${remainingSecs > 0 ? ` ${remainingSecs}s` : ""}` - : `${eventData.adBreakDuration}s`; + const friendlyDuration = + mins > 0 + ? `${mins}m${remainingSecs > 0 ? ` ${remainingSecs}s` : ""}` + : `${eventData.adBreakDuration}s`; - return `**${friendlyDuration}** **${eventData.isAdBreakScheduled ? "scheduled" : "manual"}** ad break started`; + return `**${friendlyDuration}** **${ + eventData.isAdBreakScheduled ? "scheduled" : "manual" + }** ad break started`; } } }, @@ -1061,13 +1157,16 @@ module.exports = { const mins = Math.floor(eventData.adBreakDuration / 60); const remainingSecs = eventData.adBreakDuration % 60; - const friendlyDuration = mins > 0 - ? `${mins}m${remainingSecs > 0 ? ` ${remainingSecs}s` : ""}` - : `${eventData.adBreakDuration}s`; + const friendlyDuration = + mins > 0 + ? `${mins}m${remainingSecs > 0 ? ` ${remainingSecs}s` : ""}` + : `${eventData.adBreakDuration}s`; - return `**${friendlyDuration}** **${eventData.isAdBreakScheduled ? "scheduled" : "manual"}** ad break ended`; + return `**${friendlyDuration}** **${ + eventData.isAdBreakScheduled ? "scheduled" : "manual" + }** ad break ended`; } } } ] -}; \ No newline at end of file +}; diff --git a/src/backend/events/filters/builtin/raid-viewer-count.js b/src/backend/events/filters/builtin/raid-viewer-count.js index d4fc1d3bb..54f76457f 100644 --- a/src/backend/events/filters/builtin/raid-viewer-count.js +++ b/src/backend/events/filters/builtin/raid-viewer-count.js @@ -5,9 +5,10 @@ const { ComparisonType } = require("../../../../shared/filter-constants"); module.exports = { id: "firebot:raid-viewer-count", name: "Raid Viewer Count", - description: "Filter by how many viewers have been brought over by the raid.", + description: "Filter by how many viewers have been brought or are being sent over by the raid.", events: [ - { eventSourceId: "twitch", eventId: "raid" } + { eventSourceId: "twitch", eventId: "raid" }, + { eventSourceId: "twitch", eventId: "raid-sent-off" } ], comparisonTypes: [ ComparisonType.IS, @@ -19,7 +20,6 @@ module.exports = { ], valueType: "number", predicate: (filterSettings, eventData) => { - const { comparisonType, value } = filterSettings; const { eventMeta } = eventData; @@ -48,4 +48,4 @@ module.exports = { return false; } } -}; \ No newline at end of file +}; diff --git a/src/backend/events/twitch-events/raid.ts b/src/backend/events/twitch-events/raid.ts index 10d582972..89e598213 100644 --- a/src/backend/events/twitch-events/raid.ts +++ b/src/backend/events/twitch-events/raid.ts @@ -1,6 +1,6 @@ import eventManager from "../../events/EventManager"; -export function triggerRaid( +export function triggerIncomingRaid( username: string, userId: string, userDisplayName: string, @@ -12,4 +12,23 @@ export function triggerRaid( userDisplayName, viewerCount }); +} +export function triggerRaidSentOff( + username: string, + userId: string, + userDisplayName: string, + raidTargetUsername: string, + raidTargetUserId: string, + raidTargetUserDisplayName: string, + viewerCount = 0 +): void { + eventManager.triggerEvent("twitch", "raid-sent-off", { + username, + userId, + userDisplayName, + raidTargetUsername, + raidTargetUserId, + raidTargetUserDisplayName, + viewerCount + }); } \ No newline at end of file diff --git a/src/backend/twitch-api/eventsub/eventsub-client.ts b/src/backend/twitch-api/eventsub/eventsub-client.ts index b721a89b3..5d6b332fb 100644 --- a/src/backend/twitch-api/eventsub/eventsub-client.ts +++ b/src/backend/twitch-api/eventsub/eventsub-client.ts @@ -133,16 +133,33 @@ class TwitchEventSubClient { }); this._subscriptions.push(customRewardRedemptionUpdateSubscription); - // Raid - const raidSubscription = this._eventSubListener.onChannelRaidTo(streamer.userId, (event) => { - twitchEventsHandler.raid.triggerRaid( + // Incoming Raid + const incomingRaidSubscription = this._eventSubListener.onChannelRaidTo(streamer.userId, (event) => { + twitchEventsHandler.raid.triggerIncomingRaid( event.raidingBroadcasterName, event.raidingBroadcasterId, event.raidingBroadcasterDisplayName, event.viewers ); }); - this._subscriptions.push(raidSubscription); + this._subscriptions.push(incomingRaidSubscription); + + // Outbound Raid Sent Off + const outboundRaidSubscription = this._eventSubListener.onChannelRaidFrom(streamer.userId, (event) => { + // sent off + if (event.raidingBroadcasterId === streamer.userId && event.raidedBroadcasterId !== streamer.userId) { + twitchEventsHandler.raid.triggerRaidSentOff( + event.raidingBroadcasterName, + event.raidingBroadcasterId, + event.raidingBroadcasterDisplayName, + event.raidedBroadcasterName, + event.raidedBroadcasterId, + event.raidedBroadcasterDisplayName, + event.viewers + ); + } + }); + this._subscriptions.push(outboundRaidSubscription); // Shoutout sent to another channel const shoutoutSentSubscription = this._eventSubListener.onChannelShoutoutCreate(streamer.userId, streamer.userId, (event) => { diff --git a/src/backend/variables/builtin/twitch/index.ts b/src/backend/variables/builtin/twitch/index.ts index 43c97bb7d..545bce4c2 100644 --- a/src/backend/variables/builtin/twitch/index.ts +++ b/src/backend/variables/builtin/twitch/index.ts @@ -5,6 +5,7 @@ import charityVariables from './charity'; import cheerVariables from './cheer'; import cheermoteVariables from './cheermote'; import hypetrainVariables from './hype-train'; +import raidVariables from './raid'; import rewardVariables from './reward'; import streamVariables from './stream'; import subVariables from './subs'; @@ -14,7 +15,6 @@ import followCount from './follow-count'; import pollWinningChoiceName from './poll-winning-choice-name'; import pollWinningChoiceVotes from './poll-winning-choice-votes'; import predictionWinningOutcomeName from './prediction-winning-outcome-name'; -import raidViewerCounter from './raid-viewer-count'; import twitchChannelUrl from './twitch-channel-url'; import viewerCount from './viewer-count'; @@ -27,6 +27,7 @@ export default [ ...cheerVariables, ...cheermoteVariables, ...hypetrainVariables, + ...raidVariables, ...rewardVariables, ...streamVariables, ...subVariables, @@ -36,7 +37,6 @@ export default [ pollWinningChoiceName, pollWinningChoiceVotes, predictionWinningOutcomeName, - raidViewerCounter, twitchChannelUrl, viewerCount ]; \ No newline at end of file diff --git a/src/backend/variables/builtin/twitch/raid/index.ts b/src/backend/variables/builtin/twitch/raid/index.ts new file mode 100644 index 000000000..3d09e5c62 --- /dev/null +++ b/src/backend/variables/builtin/twitch/raid/index.ts @@ -0,0 +1,11 @@ +import raidViewerCount from './raid-viewer-count'; +import raidTargetUserDisplayName from './raid-target-user-display-name'; +import raidTargetUserId from './raid-target-user-id'; +import raidTargetUsername from './raid-target-username'; + +export default [ + raidViewerCount, + raidTargetUserDisplayName, + raidTargetUsername, + raidTargetUserId +]; \ No newline at end of file diff --git a/src/backend/variables/builtin/twitch/raid/raid-target-user-display-name.ts b/src/backend/variables/builtin/twitch/raid/raid-target-user-display-name.ts new file mode 100644 index 000000000..842565c8b --- /dev/null +++ b/src/backend/variables/builtin/twitch/raid/raid-target-user-display-name.ts @@ -0,0 +1,22 @@ +import { ReplaceVariable } from "../../../../../types/variables"; +import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; +import { EffectTrigger } from "../../../../../shared/effect-constants"; + +const triggers = {}; +triggers[EffectTrigger.EVENT] = ["twitch:raid-sent-off"]; +triggers[EffectTrigger.MANUAL] = true; + +const model : ReplaceVariable = { + definition: { + handle: "raidTargetUserDisplayName", + description: "Gets the formatted display name for the raid target", + triggers: triggers, + categories: [VariableCategory.USER], + possibleDataOutput: [OutputDataType.TEXT] + }, + evaluator: async (trigger) => { + return trigger.metadata.eventData?.raidTargetUserDisplayName ?? ""; + } +}; + +export default model; diff --git a/src/backend/variables/builtin/twitch/raid/raid-target-user-id.ts b/src/backend/variables/builtin/twitch/raid/raid-target-user-id.ts new file mode 100644 index 000000000..9bacc9b94 --- /dev/null +++ b/src/backend/variables/builtin/twitch/raid/raid-target-user-id.ts @@ -0,0 +1,22 @@ +import { ReplaceVariable } from "../../../../../types/variables"; +import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; +import { EffectTrigger } from "../../../../../shared/effect-constants"; + +const triggers = {}; +triggers[EffectTrigger.EVENT] = ["twitch:raid-sent-off"]; +triggers[EffectTrigger.MANUAL] = true; + +const model : ReplaceVariable = { + definition: { + handle: "raidTargetUserId", + description: "Gets the user ID for the raid target.", + triggers: triggers, + categories: [VariableCategory.USER], + possibleDataOutput: [OutputDataType.TEXT] + }, + evaluator: async (trigger) => { + return trigger.metadata.eventData?.raidTargetUserId ?? ""; + } +}; + +export default model; diff --git a/src/backend/variables/builtin/twitch/raid/raid-target-username.ts b/src/backend/variables/builtin/twitch/raid/raid-target-username.ts new file mode 100644 index 000000000..dcaabef91 --- /dev/null +++ b/src/backend/variables/builtin/twitch/raid/raid-target-username.ts @@ -0,0 +1,23 @@ +import { ReplaceVariable, Trigger } from "../../../../../types/variables"; +import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; + +import { EffectTrigger } from "../../../../../shared/effect-constants"; + +const triggers = {}; +triggers[EffectTrigger.EVENT] = ["twitch:raid-sent-off"]; +triggers[EffectTrigger.MANUAL] = true; + +const model : ReplaceVariable = { + definition: { + handle: "raidTargetUsername", + description: "The associated user (if there is one) for the given trigger", + triggers: triggers, + categories: [VariableCategory.COMMON, VariableCategory.USER], + possibleDataOutput: [OutputDataType.TEXT] + }, + evaluator: (trigger: Trigger) => { + return trigger.metadata.eventData?.raidTargetUsername; + } +}; + +export default model; \ No newline at end of file diff --git a/src/backend/variables/builtin/twitch/raid-viewer-count.ts b/src/backend/variables/builtin/twitch/raid/raid-viewer-count.ts similarity index 55% rename from src/backend/variables/builtin/twitch/raid-viewer-count.ts rename to src/backend/variables/builtin/twitch/raid/raid-viewer-count.ts index f3a227328..e05f1bbdd 100644 --- a/src/backend/variables/builtin/twitch/raid-viewer-count.ts +++ b/src/backend/variables/builtin/twitch/raid/raid-viewer-count.ts @@ -1,9 +1,9 @@ -import { ReplaceVariable } from "../../../../types/variables"; -import { OutputDataType, VariableCategory } from "../../../../shared/variable-constants"; -import { EffectTrigger } from "../../../../shared/effect-constants"; +import { ReplaceVariable } from "../../../../../types/variables"; +import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; +import { EffectTrigger } from "../../../../../shared/effect-constants"; const triggers = {}; -triggers[EffectTrigger.EVENT] = ["twitch:raid"]; +triggers[EffectTrigger.EVENT] = ["twitch:raid", "twitch:raid-sent-off"]; triggers[EffectTrigger.MANUAL] = true; @@ -16,7 +16,7 @@ const model : ReplaceVariable = { possibleDataOutput: [OutputDataType.NUMBER] }, evaluator: async (trigger) => { - return trigger.metadata.eventData.viewerCount || 0; + return trigger.metadata.eventData?.viewerCount || 0; } }; diff --git a/src/gui/app/services/settings.service.js b/src/gui/app/services/settings.service.js index fe34a981a..c2ac652f1 100644 --- a/src/gui/app/services/settings.service.js +++ b/src/gui/app/services/settings.service.js @@ -266,6 +266,7 @@ const events = getDataFromFile("/settings/allowedActivityEvents"); return events == null ? [ "twitch:raid", + "twitch:raid-sent-off", "twitch:follow", "twitch:sub", "twitch:subs-gifted", From 295d61303d769f76ce204c0299734e59f948e8a3 Mon Sep 17 00:00:00 2001 From: Lee Date: Thu, 12 Sep 2024 21:52:56 -0500 Subject: [PATCH 15/17] feat: Remember Active Dashboard Chat User, Message Text, and Replying-To (#2772) * feat: Remember Active Dashboard Chat User - Caches the active chatter in the dashboard when the chat-messages controller gets reloaded. - This intentionally **does not** get saved to disk, and the application will always start back up with "Streamer" as the active chat sender. - Previously, changing component tabs then changing back would cause the chat sender to get reset to Streamer. This is *not* cool. - Implements #2353 * Revert "feat: Remember Active Dashboard Chat User" This reverts commit 294daba239a3b93e98e1303b9e225268902d2d8c. * fix: Rework PR #2772 With chatMessageService - Migrated chat/dashboard caching from settingsService to chatMessageService. - Added caching for chat sender (bot vs streamer). - Added caching for outgoing message text that has not (yet) been sent. - You can now select your chat sender, type out a beautiful message, change to another tab in the application for any reason, then come back and further edit or send the message. * debt: Cleanup html Warnings in chatMessages - 3x "Attribute value missing" warns - "href" keys inside of "a" tags aren't mandatory - But an "href" key must have a value assigned - Removed unnecessary "href" keys. - 1x "Element 'div' cannot be nested inside of element 'span' - Self-explanatory - Changed outer span to a div. * Include replies/threads in Dashboard Memory - Add message reply/threading caching to the chat messages control. - Why remember 2/3rds but ignore the last 1/3rd? - I think I'm done touching this PR now, outside of potential v5 merges. - For real this time. --------- Co-authored-by: Erik Bigler --- .../controllers/chat-messages.controller.js | 42 ++++++++----------- src/gui/app/services/chat-messages.service.js | 7 ++++ .../app/templates/chat/_chat-messages.html | 33 +++++++-------- 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/src/gui/app/controllers/chat-messages.controller.js b/src/gui/app/controllers/chat-messages.controller.js index 88f9c3a0d..f7d934157 100644 --- a/src/gui/app/controllers/chat-messages.controller.js +++ b/src/gui/app/controllers/chat-messages.controller.js @@ -11,16 +11,12 @@ utilityService, activityFeedService ) { - $scope.settings = settingsService; - $scope.afs = activityFeedService; + $scope.cms = chatMessagesService; + $scope.settings = settingsService; - $scope.chatMessage = ""; - $scope.chatSender = "Streamer"; $scope.disabledMessage = ""; - $scope.cms = chatMessagesService; - $scope.selectedUserData = {}; $scope.botLoggedIn = connectionService.accounts.bot.loggedIn; @@ -34,8 +30,6 @@ $scope.hideEventLabel = () => ((parseInt(settings.dashboardActivityFeed.replace("%", "") / 100 * $parent.width())) < 180 ? true : false); }; - $scope.threadDetails = null; - function getThreadMessages(threadOrReplyMessageId) { return chatMessagesService.chatQueue.filter((chatItem) => { return chatItem.type === "message" && (chatItem.data.id === threadOrReplyMessageId || chatItem.data.replyParentMessageId === threadOrReplyMessageId || chatItem.data.threadParentMessageId === threadOrReplyMessageId); @@ -43,10 +37,10 @@ } $scope.currentThreadMessages = () => { - if (!$scope.threadDetails?.threadParentMessageId) { + if (!chatMessagesService.threadDetails?.threadParentMessageId) { return []; } - return getThreadMessages($scope.threadDetails.threadParentMessageId); + return getThreadMessages(chatMessagesService.threadDetails.threadParentMessageId); }; $scope.updateLayoutSettings = (updatedSettings) => { @@ -126,7 +120,7 @@ }; $scope.updateChatInput = function(text) { - $scope.chatMessage = text; + chatMessagesService.chatMessage = text; focusMessageInput(); }; @@ -138,7 +132,7 @@ const threadParentId = parentReplyMessage.threadParentMessageId || parentReplyMessage.replyParentMessageId || parentReplyMessage.id; - $scope.threadDetails = { + chatMessagesService.threadDetails = { threadParentMessageId: threadParentId, replyToMessageId: threadOrReplyMessageId, replyToUserDisplayName: parentReplyMessage.username @@ -146,7 +140,7 @@ }; $scope.closeThreadPanel = () => { - $scope.threadDetails = null; + chatMessagesService.threadDetails = null; }; $scope.onReplyClicked = function(threadOrReplyMessageId) { @@ -169,14 +163,14 @@ const chatHistory = []; let currrentHistoryIndex = -1; $scope.submitChat = function() { - if ($scope.chatMessage == null || $scope.chatMessage.length < 1) { + if (chatMessagesService.chatMessage == null || chatMessagesService.chatMessage.length < 1) { return; } - chatMessagesService.submitChat($scope.chatSender, $scope.chatMessage, $scope.threadDetails?.replyToMessageId); - chatHistory.unshift($scope.chatMessage); + chatMessagesService.submitChat(chatMessagesService.chatSender, chatMessagesService.chatMessage, chatMessagesService.threadDetails?.replyToMessageId); + chatHistory.unshift(chatMessagesService.chatMessage); currrentHistoryIndex = -1; - $scope.chatMessage = ""; - $scope.threadDetails = null; + chatMessagesService.chatMessage = ""; + chatMessagesService.threadDetails = null; }; $scope.onMessageFieldUpdate = () => { @@ -188,23 +182,23 @@ if (keyCode === 38) { //up arrow if ( - $scope.chatMessage.length < 1 || - $scope.chatMessage === chatHistory[currrentHistoryIndex] + chatMessagesService.chatMessage.length < 1 || + chatMessagesService.chatMessage === chatHistory[currrentHistoryIndex] ) { if (currrentHistoryIndex + 1 < chatHistory.length) { currrentHistoryIndex++; - $scope.chatMessage = chatHistory[currrentHistoryIndex]; + chatMessagesService.chatMessage = chatHistory[currrentHistoryIndex]; } } } else if (keyCode === 40) { //down arrow if ( - $scope.chatMessage.length > 0 || - $scope.chatMessage === chatHistory[currrentHistoryIndex] + chatMessagesService.chatMessage.length > 0 || + chatMessagesService.chatMessage === chatHistory[currrentHistoryIndex] ) { if (currrentHistoryIndex - 1 >= 0) { currrentHistoryIndex--; - $scope.chatMessage = chatHistory[currrentHistoryIndex]; + chatMessagesService.chatMessage = chatHistory[currrentHistoryIndex]; } } } else if (keyCode === 13) { diff --git a/src/gui/app/services/chat-messages.service.js b/src/gui/app/services/chat-messages.service.js index 49961f45b..b85fec715 100644 --- a/src/gui/app/services/chat-messages.service.js +++ b/src/gui/app/services/chat-messages.service.js @@ -21,6 +21,13 @@ service.autodisconnected = false; + // The active chat sender identifier, either "Streamer" or "Bot" + service.chatSender = "Streamer"; + // The pending but unsent outgoing chat message text + service.messageText = ""; + // The message/thread currently being replied to + service.threadDetails = null; + // Tells us if we should process in app chat or not. service.getChatFeed = function() { return settingsService.getRealChatFeed(); diff --git a/src/gui/app/templates/chat/_chat-messages.html b/src/gui/app/templates/chat/_chat-messages.html index 4c92a4bfd..d48667bf0 100644 --- a/src/gui/app/templates/chat/_chat-messages.html +++ b/src/gui/app/templates/chat/_chat-messages.html @@ -42,7 +42,7 @@ >
@@ -112,9 +112,9 @@

-
- Replying to @{{threadDetails.replyToUserDisplayName}} - +
+ Replying to @{{cms.threadDetails.replyToUserDisplayName}} +
@@ -122,26 +122,24 @@

- - {{chatSender}} + {{cms.chatSender}}
-
+

@@ -153,7 +151,7 @@

  • - Edit events From d4a276e3f1573b19e88cd06a57007958c21e0785 Mon Sep 17 00:00:00 2001 From: Lee Date: Thu, 12 Sep 2024 21:56:01 -0500 Subject: [PATCH 16/17] fix: $hasRole and $hasRoles Usage Examples (#2790) * fix: $hasRole and $hasRoles Examples - Role name comparisons are case-sensitive. - Moderator and VIP being the names sent out. - So reflect those accurately in the examples. - ... and clear up a few lint warnings (white space, ignored param) * Backtick the $if in the `$hasRoles` example - ... since I did it for `$hasRole` --------- Co-authored-by: Erik Bigler --- .../variables/builtin/user/roles/has-role.ts | 18 ++++++++++++++---- .../variables/builtin/user/roles/has-roles.ts | 16 ++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/backend/variables/builtin/user/roles/has-role.ts b/src/backend/variables/builtin/user/roles/has-role.ts index c5246f420..c99322bb7 100644 --- a/src/backend/variables/builtin/user/roles/has-role.ts +++ b/src/backend/variables/builtin/user/roles/has-role.ts @@ -17,12 +17,22 @@ const model : ReplaceVariable = { definition: { handle: "hasRole", usage: "hasRole[user, role]", - description: "Returns true if the user has the specified role. Only valid within $if", + description: "Returns true if the user has the specified role. Only valid within `$if`", + examples: [ + { + usage: "hasRole[user, Moderator]", + description: "Returns true if user is a mod" + }, + { + usage: "hasRole[user, VIP]", + description: "Returns true if user is a VIP" + } + ], triggers: triggers, categories: [VariableCategory.COMMON, VariableCategory.USER], possibleDataOutput: [OutputDataType.ALL] }, - evaluator: async (trigger, username: string, role: string) => { + evaluator: async (_trigger, username: string, role: string) => { if (username == null || username === "") { return false; } @@ -41,8 +51,8 @@ const model : ReplaceVariable = { } catch { // Silently fail } - + return false; } }; -export default model; \ No newline at end of file +export default model; diff --git a/src/backend/variables/builtin/user/roles/has-roles.ts b/src/backend/variables/builtin/user/roles/has-roles.ts index 0862a75cc..6e002c01d 100644 --- a/src/backend/variables/builtin/user/roles/has-roles.ts +++ b/src/backend/variables/builtin/user/roles/has-roles.ts @@ -17,14 +17,14 @@ const model : ReplaceVariable = { definition: { handle: "hasRoles", usage: "hasRoles[user, any|all, role, role2, ...]", - description: "Returns true if the user has the specified roles. Only valid within $if", + description: "Returns true if the user has the specified roles. Only valid within `$if`", examples: [ { - usage: "hasRoles[$user, any, mod, vip]", + usage: "hasRoles[$user, any, Moderator, VIP]", description: "returns true if $user is a mod OR VIP" }, { - usage: "hasRoles[$user, all, mod, vip]", + usage: "hasRoles[$user, all, Moderator, VIP]", description: "Returns true if $user is a mod AND a VIP" } ], @@ -32,7 +32,7 @@ const model : ReplaceVariable = { categories: [VariableCategory.COMMON, VariableCategory.USER], possibleDataOutput: [OutputDataType.ALL] }, - evaluator: async (trigger, username: string, respective, ...roles) => { + evaluator: async (_trigger, username: string, respective, ...roles) => { if (username == null || username === "") { return false; } @@ -55,14 +55,14 @@ const model : ReplaceVariable = { if (user == null) { return false; } - + const userRoles = await roleHelpers.getAllRolesForViewer(user.id); - + // any if (respective === "any") { return userRoles.some(r => roles.includes(r.name)); } - + // all return roles.length === userRoles.filter(r => roles.includes(r.name)).length; } catch { @@ -73,4 +73,4 @@ const model : ReplaceVariable = { } }; -export default model; \ No newline at end of file +export default model; From a3bb74eac67f15622358ba63dba68010b44b5efc Mon Sep 17 00:00:00 2001 From: CKY- Date: Sat, 14 Sep 2024 18:55:05 -0600 Subject: [PATCH 17/17] Fix: incorrect unban function --- src/backend/effects/builtin/moderator-ban.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/effects/builtin/moderator-ban.js b/src/backend/effects/builtin/moderator-ban.js index fe656e2af..d9b98a849 100644 --- a/src/backend/effects/builtin/moderator-ban.js +++ b/src/backend/effects/builtin/moderator-ban.js @@ -69,7 +69,7 @@ const model = { const user = await twitchApi.users.getUserByName(event.effect.username); if (user != null) { - const result = await twitchApi.moderation.unban(user.id); + const result = await twitchApi.moderation.unbanUser(user.id); if (result === true) { logger.debug(`${event.effect.username} was unbanned via the Ban effect.`);