From e171037d5e0091e9ce462bce859a37e287abd000 Mon Sep 17 00:00:00 2001 From: Rstar284 Date: Fri, 22 Apr 2022 15:52:39 +0400 Subject: [PATCH 1/8] feat: add tag aliases support --- backend/src/data/GuildTagAliases.ts | 66 +++++++++++++++++++ backend/src/data/entities/TagAlias.ts | 20 ++++++ backend/src/plugins/Tags/TagsPlugin.ts | 2 + .../src/plugins/Tags/commands/TagCreateCmd.ts | 9 ++- .../src/plugins/Tags/commands/TagDeleteCmd.ts | 18 ++++- .../src/plugins/Tags/commands/TagListCmd.ts | 7 +- .../src/plugins/Tags/commands/TagSourceCmd.ts | 12 ++-- backend/src/plugins/Tags/types.ts | 2 + .../src/plugins/Tags/util/findTagByName.ts | 12 +++- .../Tags/util/matchAndRenderTagFromString.ts | 44 +++++++------ 10 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 backend/src/data/GuildTagAliases.ts create mode 100644 backend/src/data/entities/TagAlias.ts diff --git a/backend/src/data/GuildTagAliases.ts b/backend/src/data/GuildTagAliases.ts new file mode 100644 index 00000000..a5f7983f --- /dev/null +++ b/backend/src/data/GuildTagAliases.ts @@ -0,0 +1,66 @@ +import { getRepository, Repository } from "typeorm"; +import { BaseGuildRepository } from "./BaseGuildRepository"; +import { TagAlias } from "./entities/TagAlias"; + +export class GuildTagAliases extends BaseGuildRepository { + private tagAliases: Repository; + + constructor(guildId) { + super(guildId); + this.tagAliases = getRepository(TagAlias); + } + + async all(): Promise { + return this.tagAliases.find({ + where: { + guild_id: this.guildId, + }, + }); + } + + async find(alias): Promise { + return this.tagAliases.findOne({ + where: { + guild_id: this.guildId, + alias, + }, + }); + } + + async findAllWithTag(tag): Promise { + const all = await this.all(); + const aliases = all.filter((a) => a.tag === tag); + return aliases.length > 0 ? aliases : undefined; + } + + async createOrUpdate(alias, tag, userId) { + const existingTagAlias = await this.find(alias); + if (existingTagAlias) { + await this.tagAliases + .createQueryBuilder() + .update() + .set({ + tag, + user_id: userId, + created_at: () => "NOW()", + }) + .where("guild_id = :guildId", { guildId: this.guildId }) + .andWhere("alias = :alias", { alias }) + .execute(); + } else { + await this.tagAliases.insert({ + guild_id: this.guildId, + user_id: userId, + alias, + tag, + }); + } + } + + async delete(alias) { + await this.tagAliases.delete({ + guild_id: this.guildId, + alias, + }); + } +} diff --git a/backend/src/data/entities/TagAlias.ts b/backend/src/data/entities/TagAlias.ts new file mode 100644 index 00000000..80dc385b --- /dev/null +++ b/backend/src/data/entities/TagAlias.ts @@ -0,0 +1,20 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity("tag_aliases") +export class TagAlias { + @Column() + @PrimaryColumn() + guild_id: string; + + @Column() + @PrimaryColumn() + alias: string; + + @Column() + @PrimaryColumn() + tag: string; + + @Column() user_id: string; + + @Column() created_at: string; +} diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index 0bfe05da..fe44af2d 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -22,6 +22,7 @@ import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageDelete } from "./util/onMessageDelete"; import { renderTagBody } from "./util/renderTagBody"; import { LogsPlugin } from "../Logs/LogsPlugin"; +import { GuildTagAliases } from "src/data/GuildTagAliases"; const defaultOptions: PluginOptions = { config: { @@ -110,6 +111,7 @@ export const TagsPlugin = zeppelinGuildPlugin()({ state.archives = GuildArchives.getGuildInstance(guild.id); state.tags = GuildTags.getGuildInstance(guild.id); + state.tagAliases = GuildTagAliases.getGuildInstance(guild.id); state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id); state.logs = new GuildLogs(guild.id); diff --git a/backend/src/plugins/Tags/commands/TagCreateCmd.ts b/backend/src/plugins/Tags/commands/TagCreateCmd.ts index 0595e4bb..aba519d5 100644 --- a/backend/src/plugins/Tags/commands/TagCreateCmd.ts +++ b/backend/src/plugins/Tags/commands/TagCreateCmd.ts @@ -8,11 +8,19 @@ export const TagCreateCmd = tagsCmd({ permission: "can_create", signature: { + alias: ct.bool({ option: true, shortcut: "a", isSwitch: true }), tag: ct.string(), body: ct.string({ catchAll: true }), }, async run({ message: msg, args, pluginData }) { + const prefix = pluginData.config.get().prefix; + + if (args.alias) { + 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; + } try { parseTemplate(args.body); } catch (e) { @@ -26,7 +34,6 @@ export const TagCreateCmd = tagsCmd({ await pluginData.state.tags.createOrUpdate(args.tag, args.body, msg.author.id); - const prefix = pluginData.config.get().prefix; sendSuccessMessage(pluginData, msg.channel, `Tag set! Use it with: \`${prefix}${args.tag}\``); }, }); diff --git a/backend/src/plugins/Tags/commands/TagDeleteCmd.ts b/backend/src/plugins/Tags/commands/TagDeleteCmd.ts index 47cb623d..f0373eed 100644 --- a/backend/src/plugins/Tags/commands/TagDeleteCmd.ts +++ b/backend/src/plugins/Tags/commands/TagDeleteCmd.ts @@ -11,13 +11,25 @@ export const TagDeleteCmd = tagsCmd({ }, async run({ message: msg, args, pluginData }) { + const alias = await pluginData.state.tagAliases.find(args.tag); const tag = await pluginData.state.tags.find(args.tag); - if (!tag) { + + if (!tag && !alias) { sendErrorMessage(pluginData, msg.channel, "No tag with that name"); return; } - await pluginData.state.tags.delete(args.tag); - sendSuccessMessage(pluginData, msg.channel, "Tag deleted!"); + if (tag) { + const aliasesOfTag = await pluginData.state.tagAliases.findAllWithTag(tag?.tag); + if (aliasesOfTag) { + // tslint:disable-next-line:no-shadowed-variable + aliasesOfTag.forEach((alias) => pluginData.state.tagAliases.delete(alias.alias)); + } + await pluginData.state.tags.delete(args.tag); + } else { + await pluginData.state.tagAliases.delete(alias?.alias); + } + + sendSuccessMessage(pluginData, msg.channel, `${tag ? "Tag" : "Alias"} deleted!`); }, }); diff --git a/backend/src/plugins/Tags/commands/TagListCmd.ts b/backend/src/plugins/Tags/commands/TagListCmd.ts index c50baaa5..f339e27f 100644 --- a/backend/src/plugins/Tags/commands/TagListCmd.ts +++ b/backend/src/plugins/Tags/commands/TagListCmd.ts @@ -7,6 +7,7 @@ export const TagListCmd = tagsCmd({ async run({ message: msg, pluginData }) { const tags = await pluginData.state.tags.all(); + const aliases = await pluginData.state.tagAliases.all(); if (tags.length === 0) { msg.channel.send(`No tags created yet! Use \`tag create\` command to create one.`); return; @@ -14,7 +15,11 @@ export const TagListCmd = tagsCmd({ const prefix = (await pluginData.config.getForMessage(msg)).prefix; const tagNames = tags.map((tag) => tag.tag).sort(); + const tagAliasesNames = aliases.map((alias) => alias.alias).sort(); + const tagAndAliasesNames = tagNames + .join(", ") + .concat(tagAliasesNames.length > 0 ? `, ${tagAliasesNames.join(", ")}` : ""); - createChunkedMessage(msg.channel, `Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\``); + createChunkedMessage(msg.channel, `Available tags (use with ${prefix}tag): \`\`\`${tagAndAliasesNames}\`\`\``); }, }); diff --git a/backend/src/plugins/Tags/commands/TagSourceCmd.ts b/backend/src/plugins/Tags/commands/TagSourceCmd.ts index 9e0e5cef..45d3b521 100644 --- a/backend/src/plugins/Tags/commands/TagSourceCmd.ts +++ b/backend/src/plugins/Tags/commands/TagSourceCmd.ts @@ -14,19 +14,23 @@ export const TagSourceCmd = tagsCmd({ }, async run({ message: msg, args, pluginData }) { + const alias = await pluginData.state.tagAliases.find(args.tag); + const tag = (await pluginData.state.tags.find(args.tag)) || (await pluginData.state.tags.find(alias?.tag ?? null)); + if (args.delete) { const actualTag = await pluginData.state.tags.find(args.tag); - if (!actualTag) { + const aliasedTag = await pluginData.state.tags.find(alias?.tag ?? null); + + if (!actualTag && !aliasedTag) { sendErrorMessage(pluginData, msg.channel, "No tag with that name"); return; } - await pluginData.state.tags.delete(args.tag); - sendSuccessMessage(pluginData, msg.channel, "Tag deleted!"); + actualTag ? pluginData.state.tags.delete(args.tag) : pluginData.state.tagAliases.delete(args.tag); + sendSuccessMessage(pluginData, msg.channel, `${actualTag ? "Tag" : "Alias"} deleted!`); return; } - const tag = await pluginData.state.tags.find(args.tag); if (!tag) { sendErrorMessage(pluginData, msg.channel, "No tag with that name"); return; diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index c87110f5..eb61441b 100644 --- a/backend/src/plugins/Tags/types.ts +++ b/backend/src/plugins/Tags/types.ts @@ -4,6 +4,7 @@ import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildTags } from "../../data/GuildTags"; +import { GuildTagAliases } from "../../data/GuildTagAliases"; import { tEmbed, tNullable } from "../../utils"; export const Tag = t.union([t.string, tEmbed]); @@ -50,6 +51,7 @@ export interface TagsPluginType extends BasePluginType { state: { archives: GuildArchives; tags: GuildTags; + tagAliases: GuildTagAliases; savedMessages: GuildSavedMessages; logs: GuildLogs; diff --git a/backend/src/plugins/Tags/util/findTagByName.ts b/backend/src/plugins/Tags/util/findTagByName.ts index 4ec61ec5..5661fedf 100644 --- a/backend/src/plugins/Tags/util/findTagByName.ts +++ b/backend/src/plugins/Tags/util/findTagByName.ts @@ -20,8 +20,18 @@ export async function findTagByName( return config.categories[categoryName]?.tags[tagName] ?? null; } + let tag: string | null; + // Dynamic tag // Format: "tag" const dynamicTag = await pluginData.state.tags.find(name); - return dynamicTag?.body ?? null; + tag = dynamicTag?.body ?? null; + + // Aliased tag + // Format: "alias" + const aliasedTagName = await pluginData.state.tagAliases.find(name); + const aliasedTag = await pluginData.state.tags.find(aliasedTagName?.tag); + tag ? (tag = tag) : (tag = aliasedTag?.body ?? null); + + return tag; } diff --git a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts index db436067..e5ad7319 100644 --- a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts +++ b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts @@ -44,7 +44,8 @@ export async function matchAndRenderTagFromString( const withoutPrefix = str.slice(prefix.length); - for (const [tagName, tagBody] of Object.entries(category.tags)) { + // tslint:disable-next-line:no-shadowed-variable + for (const [tagName, _tagBody] of Object.entries(category.tags)) { const regex = new RegExp(`^${escapeStringRegexp(tagName)}(?:\\s|$)`); if (regex.test(withoutPrefix)) { const renderedContent = await renderTagFromString( @@ -70,43 +71,46 @@ export async function matchAndRenderTagFromString( } } - // Dynamic tags + // Dynamic + Aliased tags if (config.can_use !== true) { return null; } - const dynamicTagPrefix = config.prefix; - if (!str.startsWith(dynamicTagPrefix)) { + const tagPrefix = config.prefix; + if (!str.startsWith(tagPrefix)) { return null; } - const dynamicTagNameMatch = str.slice(dynamicTagPrefix.length).match(/^\S+/); - if (dynamicTagNameMatch === null) { + const tagNameMatch = str.slice(tagPrefix.length).match(/^[a-z0-9_-]*/); + if (tagNameMatch == null) { return null; } - const dynamicTagName = dynamicTagNameMatch[0]; - const dynamicTag = await pluginData.state.tags.find(dynamicTagName); - if (!dynamicTag) { + const tagName = tagNameMatch[0]; + + const aliasName = await pluginData.state.tagAliases.find(tagName); + const aliasedTag = await pluginData.state.tags.find(aliasName?.tag); + const dynamicTag = await pluginData.state.tags.find(tagName); + + if (!aliasedTag && !dynamicTag) { return null; } - const renderedDynamicTagContent = await renderTagFromString( - pluginData, - str, - dynamicTagPrefix, - dynamicTagName, - dynamicTag.body, - member, - ); + const tagBody = aliasedTag?.body ?? dynamicTag?.body; - if (renderedDynamicTagContent == null) { + if (!tagBody) { + return null; + } + + const renderedTagContent = await renderTagFromString(pluginData, str, tagPrefix, tagName, tagBody, member); + + if (renderedTagContent == null) { return null; } return { - renderedContent: renderedDynamicTagContent, - tagName: dynamicTagName, + renderedContent: renderedTagContent, + tagName, categoryName: null, category: null, }; From c32a8f363b93d9caf300e43ddc8d18340c1196c1 Mon Sep 17 00:00:00 2001 From: Rstar284 Date: Fri, 22 Apr 2022 16:17:39 +0400 Subject: [PATCH 2/8] fix: fix permission names errors --- backend/src/utils/permissionNames.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/utils/permissionNames.ts b/backend/src/utils/permissionNames.ts index 3bf0c4f1..20ef3d41 100644 --- a/backend/src/utils/permissionNames.ts +++ b/backend/src/utils/permissionNames.ts @@ -22,6 +22,7 @@ export const PERMISSION_NAMES: Record = { MANAGE_ROLES: "Manage Roles", MANAGE_THREADS: "Manage Threads", MANAGE_WEBHOOKS: "Manage Webhooks", + MANAGE_EVENTS: "Manage Events", MENTION_EVERYONE: `Mention @${EMPTY_CHAR}everyone, @${EMPTY_CHAR}here, and All Roles`, MOVE_MEMBERS: "Move Members", MUTE_MEMBERS: "Mute Members", @@ -43,4 +44,5 @@ export const PERMISSION_NAMES: Record = { VIEW_AUDIT_LOG: "View Audit Log", VIEW_CHANNEL: "View Channels", VIEW_GUILD_INSIGHTS: "View Guild Insights", + MODERATE_MEMBERS: "Moderate Members", }; From 4dd538d95291628b52267c070236b9c899584608 Mon Sep 17 00:00:00 2001 From: Rstar284 Date: Sat, 23 Apr 2022 17:19:34 +0400 Subject: [PATCH 3/8] add command for listing tag aliases by tag, add flag to not show aliases when listing tags --- backend/src/plugins/Tags/TagsPlugin.ts | 4 ++- .../Tags/commands/TagListAliasesCmd.ts | 28 +++++++++++++++++++ .../src/plugins/Tags/commands/TagListCmd.ts | 11 ++++++-- .../src/plugins/Tags/commands/TagSourceCmd.ts | 22 ++++++++++++--- 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 backend/src/plugins/Tags/commands/TagListAliasesCmd.ts diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index 86305c2d..63930a21 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -16,6 +16,7 @@ import { TagCreateCmd } from "./commands/TagCreateCmd"; import { TagDeleteCmd } from "./commands/TagDeleteCmd"; import { TagEvalCmd } from "./commands/TagEvalCmd"; import { TagListCmd } from "./commands/TagListCmd"; +import { TagListAliasesCmd } from "./commands/TagListAliasesCmd"; import { TagSourceCmd } from "./commands/TagSourceCmd"; import { ConfigSchema, TagsPluginType } from "./types"; import { findTagByName } from "./util/findTagByName"; @@ -69,7 +70,7 @@ export const TagsPlugin = zeppelinGuildPlugin()({ You use them by adding a \`{}\` on your tag. Here are the functions you can use in your tags: - + ${generateTemplateMarkdown(TemplateFunctions)} `), }, @@ -83,6 +84,7 @@ export const TagsPlugin = zeppelinGuildPlugin()({ TagEvalCmd, TagDeleteCmd, TagListCmd, + TagListAliasesCmd, TagSourceCmd, TagCreateCmd, ], diff --git a/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts b/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts new file mode 100644 index 00000000..4d5482bc --- /dev/null +++ b/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts @@ -0,0 +1,28 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { createChunkedMessage } from "../../../utils"; +import { tagsCmd } from "../types"; + +export const TagListAliasesCmd = tagsCmd({ + trigger: ["tag list-aliases", "tagaliases"], + permission: "can_list", + + signature: { + tag: ct.string(), + }, + + async run({ message: msg, args, pluginData }) { + const prefix = (await pluginData.config.getForMessage(msg)).prefix; + const aliases = await pluginData.state.tagAliases.findAllWithTag(args.tag); + let aliasesArr: string[] = []; + if (!aliases) { + msg.channel.send(`No aliases found for tag \`${args.tag}\``); + return; + } + aliasesArr = aliases.map((a) => a.alias); + createChunkedMessage( + msg.channel, + `Available aliases for tag \`${prefix + args.tag}\`: \`\`\`${aliasesArr.join(", ")}\`\`\``, + ); + return; + }, +}); diff --git a/backend/src/plugins/Tags/commands/TagListCmd.ts b/backend/src/plugins/Tags/commands/TagListCmd.ts index f339e27f..36f5595d 100644 --- a/backend/src/plugins/Tags/commands/TagListCmd.ts +++ b/backend/src/plugins/Tags/commands/TagListCmd.ts @@ -1,3 +1,4 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; import { createChunkedMessage } from "../../../utils"; import { tagsCmd } from "../types"; @@ -5,7 +6,12 @@ export const TagListCmd = tagsCmd({ trigger: ["tag list", "tags", "taglist"], permission: "can_list", - async run({ message: msg, pluginData }) { + signature: { + noaliases: ct.bool({ option: true, isSwitch: true, shortcut: "na" }), + }, + + async run({ message: msg, args, pluginData }) { + const prefix = (await pluginData.config.getForMessage(msg)).prefix; const tags = await pluginData.state.tags.all(); const aliases = await pluginData.state.tagAliases.all(); if (tags.length === 0) { @@ -13,12 +19,11 @@ export const TagListCmd = tagsCmd({ return; } - const prefix = (await pluginData.config.getForMessage(msg)).prefix; const tagNames = tags.map((tag) => tag.tag).sort(); const tagAliasesNames = aliases.map((alias) => alias.alias).sort(); const tagAndAliasesNames = tagNames .join(", ") - .concat(tagAliasesNames.length > 0 ? `, ${tagAliasesNames.join(", ")}` : ""); + .concat(args.noaliases ? "" : tagAliasesNames.length > 0 ? `, ${tagAliasesNames.join(", ")}` : ""); createChunkedMessage(msg.channel, `Available tags (use with ${prefix}tag): \`\`\`${tagAndAliasesNames}\`\`\``); }, diff --git a/backend/src/plugins/Tags/commands/TagSourceCmd.ts b/backend/src/plugins/Tags/commands/TagSourceCmd.ts index 45d3b521..ad04c14c 100644 --- a/backend/src/plugins/Tags/commands/TagSourceCmd.ts +++ b/backend/src/plugins/Tags/commands/TagSourceCmd.ts @@ -15,27 +15,41 @@ export const TagSourceCmd = tagsCmd({ async run({ message: msg, args, pluginData }) { const alias = await pluginData.state.tagAliases.find(args.tag); - const tag = (await pluginData.state.tags.find(args.tag)) || (await pluginData.state.tags.find(alias?.tag ?? null)); + const aliasedTag = await pluginData.state.tags.find(alias?.tag ?? null); + const tag = (await pluginData.state.tags.find(args.tag)) || aliasedTag; if (args.delete) { const actualTag = await pluginData.state.tags.find(args.tag); - const aliasedTag = await pluginData.state.tags.find(alias?.tag ?? null); if (!actualTag && !aliasedTag) { sendErrorMessage(pluginData, msg.channel, "No tag with that name"); return; } - actualTag ? pluginData.state.tags.delete(args.tag) : pluginData.state.tagAliases.delete(args.tag); + if (actualTag) { + const aliasesOfTag = await pluginData.state.tagAliases.findAllWithTag(actualTag?.tag); + if (aliasesOfTag) { + // tslint:disable-next-line:no-shadowed-variable + aliasesOfTag.forEach((alias) => pluginData.state.tagAliases.delete(alias.alias)); + } + await pluginData.state.tags.delete(args.tag); + } else { + await pluginData.state.tagAliases.delete(alias?.alias); + } + sendSuccessMessage(pluginData, msg.channel, `${actualTag ? "Tag" : "Alias"} deleted!`); return; } - if (!tag) { + if (!tag && !aliasedTag) { sendErrorMessage(pluginData, msg.channel, "No tag with that name"); return; } + if (!tag?.body) { + return; + } + const archiveId = await pluginData.state.archives.create(tag.body, moment.utc().add(10, "minutes")); const url = pluginData.state.archives.getUrl(getBaseUrl(pluginData), archiveId); From 57adaf0b88c2cc8ca597b96619b52a33affd008f Mon Sep 17 00:00:00 2001 From: Rstar284 Date: Sat, 23 Apr 2022 18:29:56 +0400 Subject: [PATCH 4/8] add migration to create the table --- .../1650721595278-CreateTagAliasesTable.ts | 43 +++++++++++++++++++ .../Tags/commands/TagListAliasesCmd.ts | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 backend/src/migrations/1650721595278-CreateTagAliasesTable.ts diff --git a/backend/src/migrations/1650721595278-CreateTagAliasesTable.ts b/backend/src/migrations/1650721595278-CreateTagAliasesTable.ts new file mode 100644 index 00000000..4b69efc8 --- /dev/null +++ b/backend/src/migrations/1650721595278-CreateTagAliasesTable.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateTagAliasesTable1650721595278 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "tag_aliases", + columns: [ + { + name: "guild_id", + type: "bigint", + isPrimary: true, + }, + { + name: "alias", + type: "varchar", + length: "255", + isPrimary: true, + }, + { + name: "tag", + type: "varchar", + length: "255", + isPrimary: true, + }, + { + name: "user_id", + type: "bigint", + }, + { + name: "created_at", + type: "datetime", + default: "now()", + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + queryRunner.dropTable("tag_aliases"); + } +} diff --git a/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts b/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts index 4d5482bc..b53a58fd 100644 --- a/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts +++ b/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts @@ -21,7 +21,7 @@ export const TagListAliasesCmd = tagsCmd({ aliasesArr = aliases.map((a) => a.alias); createChunkedMessage( msg.channel, - `Available aliases for tag \`${prefix + args.tag}\`: \`\`\`${aliasesArr.join(", ")}\`\`\``, + `Available aliases for tag \`${args.tag}\` (use with \`${prefix}alias\`: \`\`\`${aliasesArr.join(", ")}\`\`\``, ); return; }, From 824b2375154d6495a66c07d9ade7eaa09798ed8f Mon Sep 17 00:00:00 2001 From: Rstar284 Date: Tue, 26 Apr 2022 15:34:22 +0400 Subject: [PATCH 5/8] 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; From ee53e2ddd076d0abf849836704f85804536cb3bd Mon Sep 17 00:00:00 2001 From: rubyowo Date: Fri, 20 May 2022 14:41:54 +0400 Subject: [PATCH 6/8] transfered code from `TagListAliasesCmd` into `TagListCmd` and refactored `TagListCmd` --- backend/src/plugins/Tags/TagsPlugin.ts | 2 - .../Tags/commands/TagListAliasesCmd.ts | 28 ------------ .../src/plugins/Tags/commands/TagListCmd.ts | 44 +++++++++++++++++-- .../Tags/util/matchAndRenderTagFromString.ts | 1 + 4 files changed, 42 insertions(+), 33 deletions(-) delete mode 100644 backend/src/plugins/Tags/commands/TagListAliasesCmd.ts diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index 63930a21..d7453076 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -16,7 +16,6 @@ import { TagCreateCmd } from "./commands/TagCreateCmd"; import { TagDeleteCmd } from "./commands/TagDeleteCmd"; import { TagEvalCmd } from "./commands/TagEvalCmd"; import { TagListCmd } from "./commands/TagListCmd"; -import { TagListAliasesCmd } from "./commands/TagListAliasesCmd"; import { TagSourceCmd } from "./commands/TagSourceCmd"; import { ConfigSchema, TagsPluginType } from "./types"; import { findTagByName } from "./util/findTagByName"; @@ -84,7 +83,6 @@ export const TagsPlugin = zeppelinGuildPlugin()({ TagEvalCmd, TagDeleteCmd, TagListCmd, - TagListAliasesCmd, TagSourceCmd, TagCreateCmd, ], diff --git a/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts b/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts deleted file mode 100644 index b53a58fd..00000000 --- a/backend/src/plugins/Tags/commands/TagListAliasesCmd.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { createChunkedMessage } from "../../../utils"; -import { tagsCmd } from "../types"; - -export const TagListAliasesCmd = tagsCmd({ - trigger: ["tag list-aliases", "tagaliases"], - permission: "can_list", - - signature: { - tag: ct.string(), - }, - - async run({ message: msg, args, pluginData }) { - const prefix = (await pluginData.config.getForMessage(msg)).prefix; - const aliases = await pluginData.state.tagAliases.findAllWithTag(args.tag); - let aliasesArr: string[] = []; - if (!aliases) { - msg.channel.send(`No aliases found for tag \`${args.tag}\``); - return; - } - aliasesArr = aliases.map((a) => a.alias); - createChunkedMessage( - msg.channel, - `Available aliases for tag \`${args.tag}\` (use with \`${prefix}alias\`: \`\`\`${aliasesArr.join(", ")}\`\`\``, - ); - return; - }, -}); diff --git a/backend/src/plugins/Tags/commands/TagListCmd.ts b/backend/src/plugins/Tags/commands/TagListCmd.ts index 36f5595d..d59fd9f5 100644 --- a/backend/src/plugins/Tags/commands/TagListCmd.ts +++ b/backend/src/plugins/Tags/commands/TagListCmd.ts @@ -1,3 +1,5 @@ +import { sendErrorMessage } from "src/pluginUtils"; + import { commandTypeHelpers as ct } from "../../../commandTypes"; import { createChunkedMessage } from "../../../utils"; import { tagsCmd } from "../types"; @@ -8,23 +10,59 @@ export const TagListCmd = tagsCmd({ signature: { noaliases: ct.bool({ option: true, isSwitch: true, shortcut: "na" }), + aliasesonly: ct.bool({ option: true, isSwitch: true, shortcut: "ao" }), + tag: ct.string({ option: true }), }, async run({ message: msg, args, pluginData }) { const prefix = (await pluginData.config.getForMessage(msg)).prefix; const tags = await pluginData.state.tags.all(); - const aliases = await pluginData.state.tagAliases.all(); if (tags.length === 0) { msg.channel.send(`No tags created yet! Use \`tag create\` command to create one.`); return; } + const allAliases = await pluginData.state.tagAliases.all(); + + if (args.aliasesonly) { + let aliasesArr: string[] = []; + if (args.tag) { + const tag = await pluginData.state.tags.find(args.tag); + if (!tag) { + sendErrorMessage(pluginData, msg.channel, `Tag \`${args.tag}\` doesn't exist.`); + return; + } + const aliasesForTag = await pluginData.state.tagAliases.findAllWithTag(args.tag); + if (!aliasesForTag) { + sendErrorMessage(pluginData, msg.channel, `No aliases for tag \`${args.tag}\`.`); + return; + } + aliasesArr = aliasesForTag.map((a) => a.alias); + createChunkedMessage( + msg.channel, + `Available aliases for tag \`${args.tag}\` (use with \`${prefix}alias\`: \`\`\`${aliasesArr.join( + ", ", + )}\`\`\``, + ); + return; + } + aliasesArr = allAliases.map((a) => a.alias); + createChunkedMessage( + msg.channel, + `Available aliases (use with \`${prefix}alias\`: \`\`\`${aliasesArr.join(", ")}\`\`\``, + ); + return; + } + const tagNames = tags.map((tag) => tag.tag).sort(); - const tagAliasesNames = aliases.map((alias) => alias.alias).sort(); + const tagAliasesNames = allAliases.map((alias) => alias.alias).sort(); const tagAndAliasesNames = tagNames .join(", ") .concat(args.noaliases ? "" : tagAliasesNames.length > 0 ? `, ${tagAliasesNames.join(", ")}` : ""); - createChunkedMessage(msg.channel, `Available tags (use with ${prefix}tag): \`\`\`${tagAndAliasesNames}\`\`\``); + createChunkedMessage( + msg.channel, + `Available tags (use with ${prefix}tag/alias): \`\`\`${tagAndAliasesNames}\`\`\``, + ); }, }); diff --git a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts index cc1e90a7..8502c3af 100644 --- a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts +++ b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts @@ -123,6 +123,7 @@ export async function matchAndRenderTagFromString( const content: StrictMessageContent = { content: `Did you mean:\n${lowest[1].join("\n")}`, }; + return { renderedContent: content, tagName: "", From 2495336f1c48c04e097b00bd64fd29db4d6fefe7 Mon Sep 17 00:00:00 2001 From: rubyowo Date: Fri, 20 May 2022 17:53:23 +0400 Subject: [PATCH 7/8] fix regex --- backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts index 8502c3af..08efc89b 100644 --- a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts +++ b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts @@ -84,7 +84,7 @@ export async function matchAndRenderTagFromString( return null; } - const tagNameMatch = str.slice(tagPrefix.length).match(/^[a-z0-9_-]*/); + const tagNameMatch = str.slice(tagPrefix.length).match(/^\S+/); if (tagNameMatch == null) { return null; } From 02759f2e87d1619b442755c0e6d369299fe34601 Mon Sep 17 00:00:00 2001 From: rubyowo Date: Fri, 20 May 2022 17:53:46 +0400 Subject: [PATCH 8/8] fix tag matching regex --- backend/ormconfig.js | 1 + backend/package-lock.json | 22 ++++++++++++++-------- backend/package.json | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/ormconfig.js b/backend/ormconfig.js index c62e60c1..164962d8 100644 --- a/backend/ormconfig.js +++ b/backend/ormconfig.js @@ -30,6 +30,7 @@ const migrationsDir = path.relative(process.cwd(), path.resolve(backendRoot, "sr module.exports = { type: "mysql", host: process.env.DB_HOST, + port: 13306, username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, diff --git a/backend/package-lock.json b/backend/package-lock.json index 1647e84c..c3e5f2a0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,7 @@ "cors": "^2.8.5", "cross-env": "^5.2.0", "deep-diff": "^1.0.2", - "discord-api-types": "^0.31.0", + "discord-api-types": "^0.31.2", "discord.js": "^13.6.0", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", @@ -1693,8 +1693,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.31.0", - "license": "MIT" + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.31.2.tgz", + "integrity": "sha512-gpzXTvFVg7AjKVVJFH0oJGC0q0tO34iJGSHZNz9u3aqLxlD6LfxEs9wWVVikJqn9gra940oUTaPFizCkRDcEiA==" }, "node_modules/discord.js": { "version": "13.6.0", @@ -3018,8 +3019,9 @@ } }, "node_modules/moment": { - "version": "2.24.0", - "license": "MIT", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "engines": { "node": "*" } @@ -6346,7 +6348,9 @@ } }, "discord-api-types": { - "version": "0.31.0" + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.31.2.tgz", + "integrity": "sha512-gpzXTvFVg7AjKVVJFH0oJGC0q0tO34iJGSHZNz9u3aqLxlD6LfxEs9wWVVikJqn9gra940oUTaPFizCkRDcEiA==" }, "discord.js": { "version": "13.6.0", @@ -6425,7 +6429,7 @@ }, "erlpack": { "version": "git+ssh://git@github.com/discord/erlpack.git#3b793a333dd3f6a140b9168ea91e9fa9660753ce", - "from": "erlpack@github:discord/erlpack.git", + "from": "erlpack@github:discord/erlpack", "requires": { "bindings": "^1.5.0", "nan": "^2.15.0" @@ -7175,7 +7179,9 @@ } }, "moment": { - "version": "2.24.0" + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" }, "moment-timezone": { "version": "0.5.27", diff --git a/backend/package.json b/backend/package.json index fbdac67f..8c26b4f9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,7 +28,7 @@ "cors": "^2.8.5", "cross-env": "^5.2.0", "deep-diff": "^1.0.2", - "discord-api-types": "^0.31.0", + "discord-api-types": "^0.31.2", "discord.js": "^13.6.0", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0",