feat: tag system

This commit is contained in:
Jozef Steinhübl 2023-08-25 07:18:16 +02:00
parent d2ef2e9050
commit f3d0fad63d
6 changed files with 161 additions and 14 deletions

View file

@ -1,7 +1,7 @@
import { SlashCommandStringOption, SlashCommandUserOption } from "discord.js"; import { SlashCommandStringOption, SlashCommandUserOption } from "discord.js";
import { defineCommand } from "../loaders/commands"; import { defineCommand } from "../loaders/commands.ts";
import { AutocompleteContext } from "../structs/context/AutocompleteContext"; import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts";
import { InteractionCommandContext } from "../structs/context/CommandContext"; import { InteractionCommandContext } from "../structs/context/CommandContext.ts";
import algoliasearch from "algoliasearch"; import algoliasearch from "algoliasearch";
const algoliaClient = algoliasearch("2527C13E0N", "4efc87205e1fce4a1f267cadcab42cb2"); const algoliaClient = algoliasearch("2527C13E0N", "4efc87205e1fce4a1f267cadcab42cb2");
@ -74,7 +74,6 @@ defineCommand({
content, content,
allowedMentions: { allowedMentions: {
parse: [ "users" ], parse: [ "users" ],
repliedUser: true,
} }
}); });
} }

View file

@ -1,5 +1,6 @@
import "./version.ts"; import "./version.ts";
import "./docs.ts"; import "./docs.ts";
import "./tag.ts";
import { registerCommands } from "../loaders/commands.ts"; import { registerCommands } from "../loaders/commands.ts";
await registerCommands(); await registerCommands();

87
src/commands/tag.ts Normal file
View file

@ -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" ]
}
});
}
})

View file

@ -3,6 +3,8 @@ import { readFileSync } from "node:fs";
import { globSync as glob } from "glob"; import { globSync as glob } from "glob";
import { join } from "node:path"; import { join } from "node:path";
import { Tag } from "../structs/Tag"; import { Tag } from "../structs/Tag";
import { APIApplicationCommandOptionChoice } from "discord.js";
import { safeSlice } from "../util";
const tags = glob(join(__dirname, "..", "..", "data", "tags", "*.md")); const tags = glob(join(__dirname, "..", "..", "data", "tags", "*.md"));
@ -18,3 +20,65 @@ for (const tag of tags) {
answer: frontMatter.content answer: frontMatter.content
}); });
} }
export function getTags(length: number): APIApplicationCommandOptionChoice[] {
return safeSlice(
TAGS.map((tag) => (
{
name: `🚀 ${tag.question}`,
value: tag.question
}
)),
length);
}
export function searchTag<T extends boolean>(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;
}

View file

@ -3,7 +3,6 @@ import type { Command } from "../Command.ts";
export interface CommandContext<T extends boolean> { export interface CommandContext<T extends boolean> {
command: Command; command: Command;
isInteraction: T;
user: User; user: User;
member: GuildMember | APIInteractionGuildMember; member: GuildMember | APIInteractionGuildMember;
@ -21,10 +20,6 @@ export class InteractionCommandContext implements CommandContext<true> {
this.interaction = interaction; this.interaction = interaction;
} }
get isInteraction(): true {
return true;
}
get user() { get user() {
return this.interaction.user; return this.interaction.user;
} }
@ -49,16 +44,14 @@ export class InteractionCommandContext implements CommandContext<true> {
export class MessageCommandContext implements CommandContext<false> { export class MessageCommandContext implements CommandContext<false> {
public command: Command; public command: Command;
public message: Message; public message: Message;
public options: string[];
public constructor(command: Command, message: Message, args: string[]) { public constructor(command: Command, message: Message, args: string[]) {
this.command = command; this.command = command;
this.message = message; this.message = message;
// change args structure to application commands like // TODO: change args structure to application commands like
} this.options = args;
get isInteraction(): false {
return false;
} }
get user() { get user() {

3
src/util.ts Normal file
View file

@ -0,0 +1,3 @@
export function safeSlice<T>(array: T[], length: number) {
return array.length > length ? array.slice(0, length) : array;
}