From f3d0fad63dd9174ca6027d977de8bbb4fb07f604 Mon Sep 17 00:00:00 2001 From: xHyroM Date: Fri, 25 Aug 2023 07:18:16 +0200 Subject: [PATCH] feat: tag system --- src/commands/docs.ts | 7 +-- src/commands/index.ts | 1 + src/commands/tag.ts | 87 +++++++++++++++++++++++++++ src/loaders/tags.ts | 64 ++++++++++++++++++++ src/structs/context/CommandContext.ts | 13 +--- src/util.ts | 3 + 6 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 src/commands/tag.ts create mode 100644 src/util.ts diff --git a/src/commands/docs.ts b/src/commands/docs.ts index 06eef22..c9816b1 100644 --- a/src/commands/docs.ts +++ b/src/commands/docs.ts @@ -1,7 +1,7 @@ import { SlashCommandStringOption, SlashCommandUserOption } from "discord.js"; -import { defineCommand } from "../loaders/commands"; -import { AutocompleteContext } from "../structs/context/AutocompleteContext"; -import { InteractionCommandContext } from "../structs/context/CommandContext"; +import { defineCommand } from "../loaders/commands.ts"; +import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts"; +import { InteractionCommandContext } from "../structs/context/CommandContext.ts"; import algoliasearch from "algoliasearch"; const algoliaClient = algoliasearch("2527C13E0N", "4efc87205e1fce4a1f267cadcab42cb2"); @@ -74,7 +74,6 @@ defineCommand({ content, allowedMentions: { parse: [ "users" ], - repliedUser: true, } }); } diff --git a/src/commands/index.ts b/src/commands/index.ts index 4f34f75..9c30f04 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,5 +1,6 @@ import "./version.ts"; import "./docs.ts"; +import "./tag.ts"; import { registerCommands } from "../loaders/commands.ts"; await registerCommands(); diff --git a/src/commands/tag.ts b/src/commands/tag.ts new file mode 100644 index 0000000..5a839b4 --- /dev/null +++ b/src/commands/tag.ts @@ -0,0 +1,87 @@ +import { SlashCommandStringOption, SlashCommandUserOption, User } from "discord.js"; +import { defineCommand } from "../loaders/commands.ts"; +import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts"; +import { getTags, searchTag } from "../loaders/tags.ts"; +import { InteractionCommandContext, MessageCommandContext } from "../structs/context/CommandContext.ts"; +import { Bubu } from "../structs/Client.ts"; + +defineCommand({ + name: "tag", + description: "Get tag", + options: [ + { + ...new SlashCommandStringOption() + .setName("query") + .setRequired(true) + .setAutocomplete(true) + .setDescription("Select query") + .toJSON(), + run: async(context: AutocompleteContext) => { + const query = context.options.getString("query"); + if (!query) { + return context.respond(getTags(25)); + } + + const tags = searchTag(query, true); + if (tags.length > 0) + return context.respond(tags); + + return context.respond(getTags(25)); + }, + }, + { + ...new SlashCommandUserOption() + .setName("target") + .setRequired(false) + .setDescription("User to mention") + .toJSON() + } + ], + run: (ctx: InteractionCommandContext) => { + const query = ctx.interaction.options.getString("query"); + const target = ctx.interaction.options.getUser("target"); + + const tag = searchTag(query, false); + if (!tag) { + return ctx.reply({ + content: `\`❌\` Could not find a tag \`${query}\``, + ephemeral: true, + }); + } + + ctx.reply({ + content: [ + target ? `*Suggestion for <@${target.id}>:*\n` : "", + `**${tag.question}**`, + tag.answer + ].join("\n"), + allowedMentions: { + parse: [ "users" ] + } + }); + }, + runMessage: (ctx: MessageCommandContext) => { + const keyword = ctx.options?.[0] ?? "what-is-bun"; + + const target = ctx.options?.[1]?.match(/([0-9]+)/)?.[0]; + const resolvedTarget = target ? Bubu.users.cache.get(target) : null; + + const tag = searchTag(keyword, false); + if (!keyword || !tag) { + return ctx.reply({ + content: `\`❌\` Could not find a tag \`${keyword}\``, + }); + } + + ctx.reply({ + content: [ + resolvedTarget ? `*Suggestion for <@${resolvedTarget.id}>:*\n` : "", + `**${tag.question}**`, + tag.answer + ].join("\n"), + allowedMentions: { + parse: [ "users" ] + } + }); + } +}) diff --git a/src/loaders/tags.ts b/src/loaders/tags.ts index 44c8e14..b722997 100644 --- a/src/loaders/tags.ts +++ b/src/loaders/tags.ts @@ -3,6 +3,8 @@ import { readFileSync } from "node:fs"; import { globSync as glob } from "glob"; import { join } from "node:path"; import { Tag } from "../structs/Tag"; +import { APIApplicationCommandOptionChoice } from "discord.js"; +import { safeSlice } from "../util"; const tags = glob(join(__dirname, "..", "..", "data", "tags", "*.md")); @@ -18,3 +20,65 @@ for (const tag of tags) { answer: frontMatter.content }); } + +export function getTags(length: number): APIApplicationCommandOptionChoice[] { + return safeSlice( + TAGS.map((tag) => ( + { + name: `🚀 ${tag.question}`, + value: tag.question + } + )), + length); +} + +export function searchTag(providedQuery: string, multiple?: T): T extends true ? APIApplicationCommandOptionChoice[] : Tag { + const query = providedQuery?.toLowerCase()?.replace(/-/g, " "); + + if (!multiple) { + const exactKeyword = TAGS.find(tag => tag.keywords.find((k) => k.toLowerCase() === query)); + const keywordMatch = TAGS.find(tag => tag.keywords.find((k) => k.toLowerCase().includes(query))); + const questionMatch = TAGS.find(tag => tag.question.toLowerCase().includes(query)); + const answerMatch = TAGS.find(tag => tag.answer.toLowerCase().includes(query)); + + const tag = exactKeyword ?? questionMatch ?? keywordMatch ?? answerMatch; + return tag as T extends true ? APIApplicationCommandOptionChoice[] : Tag; + } + + const exactKeywords: APIApplicationCommandOptionChoice[] = []; + const keywordMatches: APIApplicationCommandOptionChoice[] = []; + const questionMatches: APIApplicationCommandOptionChoice[] = []; + const answerMatches: APIApplicationCommandOptionChoice[] = []; + + for (const tag of TAGS) { + const exactKeyword = tag.keywords.find((t) => t.toLowerCase() === query); + const includesKeyword = tag.keywords.find((t) => t.toLowerCase().includes(query)); + const questionMatch = tag.question.toLowerCase().includes(query); + const answerMatch = tag.answer.toLowerCase().includes(query); + + if (exactKeyword) { + exactKeywords.push({ + name: `✅ ${tag.question}`, + value: tag.question + }); + } else if (includesKeyword) { + keywordMatches.push({ + name: `🔑 ${tag.question}`, + value: tag.question, + }) + } else if(questionMatch) { + questionMatches.push({ + name: `❓ ${tag.question}`, + value: tag.question + }) + } else if (answerMatch) { + answerMatches.push({ + name: `📄 ${tag.question}`, + value: tag.question + }) + } + } + + const tags = [...exactKeywords, ...questionMatches, ...keywordMatches, ...answerMatches]; + return tags as T extends true ? APIApplicationCommandOptionChoice[] : Tag; +} diff --git a/src/structs/context/CommandContext.ts b/src/structs/context/CommandContext.ts index c4b1c49..b1bfdbf 100644 --- a/src/structs/context/CommandContext.ts +++ b/src/structs/context/CommandContext.ts @@ -3,7 +3,6 @@ import type { Command } from "../Command.ts"; export interface CommandContext { command: Command; - isInteraction: T; user: User; member: GuildMember | APIInteractionGuildMember; @@ -21,10 +20,6 @@ export class InteractionCommandContext implements CommandContext { this.interaction = interaction; } - get isInteraction(): true { - return true; - } - get user() { return this.interaction.user; } @@ -49,16 +44,14 @@ export class InteractionCommandContext implements CommandContext { export class MessageCommandContext implements CommandContext { public command: Command; public message: Message; + public options: string[]; public constructor(command: Command, message: Message, args: string[]) { this.command = command; this.message = message; - // change args structure to application commands like - } - - get isInteraction(): false { - return false; + // TODO: change args structure to application commands like + this.options = args; } get user() { diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..4d0777b --- /dev/null +++ b/src/util.ts @@ -0,0 +1,3 @@ +export function safeSlice(array: T[], length: number) { + return array.length > length ? array.slice(0, length) : array; +}