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", 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/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/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index c2cc6a1d..d7453076 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -7,6 +7,7 @@ import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildTags } from "../../data/GuildTags"; +import { GuildTagAliases } from "src/data/GuildTagAliases"; import { mapToPublicFn } from "../../pluginUtils"; import { convertDelayStringToMS, trimPluginDescription } from "../../utils"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; @@ -68,7 +69,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)} `), }, @@ -123,6 +124,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..855adc38 100644 --- a/backend/src/plugins/Tags/commands/TagCreateCmd.ts +++ b/backend/src/plugins/Tags/commands/TagCreateCmd.ts @@ -8,11 +8,24 @@ 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) { + 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; + } try { parseTemplate(args.body); } catch (e) { @@ -26,7 +39,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..d59fd9f5 100644 --- a/backend/src/plugins/Tags/commands/TagListCmd.ts +++ b/backend/src/plugins/Tags/commands/TagListCmd.ts @@ -1,3 +1,6 @@ +import { sendErrorMessage } from "src/pluginUtils"; + +import { commandTypeHelpers as ct } from "../../../commandTypes"; import { createChunkedMessage } from "../../../utils"; import { tagsCmd } from "../types"; @@ -5,16 +8,61 @@ 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" }), + 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(); if (tags.length === 0) { msg.channel.send(`No tags created yet! Use \`tag create\` command to create one.`); return; } - const prefix = (await pluginData.config.getForMessage(msg)).prefix; - const tagNames = tags.map((tag) => tag.tag).sort(); + const allAliases = await pluginData.state.tagAliases.all(); - createChunkedMessage(msg.channel, `Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\``); + 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 = 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/alias): \`\`\`${tagAndAliasesNames}\`\`\``, + ); }, }); diff --git a/backend/src/plugins/Tags/commands/TagSourceCmd.ts b/backend/src/plugins/Tags/commands/TagSourceCmd.ts index 9e0e5cef..ad04c14c 100644 --- a/backend/src/plugins/Tags/commands/TagSourceCmd.ts +++ b/backend/src/plugins/Tags/commands/TagSourceCmd.ts @@ -14,24 +14,42 @@ export const TagSourceCmd = tagsCmd({ }, async run({ message: msg, args, pluginData }) { + const alias = await pluginData.state.tagAliases.find(args.tag); + 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); - if (!actualTag) { + + 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!"); + 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; } - const tag = await pluginData.state.tags.find(args.tag); - 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); diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index 7c0ce10c..ccce4198 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/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 db436067..08efc89b 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 { @@ -44,7 +47,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 +74,79 @@ 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(/^\S+/); + 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) { + // 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; + + if (!tagBody) { return null; } - const renderedDynamicTagContent = await renderTagFromString( - pluginData, - str, - dynamicTagPrefix, - dynamicTagName, - dynamicTag.body, - member, - ); + const renderedTagContent = await renderTagFromString(pluginData, str, tagPrefix, tagName, tagBody, member); - if (renderedDynamicTagContent == null) { + if (renderedTagContent == null) { return null; } return { - renderedContent: renderedDynamicTagContent, - tagName: dynamicTagName, + renderedContent: renderedTagContent, + tagName, categoryName: null, category: null, }; 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;