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, };