From 625ed3c4fb67bcff7c44cf5c79a13259643b8b65 Mon Sep 17 00:00:00 2001 From: negue Date: Thu, 25 May 2023 21:09:01 +0200 Subject: [PATCH 1/4] parse & replace variables in recipe command config argument values --- package-lock.json | 16 +++++- package.json | 1 + .../action-variable-input.component.html | 3 +- projects/contracts/src/lib/types.ts | 4 ++ .../src/lib/command-blocks.generic.ts | 8 +-- .../src/lib/command-blocks.memebox.ts | 12 ++-- .../recipe-core/src/lib/command-blocks.obs.ts | 32 +++++------ .../src/lib/command-blocks.twitch.ts | 40 ++++++++------ .../src/lib/generateCodeByRecipe.ts | 27 +++++++-- projects/recipe-core/src/lib/recipe.types.ts | 23 +++++++- .../src/lib/recipeStepConfigArgument.ts | 6 ++ projects/recipe-core/src/lib/utils.ts | 8 ++- .../command-setting-dialog.module.ts | 8 ++- ...mand-variables-example-list.component.html | 7 +++ ...mand-variables-example-list.component.scss | 0 ...ommand-variables-example-list.component.ts | 41 ++++++++++++++ .../should-show-variables-panel.pipe.ts | 21 +++++++ .../step-setting-dialog.component.html | 14 ++++- .../step-setting-dialog.component.ts | 6 ++ .../actions/scripts/script.context.ts | 55 +++++++++++++++++-- 20 files changed, 270 insertions(+), 62 deletions(-) create mode 100644 projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html create mode 100644 projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss create mode 100644 projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.ts create mode 100644 projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts diff --git a/package-lock.json b/package-lock.json index 72324022..288274d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "express-validator": "6.14.2", "immer": "8.0.4", "js-yaml": "4.1.0", + "jsonata": "2.0.3", "jwt-decode": "3.1.2", "lodash": "4.17.21", "marked": "4.2.2", @@ -153,7 +154,7 @@ "webdriver-manager": "12.1.8" }, "engines": { - "node": ">=10.13.0" + "node": "16" } }, "node_modules/@aduh95/viz.js": { @@ -32522,6 +32523,14 @@ "node": ">=6" } }, + "node_modules/jsonata": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.3.tgz", + "integrity": "sha512-Up2H81MUtjqI/dWwWX7p4+bUMfMrQJVMN/jW6clFMTiYP528fBOBNtRu944QhKTs3+IsVWbgMeUTny5fw2VMUA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/jsonc-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", @@ -74018,6 +74027,11 @@ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "dev": true }, + "jsonata": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.3.tgz", + "integrity": "sha512-Up2H81MUtjqI/dWwWX7p4+bUMfMrQJVMN/jW6clFMTiYP528fBOBNtRu944QhKTs3+IsVWbgMeUTny5fw2VMUA==" + }, "jsonc-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", diff --git a/package.json b/package.json index e4b54e48..424f5d94 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,7 @@ "express-validator": "6.14.2", "immer": "8.0.4", "js-yaml": "4.1.0", + "jsonata": "2.0.3", "jwt-decode": "3.1.2", "lodash": "4.17.21", "marked": "4.2.2", diff --git a/projects/action-variables-ui/src/lib/action-variable-input/action-variable-input.component.html b/projects/action-variables-ui/src/lib/action-variable-input/action-variable-input.component.html index d34c86de..5ca44028 100644 --- a/projects/action-variables-ui/src/lib/action-variable-input/action-variable-input.component.html +++ b/projects/action-variables-ui/src/lib/action-variable-input/action-variable-input.component.html @@ -12,8 +12,7 @@ [matAutosizeMinRows]="4" (ngModelChange)="valueChanged.emit(value)"> - - + diff --git a/projects/contracts/src/lib/types.ts b/projects/contracts/src/lib/types.ts index 04bfd485..e0fc9c3b 100644 --- a/projects/contracts/src/lib/types.ts +++ b/projects/contracts/src/lib/types.ts @@ -36,6 +36,10 @@ export interface HasExtendedData { extended?: Dictionary; } +// TODO split Action into a union of each subtype +// - with that only properties that are used for type X +// - are known in each sub type + export interface Action extends HasId, ActionOverridableProperties, HasExtendedData { name: string; previewUrl?: string; diff --git a/projects/recipe-core/src/lib/command-blocks.generic.ts b/projects/recipe-core/src/lib/command-blocks.generic.ts index b1e59d2c..949389c4 100644 --- a/projects/recipe-core/src/lib/command-blocks.generic.ts +++ b/projects/recipe-core/src/lib/command-blocks.generic.ts @@ -15,7 +15,7 @@ export function registerGenericCommandBlocks( type: "number" } ], - toScriptCode: (step, context) => `sleep.secondsAsync(${step.payload.seconds});`, + toScriptCode: (codePayload) => `sleep.secondsAsync(${codePayload.commandBlock.argument('seconds')});`, commandEntryLabelAsync: (queries, payload, parentStep) => { return Promise.resolve(`wait ${payload.seconds} seconds`); }, @@ -33,7 +33,7 @@ export function registerGenericCommandBlocks( type: "number" } ], - toScriptCode: (step, context) => `sleep.msAsync(${step.payload.ms});`, + toScriptCode: (codePayload) => `sleep.msAsync(${codePayload.commandBlock.argument('ms')});`, commandEntryLabelAsync: (queries, payload, parentStep) => { return Promise.resolve(`wait ${payload.ms}ms`); }, @@ -79,12 +79,12 @@ export function registerGenericCommandBlocks( return Promise.resolve(''); }, awaitCodeHandledInternally: true, - toScriptCode: (step, context, userData) => { + toScriptCode: ({step,userData,context}) => { const awaitCode = step.awaited ? 'await ' : ''; const functionNames: string[] = []; - const generatedFunctions = generateCodeByStep(step, context, userData).map(g => { + const generatedFunctions = generateCodeByStep({step, context, userData}).map(g => { const functionName = `randomGroup_${g.subCommand.labelId}`; diff --git a/projects/recipe-core/src/lib/command-blocks.memebox.ts b/projects/recipe-core/src/lib/command-blocks.memebox.ts index 270d2064..b0e2af59 100644 --- a/projects/recipe-core/src/lib/command-blocks.memebox.ts +++ b/projects/recipe-core/src/lib/command-blocks.memebox.ts @@ -67,7 +67,7 @@ export function registerMemeboxCommandBlocks( } } ], - toScriptCode: (step) => { + toScriptCode: ({step}) => { const actionPayload = step.payload.action as RecipeCommandConfigActionPayload; const actionOverrides = actionPayload.overrides; @@ -128,14 +128,14 @@ export function registerMemeboxCommandBlocks( entries: [] }); }, - toScriptCode: (step, context, userData) => { + toScriptCode: ({step, context, userData}) => { const actionPayload = step.payload.action as RecipeCommandConfigActionPayload; const actionOverrides = actionPayload.overrides; return `${createMemeboxApiVariable(actionPayload)} .triggerWhile(async (helpers_${step.payload._suffix}) => { - ${generateCodeByStep(step, context, userData)[0].generatedScript} + ${generateCodeByStep({step, context, userData})[0].generatedScript} } ${actionOverrides ? ',' + JSON.stringify(actionOverrides) : ''});`; }, @@ -165,7 +165,7 @@ export function registerMemeboxCommandBlocks( return false; // return step.entryType === 'command' && step.commandBlockType === 'triggerActionWhile'; }, - toScriptCode: (step) => { + toScriptCode: ({step}) => { const helpersName = `helpers_${step.payload._suffix}`; return `${helpersName}.reset();` @@ -186,7 +186,7 @@ export function registerMemeboxCommandBlocks( } ], awaitCodeHandledInternally: true, - toScriptCode: (step, context, userData) => { + toScriptCode: ({step, context, userData}) => { const awaitCode = step.awaited ? 'await ' : ''; const actionsToChooseFrom = listActionsOfActionListPayload( @@ -241,7 +241,7 @@ export function registerMemeboxCommandBlocks( }*/ ], awaitCodeHandledInternally: true, - toScriptCode: (step) => { + toScriptCode: ({step}) => { const actionPayload = step.payload.action as RecipeCommandConfigActionPayload; const overrides = actionPayload.overrides; diff --git a/projects/recipe-core/src/lib/command-blocks.obs.ts b/projects/recipe-core/src/lib/command-blocks.obs.ts index 36e15491..bb6d24bd 100644 --- a/projects/recipe-core/src/lib/command-blocks.obs.ts +++ b/projects/recipe-core/src/lib/command-blocks.obs.ts @@ -21,10 +21,8 @@ export function registerObsCommandBlocks ( type: "obs:scene" } ], - toScriptCode: (step) => { - const scenePayload = step.payload.scene as string; - - return `obs.switchToScene('${scenePayload}');`; + toScriptCode: ({step, commandBlock}) => { + return `obs.switchToScene(${commandBlock.argument('scene')});`; }, commandEntryLabelAsync: (queries, payload) => { const scenePayload = payload.scene as string; @@ -50,10 +48,8 @@ export function registerObsCommandBlocks ( } ], - toScriptCode: (step) => { - const scenePayload = step.payload.sourceName as string; - - return `obs.setSourceVisibility('${scenePayload}', ${step.payload.visible});`; + toScriptCode: ({step, commandBlock}) => { + return `obs.setSourceVisibility(${commandBlock.argument('scene')}, ${step.payload.visible});`; }, commandEntryLabelAsync: (queries, payload) => { const sourceName = payload.sourceName as string; @@ -78,10 +74,8 @@ export function registerObsCommandBlocks ( type: "boolean" } ], - toScriptCode: (step) => { - const scenePayload = step.payload.sourceName as string; - - return `obs.setSourceMute('${scenePayload}', ${step.payload.muted});`; + toScriptCode: ({step, commandBlock}) => { + return `obs.setSourceMute(${commandBlock.argument('sourceName')}, ${step.payload.muted});`; }, commandEntryLabelAsync: (queries, payload) => { const sourceName = payload.sourceName as string; @@ -107,7 +101,7 @@ export function registerObsCommandBlocks ( type: "boolean" } ], - toScriptCode: (step) => { + toScriptCode: ({step}) => { const filterPayload = step.payload.filter as RecipeCommandConfigObsSetFilterStatePayload; const enabled = step.payload.enabled as boolean; @@ -131,15 +125,21 @@ export function registerObsCommandBlocks ( { name: "command", label: "Command", - type: "text" + type: "text", + flags: { + canUseVariables: false, + } }, { name: "obsPayload", label: "Payload", - type: "textarea" + type: "textarea", + flags: { + canUseVariables: false, + } } ], - toScriptCode: (step) => { + toScriptCode: ({step}) => { const obsCommand = step.payload.command as string; const obsPayload = step.payload.obsPayload as string; diff --git a/projects/recipe-core/src/lib/command-blocks.twitch.ts b/projects/recipe-core/src/lib/command-blocks.twitch.ts index f76de48d..04405cef 100644 --- a/projects/recipe-core/src/lib/command-blocks.twitch.ts +++ b/projects/recipe-core/src/lib/command-blocks.twitch.ts @@ -20,13 +20,14 @@ export function registerTwitchCommandBlocks ( { name: "text", label: "Message to write", - type: "textarea" + type: "textarea", + flags: { + canUseVariables: true, + } } ], - toScriptCode: (step) => { - const textToSay = step.payload.text as string; - - return `twitch.say('${textToSay}');`; + toScriptCode: ({step, commandBlock}) => { + return `twitch.say(${commandBlock.argument('text')});`; }, commandEntryLabelAsync: (queries, payload) => { const textToSay = payload.text as string; @@ -43,13 +44,14 @@ export function registerTwitchCommandBlocks ( { name: "username", label: "Username to Shoutout", - type: "text" + type: "text", + flags: { + canUseVariables: true, + } } ], - toScriptCode: (step) => { - const username = step.payload.username as string; - - return `twitch.shoutout('${username}');`; + toScriptCode: ({step, commandBlock}) => { + return `twitch.shoutout(${commandBlock.argument('username')});`; }, commandEntryLabelAsync: (queries, payload) => { const username = payload.username as string; @@ -66,7 +68,10 @@ export function registerTwitchCommandBlocks ( { name: "text", label: "Announcement to send", - type: "textarea" + type: "textarea", + flags: { + canUseVariables: true, + } }, { name: "color", @@ -82,11 +87,10 @@ export function registerTwitchCommandBlocks ( ] } ], - toScriptCode: (step) => { - const textToSay = step.payload.text as string; + toScriptCode: ({step, commandBlock}) => { const color = step.payload.color as string; - return `twitch.sendAnnouncement('${textToSay}','${color}');`; + return `twitch.sendAnnouncement(${commandBlock.argument('text')},'${color}');`; }, commandEntryLabelAsync: (queries, payload) => { const textToSay = payload.text as string; @@ -150,8 +154,8 @@ export function registerTwitchCommandBlocks ( ] } ], - toScriptCode: (command) => { - const length = command.payload.length as string; + toScriptCode: ({step}) => { + const length = step.payload.length as string; return `twitch.startCommercial(${+length});`; }, commandEntryLabelAsync: (queries, payload) => { @@ -206,7 +210,7 @@ export function registerTwitchCommandBlocks ( configArguments: [ ...chatSettingsArray ], - toScriptCode: (step) => { + toScriptCode: ({step}) => { return `twitch.updateChatSettings(${JSON.stringify(step.payload)});`; }, commandEntryLabelAsync: (queries, payload) => { @@ -228,7 +232,7 @@ export function registerTwitchCommandBlocks ( label: 'Mode Active' } ], - toScriptCode: (step) => { + toScriptCode: ({step}) => { return `twitch.updateChatSettings(${JSON.stringify(step.payload)});`; }, commandEntryLabelAsync: (queries, payload) => { diff --git a/projects/recipe-core/src/lib/generateCodeByRecipe.ts b/projects/recipe-core/src/lib/generateCodeByRecipe.ts index 17f76443..f76079a1 100644 --- a/projects/recipe-core/src/lib/generateCodeByRecipe.ts +++ b/projects/recipe-core/src/lib/generateCodeByRecipe.ts @@ -1,7 +1,7 @@ import { + GenerateCodeByStepPayload, generatedCodeBySubCommandBlock, RecipeContext, - RecipeEntry, RecipeEntryCommandCall, RecipeEntryCommandPayload } from "./recipe.types"; @@ -14,7 +14,7 @@ import {RecipeCommandRegistry} from "./recipeCommandRegistry"; import {registerGenericCommandBlocks} from "./command-blocks.generic"; -function generateCodeByStepAsync (step: RecipeEntry, context: RecipeContext, userData: UserDataState): generatedCodeBySubCommandBlock[] { +function generateCodeByStepAsync ({step, context, userData}: GenerateCodeByStepPayload): generatedCodeBySubCommandBlock[] { const result: generatedCodeBySubCommandBlock[] = []; for (const subStepInfo of step.subCommandBlocks) { @@ -30,11 +30,30 @@ function generateCodeByStepAsync (step: RecipeEntry, context: RecipeContext, use // result.push(`logger.log('Pre: ${subEntry.commandType}');`); + scriptCode.push(`// ${subEntry.id} - ${JSON.stringify(subEntry)}`) + if (!entryDefinition.awaitCodeHandledInternally && subEntry.awaited) { scriptCode.push('await '); } - const createdStepCode = entryDefinition.toScriptCode(subEntry, context, userData); + // todo "Mark Scripts / Recipes to know which source they might be triggered from" + // => inline recipe in twitch triggers which sets the context inside for example trigger variables + // todo fill up commandBlockData + // todo think of way to use other commadn block results in the current one + // commandBlockData should cache if there is no dynamic data to speed up things + + const createdStepCode = entryDefinition.toScriptCode({ + step: subEntry, + context, + commandBlock: { + argument(name) { + // todo check of config if name exist + // also check that on the UI during edit + return `await commandBlockData['${subEntry.id}']['${name}']()` + } + }, + userData + }); scriptCode.push(createdStepCode.trim()); @@ -59,7 +78,7 @@ export function generateCodeByRecipe( ): string { const rootEntry = recipeContext.entries[recipeContext.rootEntry]; - return generateCodeByStepAsync(rootEntry, recipeContext, userData) + return generateCodeByStepAsync({step: rootEntry, context: recipeContext, userData}) .map(g => g.generatedScript) .join('\r\n'); } diff --git a/projects/recipe-core/src/lib/recipe.types.ts b/projects/recipe-core/src/lib/recipe.types.ts index 273c1ee0..f245320d 100644 --- a/projects/recipe-core/src/lib/recipe.types.ts +++ b/projects/recipe-core/src/lib/recipe.types.ts @@ -101,6 +101,22 @@ export interface RecipeCommandConfigObsSetFilterStatePayload { // TODO have a different Interface for queries / AppQueries +// Refactor to use command block results as variables for the next call + +export type GenerateCodeByStepPayload = { + step: RecipeEntry; + context: RecipeContext; + userData: UserDataState; +} + +export type CommandBlockCodeGenerationPayload = GenerateCodeByStepPayload & { + step: RecipeEntryCommandCall + commandBlock: { + argument: (name: string) => string + } +} + + // Registry Types export interface RecipeCommandDefinition { pickerLabel: string; @@ -112,7 +128,7 @@ export interface RecipeCommandDefinition { extendCommandBlock?: (step: RecipeEntryCommandCall, parentStep: RecipeEntry) => void; allowedToBeAdded?: (step: RecipeEntry, context: RecipeContext) => boolean; - toScriptCode: (step: RecipeEntryCommandCall, context: RecipeContext, userData: UserDataState) => string; + toScriptCode: (codePayload: CommandBlockCodeGenerationPayload) => string; awaitCodeHandledInternally?: boolean; extendCommandBlockOnEdit?: boolean; commandType?: string; @@ -130,4 +146,7 @@ export interface generatedCodeBySubCommandBlock { subCommand: RecipeSubCommandBlock; generatedScript: string; } -export type generateCodeByStep = (step: RecipeEntry, context: RecipeContext, userData: UserDataState) => generatedCodeBySubCommandBlock[]; + + + +export type generateCodeByStep = (payload: GenerateCodeByStepPayload) => generatedCodeBySubCommandBlock[]; diff --git a/projects/recipe-core/src/lib/recipeStepConfigArgument.ts b/projects/recipe-core/src/lib/recipeStepConfigArgument.ts index 9ff84ccb..c9274610 100644 --- a/projects/recipe-core/src/lib/recipeStepConfigArgument.ts +++ b/projects/recipe-core/src/lib/recipeStepConfigArgument.ts @@ -11,10 +11,16 @@ export interface RecipeStepConfigBooleanArgument extends RecipeStepConfigArgumen export interface RecipeStepConfigTextArgument extends RecipeStepConfigArgument { type: 'text'; + flags: { + canUseVariables: boolean; + } } export interface RecipeStepConfigTextareaArgument extends RecipeStepConfigArgument { type: 'textarea'; + flags: { + canUseVariables: boolean; + } } export interface RecipeStepConfigNumberArgument extends RecipeStepConfigArgument { diff --git a/projects/recipe-core/src/lib/utils.ts b/projects/recipe-core/src/lib/utils.ts index d1b68e9a..ef063198 100644 --- a/projects/recipe-core/src/lib/utils.ts +++ b/projects/recipe-core/src/lib/utils.ts @@ -20,12 +20,16 @@ export function generateRandomCharacters(length: number): string { export function* listAllEntriesOfTypes( recipeContext: RecipeContext, currentCommandToCheck: string, - commandTypeList: string[] + commandTypeList?: string[] ): IterableIterator { const entry = recipeContext.entries[currentCommandToCheck]; if (entry.entryType === 'command') { - if (commandTypeList.includes(entry.commandBlockType)) { + if (commandTypeList) { + if ( commandTypeList.includes(entry.commandBlockType)) { + yield entry; + } + } else { yield entry; } } diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/command-setting-dialog.module.ts b/projects/recipe-ui/src/lib/command-setting-dialog/command-setting-dialog.module.ts index 5660b707..51313b8a 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/command-setting-dialog.module.ts +++ b/projects/recipe-ui/src/lib/command-setting-dialog/command-setting-dialog.module.ts @@ -27,6 +27,10 @@ import {MatIconModule} from "@angular/material/icon"; import {MatTooltipModule} from "@angular/material/tooltip"; import {ObsSourceSelectionComponent} from "./obs-source-selection/obs-source-selection.component"; import {ShouldShowSettingLabelAbovePipe} from './should-show-setting-label-above.pipe'; +import {ShouldShowVariablesPanelPipe} from "./should-show-variables-panel.pipe"; +import { + CommandVariablesExampleListComponent +} from './command-variables-example-list/command-variables-example-list.component'; // todo extract this module to its own internal library ^ @@ -39,7 +43,9 @@ import {ShouldShowSettingLabelAbovePipe} from './should-show-setting-label-above ActionSelectionComponent, ActionListSettingsComponent, ObsSourceSelectionComponent, - ShouldShowSettingLabelAbovePipe + ShouldShowSettingLabelAbovePipe, + ShouldShowVariablesPanelPipe, + CommandVariablesExampleListComponent ], imports: [ CommonModule, diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html new file mode 100644 index 00000000..58c6d1f0 --- /dev/null +++ b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html @@ -0,0 +1,7 @@ +
+

{{ exampleEntry.title}}:

+ + {{exampleEntry.variableExample}} +
+ {{exampleEntry.resultingValue}} +
diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.ts b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.ts new file mode 100644 index 00000000..6df3d6d1 --- /dev/null +++ b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.ts @@ -0,0 +1,41 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-command-variables-example-list', + templateUrl: './command-variables-example-list.component.html', + styleUrls: ['./command-variables-example-list.component.scss'] +}) +export class CommandVariablesExampleListComponent { + + public examples: { + title: string, + variableExample: string, + resultingValue: string + }[] = [ + { + title: 'Twitch Message: Username', + resultingValue: 'thatn00b__', + variableExample: '${{byTwitch.payload.userstate.username}}' + }, + { + title: 'Twitch Message: Message', + resultingValue: 'Yello all', + variableExample: '${{byTwitch.payload.message}}' + }, + { + title: 'Twitch Message: First Word of Message', + resultingValue: 'Yello', + variableExample: '${{ $split(byTwitch.payload.message, " ")[0] }}' + }, + { + title: 'Twitch Raid: Channel that raided you', + resultingValue: 'thatn00b__', + variableExample: '${{byTwitch.payload.channel}}' + }, + { + title: 'Twitch Raid: Raider Count', + resultingValue: '1337', + variableExample: '${{byTwitch.payload.viewers}}' + } + ]; +} diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts b/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts new file mode 100644 index 00000000..1055305b --- /dev/null +++ b/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts @@ -0,0 +1,21 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {RecipeStepConfigArguments} from "@memebox/recipe-core"; + +// todo maybe at some point this needs to be in an config type ... metadata config..something + +@Pipe({ + name: 'shouldShowVariablesPanel' +}) +export class ShouldShowVariablesPanelPipe implements PipeTransform { + + transform(config: RecipeStepConfigArguments): boolean { + switch (config.type){ + case "text": + case "textarea": + return config.flags.canUseVariables; + default: + return false; + } + } + +} diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html index 94007b59..abe1ee4e 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html +++ b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html @@ -5,7 +5,8 @@

- +
+
{{config.label}}: @@ -23,6 +24,9 @@

[fullWidth]="true" (valueChanged)="payload[config.name] = $event"> + + [fullWidth]="true" (valueChanged)="payload[config.name] = $event"> +

+
+ +
+ +
+
diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts index db9ad464..a2c4176d 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts +++ b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts @@ -31,6 +31,8 @@ export class StepSettingDialogComponent { public payload: RecipeEntryCommandPayload = {}; + public showVariablesPanel = false; + actionDictionary$: Observable> = this.appQuery.actionMap$; @@ -104,6 +106,10 @@ export class StepSettingDialogComponent { this.dialogRef.close(this.payload); } + toggleVariablesPanel (){ + this.showVariablesPanel = !this.showVariablesPanel; + } + private _getOrPreparePayloadForAction (configName: string): RecipeCommandConfigActionPayload { let actionPayload = this.payload[configName] as any as RecipeCommandConfigActionPayload; diff --git a/server/providers/actions/scripts/script.context.ts b/server/providers/actions/scripts/script.context.ts index 9ea2c5da..852fa045 100644 --- a/server/providers/actions/scripts/script.context.ts +++ b/server/providers/actions/scripts/script.context.ts @@ -11,6 +11,9 @@ import {TwitchApi} from "./apis/twitch.api"; import {CanDispose} from "./apis/disposableBase"; import {DummyWebSocketServer, RealWebSocketServer, WebSocketServerApi} from "./apis/wss.api"; import {EventBusApi} from "./apis/eventbus.api"; +import {listAllEntriesOfTypes, RecipeCommandRegistry} from "@memebox/recipe-core"; +import jsonata from "jsonata"; + class ScriptCompileError extends Error { constructor(script: Action, @@ -31,11 +34,16 @@ interface SharedScriptPayload { eventBus: EventBusApi; } -const SHARED_API_ARGUMENTS = 'variables, store, memebox, logger, obs, twitch, wss, eventBus'; +const SHARED_API_ARGUMENTS = 'variables, store, memebox, logger, obs, twitch, wss, eventBus, commandBlockData'; + +const JSONATA_REGEX = /\${{\s*(.*)\s*}}/gm; interface ExecutionScriptPayload extends SharedScriptPayload { bootstrap: Record; triggerPayload: TriggerAction; + commandBlockData: {[key: string]: { + [configName: string]: () => Promise + }} } type ExecutionScript = ( @@ -142,13 +150,13 @@ export class ScriptContext implements CanDispose { } } - public async execute(payloadObs: TriggerAction) : Promise { + public async execute(triggerActionPayload: TriggerAction) : Promise { // TODO apply variable overrides from TriggerClip const variables = getScriptVariablesOrFallbackValues( this.scriptConfig.variablesConfig ?? [], this.script.extended, - payloadObs?.overrides?.action?.variables + triggerActionPayload?.overrides?.action?.variables ); if (!this.scriptToCall) { @@ -164,16 +172,53 @@ export class ScriptContext implements CanDispose { const scriptArguments: ExecutionScriptPayload = { variables, bootstrap: this.bootstrap_variables, - triggerPayload: payloadObs, + triggerPayload: triggerActionPayload, store: this.store, memebox: this.memeboxApi, logger: this.logger, obs: this.obsApi, twitch: this.twitchApi, wss: this.wss, - eventBus: this.eventBus + eventBus: this.eventBus, + commandBlockData: {} }; + if (this.script.type === ActionType.Recipe && this.script.recipe) { + for (const recipeCommand of listAllEntriesOfTypes( + this.script.recipe, this.script.recipe.rootEntry + )) { + const commandConfig = RecipeCommandRegistry[recipeCommand.commandBlockType]; + + for (const configArgument of commandConfig.configArguments) { + const configArgumentValue = recipeCommand.payload[configArgument.name].toString(); + scriptArguments.commandBlockData[recipeCommand.id] ??= {}; + + if (configArgumentValue.includes('${{')) { + scriptArguments.commandBlockData[recipeCommand.id][configArgument.name] = async () => { + let replacedValue = configArgumentValue; + this.logger.log(`Config ${configArgument.name} Argument RAW: ${configArgumentValue}`); + for (const matchAllElement of configArgumentValue.matchAll(JSONATA_REGEX)) { + // todo cache jsonataQuery + const jsonataQuery = matchAllElement[1].trim(); + const jsonataExpression = jsonata(jsonataQuery); + replacedValue = replacedValue.replaceAll(matchAllElement[0], await jsonataExpression.evaluate(triggerActionPayload)) + } + + this.logger.log(`Config ${configArgument.name} Argument Replaced: ${replacedValue}`); + return replacedValue; + }; + } else { + scriptArguments.commandBlockData[recipeCommand.id][configArgument.name] = () => { + return Promise.resolve(recipeCommand.payload[configArgument.name]); + }; + } + } + + recipeCommand.entryType + + } + } + await this.scriptToCall(scriptArguments); } From c406b939f3b79032b6b78a9525097d7a504f4a69 Mon Sep 17 00:00:00 2001 From: negue Date: Tue, 6 Jun 2023 19:20:01 +0200 Subject: [PATCH 2/4] improve variable example list --- ...mand-variables-example-list.component.html | 20 +++++++--- ...mand-variables-example-list.component.scss | 6 +++ .../step-setting-dialog.component.html | 2 +- .../step-setting-dialog.component.scss | 4 ++ tutorials/examples/import_export_settings.md | 4 +- .../recipes/Shoutout Raider-Recipe.md | 40 +++++++++++++++++++ .../Shoutout a user by chat message-Recipe.md | 40 +++++++++++++++++++ 7 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 tutorials/examples/recipes/Shoutout Raider-Recipe.md create mode 100644 tutorials/examples/recipes/Shoutout a user by chat message-Recipe.md diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html index 58c6d1f0..a292dd1c 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html +++ b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.html @@ -1,7 +1,15 @@ -
-

{{ exampleEntry.title}}:

+Command Variables using the awesome Library: JSONata to get the variable value out of the trigger payload. + +
+
+ + + + {{ exampleEntry.title}}: Example: {{exampleEntry.resultingValue}} +
+
+ {{exampleEntry.variableExample}} +
+
+
- {{exampleEntry.variableExample}} -
- {{exampleEntry.resultingValue}} -
diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss index e69de29b..07f9648e 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss +++ b/projects/recipe-ui/src/lib/command-setting-dialog/command-variables-example-list/command-variables-example-list.component.scss @@ -0,0 +1,6 @@ +code { + overflow-x: auto; + width: 100%; + display: inline-block; + white-space: nowrap; +} diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html index abe1ee4e..f1437448 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html +++ b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html @@ -178,7 +178,7 @@

-
+
diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.scss b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.scss index e84b40d6..71f39cfd 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.scss +++ b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.scss @@ -2,6 +2,10 @@ gap: 1rem; } +.width-50 { + width: 50%; +} + strong { display: block; margin-bottom: 0.5rem; diff --git a/tutorials/examples/import_export_settings.md b/tutorials/examples/import_export_settings.md index 751ed76e..13ba2720 100644 --- a/tutorials/examples/import_export_settings.md +++ b/tutorials/examples/import_export_settings.md @@ -1,8 +1,8 @@ # Import / Export -Currently it is only possible to im-/export Scripts or Widgets. +Currently it is only possible to im-/export Scripts/Recipes or Widgets. -## Scripts +## [Scripts](./scripts) / [Recipes](./recipes) When you edit a script and then open "Edit Script+Config", then you can find those Export / Import Buttons diff --git a/tutorials/examples/recipes/Shoutout Raider-Recipe.md b/tutorials/examples/recipes/Shoutout Raider-Recipe.md new file mode 100644 index 00000000..db83001b --- /dev/null +++ b/tutorials/examples/recipes/Shoutout Raider-Recipe.md @@ -0,0 +1,40 @@ +--- +title: Shoutout Raider +settings: {} +type: 101 +--- + +Once this is selected as action for a Raid Trigger, it handles the shoutouts automagically. + +# recipe + +```json +{ + "rootEntry": "1524497c-347e-4a86-866d-83f7bb546b72", + "entries": { + "1524497c-347e-4a86-866d-83f7bb546b72": { + "id": "1524497c-347e-4a86-866d-83f7bb546b72", + "entryType": "group", + "awaited": false, + "subCommandBlocks": [ + { + "labelId": "recipeRoot", + "entries": [ + "5ad7a8c8-4308-4bd4-b53e-edfc4ce45cfa" + ] + } + ] + }, + "5ad7a8c8-4308-4bd4-b53e-edfc4ce45cfa": { + "id": "5ad7a8c8-4308-4bd4-b53e-edfc4ce45cfa", + "commandBlockType": "twitch:shoutout", + "payload": { + "username": "${{byTwitch.payload.channel}}" + }, + "awaited": true, + "entryType": "command", + "subCommandBlocks": [] + } + } +} +``` diff --git a/tutorials/examples/recipes/Shoutout a user by chat message-Recipe.md b/tutorials/examples/recipes/Shoutout a user by chat message-Recipe.md new file mode 100644 index 00000000..a0248302 --- /dev/null +++ b/tutorials/examples/recipes/Shoutout a user by chat message-Recipe.md @@ -0,0 +1,40 @@ +--- +title: Shoutout a user by chat message +settings: {} +type: 101 +--- + +This needs a Twitch Chat Message trigger and it uses the value after the command text + +# recipe + +```json +{ + "rootEntry": "3972a08d-7e44-4f51-8737-04dd3197d982", + "entries": { + "3972a08d-7e44-4f51-8737-04dd3197d982": { + "id": "3972a08d-7e44-4f51-8737-04dd3197d982", + "entryType": "group", + "awaited": false, + "subCommandBlocks": [ + { + "labelId": "recipeRoot", + "entries": [ + "d388295e-b644-431a-ba7b-d1cf5ab82778" + ] + } + ] + }, + "d388295e-b644-431a-ba7b-d1cf5ab82778": { + "id": "d388295e-b644-431a-ba7b-d1cf5ab82778", + "commandBlockType": "twitch:shoutout", + "payload": { + "username": "${{ $split(byTwitch.payload.message, \" \")[1] }}" + }, + "awaited": true, + "entryType": "command", + "subCommandBlocks": [] + } + } +} +``` From b70b053e952f39ccba37d53c818aaa9a45f4c360 Mon Sep 17 00:00:00 2001 From: negue Date: Tue, 6 Jun 2023 19:49:24 +0200 Subject: [PATCH 3/4] cleanup + changelog --- CHANGELOG.md | 10 +++ .../src/lib/generateCodeByRecipe.ts | 5 +- .../should-show-variables-panel.pipe.ts | 2 - .../actions/scripts/script.context.ts | 73 ++++++++++--------- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5062b8..eb60cd07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2023.1.0 + +### Shoutout Users by Command Blocks +(insert screenshot here) +You can now shoutout a specific person by adding a Shoutout Command .. or + +### Parse Trigger Payload as Command Block argument variables (maybe need to rephrase it) +With the help of a [very powerful json query library: JSONata](https://jsonata.org) it is now possible to parse trigger payloads (twitch message, raid, etc) and use those as variables for Arguments of Command Blocks. +(insert screenshot here) + ## 2022.1.1 ### New Command Block "Random Command Group" diff --git a/projects/recipe-core/src/lib/generateCodeByRecipe.ts b/projects/recipe-core/src/lib/generateCodeByRecipe.ts index f76079a1..7f196dcc 100644 --- a/projects/recipe-core/src/lib/generateCodeByRecipe.ts +++ b/projects/recipe-core/src/lib/generateCodeByRecipe.ts @@ -30,17 +30,14 @@ function generateCodeByStepAsync ({step, context, userData}: GenerateCodeByStepP // result.push(`logger.log('Pre: ${subEntry.commandType}');`); - scriptCode.push(`// ${subEntry.id} - ${JSON.stringify(subEntry)}`) - if (!entryDefinition.awaitCodeHandledInternally && subEntry.awaited) { scriptCode.push('await '); } // todo "Mark Scripts / Recipes to know which source they might be triggered from" // => inline recipe in twitch triggers which sets the context inside for example trigger variables - // todo fill up commandBlockData // todo think of way to use other commadn block results in the current one - // commandBlockData should cache if there is no dynamic data to speed up things + // todo commandBlockData should cache if there is no dynamic data to speed up things const createdStepCode = entryDefinition.toScriptCode({ step: subEntry, diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts b/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts index 1055305b..f6f744f9 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts +++ b/projects/recipe-ui/src/lib/command-setting-dialog/should-show-variables-panel.pipe.ts @@ -1,8 +1,6 @@ import {Pipe, PipeTransform} from '@angular/core'; import {RecipeStepConfigArguments} from "@memebox/recipe-core"; -// todo maybe at some point this needs to be in an config type ... metadata config..something - @Pipe({ name: 'shouldShowVariablesPanel' }) diff --git a/server/providers/actions/scripts/script.context.ts b/server/providers/actions/scripts/script.context.ts index 852fa045..0555c3b3 100644 --- a/server/providers/actions/scripts/script.context.ts +++ b/server/providers/actions/scripts/script.context.ts @@ -183,43 +183,50 @@ export class ScriptContext implements CanDispose { commandBlockData: {} }; - if (this.script.type === ActionType.Recipe && this.script.recipe) { - for (const recipeCommand of listAllEntriesOfTypes( - this.script.recipe, this.script.recipe.rootEntry - )) { - const commandConfig = RecipeCommandRegistry[recipeCommand.commandBlockType]; - - for (const configArgument of commandConfig.configArguments) { - const configArgumentValue = recipeCommand.payload[configArgument.name].toString(); - scriptArguments.commandBlockData[recipeCommand.id] ??= {}; - - if (configArgumentValue.includes('${{')) { - scriptArguments.commandBlockData[recipeCommand.id][configArgument.name] = async () => { - let replacedValue = configArgumentValue; - this.logger.log(`Config ${configArgument.name} Argument RAW: ${configArgumentValue}`); - for (const matchAllElement of configArgumentValue.matchAll(JSONATA_REGEX)) { - // todo cache jsonataQuery - const jsonataQuery = matchAllElement[1].trim(); - const jsonataExpression = jsonata(jsonataQuery); - replacedValue = replacedValue.replaceAll(matchAllElement[0], await jsonataExpression.evaluate(triggerActionPayload)) - } - - this.logger.log(`Config ${configArgument.name} Argument Replaced: ${replacedValue}`); - return replacedValue; - }; - } else { - scriptArguments.commandBlockData[recipeCommand.id][configArgument.name] = () => { - return Promise.resolve(recipeCommand.payload[configArgument.name]); - }; - } - } + this.attacheRecipeCommandArguments(scriptArguments); - recipeCommand.entryType + await this.scriptToCall(scriptArguments); + } - } + private attacheRecipeCommandArguments(scriptArguments: ExecutionScriptPayload) { + if (this.script.type !== ActionType.Recipe) { + return; } - await this.scriptToCall(scriptArguments); + if (!this.script.recipe) { + return; + } + + for (const recipeCommand of listAllEntriesOfTypes( + this.script.recipe, this.script.recipe.rootEntry + )) { + const commandConfig = RecipeCommandRegistry[recipeCommand.commandBlockType]; + + for (const configArgument of commandConfig.configArguments) { + const configArgumentValue = recipeCommand.payload[configArgument.name].toString(); + scriptArguments.commandBlockData[recipeCommand.id] ??= {}; + + if (configArgumentValue.includes('${{')) { + scriptArguments.commandBlockData[recipeCommand.id][configArgument.name] = async () => { + let replacedValue = configArgumentValue; + this.logger.log(`Config ${configArgument.name} Argument RAW: ${configArgumentValue}`); + for (const matchAllElement of configArgumentValue.matchAll(JSONATA_REGEX)) { + // todo cache jsonataQuery + const jsonataQuery = matchAllElement[1].trim(); + const jsonataExpression = jsonata(jsonataQuery); + replacedValue = replacedValue.replaceAll(matchAllElement[0], await jsonataExpression.evaluate(triggerActionPayload)) + } + + this.logger.log(`Config ${configArgument.name} Argument Replaced: ${replacedValue}`); + return replacedValue; + }; + } else { + scriptArguments.commandBlockData[recipeCommand.id][configArgument.name] = () => { + return Promise.resolve(recipeCommand.payload[configArgument.name]); + }; + } + } + } } public dispose(): void { From fba0b13a6eeec3e976e458752a6c3717c968a837 Mon Sep 17 00:00:00 2001 From: negue Date: Tue, 6 Jun 2023 19:53:56 +0200 Subject: [PATCH 4/4] fix compile --- server/providers/actions/scripts/script.context.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/providers/actions/scripts/script.context.ts b/server/providers/actions/scripts/script.context.ts index 0555c3b3..1b3d4425 100644 --- a/server/providers/actions/scripts/script.context.ts +++ b/server/providers/actions/scripts/script.context.ts @@ -183,12 +183,15 @@ export class ScriptContext implements CanDispose { commandBlockData: {} }; - this.attacheRecipeCommandArguments(scriptArguments); + this.attacheRecipeCommandArguments(scriptArguments, triggerActionPayload); await this.scriptToCall(scriptArguments); } - private attacheRecipeCommandArguments(scriptArguments: ExecutionScriptPayload) { + private attacheRecipeCommandArguments( + scriptArguments: ExecutionScriptPayload, + triggerActionPayload: TriggerAction + ) { if (this.script.type !== ActionType.Recipe) { return; }