diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 6335e93b1..599d6b850 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -71,6 +71,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; +import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -176,7 +177,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sendas', callback: sendMessageAs, - returns: 'The text of the sent message', + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', @@ -195,6 +196,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -223,7 +232,7 @@ export function initDefaultSlashCommands() { name: 'sys', callback: sendNarratorMessage, aliases: ['nar'], - returns: 'The text of the sent message', + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -239,6 +248,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -278,7 +295,7 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'comment', callback: sendCommentMessage, - returns: 'The text of the sent message', + returns: 'Optionally the text of the sent message, if specified in the "return" argument', namedArgumentList: [ new SlashCommandNamedArgument( 'compact', @@ -294,6 +311,14 @@ export function initDefaultSlashCommands() { typeList: [ARGUMENT_TYPE.NUMBER], enumProvider: commonEnumProviders.messages({ allowIdAfter: true }), }), + SlashCommandNamedArgument.fromProps({ + name: 'return', + description: 'The way how you want the return value to be provided', + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: 'none', + enumList: slashCommandReturnHelper.enumList({ allowObject: true }), + forceEnum: true, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -3210,7 +3235,7 @@ export async function sendMessageAs(args, text) { await saveChatConditional(); } - return message.mes; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } export async function sendNarratorMessage(args, text) { @@ -3264,7 +3289,7 @@ export async function sendNarratorMessage(args, text) { await saveChatConditional(); } - return message.mes; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } export async function promptQuietForLoudResponse(who, text) { @@ -3353,7 +3378,7 @@ async function sendCommentMessage(args, text) { await saveChatConditional(); } - return message.mes; + return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes }); } /** diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 5612f47b5..bd5a7e8a3 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -36,6 +36,7 @@ export const enumIcons = { message: 'đŸ’Ŧ', voice: '🎤', server: 'đŸ–Ĩī¸', + popup: '🗔', true: '✔ī¸', false: '❌', diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js new file mode 100644 index 000000000..90ec0d783 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js @@ -0,0 +1,74 @@ +import { sendSystemMessage, system_message_types } from '../../script.js'; +import { callGenericPopup, POPUP_TYPE } from '../popup.js'; +import { escapeHtml } from '../utils.js'; +import { enumIcons } from './SlashCommandCommonEnumsProvider.js'; +import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js'; + +/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */ + +export const slashCommandReturnHelper = { + /** + * Gets/creates the enum list of types of return relevant for a slash command + * + * @param {object} [options={}] Options + * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value + * @param {boolean} [options.allowObject=false] Allow option to return the value as an object + * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message + * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup + * @returns {SlashCommandEnumValue[]} The enum list + */ + enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false } = {}) => [ + allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'), + allowObject && new SlashCommandEnumValue('object', 'Return as an object to the pipe for the next command', enumTypes.variable, enumIcons.dictionary), + allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message), + allowChat && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message), + new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup), + new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup), + new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹī¸'), + new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹī¸'), + new SlashCommandEnumValue('console', 'Log the return value to the console', enumTypes.enum, '>'), + new SlashCommandEnumValue('none', 'No return value'), + ].filter(x => !!x), + + /** + * Handles the return value based on the specified type + * + * @param {SlashCommandReturnType} type The type of return + * @param {object|number|string} value The value to return + * @param {object} [options={}] Options + * @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type + * @returns {Promise<*>} The processed return value + */ + async doReturn(type, value, { objectToStringFunc = o => o?.toString() } = {}) { + const stringValue = typeof value !== 'string' ? objectToStringFunc(value) : value; + + switch (type) { + case 'popup-html': + case 'popup-text': + case 'chat-text': + case 'chat-html': + case 'toast-text': + case 'toast-html': { + const shouldHtml = type.endsWith('html'); + const makeHtml = (str) => (new showdown.Converter()).makeHtml(str); + + if (type.startsWith('popup')) await callGenericPopup(shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue), POPUP_TYPE.TEXT); + if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue)); + if (type.startsWith('toast')) toastr.info(stringValue, null, { escapeHtml: !shouldHtml }); // Toastr handles HTML conversion internally already + + return ''; + } + case 'pipe': + return stringValue ?? ''; + case 'object': + return JSON.stringify(value); + case 'console': + console.info(value); + return ''; + case 'none': + return ''; + default: + throw new Error(`Unknown return type: ${type}`); + } + }, +};