From 824b2375154d6495a66c07d9ade7eaa09798ed8f Mon Sep 17 00:00:00 2001 From: Rstar284 Date: Tue, 26 Apr 2022 15:34:22 +0400 Subject: [PATCH] feat: allow the bot to search through the list of tags and aliases to get the closest matching tag --- .../src/plugins/Tags/commands/TagCreateCmd.ts | 5 +++ backend/src/plugins/Tags/util/fuzzySearch.ts | 30 ++++++++++++++ .../Tags/util/matchAndRenderTagFromString.ts | 39 ++++++++++++++++++- .../src/plugins/Tags/util/onMessageCreate.ts | 6 ++- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 backend/src/plugins/Tags/util/fuzzySearch.ts diff --git a/backend/src/plugins/Tags/commands/TagCreateCmd.ts b/backend/src/plugins/Tags/commands/TagCreateCmd.ts index aba519d5..855adc38 100644 --- a/backend/src/plugins/Tags/commands/TagCreateCmd.ts +++ b/backend/src/plugins/Tags/commands/TagCreateCmd.ts @@ -17,6 +17,11 @@ export const TagCreateCmd = tagsCmd({ const prefix = pluginData.config.get().prefix; if (args.alias) { + const existingTag = await pluginData.state.tagAliases.find(args.body); + if (existingTag) { + sendErrorMessage(pluginData, msg.channel, `You cannot create an alias of an alias`); + return; + } await pluginData.state.tagAliases.createOrUpdate(args.tag, args.body, msg.author.id); sendSuccessMessage(pluginData, msg.channel, `Alias set! Use it with: \`${prefix}${args.tag}\``); return; diff --git a/backend/src/plugins/Tags/util/fuzzySearch.ts b/backend/src/plugins/Tags/util/fuzzySearch.ts new file mode 100644 index 00000000..de6c5821 --- /dev/null +++ b/backend/src/plugins/Tags/util/fuzzySearch.ts @@ -0,0 +1,30 @@ +// https://rosettacode.org/wiki/Levenshtein_distance#JavaScript + +function levenshtein(a: string, b: string): number { + let t: number[] = []; + let u: number[] = []; + const m = a.length; + const n = b.length; + if (!m) { + return n; + } + if (!n) { + return m; + } + for (let j = 0; j <= n; j++) { + t[j] = j; + } + for (let i = 1; i <= m; i++) { + let j: number; + // tslint:disable-next-line:ban-comma-operator + for (u = [i], j = 1; j <= n; j++) { + u[j] = a[i - 1] === b[j - 1] ? t[j - 1] : Math.min(t[j - 1], t[j], u[j - 1]) + 1; + } + t = u; + } + return u[n]; +} + +export function distance(str: string, t: string): number { + return levenshtein(str, t); +} diff --git a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts index e5ad7319..cc1e90a7 100644 --- a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts +++ b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts @@ -1,9 +1,12 @@ -import { GuildMember } from "discord.js"; +import { GuildMember, Snowflake, TextChannel } from "discord.js"; import escapeStringRegexp from "escape-string-regexp"; import { GuildPluginData } from "knub"; import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; + import { StrictMessageContent } from "../../../utils"; import { TagsPluginType, TTagCategory } from "../types"; + +import { distance } from "./fuzzySearch"; import { renderTagFromString } from "./renderTagFromString"; interface BaseResult { @@ -93,7 +96,39 @@ export async function matchAndRenderTagFromString( const dynamicTag = await pluginData.state.tags.find(tagName); if (!aliasedTag && !dynamicTag) { - return null; + // fuzzy search the list of aliases and tags to see if there's a match and + // inform the user + const tags = await pluginData.state.tags.all(); + const aliases = await pluginData.state.tagAliases.all(); + let lowest: [number, [string]] = [999999, [""]]; + tags.forEach((tag) => { + const tagname = tag?.tag; + const dist = distance(tagname, tagName); + if (dist < lowest[0]) { + lowest = [dist, [`**${tagname}**`]]; + } else if (dist === lowest[0]) { + lowest[1].push(`**${tagname}**`); + } + }); + aliases.forEach((alias) => { + const aliasname = alias?.alias; + const dist = distance(aliasname, tagName); + if (dist < lowest[0]) { + lowest = [dist, [`**${aliasname}**`]]; + } else if (dist === lowest[0]) { + lowest[1].push(`**${aliasname}**`); + } + }); + if (lowest[0] > 6) return null; + const content: StrictMessageContent = { + content: `Did you mean:\n${lowest[1].join("\n")}`, + }; + return { + renderedContent: content, + tagName: "", + category: null, + categoryName: null, + }; } const tagBody = aliasedTag?.body ?? dynamicTag?.body; diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts index 052c6395..a7429f28 100644 --- a/backend/src/plugins/Tags/util/onMessageCreate.ts +++ b/backend/src/plugins/Tags/util/onMessageCreate.ts @@ -1,14 +1,16 @@ import { Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions"; + import { SavedMessage } from "../../../data/entities/SavedMessage"; import { LogType } from "../../../data/LogType"; import { convertDelayStringToMS, resolveMember, tStrictMessageContent } from "../../../utils"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { validate } from "../../../validatorUtils"; -import { TagsPluginType } from "../types"; -import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString"; import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { TagsPluginType } from "../types"; + +import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString"; export async function onMessageCreate(pluginData: GuildPluginData, msg: SavedMessage) { if (msg.is_bot) return;