diff --git a/backend/src/plugins/Tags/commands/TagEvalCmd.ts b/backend/src/plugins/Tags/commands/TagEvalCmd.ts index d16afbbd..0e585f4e 100644 --- a/backend/src/plugins/Tags/commands/TagEvalCmd.ts +++ b/backend/src/plugins/Tags/commands/TagEvalCmd.ts @@ -1,9 +1,9 @@ import { tagsCmd } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { renderTag } from "../util/renderTag"; import { MessageContent } from "eris"; import { TemplateParseError } from "../../../templateFormatter"; import { sendErrorMessage } from "../../../pluginUtils"; +import { renderTagBody } from "../util/renderTagBody"; export const TagEvalCmd = tagsCmd({ trigger: "tag eval", @@ -15,7 +15,7 @@ export const TagEvalCmd = tagsCmd({ async run({ message: msg, args, pluginData }) { try { - const rendered = await renderTag(pluginData, args.body); + const rendered = await renderTagBody(pluginData, args.body); msg.channel.createMessage(rendered); } catch (e) { if (e instanceof TemplateParseError) { diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index b57c7128..cbbb6f97 100644 --- a/backend/src/plugins/Tags/types.ts +++ b/backend/src/plugins/Tags/types.ts @@ -8,7 +8,7 @@ import { GuildLogs } from "src/data/GuildLogs"; export const Tag = t.union([t.string, tEmbed]); -const TagCategory = t.type({ +export const TagCategory = t.type({ prefix: tNullable(t.string), delete_with_command: tNullable(t.boolean), @@ -21,6 +21,7 @@ const TagCategory = t.type({ can_use: tNullable(t.boolean), }); +export type TTagCategory = t.TypeOf; export const ConfigSchema = t.type({ prefix: t.string, diff --git a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts new file mode 100644 index 00000000..993427e4 --- /dev/null +++ b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts @@ -0,0 +1,94 @@ +import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; +import { PluginData } from "knub"; +import { TagsPluginType, TTagCategory } from "../types"; +import { renderTagFromString } from "./renderTagFromString"; +import { convertDelayStringToMS } from "../../../utils"; +import escapeStringRegexp from "escape-string-regexp"; +import { Member, MessageContent } from "eris"; + +interface Result { + renderedContent: MessageContent; + tagName: string; + categoryName: string | null; + category: TTagCategory | null; +} + +export async function matchAndRenderTagFromString( + pluginData: PluginData, + str: string, + member: Member, + extraMatchParams: ExtendedMatchParams = {}, +): Promise { + const config = pluginData.config.getMatchingConfig({ + ...extraMatchParams, + member, + }); + + // Hard-coded tags in categories + for (const [name, category] of Object.entries(config.categories)) { + const canUse = category.can_use != null ? category.can_use : config.can_use; + if (canUse !== true) continue; + + const prefix = category.prefix != null ? category.prefix : config.prefix; + if (prefix !== "" && !str.startsWith(prefix)) continue; + + const withoutPrefix = str.slice(prefix.length); + + for (const [tagName, tagBody] of Object.entries(category.tags)) { + const regex = new RegExp(`^${escapeStringRegexp(tagName)}(?:\\s|$)`); + if (regex.test(withoutPrefix)) { + const renderedContent = await renderTagFromString( + pluginData, + str, + prefix, + tagName, + category.tags[tagName], + member, + ); + + return { + renderedContent, + tagName, + categoryName: name, + category, + }; + } + } + } + + // Dynamic tags + if (config.can_use !== true) { + return null; + } + + const dynamicTagPrefix = config.prefix; + if (!str.startsWith(dynamicTagPrefix)) { + return null; + } + + const dynamicTagNameMatch = str.slice(dynamicTagPrefix.length).match(/^\S+/); + if (dynamicTagNameMatch === null) { + return null; + } + + const dynamicTagName = dynamicTagNameMatch[0]; + const dynamicTag = await pluginData.state.tags.find(dynamicTagName); + if (!dynamicTag) { + return null; + } + + const renderedDynamicTagContent = await renderTagFromString( + pluginData, + str, + dynamicTagPrefix, + dynamicTagName, + dynamicTag.body, + member, + ); + return { + renderedContent: renderedDynamicTagContent, + tagName: dynamicTagName, + categoryName: null, + category: null, + }; +} diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts index 62af8030..c665a2fd 100644 --- a/backend/src/plugins/Tags/util/onMessageCreate.ts +++ b/backend/src/plugins/Tags/util/onMessageCreate.ts @@ -1,12 +1,11 @@ import { TagsPluginType } from "../types"; import { SavedMessage } from "src/data/entities/SavedMessage"; import { PluginData } from "knub"; -import { resolveMember, convertDelayStringToMS, tStrictMessageContent } from "src/utils"; -import escapeStringRegexp from "escape-string-regexp"; +import { convertDelayStringToMS, resolveMember, tStrictMessageContent } from "src/utils"; import { validate } from "src/validatorUtils"; import { LogType } from "src/data/LogType"; import { TextChannel } from "eris"; -import { renderSafeTagFromMessage } from "./renderSafeTagFromMessage"; +import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString"; export async function onMessageCreate(pluginData: PluginData, msg: SavedMessage) { if (msg.is_bot) return; @@ -21,104 +20,58 @@ export async function onMessageCreate(pluginData: PluginData, ms channelId: msg.channel_id, categoryId: channel.parentID, }); - let deleteWithCommand = false; - // Find potential matching tag, looping through categories first and checking dynamic tags last - let renderedTag = null; - let matchedTagName; + const tagResult = await matchAndRenderTagFromString(pluginData, msg.data.content, member, { + channelId: msg.channel_id, + categoryId: channel.parentID, + }); + + if (!tagResult) { + return; + } + + // Check for cooldowns const cooldowns = []; - for (const [name, category] of Object.entries(config.categories)) { - const canUse = category.can_use != null ? category.can_use : config.can_use; - if (canUse !== true) continue; - - const prefix = category.prefix != null ? category.prefix : config.prefix; - if (prefix !== "" && !msg.data.content.startsWith(prefix)) continue; - - const withoutPrefix = msg.data.content.slice(prefix.length); - - for (const [tagName, tagBody] of Object.entries(category.tags)) { - const regex = new RegExp(`^${escapeStringRegexp(tagName)}(?:\\s|$)`); - if (regex.test(withoutPrefix)) { - renderedTag = await renderSafeTagFromMessage( - pluginData, - msg.data.content, - prefix, - tagName, - category.tags[tagName], - member, - ); - if (renderedTag) { - matchedTagName = tagName; - break; - } - } + if (tagResult.category) { + // Category-specific cooldowns + if (tagResult.category.user_tag_cooldown) { + const delay = convertDelayStringToMS(String(tagResult.category.user_tag_cooldown), "s"); + cooldowns.push([`tags-category-${tagResult.categoryName}-user-${msg.user_id}-tag-${tagResult.tagName}`, delay]); + } + if (tagResult.category.global_tag_cooldown) { + const delay = convertDelayStringToMS(String(tagResult.category.global_tag_cooldown), "s"); + cooldowns.push([`tags-category-${tagResult.categoryName}-tag-${tagResult.tagName}`, delay]); + } + if (tagResult.category.user_category_cooldown) { + const delay = convertDelayStringToMS(String(tagResult.category.user_category_cooldown), "s"); + cooldowns.push([`tags-category-${tagResult.categoryName}-user--${msg.user_id}`, delay]); + } + if (tagResult.category.global_category_cooldown) { + const delay = convertDelayStringToMS(String(tagResult.category.global_category_cooldown), "s"); + cooldowns.push([`tags-category-${tagResult.categoryName}`, delay]); + } + } else { + // Dynamic tag cooldowns + if (config.user_tag_cooldown) { + const delay = convertDelayStringToMS(String(config.user_tag_cooldown), "s"); + cooldowns.push([`tags-user-${msg.user_id}-tag-${tagResult.tagName}`, delay]); } - if (renderedTag) { - if (category.user_tag_cooldown) { - const delay = convertDelayStringToMS(String(category.user_tag_cooldown), "s"); - cooldowns.push([`tags-category-${name}-user-${msg.user_id}-tag-${matchedTagName}`, delay]); - } - if (category.global_tag_cooldown) { - const delay = convertDelayStringToMS(String(category.global_tag_cooldown), "s"); - cooldowns.push([`tags-category-${name}-tag-${matchedTagName}`, delay]); - } - if (category.user_category_cooldown) { - const delay = convertDelayStringToMS(String(category.user_category_cooldown), "s"); - cooldowns.push([`tags-category-${name}-user--${msg.user_id}`, delay]); - } - if (category.global_category_cooldown) { - const delay = convertDelayStringToMS(String(category.global_category_cooldown), "s"); - cooldowns.push([`tags-category-${name}`, delay]); - } - - deleteWithCommand = - category.delete_with_command != null ? category.delete_with_command : config.delete_with_command; - - break; + if (config.global_tag_cooldown) { + const delay = convertDelayStringToMS(String(config.global_tag_cooldown), "s"); + cooldowns.push([`tags-tag-${tagResult.tagName}`, delay]); } - } - // Matching tag was not found from the config, try a dynamic tag - if (!renderedTag) { - if (config.can_use !== true) return; + if (config.user_cooldown) { + const delay = convertDelayStringToMS(String(config.user_cooldown), "s"); + cooldowns.push([`tags-user-${tagResult.tagName}`, delay]); + } - const prefix = config.prefix; - if (!msg.data.content.startsWith(prefix)) return; - - const tagNameMatch = msg.data.content.slice(prefix.length).match(/^\S+/); - if (tagNameMatch === null) return; - - const tagName = tagNameMatch[0]; - const tag = await pluginData.state.tags.find(tagName); - if (!tag) return; - - matchedTagName = tagName; - - renderedTag = await renderSafeTagFromMessage(pluginData, msg.data.content, prefix, tagName, tag.body, member); - } - - if (!renderedTag) return; - - if (config.user_tag_cooldown) { - const delay = convertDelayStringToMS(String(config.user_tag_cooldown), "s"); - cooldowns.push([`tags-user-${msg.user_id}-tag-${matchedTagName}`, delay]); - } - - if (config.global_tag_cooldown) { - const delay = convertDelayStringToMS(String(config.global_tag_cooldown), "s"); - cooldowns.push([`tags-tag-${matchedTagName}`, delay]); - } - - if (config.user_cooldown) { - const delay = convertDelayStringToMS(String(config.user_cooldown), "s"); - cooldowns.push([`tags-user-${matchedTagName}`, delay]); - } - - if (config.global_cooldown) { - const delay = convertDelayStringToMS(String(config.global_cooldown), "s"); - cooldowns.push([`tags`, delay]); + if (config.global_cooldown) { + const delay = convertDelayStringToMS(String(config.global_cooldown), "s"); + cooldowns.push([`tags`, delay]); + } } const isOnCooldown = cooldowns.some(cd => pluginData.cooldowns.isOnCooldown(cd[0])); @@ -128,26 +81,25 @@ export async function onMessageCreate(pluginData: PluginData, ms pluginData.cooldowns.setCooldown(cd[0], cd[1]); } - deleteWithCommand = config.delete_with_command; - - const validationError = await validate(tStrictMessageContent, renderedTag); + const validationError = await validate(tStrictMessageContent, tagResult.renderedContent); if (validationError) { pluginData.state.logs.log(LogType.BOT_ALERT, { - body: `Rendering tag ${matchedTagName} resulted in an invalid message: ${validationError.message}`, + body: `Rendering tag ${tagResult.tagName} resulted in an invalid message: ${validationError.message}`, }); return; } - if (typeof renderedTag === "string" && renderedTag.trim() === "") { + if (typeof tagResult.renderedContent === "string" && tagResult.renderedContent.trim() === "") { pluginData.state.logs.log(LogType.BOT_ALERT, { - body: `Tag \`${matchedTagName}\` resulted in an empty message, so it couldn't be sent`, + body: `Tag \`${tagResult.tagName}\` resulted in an empty message, so it couldn't be sent`, }); return; } - const responseMsg = await channel.createMessage(renderedTag); + const responseMsg = await channel.createMessage(tagResult.renderedContent); // Save the command-response message pair once the message is in our database + const deleteWithCommand = tagResult.category?.delete_with_command ?? config.delete_with_command; if (deleteWithCommand) { pluginData.state.savedMessages.onceMessageAvailable(responseMsg.id, async () => { await pluginData.state.tags.addResponse(msg.id, responseMsg.id); diff --git a/backend/src/plugins/Tags/util/renderTag.ts b/backend/src/plugins/Tags/util/renderTagBody.ts similarity index 57% rename from backend/src/plugins/Tags/util/renderTag.ts rename to backend/src/plugins/Tags/util/renderTagBody.ts index 7778e6a4..cddc86c6 100644 --- a/backend/src/plugins/Tags/util/renderTag.ts +++ b/backend/src/plugins/Tags/util/renderTagBody.ts @@ -1,8 +1,15 @@ -import { renderTemplate } from "src/templateFormatter"; +import { renderTemplate } from "../../../templateFormatter"; import { PluginData, plugin } from "knub"; -import { TagsPluginType } from "../types"; +import { Tag, TagsPluginType } from "../types"; +import { renderRecursively, StrictMessageContent } from "../../../utils"; +import * as t from "io-ts"; -export async function renderTag(pluginData: PluginData, body, args = [], extraData = {}) { +export async function renderTagBody( + pluginData: PluginData, + body: t.TypeOf, + args = [], + extraData = {}, +): Promise { const dynamicVars = {}; const maxTagFnCalls = 25; let tagFnCalls = 0; @@ -22,6 +29,7 @@ export async function renderTag(pluginData: PluginData, body, ar if (tagFnCalls++ > maxTagFnCalls) return "\\_recursion\\_"; if (typeof name !== "string") return ""; if (name === "") return ""; + // TODO: Incorporate tag categories here const subTag = await pluginData.state.tags.find(name); if (!subTag) return ""; @@ -29,5 +37,11 @@ export async function renderTag(pluginData: PluginData, body, ar }, }; - return renderTemplate(body, data); + if (typeof body === "string") { + // Plain text tag + return { content: await renderTemplate(body, data) }; + } else { + // Embed + return renderRecursively(body, str => renderTemplate(str, data)); + } } diff --git a/backend/src/plugins/Tags/util/renderSafeTagFromMessage.ts b/backend/src/plugins/Tags/util/renderTagFromString.ts similarity index 68% rename from backend/src/plugins/Tags/util/renderSafeTagFromMessage.ts rename to backend/src/plugins/Tags/util/renderTagFromString.ts index 2a662f00..fd63c82d 100644 --- a/backend/src/plugins/Tags/util/renderSafeTagFromMessage.ts +++ b/backend/src/plugins/Tags/util/renderTagFromString.ts @@ -5,12 +5,12 @@ import { renderRecursively, StrictMessageContent, stripObjectToScalars } from "s import { parseArguments } from "knub-command-manager"; import { TemplateParseError } from "src/templateFormatter"; import { PluginData } from "knub"; -import { renderTag } from "./renderTag"; import { logger } from "src/logger"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogType } from "../../../data/LogType"; +import { renderTagBody } from "./renderTagBody"; -export async function renderSafeTagFromMessage( +export async function renderTagFromString( pluginData: PluginData, str: string, prefix: string, @@ -21,21 +21,9 @@ export async function renderSafeTagFromMessage( const variableStr = str.slice(prefix.length + tagName.length).trim(); const tagArgs = parseArguments(variableStr).map(v => v.value); - const renderTagString = async _str => { - let rendered = await renderTag(pluginData, _str, tagArgs, { - member: stripObjectToScalars(member, ["user"]), - user: stripObjectToScalars(member.user), - }); - rendered = rendered.trim(); - - return rendered; - }; - // Format the string try { - return typeof tagBody === "string" - ? { content: await renderTagString(tagBody) } - : await renderRecursively(tagBody, renderTagString); + return renderTagBody(pluginData, tagBody, tagArgs); } catch (e) { if (e instanceof TemplateParseError) { const logs = pluginData.getPlugin(LogsPlugin);