diff --git a/backend/src/plugins/CommandAliases/CommandAliasesPlugin.ts b/backend/src/plugins/CommandAliases/CommandAliasesPlugin.ts new file mode 100644 index 000000000..b86c7bb8b --- /dev/null +++ b/backend/src/plugins/CommandAliases/CommandAliasesPlugin.ts @@ -0,0 +1,21 @@ +import { guildPlugin } from "vety"; +import { DispatchAliasEvt } from "./events/DispatchAliasEvt.js"; +import { CommandAliasesPluginType, zCommandAliasesConfig } from "./types.js"; +import { normalizeAliases } from "./functions/normalizeAliases.js"; +import { buildAliasMatchers } from "./functions/buildAliasMatchers.js"; +import { getGuildPrefix } from "../../utils/getGuildPrefix.js"; + +export const CommandAliasesPlugin = guildPlugin()({ + name: "command_aliases", + configSchema: zCommandAliasesConfig, + + beforeLoad(pluginData) { + const prefix = getGuildPrefix(pluginData); + const config = pluginData.config.get(); + const normalizedAliases = normalizeAliases(config.aliases); + + pluginData.state.matchers = buildAliasMatchers(prefix, normalizedAliases); + }, + + events: [DispatchAliasEvt], +}); diff --git a/backend/src/plugins/CommandAliases/docs.ts b/backend/src/plugins/CommandAliases/docs.ts new file mode 100644 index 000000000..85a796cab --- /dev/null +++ b/backend/src/plugins/CommandAliases/docs.ts @@ -0,0 +1,31 @@ +import { ZeppelinPluginDocs } from "../../types.js"; +import { zCommandAliasesConfig } from "./types.js"; + +export const commandAliasesPluginDocs: ZeppelinPluginDocs = { + type: "stable", + prettyName: "Command Aliases", + configSchema: zCommandAliasesConfig, + description: "This plugin lets you create shortcuts for existing commands.", + usageGuide: ` +For example, you can make \`!b\` work the same as \`!ban\`, or \`!c\` work the same as \`!cases\`. + +### Example + +\`\`\`yaml +plugins: + command_aliases: + config: + aliases: + "b": "ban" + "c": "cases" + "b2": "ban -d 2" + "ownerinfo": "info 754421392988045383" +\`\`\` + +With this setup: +- \`!b @User\` runs \`!ban @User\` +- \`!c\` runs \`!cases\` +- \`!b2 @User\` runs \`!ban -d 2 @User\` +- \`!ownerinfo\` runs \`!info 754421392988045383\` + ` +}; diff --git a/backend/src/plugins/CommandAliases/events/DispatchAliasEvt.ts b/backend/src/plugins/CommandAliases/events/DispatchAliasEvt.ts new file mode 100644 index 000000000..3b6266e1d --- /dev/null +++ b/backend/src/plugins/CommandAliases/events/DispatchAliasEvt.ts @@ -0,0 +1,24 @@ +import { Message } from "discord.js"; +import { commandAliasesEvt } from "../types.js"; + +export const DispatchAliasEvt = commandAliasesEvt({ + event: "messageCreate", + async listener({ args: { message: msg }, pluginData }) { + if (!msg.guild || !msg.content) return; + if (msg.author.bot || msg.webhookId) return; + + const matchers = pluginData.state.matchers ?? []; + if (matchers.length === 0) return; + + const matchingAlias = matchers.find((matcher) => matcher.regex.test(msg.content)); + if (!matchingAlias) return; + + const newContent = msg.content.replace(matchingAlias.regex, matchingAlias.replacement); + if (newContent === msg.content) return; + + const copiedMessage = Object.create(msg); + copiedMessage.content = newContent; + + await pluginData.getKnubInstance().dispatchMessageCommands(copiedMessage as Message); + }, +}); diff --git a/backend/src/plugins/CommandAliases/functions/buildAliasMatchers.ts b/backend/src/plugins/CommandAliases/functions/buildAliasMatchers.ts new file mode 100644 index 000000000..ac6bdce8c --- /dev/null +++ b/backend/src/plugins/CommandAliases/functions/buildAliasMatchers.ts @@ -0,0 +1,17 @@ +import escapeStringRegexp from "escape-string-regexp"; +import { NormalizedAlias } from "./normalizeAliases.js"; + +export interface AliasMatcher { + regex: RegExp; + replacement: string; +} + +export function buildAliasMatchers(prefix: string, aliases: NormalizedAlias[]): AliasMatcher[] { + return aliases.map((alias) => { + const pattern = `^${escapeStringRegexp(prefix)}${escapeStringRegexp(alias.alias)}\\b`; + return { + regex: new RegExp(pattern, "i"), + replacement: `${prefix}${alias.target}`, + }; + }); +} diff --git a/backend/src/plugins/CommandAliases/functions/normalizeAliases.ts b/backend/src/plugins/CommandAliases/functions/normalizeAliases.ts new file mode 100644 index 000000000..a65babd85 --- /dev/null +++ b/backend/src/plugins/CommandAliases/functions/normalizeAliases.ts @@ -0,0 +1,27 @@ +export interface NormalizedAlias { + alias: string; + target: string; +} + +export function normalizeAliases(aliases: Record | undefined | null): NormalizedAlias[] { + if (!aliases) { + return []; + } + + const normalized: NormalizedAlias[] = []; + for (const [rawAlias, rawTarget] of Object.entries(aliases)) { + const alias = rawAlias.trim(); + const target = rawTarget.trim(); + + if (!alias || !target) { + continue; + } + + normalized.push({ + alias, + target, + }); + } + + return normalized; +} diff --git a/backend/src/plugins/CommandAliases/types.ts b/backend/src/plugins/CommandAliases/types.ts new file mode 100644 index 000000000..7c00aec9e --- /dev/null +++ b/backend/src/plugins/CommandAliases/types.ts @@ -0,0 +1,16 @@ +import { BasePluginType, guildPluginEventListener } from "vety"; +import z from "zod"; +import { AliasMatcher } from "./functions/buildAliasMatchers.js"; + +export const zCommandAliasesConfig = z.strictObject({ + aliases: z.record(z.string().min(1), z.string().min(1)).optional(), +}); + +export interface CommandAliasesPluginType extends BasePluginType { + configSchema: typeof zCommandAliasesConfig; + state: { + matchers: AliasMatcher[]; + }; +} + +export const commandAliasesEvt = guildPluginEventListener(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 46653ce8f..d326fd394 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -77,6 +77,8 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin.js"; import { utilityPluginDocs } from "./Utility/docs.js"; import { WelcomeMessagePlugin } from "./WelcomeMessage/WelcomeMessagePlugin.js"; import { welcomeMessagePluginDocs } from "./WelcomeMessage/docs.js"; +import { CommandAliasesPlugin } from "./CommandAliases/CommandAliasesPlugin.js"; +import { commandAliasesPluginDocs } from "./CommandAliases/docs.js"; export const availableGuildPlugins: ZeppelinGuildPluginInfo[] = [ { @@ -100,6 +102,10 @@ export const availableGuildPlugins: ZeppelinGuildPluginInfo[] = [ plugin: CensorPlugin, docs: censorPluginDocs, }, + { + plugin: CommandAliasesPlugin, + docs: commandAliasesPluginDocs, + }, { plugin: CompanionChannelsPlugin, docs: companionChannelsPluginDocs,