2024-10-17 10:12:28 +02:00
|
|
|
|
import { Handlebars, moment, seedrandom, droll } from '../lib.js';
|
2024-04-21 13:06:33 +02:00
|
|
|
|
import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';
|
2024-06-25 21:44:00 +02:00
|
|
|
|
import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js';
|
2024-01-12 10:47:00 +01:00
|
|
|
|
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
2024-11-01 20:47:25 +01:00
|
|
|
|
import { getInstructMacros } from './instruct-mode.js';
|
|
|
|
|
import { getVariableMacros } from './variables.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @typedef Macro
|
|
|
|
|
* @property {RegExp} regex - Regular expression to match the macro
|
|
|
|
|
* @property {(substring: string, ...args: any[]) => string} replace - Function to replace the macro
|
|
|
|
|
*/
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
2024-03-30 11:26:21 +01:00
|
|
|
|
// Register any macro that you want to leave in the compiled story string
|
|
|
|
|
Handlebars.registerHelper('trim', () => '{{trim}}');
|
2024-04-21 13:06:33 +02:00
|
|
|
|
// Catch-all helper for any macro that is not defined for story strings
|
|
|
|
|
Handlebars.registerHelper('helperMissing', function () {
|
|
|
|
|
const options = arguments[arguments.length - 1];
|
|
|
|
|
const macroName = options.name;
|
|
|
|
|
return substituteParams(`{{${macroName}}}`);
|
|
|
|
|
});
|
2024-03-30 11:26:21 +01:00
|
|
|
|
|
2024-06-26 20:58:57 +02:00
|
|
|
|
/**
|
|
|
|
|
* @typedef {Object<string, *>} EnvObject
|
|
|
|
|
* @typedef {(nonce: string) => string} MacroFunction
|
|
|
|
|
*/
|
|
|
|
|
|
2024-06-25 20:53:10 +02:00
|
|
|
|
export class MacrosParser {
|
|
|
|
|
/**
|
|
|
|
|
* A map of registered macros.
|
2024-06-26 20:58:57 +02:00
|
|
|
|
* @type {Map<string, string|MacroFunction>}
|
2024-06-25 20:53:10 +02:00
|
|
|
|
*/
|
|
|
|
|
static #macros = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registers a global macro that can be used anywhere where substitution is allowed.
|
|
|
|
|
* @param {string} key Macro name (key)
|
2024-06-26 20:58:57 +02:00
|
|
|
|
* @param {string|MacroFunction} value A string or a function that returns a string
|
2024-06-25 20:53:10 +02:00
|
|
|
|
*/
|
|
|
|
|
static registerMacro(key, value) {
|
|
|
|
|
if (typeof key !== 'string') {
|
|
|
|
|
throw new Error('Macro key must be a string');
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 21:02:05 +02:00
|
|
|
|
// Allowing surrounding whitespace would just create more confusion...
|
|
|
|
|
key = key.trim();
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
throw new Error('Macro key must not be empty or whitespace only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key.startsWith('{{') || key.endsWith('}}')) {
|
|
|
|
|
throw new Error('Macro key must not include the surrounding braces');
|
2024-06-25 20:53:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof value !== 'string' && typeof value !== 'function') {
|
2024-06-25 21:15:40 +02:00
|
|
|
|
console.warn(`Macro value for "${key}" will be converted to a string`);
|
|
|
|
|
value = this.sanitizeMacroValue(value);
|
2024-06-25 20:53:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 21:02:05 +02:00
|
|
|
|
if (this.#macros.has(key)) {
|
|
|
|
|
console.warn(`Macro ${key} is already registered`);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 20:53:10 +02:00
|
|
|
|
this.#macros.set(key, value);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 03:01:33 +02:00
|
|
|
|
/**
|
|
|
|
|
* Unregisters a global macro with the given key
|
|
|
|
|
*
|
|
|
|
|
* @param {string} key Macro name (key)
|
|
|
|
|
*/
|
|
|
|
|
static unregisterMacro(key) {
|
|
|
|
|
if (typeof key !== 'string') {
|
|
|
|
|
throw new Error('Macro key must be a string');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allowing surrounding whitespace would just create more confusion...
|
|
|
|
|
key = key.trim();
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
throw new Error('Macro key must not be empty or whitespace only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleted = this.#macros.delete(key);
|
|
|
|
|
|
|
|
|
|
if (!deleted) {
|
|
|
|
|
console.warn(`Macro ${key} was not registered`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 20:53:10 +02:00
|
|
|
|
/**
|
|
|
|
|
* Populate the env object with macro values from the current context.
|
2024-06-26 20:58:57 +02:00
|
|
|
|
* @param {EnvObject} env Env object for the current evaluation context
|
2024-06-25 20:53:10 +02:00
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
static populateEnv(env) {
|
|
|
|
|
if (!env || typeof env !== 'object') {
|
|
|
|
|
console.warn('Env object is not provided');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No macros are registered
|
|
|
|
|
if (this.#macros.size === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of this.#macros) {
|
|
|
|
|
env[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-25 21:15:40 +02:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Performs a type-check on the macro value and returns a sanitized version of it.
|
|
|
|
|
* @param {any} value Value returned by a macro
|
|
|
|
|
* @returns {string} Sanitized value
|
|
|
|
|
*/
|
|
|
|
|
static sanitizeMacroValue(value) {
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-26 20:58:57 +02:00
|
|
|
|
if (value instanceof Promise) {
|
|
|
|
|
console.warn('Promises are not supported as macro values');
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'function') {
|
|
|
|
|
console.warn('Functions are not supported as macro values');
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value instanceof Date) {
|
|
|
|
|
return value.toISOString();
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 21:15:40 +02:00
|
|
|
|
if (typeof value === 'object') {
|
|
|
|
|
return JSON.stringify(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return String(value);
|
|
|
|
|
}
|
2024-06-25 20:53:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-08 14:10:15 +02:00
|
|
|
|
/**
|
|
|
|
|
* Gets a hashed id of the current chat from the metadata.
|
|
|
|
|
* If no metadata exists, creates a new hash and saves it.
|
|
|
|
|
* @returns {number} The hashed chat id
|
|
|
|
|
*/
|
|
|
|
|
function getChatIdHash() {
|
|
|
|
|
const cachedIdHash = chat_metadata['chat_id_hash'];
|
|
|
|
|
|
|
|
|
|
// If chat_id_hash is not already set, calculate it
|
|
|
|
|
if (!cachedIdHash) {
|
|
|
|
|
// Use the main_chat if it's available, otherwise get the current chat ID
|
|
|
|
|
const chatId = chat_metadata['main_chat'] ?? getCurrentChatId();
|
|
|
|
|
const chatIdHash = getStringHash(chatId);
|
|
|
|
|
chat_metadata['chat_id_hash'] = chatIdHash;
|
|
|
|
|
return chatIdHash;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cachedIdHash;
|
|
|
|
|
}
|
2024-04-07 00:06:38 +02:00
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the ID of the last message in the chat
|
|
|
|
|
*
|
|
|
|
|
* Optionally can only choose specific messages, if a filter is provided.
|
|
|
|
|
*
|
|
|
|
|
* @param {object} param0 - Optional arguments
|
|
|
|
|
* @param {boolean} [param0.exclude_swipe_in_propress=true] - Whether a message that is currently being swiped should be ignored
|
|
|
|
|
* @param {function(object):boolean} [param0.filter] - A filter applied to the search, ignoring all messages that don't match the criteria. For example to only find user messages, etc.
|
|
|
|
|
* @returns {number|null} The message id, or null if none was found
|
2024-01-09 19:23:51 +01:00
|
|
|
|
*/
|
2024-04-07 00:06:38 +02:00
|
|
|
|
export function getLastMessageId({ exclude_swipe_in_propress = true, filter = null } = {}) {
|
|
|
|
|
for (let i = chat?.length - 1; i >= 0; i--) {
|
|
|
|
|
let message = chat[i];
|
|
|
|
|
|
|
|
|
|
// If ignoring swipes and the message is being swiped, continue
|
|
|
|
|
// We can check if a message is being swiped by checking whether the current swipe id is not in the list of finished swipes yet
|
|
|
|
|
if (exclude_swipe_in_propress && message.swipes && message.swipe_id >= message.swipes.length) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
2024-04-07 00:06:38 +02:00
|
|
|
|
// Check if no filter is provided, or if the message passes the filter
|
|
|
|
|
if (!filter || filter(message)) {
|
|
|
|
|
return i;
|
|
|
|
|
}
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-07 00:06:38 +02:00
|
|
|
|
return null;
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the ID of the first message included in the context
|
|
|
|
|
*
|
|
|
|
|
* @returns {number|null} The ID of the first message in the context
|
2024-01-09 19:23:51 +01:00
|
|
|
|
*/
|
|
|
|
|
function getFirstIncludedMessageId() {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
const index = Number(document.querySelector('.lastInContext')?.getAttribute('mesid'));
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
|
|
|
|
if (!isNaN(index) && index >= 0) {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
return index;
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-07 00:06:38 +02:00
|
|
|
|
return null;
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the last message in the chat
|
|
|
|
|
*
|
|
|
|
|
* @returns {string} The last message in the chat
|
2024-01-09 19:23:51 +01:00
|
|
|
|
*/
|
|
|
|
|
function getLastMessage() {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
const mid = getLastMessageId();
|
|
|
|
|
return chat[mid]?.mes ?? '';
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-17 01:45:22 +01:00
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the last message from the user
|
|
|
|
|
*
|
|
|
|
|
* @returns {string} The last message from the user
|
2024-03-17 01:45:22 +01:00
|
|
|
|
*/
|
|
|
|
|
function getLastUserMessage() {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
const mid = getLastMessageId({ filter: m => m.is_user && !m.is_system });
|
|
|
|
|
return chat[mid]?.mes ?? '';
|
2024-03-17 01:45:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the last message from the bot
|
|
|
|
|
*
|
|
|
|
|
* @returns {string} The last message from the bot
|
2024-03-17 01:45:22 +01:00
|
|
|
|
*/
|
|
|
|
|
function getLastCharMessage() {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
const mid = getLastMessageId({ filter: m => !m.is_user && !m.is_system });
|
|
|
|
|
return chat[mid]?.mes ?? '';
|
2024-03-17 01:45:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the 1-based ID (number) of the last swipe
|
|
|
|
|
*
|
|
|
|
|
* @returns {number|null} The 1-based ID of the last swipe
|
2024-01-09 19:23:51 +01:00
|
|
|
|
*/
|
|
|
|
|
function getLastSwipeId() {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
// For swipe macro, we are accepting using the message that is currently being swiped
|
|
|
|
|
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
|
|
|
|
|
const swipes = chat[mid]?.swipes;
|
|
|
|
|
return swipes?.length;
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2024-04-07 00:06:38 +02:00
|
|
|
|
* Returns the 1-based ID (number) of the current swipe
|
|
|
|
|
*
|
|
|
|
|
* @returns {number|null} The 1-based ID of the current swipe
|
2024-01-09 19:23:51 +01:00
|
|
|
|
*/
|
|
|
|
|
function getCurrentSwipeId() {
|
2024-04-07 00:06:38 +02:00
|
|
|
|
// For swipe macro, we are accepting using the message that is currently being swiped
|
|
|
|
|
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
|
|
|
|
|
const swipeId = chat[mid]?.swipe_id;
|
2024-07-02 01:28:52 +02:00
|
|
|
|
return swipeId !== null ? swipeId + 1 : null;
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replaces banned words in macros with an empty string.
|
|
|
|
|
* Adds them to textgenerationwebui ban list.
|
2024-11-01 20:47:25 +01:00
|
|
|
|
* @returns {Macro}
|
2024-01-09 19:23:51 +01:00
|
|
|
|
*/
|
2024-11-01 20:47:25 +01:00
|
|
|
|
function getBannedWordsMacro() {
|
2024-01-09 19:23:51 +01:00
|
|
|
|
const banPattern = /{{banned "(.*)"}}/gi;
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const banReplace = (match, bannedWord) => {
|
|
|
|
|
if (main_api == 'textgenerationwebui') {
|
|
|
|
|
console.log('Found banned word in macros: ' + bannedWord);
|
|
|
|
|
textgenerationwebui_banned_in_macros.push(bannedWord);
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return '';
|
|
|
|
|
};
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return { regex: banPattern, replace: banReplace };
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTimeSinceLastMessage() {
|
|
|
|
|
const now = moment();
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(chat) && chat.length > 0) {
|
|
|
|
|
let lastMessage;
|
|
|
|
|
let takeNext = false;
|
|
|
|
|
|
|
|
|
|
for (let i = chat.length - 1; i >= 0; i--) {
|
|
|
|
|
const message = chat[i];
|
|
|
|
|
|
|
|
|
|
if (message.is_system) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.is_user && takeNext) {
|
|
|
|
|
lastMessage = message;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
takeNext = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lastMessage?.send_date) {
|
|
|
|
|
const lastMessageDate = timestampToMoment(lastMessage.send_date);
|
|
|
|
|
const duration = moment.duration(now.diff(lastMessageDate));
|
|
|
|
|
return duration.humanize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'just now';
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
/**
|
|
|
|
|
* Returns a macro that picks a random item from a list.
|
|
|
|
|
* @returns {Macro} The random replace macro
|
|
|
|
|
*/
|
|
|
|
|
function getRandomReplaceMacro() {
|
2024-04-02 01:02:02 +02:00
|
|
|
|
const randomPattern = /{{random\s?::?([^}]+)}}/gi;
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const randomReplace = (match, listString) => {
|
2024-04-02 01:02:02 +02:00
|
|
|
|
// Split on either double colons or comma. If comma is the separator, we are also trimming all items.
|
2024-04-02 00:16:25 +02:00
|
|
|
|
const list = listString.includes('::')
|
2024-04-02 01:02:02 +02:00
|
|
|
|
? listString.split('::')
|
2024-06-02 14:02:54 +02:00
|
|
|
|
// Replaced escaped commas with a placeholder to avoid splitting on them
|
|
|
|
|
: listString.replace(/\\,/g, '##<23>COMMA<4D>##').split(',').map(item => item.trim().replace(/##<23>COMMA<4D>##/g, ','));
|
2024-04-02 00:16:25 +02:00
|
|
|
|
|
2024-03-22 23:57:33 +01:00
|
|
|
|
if (list.length === 0) {
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return '';
|
2024-03-22 23:57:33 +01:00
|
|
|
|
}
|
2024-10-17 01:19:47 +02:00
|
|
|
|
const rng = seedrandom('added entropy.', { entropy: true });
|
2024-03-22 23:57:33 +01:00
|
|
|
|
const randomIndex = Math.floor(rng() * list.length);
|
2024-04-02 01:02:02 +02:00
|
|
|
|
return list[randomIndex];
|
2024-11-01 20:47:25 +01:00
|
|
|
|
};
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return { regex: randomPattern, replace: randomReplace };
|
|
|
|
|
}
|
2024-04-07 04:40:15 +02:00
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
/**
|
|
|
|
|
* Returns a macro that picks a random item from a list with a consistent seed.
|
|
|
|
|
* @param {string} rawContent The raw content of the string
|
|
|
|
|
* @returns {Macro} The pick replace macro
|
|
|
|
|
*/
|
|
|
|
|
function getPickReplaceMacro(rawContent) {
|
2024-04-07 04:40:15 +02:00
|
|
|
|
// We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches
|
2024-04-07 20:37:05 +02:00
|
|
|
|
// No need to save metadata here - branching and renaming will implicitly do the save for us, and until then loading it like this is consistent
|
2024-04-08 14:10:15 +02:00
|
|
|
|
const chatIdHash = getChatIdHash();
|
2024-04-01 01:47:56 +02:00
|
|
|
|
const rawContentHash = getStringHash(rawContent);
|
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const pickPattern = /{{pick\s?::?([^}]+)}}/gi;
|
|
|
|
|
const pickReplace = (match, listString, offset) => {
|
2024-04-02 01:02:02 +02:00
|
|
|
|
// Split on either double colons or comma. If comma is the separator, we are also trimming all items.
|
2024-04-01 01:47:56 +02:00
|
|
|
|
const list = listString.includes('::')
|
2024-04-02 01:02:02 +02:00
|
|
|
|
? listString.split('::')
|
2024-06-02 14:02:54 +02:00
|
|
|
|
// Replaced escaped commas with a placeholder to avoid splitting on them
|
|
|
|
|
: listString.replace(/\\,/g, '##<23>COMMA<4D>##').split(',').map(item => item.trim().replace(/##<23>COMMA<4D>##/g, ','));
|
2024-04-01 01:47:56 +02:00
|
|
|
|
|
|
|
|
|
if (list.length === 0) {
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return '';
|
2024-04-01 01:47:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-02 01:02:02 +02:00
|
|
|
|
// We build a hash seed based on: unique chat file, raw content, and the placement inside this content
|
|
|
|
|
// This allows us to get unique but repeatable picks in nearly all cases
|
2024-04-01 01:47:56 +02:00
|
|
|
|
const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`;
|
2024-10-18 10:44:54 +02:00
|
|
|
|
const finalSeed = getStringHash(combinedSeedString);
|
|
|
|
|
// @ts-ignore - have to use numbers for legacy picks
|
2024-10-17 01:19:47 +02:00
|
|
|
|
const rng = seedrandom(finalSeed);
|
2024-04-01 01:47:56 +02:00
|
|
|
|
const randomIndex = Math.floor(rng() * list.length);
|
2024-04-02 01:02:02 +02:00
|
|
|
|
return list[randomIndex];
|
2024-11-01 20:47:25 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return { regex: pickPattern, replace: pickReplace };
|
2024-04-01 01:47:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
/**
|
|
|
|
|
* @returns {Macro} The dire roll macro
|
|
|
|
|
*/
|
|
|
|
|
function getDiceRollMacro() {
|
2024-01-09 19:23:51 +01:00
|
|
|
|
const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const rollReplace = (match, matchValue) => {
|
2024-01-09 19:23:51 +01:00
|
|
|
|
let formula = matchValue.trim();
|
|
|
|
|
|
|
|
|
|
if (isDigitsOnly(formula)) {
|
|
|
|
|
formula = `1d${formula}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isValid = droll.validate(formula);
|
|
|
|
|
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
console.debug(`Invalid roll formula: ${formula}`);
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return '';
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = droll.roll(formula);
|
2024-11-01 20:47:25 +01:00
|
|
|
|
if (result === false) return '';
|
|
|
|
|
return String(result.total);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return { regex: rollPattern, replace: rollReplace };
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-11 22:38:26 +02:00
|
|
|
|
/**
|
|
|
|
|
* Returns the difference between two times. Works with any time format acceptable by moment().
|
2024-05-11 12:52:31 +02:00
|
|
|
|
* Can work with {{date}} {{time}} macros
|
2024-11-01 20:47:25 +01:00
|
|
|
|
* @returns {Macro} The time difference macro
|
2024-05-11 12:52:31 +02:00
|
|
|
|
*/
|
2024-11-01 20:47:25 +01:00
|
|
|
|
function getTimeDiffMacro() {
|
2024-05-11 12:52:31 +02:00
|
|
|
|
const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi;
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const timeDiffReplace = (_match, matchPart1, matchPart2) => {
|
2024-05-11 22:38:26 +02:00
|
|
|
|
const time1 = moment(matchPart1);
|
|
|
|
|
const time2 = moment(matchPart2);
|
2024-05-11 12:52:31 +02:00
|
|
|
|
|
|
|
|
|
const timeDifference = moment.duration(time1.diff(time2));
|
2024-07-01 23:59:01 +02:00
|
|
|
|
return timeDifference.humanize(true);
|
2024-11-01 20:47:25 +01:00
|
|
|
|
};
|
2024-05-11 22:38:26 +02:00
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
return { regex: timeDiffPattern, replace: timeDiffReplace };
|
2024-05-11 12:52:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
|
/**
|
|
|
|
|
* Substitutes {{macro}} parameters in a string.
|
|
|
|
|
* @param {string} content - The string to substitute parameters in.
|
2024-06-26 20:58:57 +02:00
|
|
|
|
* @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param
|
2024-01-12 11:19:37 +01:00
|
|
|
|
* values are functions, those functions will be called and their return values are used.
|
2024-11-01 23:44:12 +01:00
|
|
|
|
* @param {function(string): string} postProcessFn - Function to run on the macro value before replacing it.
|
2024-01-09 19:23:51 +01:00
|
|
|
|
* @returns {string} The string with substituted parameters.
|
|
|
|
|
*/
|
2024-11-01 23:44:12 +01:00
|
|
|
|
export function evaluateMacros(content, env, postProcessFn) {
|
2024-01-09 19:23:51 +01:00
|
|
|
|
if (!content) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 23:44:12 +01:00
|
|
|
|
postProcessFn = typeof postProcessFn === 'function' ? postProcessFn : (x => x);
|
2024-04-01 01:47:56 +02:00
|
|
|
|
const rawContent = content;
|
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
/**
|
|
|
|
|
* Built-ins running before the env variables
|
|
|
|
|
* @type {Macro[]}
|
|
|
|
|
* */
|
|
|
|
|
const preEnvMacros = [
|
|
|
|
|
// Legacy non-curly macros
|
|
|
|
|
{ regex: /<USER>/gi, replace: () => typeof env.user === 'function' ? env.user() : env.user },
|
|
|
|
|
{ regex: /<BOT>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char },
|
|
|
|
|
{ regex: /<CHAR>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char },
|
|
|
|
|
{ regex: /<CHARIFNOTGROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group },
|
|
|
|
|
{ regex: /<GROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group },
|
|
|
|
|
getDiceRollMacro(),
|
|
|
|
|
...getInstructMacros(env),
|
|
|
|
|
...getVariableMacros(),
|
|
|
|
|
{ regex: /{{newline}}/gi, replace: () => '\n' },
|
|
|
|
|
{ regex: /(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, replace: () => '' },
|
|
|
|
|
{ regex: /{{noop}}/gi, replace: () => '' },
|
|
|
|
|
{ regex: /{{input}}/gi, replace: () => String($('#send_textarea').val()) },
|
|
|
|
|
];
|
2024-03-16 00:38:23 +01:00
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
/**
|
|
|
|
|
* Built-ins running after the env variables
|
|
|
|
|
* @type {Macro[]}
|
|
|
|
|
*/
|
|
|
|
|
const postEnvMacros = [
|
|
|
|
|
{ regex: /{{maxPrompt}}/gi, replace: () => String(getMaxContextSize()) },
|
|
|
|
|
{ regex: /{{lastMessage}}/gi, replace: () => getLastMessage() },
|
|
|
|
|
{ regex: /{{lastMessageId}}/gi, replace: () => String(getLastMessageId() ?? '') },
|
|
|
|
|
{ regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() },
|
|
|
|
|
{ regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() },
|
|
|
|
|
{ regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') },
|
|
|
|
|
{ regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') },
|
|
|
|
|
{ regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') },
|
|
|
|
|
{ regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') },
|
|
|
|
|
{ regex: /\{\{\/\/([\s\S]*?)\}\}/gm, replace: () => '' },
|
|
|
|
|
{ regex: /{{time}}/gi, replace: () => moment().format('LT') },
|
|
|
|
|
{ regex: /{{date}}/gi, replace: () => moment().format('LL') },
|
|
|
|
|
{ regex: /{{weekday}}/gi, replace: () => moment().format('dddd') },
|
|
|
|
|
{ regex: /{{isotime}}/gi, replace: () => moment().format('HH:mm') },
|
|
|
|
|
{ regex: /{{isodate}}/gi, replace: () => moment().format('YYYY-MM-DD') },
|
|
|
|
|
{ regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) },
|
|
|
|
|
{ regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() },
|
|
|
|
|
{ regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') },
|
|
|
|
|
getTimeDiffMacro(),
|
|
|
|
|
getBannedWordsMacro(),
|
|
|
|
|
getRandomReplaceMacro(),
|
|
|
|
|
getPickReplaceMacro(rawContent),
|
|
|
|
|
];
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
2024-06-25 20:53:10 +02:00
|
|
|
|
// Add all registered macros to the env object
|
|
|
|
|
MacrosParser.populateEnv(env);
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const nonce = uuidv4();
|
|
|
|
|
const envMacros = [];
|
2024-06-25 20:53:10 +02:00
|
|
|
|
|
2024-01-12 11:19:37 +01:00
|
|
|
|
// Substitute passed-in variables
|
|
|
|
|
for (const varName in env) {
|
|
|
|
|
if (!Object.hasOwn(env, varName)) continue;
|
|
|
|
|
|
2024-11-01 20:47:25 +01:00
|
|
|
|
const envRegex = new RegExp(`{{${escapeRegex(varName)}}}`, 'gi');
|
|
|
|
|
const envReplace = () => {
|
2024-06-25 21:44:00 +02:00
|
|
|
|
const param = env[varName];
|
|
|
|
|
const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);
|
|
|
|
|
return value;
|
2024-11-01 20:47:25 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
envMacros.push({ regex: envRegex, replace: envReplace });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const macros = [...preEnvMacros, ...envMacros, ...postEnvMacros];
|
|
|
|
|
|
|
|
|
|
for (const macro of macros) {
|
|
|
|
|
// Stop if the content is empty
|
|
|
|
|
if (!content) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Short-circuit if no curly braces are found
|
|
|
|
|
if (!macro.regex.source.startsWith('<') && !content.includes('{{')) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 23:44:12 +01:00
|
|
|
|
content = content.replace(macro.regex, (...args) => postProcessFn(macro.replace(...args)));
|
2024-01-09 19:23:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return content;
|
|
|
|
|
}
|