From 8e6a395e59dff7afa9373d52d4fefad886d09003 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 18:43:55 +0300 Subject: [PATCH 01/43] feat: Switch from dynamic import to a more explicit approach --- src/commands/docs/index.ts | 2 +- src/commands/guides/index.ts | 2 +- src/commands/ping.ts | 2 +- src/commands/tips/index.ts | 2 +- src/events/has-var.ts | 2 +- src/events/interaction-create.ts | 6 +- src/events/just-ask.ts | 2 +- src/events/ready.ts | 2 +- src/index.ts | 13 +-- src/util/loaders.ts | 134 ++++++++----------------------- 10 files changed, 48 insertions(+), 119 deletions(-) diff --git a/src/commands/docs/index.ts b/src/commands/docs/index.ts index 03074f0..b4b5046 100644 --- a/src/commands/docs/index.ts +++ b/src/commands/docs/index.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType } from 'discord.js'; import { createCommands } from '../index.js'; import { type DocProvider, docProviders, executeDocCommand } from './providers.js'; -export default createCommands( +export const docsCommands = createCommands( Object.entries(docProviders).map(([providerKey, providerConfig]) => ({ data: { name: providerKey, diff --git a/src/commands/guides/index.ts b/src/commands/guides/index.ts index f09bc3c..b4db47d 100644 --- a/src/commands/guides/index.ts +++ b/src/commands/guides/index.ts @@ -18,7 +18,7 @@ const loadChoices = async (): Promise => { await loadChoices(); -export default createCommand( +export const guidesCommand = createCommand( { name: 'guides', description: 'Get a guide on a specific subject', diff --git a/src/commands/ping.ts b/src/commands/ping.ts index bffa048..5739a00 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,6 +1,6 @@ import { createCommand } from './index.js'; -export default createCommand( +export const pingCommand = createCommand( { name: 'ping', description: 'Replies with Pong!', diff --git a/src/commands/tips/index.ts b/src/commands/tips/index.ts index 3f8e534..be188b6 100644 --- a/src/commands/tips/index.ts +++ b/src/commands/tips/index.ts @@ -92,4 +92,4 @@ const contextMenuCommands = Array.from(subjectChoices).map(([key, value]) => ) ); -export default [slashCommand, ...contextMenuCommands]; +export const tipsCommands = [slashCommand, ...contextMenuCommands]; diff --git a/src/events/has-var.ts b/src/events/has-var.ts index 05ee17d..2f2e627 100644 --- a/src/events/has-var.ts +++ b/src/events/has-var.ts @@ -37,7 +37,7 @@ const hasVarDeclaration = (code: string, language: string): boolean => { } }; -export default createEvent( +export const hasVarEvent = createEvent( { name: Events.MessageCreate, once: false, diff --git a/src/events/interaction-create.ts b/src/events/interaction-create.ts index af4aa6e..7887a02 100644 --- a/src/events/interaction-create.ts +++ b/src/events/interaction-create.ts @@ -1,10 +1,8 @@ import { Events } from 'discord.js'; -import { getCommands } from '../util/loaders.js'; +import { commands } from '../util/loaders.js'; import { createEvent } from './index.js'; -const commands = await getCommands(new URL('../commands/', import.meta.url)); - -export default createEvent( +export const interactionCreateEvent = createEvent( { name: Events.InteractionCreate, }, diff --git a/src/events/just-ask.ts b/src/events/just-ask.ts index 7942d92..f71b12e 100644 --- a/src/events/just-ask.ts +++ b/src/events/just-ask.ts @@ -33,7 +33,7 @@ const [response] = await loadMarkdownOptions<{ name: string }>( const { canRun, reset } = rateLimit(10 * MINUTE); -export default createEvent( +export const justAskEvent = createEvent( { name: Events.MessageCreate, }, diff --git a/src/events/ready.ts b/src/events/ready.ts index a84ff3c..c0eb24b 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,7 +1,7 @@ import { Events } from 'discord.js'; import { createEvent } from './index.js'; -export default createEvent( +export const readyEvent = createEvent( { name: Events.ClientReady, once: true, diff --git a/src/index.ts b/src/index.ts index d6fb793..8c5b255 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { ActivityType, Client, GatewayIntentBits } from 'discord.js'; import { config } from './env.js'; -import { getCommands, getEvents, loadCommands, loadEvents } from './util/loaders.js'; +import { commands, events, registerCommands, registerEvents } from './util/loaders.js'; // Create a new client instance const client = new Client({ @@ -22,13 +22,8 @@ const client = new Client({ }, }); -// Load events and commands -const events = await getEvents(new URL('events/', import.meta.url)); -const commands = await getCommands(new URL('commands/', import.meta.url)); - -// use path utils etc to get the paths - -await loadEvents(client, events); -await loadCommands(client, commands); +// Register events and commands +await registerEvents(client, events); +await registerCommands(client, commands); void client.login(config.discord.token); diff --git a/src/util/loaders.ts b/src/util/loaders.ts index 2904e6d..d99666c 100644 --- a/src/util/loaders.ts +++ b/src/util/loaders.ts @@ -1,8 +1,14 @@ -import type { PathLike } from 'node:fs'; -import { readdir, stat } from 'node:fs/promises'; import type { Client } from 'discord.js'; +import { docsCommands } from '../commands/docs/index.js'; +import { guidesCommand } from '../commands/guides/index.js'; import { type Command, predicate as commandPredicate } from '../commands/index.js'; +import { pingCommand } from '../commands/ping.js'; +import { tipsCommands } from '../commands/tips/index.js'; +import { hasVarEvent } from '../events/has-var.js'; import { type DiscordEvent, predicate as eventPredicate } from '../events/index.js'; +import { interactionCreateEvent } from '../events/interaction-create.js'; +import { justAskEvent } from '../events/just-ask.js'; +import { readyEvent } from '../events/ready.js'; /** * A predicate to check if the structure is valid @@ -10,105 +16,12 @@ import { type DiscordEvent, predicate as eventPredicate } from '../events/index. export type StructurePredicate = (structure: unknown) => structure is T; /** - * Loads all structures in the provided directory - * - * @param dir - The directory to load the structures from - * @param predicate - The predicate to check if the structure is valid - * @param recursive- Whether to load structures recursively - * @returns - */ -export const loadStructures = async ( - dir: PathLike, - predicate: StructurePredicate, - recursive = true -): Promise => { - const statDir = await stat(dir); - - if (!statDir.isDirectory()) { - throw new Error(`The path ${dir} is not a directory`); - } - - // Get all files in the directory - const files = await readdir(dir); - - // Create an empty array to store the structures - const structures: T[] = []; - - // Loop through all files in the directory - for (const file of files) { - const fileUrl = new URL(`${dir}/${file}`, import.meta.url); - - // Get the stats of the file - const fileStat = await stat(fileUrl); - - // If the file is a directory and recursive is true, load the structures in the directory - if (fileStat.isDirectory() && recursive) { - structures.push(...(await loadStructures(fileUrl, predicate, recursive))); - continue; - } - - // If the file is index.js or the file does not end with .js, skip it - if ( - // file === 'index.js' || - // file === 'index.ts' || - !file.endsWith('.js') && - !file.endsWith('.ts') - ) { - continue; - } - - // Import the structure from the file - const { default: structure } = await import(fileUrl.href); - - // If the structure is an array, loop through all structures in the array and check if they are valid - // If the structure is not an array, check if it is valid - if (Array.isArray(structure)) { - for (const str of structure) { - if (predicate(str)) { - structures.push(str); - } - } - } else if (predicate(structure)) { - structures.push(structure); - } - } - return structures; -}; - -/** - * Gets all the commands in the provided directory - * - * @param dir - The directory to load the commands from - * @param recursive - Whether to load commands recursively - * @returns A map of command names to commands - */ -export const getCommands = async ( - dir: PathLike, - recursive = true -): Promise> => { - const commands = await loadStructures(dir, commandPredicate, recursive); - - return new Map(commands.map((command) => [command.data.name, command])); -}; - -/** - * Gets all the events in the provided directory - * - * @param dir - The directory to load the events from - * @param recursive - Whether to load events recursively - * @returns An array of events - */ -export const getEvents = async (dir: PathLike, recursive = true): Promise => { - return loadStructures(dir, eventPredicate, recursive); -}; - -/** - * Loads commands to the Discord API + * Register commands to the Discord API * * @param client - The Discord client * @param commands - A map of command names to commands */ -export const loadCommands = async ( +export const registerCommands = async ( client: Client, commands: Map ): Promise => { @@ -128,12 +41,12 @@ export const loadCommands = async ( }; /** - * Loads events to the Discord client + * Register events to the Discord client * * @param client - The Discord client * @param events - An array of events */ -export const loadEvents = async (client: Client, events: DiscordEvent[]): Promise => { +export const registerEvents = async (client: Client, events: DiscordEvent[]): Promise => { // Loop through all events for (const event of events) { console.log(`Loading event: ${event.name}`); @@ -148,3 +61,26 @@ export const loadEvents = async (client: Client, events: DiscordEvent[]): Promis }); } }; + +/** + * + * @returns An array of events + */ +const loadEvents = (): DiscordEvent[] => { + const events = [hasVarEvent, readyEvent, justAskEvent, interactionCreateEvent].filter( + eventPredicate + ); + return events as DiscordEvent[]; +}; + +/** + * + * @returns A map of command names to commands + */ +const loadCommands = (): Map => { + const commands = [pingCommand, tipsCommands, guidesCommand, docsCommands].flat(); + return new Map(commands.filter(commandPredicate).map((command) => [command.data.name, command])); +}; + +export const commands = loadCommands(); +export const events = loadEvents(); From eff689d62b239d9f6282d2286a63b8943e2e027d Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 18:53:54 +0300 Subject: [PATCH 02/43] fix: Fix deploy.ts to use the exported commands Map --- src/util/deploy.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/util/deploy.ts b/src/util/deploy.ts index 349c2fc..14c00b6 100644 --- a/src/util/deploy.ts +++ b/src/util/deploy.ts @@ -1,12 +1,9 @@ -// deploy.ts -import { URL } from 'node:url'; import { API } from '@discordjs/core/http-only'; import { REST, type RESTPutAPIApplicationCommandsResult } from 'discord.js'; import { config } from '../env.js'; -import { getCommands } from './loaders.js'; +import { commands } from './loaders.js'; export async function deployCommands(): Promise { - const commands = await getCommands(new URL('../commands/', import.meta.url)); const commandData = [...commands.values()].map((command) => command.data); const rest = new REST({ version: '10' }).setToken(config.discord.token); From 9e558f4b5f99266f485cf437542aac403299f258 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:22:11 +0300 Subject: [PATCH 03/43] feat: Add typecheck script and remove zod --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9bf5758..16de36f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "format": "biome format --write .", "check": "biome check .", "check:fix": "biome check --write .", + "typecheck": "tsc --noEmit", "test": "node --test", "prepare": "husky", "pre-commit": "lint-staged" @@ -35,8 +36,7 @@ "lint-staged": "^16.2.1", "tsup": "^8.5.0", "tsx": "^4.20.6", - "typescript": "^5.9.2", - "zod": "^4.1.11" + "typescript": "^5.9.2" }, "lint-staged": { "*.{ts,js}": [ From 6660944cfafe180c20bd4b4d5eaffcf9410e8bbb Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:22:38 +0300 Subject: [PATCH 04/43] feat: remove zod --- pnpm-lock.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c60c2b5..6e0aa3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,9 +36,6 @@ importers: typescript: specifier: ^5.9.2 version: 5.9.2 - zod: - specifier: ^4.1.11 - version: 4.1.11 packages: @@ -970,9 +967,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - zod@4.1.11: - resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} - snapshots: '@biomejs/biome@2.2.4': @@ -1798,5 +1792,3 @@ snapshots: ws@8.18.3: {} yaml@2.8.1: {} - - zod@4.1.11: {} From 4cd0d16df963fb10daac22c3221efbf9b8e79d98 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:23:42 +0300 Subject: [PATCH 05/43] feat: create a file for event types --- src/events/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/events/types.ts diff --git a/src/events/types.ts b/src/events/types.ts new file mode 100644 index 0000000..ad6db08 --- /dev/null +++ b/src/events/types.ts @@ -0,0 +1,7 @@ +import type { ClientEvents } from 'discord.js'; + +export type DiscordEvent = { + name: T; + once?: boolean; + execute: (...args: ClientEvents[T]) => Promise | void; +}; From ce1c40fe3bebdab32856f347c1b383ad741ba8ff Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:24:33 +0300 Subject: [PATCH 06/43] feat: create event utils --- src/util/events.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/util/events.ts diff --git a/src/util/events.ts b/src/util/events.ts new file mode 100644 index 0000000..3636182 --- /dev/null +++ b/src/util/events.ts @@ -0,0 +1,37 @@ +import type { Client, ClientEvents } from 'discord.js'; +import type { DiscordEvent } from '../events/types.js'; + +export const createEvent = ( + data: { + name: T; + once?: boolean; + }, + execute: (...args: ClientEvents[T]) => Promise | void +): DiscordEvent => { + return { ...data, execute } satisfies DiscordEvent; +}; + +export const createEvents = ( + events: Array<{ + data: { + name: T; + once?: boolean; + }; + execute: (...args: ClientEvents[T]) => Promise | void; + }> +): DiscordEvent[] => { + return events.map(({ data, execute }) => createEvent(data, execute)); +}; + +export const registerEvents = async (client: Client, events: DiscordEvent[]): Promise => { + for (const event of events) { + console.log(`Loading event: ${event.name}`); + client[event.once ? 'once' : 'on'](event.name, async (...args) => { + try { + await event.execute(...args); + } catch (error) { + console.error(`Error executing event ${event.name}:`, error); + } + }); + } +}; From 9847659b5700eddf837c95f639e2ce955106036b Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:24:57 +0300 Subject: [PATCH 07/43] feat: add events barrel export --- src/events/index.ts | 92 ++++++--------------------------------------- 1 file changed, 12 insertions(+), 80 deletions(-) diff --git a/src/events/index.ts b/src/events/index.ts index 9f4f72b..f4c5fd0 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,80 +1,12 @@ -import type { ClientEvents } from 'discord.js'; -import z from 'zod'; -import type { StructurePredicate } from '../util/loaders.js'; - -/** - * Defines the structure of an Event - */ -export type DiscordEvent = { - /** - * The name of the event to listen to - */ - name: T; - /** - * Whether the event should be listened to only once - * - * @default false - */ - once?: boolean; - /** - * The function to execute when the event is triggered - * - * @param args - The arguments passed by the event - */ - execute: (...args: ClientEvents[T]) => Promise | void; - - __isEvent__: true; -}; - -/** - * Defines the schema for an event - */ -export const schema = z.object({ - name: z.string(), - once: z.boolean().optional().default(false), - execute: z.function(), - __isEvent__: z.literal(true), -}); - -/** - * Defines the predicate to check if an object is a valid Event type. - */ -export const predicate: StructurePredicate = (obj: unknown): obj is DiscordEvent => - schema.safeParse(obj).success; - -/** - * - * Creates an event object - * - * @param data - The event data - * @param execute - The function to execute when the event is triggered - * @returns - */ -export const createEvent = ( - data: { - name: T; - once?: boolean; - }, - execute: (...args: ClientEvents[T]) => Promise | void -): DiscordEvent => { - return { ...data, execute, __isEvent__: true } satisfies DiscordEvent; -}; - -/** - * - * Creates multiple events - * - * @param events - An array of event data and execute functions - * @returns - */ -export const createEvents = ( - events: Array<{ - data: { - name: T; - once?: boolean; - }; - execute: (...args: ClientEvents[T]) => Promise | void; - }> -): DiscordEvent[] => { - return events.map(({ data, execute }) => createEvent(data, execute)); -}; +import { hasVarEvent } from './has-var.js'; +import { interactionCreateEvent } from './interaction-create.js'; +import { justAskEvent } from './just-ask.js'; +import { readyEvent } from './ready.js'; +import type { DiscordEvent } from './types.js'; + +export const events = [ + readyEvent, + justAskEvent, + hasVarEvent, + interactionCreateEvent, +] as DiscordEvent[]; From a09150dab9d808ed1d7d961e0c10133c6ec329a0 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:25:21 +0300 Subject: [PATCH 08/43] feat: add commands type --- src/commands/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/commands/types.ts diff --git a/src/commands/types.ts b/src/commands/types.ts new file mode 100644 index 0000000..b377ad0 --- /dev/null +++ b/src/commands/types.ts @@ -0,0 +1,6 @@ +import type { CommandInteraction, RESTPostAPIApplicationCommandsJSONBody } from 'discord.js'; + +export type Command = { + data: RESTPostAPIApplicationCommandsJSONBody; + execute: (interaction: CommandInteraction) => Promise | void; +}; From 29ad0859e2bce4f4af81257f139fc5d68976c07b Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:25:40 +0300 Subject: [PATCH 09/43] feat: add commands utils --- src/util/commands.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/util/commands.ts diff --git a/src/util/commands.ts b/src/util/commands.ts new file mode 100644 index 0000000..219f806 --- /dev/null +++ b/src/util/commands.ts @@ -0,0 +1,39 @@ +import type { + Client, + CommandInteraction, + RESTPostAPIApplicationCommandsJSONBody, +} from 'discord.js'; +import type { Command } from '../commands/types.js'; + +export const createCommand = ( + data: RESTPostAPIApplicationCommandsJSONBody, + execute: (interaction: CommandInteraction) => Promise | void +): Command => { + return { data, execute } satisfies Command; +}; + +export const createCommands = ( + commands: Array<{ + data: RESTPostAPIApplicationCommandsJSONBody; + execute: (interaction: CommandInteraction) => Promise | void; + }> +): Command[] => { + return commands.map(({ data, execute }) => createCommand(data, execute)); +}; + +export const registerCommands = async ( + client: Client, + commands: Map +): Promise => { + const commandArray = Array.from(commands.values()).map((cmd) => cmd.data); + + try { + await client.application?.commands.set(commandArray); + commandArray.forEach((cmd) => { + console.log(`Registered command: ${cmd.type}, ${cmd.name}`); + }); + console.log(`Registered ${commandArray.length} commands globally.`); + } catch (error) { + console.error('Error registering commands:', error); + } +}; From 07356b6785d4b2a41f6378ba7642730f3dbf20b0 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:26:15 +0300 Subject: [PATCH 10/43] feat: add commands barrel export --- src/commands/index.ts | 72 ++++++------------------------------------- 1 file changed, 9 insertions(+), 63 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index f95a9e6..dc97772 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,63 +1,9 @@ -import type { CommandInteraction, RESTPostAPIApplicationCommandsJSONBody } from 'discord.js'; -import { z } from 'zod'; -import type { StructurePredicate } from '../util/loaders.js'; - -export type Command = { - /** - * The data for the command - */ - data: RESTPostAPIApplicationCommandsJSONBody; - /** - * The function execute when the command is called - * - * @param interaction - The interaction that triggered the command - */ - execute: (interaction: CommandInteraction) => Promise | void; - - __isCommand__: true; -}; - -/** - * Defines a schema for a command - */ -export const schema = z.object({ - data: z.custom(), - execute: z.function(), - __isCommand__: z.literal(true), -}); - -/** - * Defines the predicate to check if an object is a Command - */ -export const predicate: StructurePredicate = (obj: unknown): obj is Command => - schema.safeParse(obj).success; - -/** - * - * Creates a command object - * - * @param data - The command data - * @param execute - The function to execute when the command is called - * @returns - */ -export const createCommand = ( - data: RESTPostAPIApplicationCommandsJSONBody, - execute: (interaction: CommandInteraction) => Promise | void -): Command => { - return { data, execute, __isCommand__: true } satisfies Command; -}; - -/** - * Creates multiple commands - * - * @param commands - An array of command data and execute functions - * @returns - */ -export const createCommands = ( - commands: Array<{ - data: RESTPostAPIApplicationCommandsJSONBody; - execute: (interaction: CommandInteraction) => Promise | void; - }> -): Command[] => { - return commands.map(({ data, execute }) => createCommand(data, execute)); -}; +import { docsCommands } from './docs/index.js'; +import { guidesCommand } from './guides/index.js'; +import { pingCommand } from './ping.js'; +import { tipsCommands } from './tips/index.js'; +import type { Command } from './types.js'; + +export const commands = new Map( + [pingCommand, guidesCommand, docsCommands, tipsCommands].flat().map((cmd) => [cmd.data.name, cmd]) +); From 18d612e847d16b9732701557decd254711440c57 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:27:05 +0300 Subject: [PATCH 11/43] refactor: adjust imports paths --- src/commands/docs/index.ts | 2 +- src/commands/guides/index.ts | 2 +- src/commands/ping.ts | 2 +- src/commands/tips/index.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/docs/index.ts b/src/commands/docs/index.ts index b4b5046..f120800 100644 --- a/src/commands/docs/index.ts +++ b/src/commands/docs/index.ts @@ -1,5 +1,5 @@ import { ApplicationCommandOptionType } from 'discord.js'; -import { createCommands } from '../index.js'; +import { createCommands } from '../../util/commands.js'; import { type DocProvider, docProviders, executeDocCommand } from './providers.js'; export const docsCommands = createCommands( diff --git a/src/commands/guides/index.ts b/src/commands/guides/index.ts index b4db47d..a02ca39 100644 --- a/src/commands/guides/index.ts +++ b/src/commands/guides/index.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, MessageFlags } from 'discord.js'; import { logToChannel } from '../../util/channel-logging.js'; +import { createCommand } from '../../util/commands.js'; import { loadMarkdownOptions } from '../../util/markdown.js'; -import { createCommand } from '../index.js'; const subjectsDir = new URL('./subjects/', import.meta.url); const subjectChoices = new Map(); diff --git a/src/commands/ping.ts b/src/commands/ping.ts index 5739a00..d0657d3 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,4 +1,4 @@ -import { createCommand } from './index.js'; +import { createCommand } from '../util/commands.js'; export const pingCommand = createCommand( { diff --git a/src/commands/tips/index.ts b/src/commands/tips/index.ts index be188b6..0970fd1 100644 --- a/src/commands/tips/index.ts +++ b/src/commands/tips/index.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, MessageFlags } from 'discord.js'; import { logToChannel } from '../../util/channel-logging.js'; +import { createCommand } from '../../util/commands.js'; import { loadMarkdownOptions } from '../../util/markdown.js'; -import { createCommand } from '../index.js'; const subjectsDir = new URL('./subjects/', import.meta.url); const subjectChoices = new Map(); From 8f6ddba85b064fed2f4332b5197eac11c0865f1f Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:27:53 +0300 Subject: [PATCH 12/43] refactor: adjust events import paths --- src/events/has-var.ts | 3 +-- src/events/interaction-create.ts | 4 ++-- src/events/just-ask.ts | 2 +- src/events/ready.ts | 2 +- src/util/deploy.ts | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/events/has-var.ts b/src/events/has-var.ts index 2f2e627..951ee1a 100644 --- a/src/events/has-var.ts +++ b/src/events/has-var.ts @@ -1,9 +1,9 @@ import { Events } from 'discord.js'; import ts from 'typescript'; import { MINUTE } from '../constants/time.js'; +import { createEvent } from '../util/events.js'; import { codeBlockRegex } from '../util/message.js'; import { rateLimit } from '../util/rate-limit.js'; -import { createEvent } from './index.js'; const { canRun, reset } = rateLimit(5 * MINUTE); @@ -64,7 +64,6 @@ export const hasVarEvent = createEvent( } } } - return; } ); diff --git a/src/events/interaction-create.ts b/src/events/interaction-create.ts index 7887a02..98a82c5 100644 --- a/src/events/interaction-create.ts +++ b/src/events/interaction-create.ts @@ -1,6 +1,6 @@ import { Events } from 'discord.js'; -import { commands } from '../util/loaders.js'; -import { createEvent } from './index.js'; +import { commands } from '../commands/index.js'; +import { createEvent } from '../util/events.js'; export const interactionCreateEvent = createEvent( { diff --git a/src/events/just-ask.ts b/src/events/just-ask.ts index f71b12e..8029c52 100644 --- a/src/events/just-ask.ts +++ b/src/events/just-ask.ts @@ -1,8 +1,8 @@ import { Events } from 'discord.js'; import { MINUTE } from '../constants/time.js'; +import { createEvent } from '../util/events.js'; import { loadMarkdownOptions } from '../util/markdown.js'; import { rateLimit } from '../util/rate-limit.js'; -import { createEvent } from './index.js'; // Subject patterns (who) const reSubject = `(?:(?:any|some|no|every)(?:one|body)|people|folks|peeps|who)`; diff --git a/src/events/ready.ts b/src/events/ready.ts index c0eb24b..9b5bbcb 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,5 +1,5 @@ import { Events } from 'discord.js'; -import { createEvent } from './index.js'; +import { createEvent } from '../util/events.js'; export const readyEvent = createEvent( { diff --git a/src/util/deploy.ts b/src/util/deploy.ts index 14c00b6..9a089a3 100644 --- a/src/util/deploy.ts +++ b/src/util/deploy.ts @@ -1,7 +1,7 @@ import { API } from '@discordjs/core/http-only'; import { REST, type RESTPutAPIApplicationCommandsResult } from 'discord.js'; +import { commands } from '../commands/index.js'; import { config } from '../env.js'; -import { commands } from './loaders.js'; export async function deployCommands(): Promise { const commandData = [...commands.values()].map((command) => command.data); From 8a9fc4c94f8465817603f450f2e3b6ae3c947246 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:29:11 +0300 Subject: [PATCH 13/43] refactor: update index.ts to accomodate barrel exports and utils --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8c5b255..af17536 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,9 @@ import { ActivityType, Client, GatewayIntentBits } from 'discord.js'; +import { commands } from './commands/index.js'; import { config } from './env.js'; -import { commands, events, registerCommands, registerEvents } from './util/loaders.js'; +import { events } from './events/index.js'; +import { registerCommands } from './util/commands.js'; +import { registerEvents } from './util/events.js'; // Create a new client instance const client = new Client({ From 1baab2172a002163dbf9ac9e3a25ecbcc6147fca Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:30:01 +0300 Subject: [PATCH 14/43] fix: fix bug in rate limiter --- src/util/rate-limit.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/util/rate-limit.ts b/src/util/rate-limit.ts index 95750ed..f2d25b3 100644 --- a/src/util/rate-limit.ts +++ b/src/util/rate-limit.ts @@ -22,8 +22,7 @@ export const rateLimit = (ms: number) => { */ const canRun = () => { const now = Date.now(); - if (now - last >= ms) { - last = now; + if (last === 0 || now - last >= ms) { return true; } return false; From c18073992bd51b4132ae73d5bd1d95c2f99b8eb2 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 5 Oct 2025 20:30:32 +0300 Subject: [PATCH 15/43] feat: remove loaders files, logic moved to other files --- src/util/loaders.ts | 86 --------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 src/util/loaders.ts diff --git a/src/util/loaders.ts b/src/util/loaders.ts deleted file mode 100644 index d99666c..0000000 --- a/src/util/loaders.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Client } from 'discord.js'; -import { docsCommands } from '../commands/docs/index.js'; -import { guidesCommand } from '../commands/guides/index.js'; -import { type Command, predicate as commandPredicate } from '../commands/index.js'; -import { pingCommand } from '../commands/ping.js'; -import { tipsCommands } from '../commands/tips/index.js'; -import { hasVarEvent } from '../events/has-var.js'; -import { type DiscordEvent, predicate as eventPredicate } from '../events/index.js'; -import { interactionCreateEvent } from '../events/interaction-create.js'; -import { justAskEvent } from '../events/just-ask.js'; -import { readyEvent } from '../events/ready.js'; - -/** - * A predicate to check if the structure is valid - */ -export type StructurePredicate = (structure: unknown) => structure is T; - -/** - * Register commands to the Discord API - * - * @param client - The Discord client - * @param commands - A map of command names to commands - */ -export const registerCommands = async ( - client: Client, - commands: Map -): Promise => { - // Convert the commands map to an array of command data - const commandArray = Array.from(commands.values()).map((cmd) => cmd.data); - - try { - // Register commands globally - await client.application?.commands.set(commandArray); - commandArray.forEach((cmd) => { - console.log(`Registered command: ${cmd.type}, ${cmd.name}`); - }); - console.log(`Registered ${commandArray.length} commands globally.`); - } catch (error) { - console.error('Error registering commands:', error); - } -}; - -/** - * Register events to the Discord client - * - * @param client - The Discord client - * @param events - An array of events - */ -export const registerEvents = async (client: Client, events: DiscordEvent[]): Promise => { - // Loop through all events - for (const event of events) { - console.log(`Loading event: ${event.name}`); - // Register the event - // If the event should be registered once, use "once", otherwise use "on" - client[event.once ? 'once' : 'on'](event.name, async (...args) => { - try { - await event.execute(...args); - } catch (error) { - console.error(`Error executing event ${event.name}:`, error); - } - }); - } -}; - -/** - * - * @returns An array of events - */ -const loadEvents = (): DiscordEvent[] => { - const events = [hasVarEvent, readyEvent, justAskEvent, interactionCreateEvent].filter( - eventPredicate - ); - return events as DiscordEvent[]; -}; - -/** - * - * @returns A map of command names to commands - */ -const loadCommands = (): Map => { - const commands = [pingCommand, tipsCommands, guidesCommand, docsCommands].flat(); - return new Map(commands.filter(commandPredicate).map((command) => [command.data.name, command])); -}; - -export const commands = loadCommands(); -export const events = loadEvents(); From e459fb23e3c0ac18d87259162216ae5c2dc16610 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 00:30:29 +0300 Subject: [PATCH 16/43] feat: Add clampText util --- src/util/text.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/util/text.ts diff --git a/src/util/text.ts b/src/util/text.ts new file mode 100644 index 0000000..5e49272 --- /dev/null +++ b/src/util/text.ts @@ -0,0 +1,4 @@ +export const clampText = (text: string, maxLength: number): string => { + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength - 3)}...`; +}; From b8f126d4dbea79ac71f3ca5e33b387ce4b9383a2 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 01:09:55 +0300 Subject: [PATCH 17/43] feat: add web-features lib add lib add patch to export types (no idea why they're not exporting them) --- package.json | 3 +- patches/web-features@3.3.0.patch | 13 ++ pnpm-lock.yaml | 265 ++++++++++++++++--------------- pnpm-workspace.yaml | 2 + 4 files changed, 156 insertions(+), 127 deletions(-) create mode 100644 patches/web-features@3.3.0.patch create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index 16de36f..9e11205 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "packageManager": "pnpm@10.17.1", "dependencies": { "@discordjs/core": "^2.2.2", - "discord.js": "^14.22.1" + "discord.js": "^14.22.1", + "web-features": "^3.3.0" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/patches/web-features@3.3.0.patch b/patches/web-features@3.3.0.patch new file mode 100644 index 0000000..9ffecd3 --- /dev/null +++ b/patches/web-features@3.3.0.patch @@ -0,0 +1,13 @@ +diff --git a/package.json b/package.json +index ab2c3c3cf87d40fd6d0f4c1735a528059f719e7e..d020595138c719be06e1c663a91b9607bdcbb9c4 100644 +--- a/package.json ++++ b/package.json +@@ -12,6 +12,8 @@ + "main": "index.js", + "exports": { + ".": "./index.js", ++ "./types": "./types.d.ts", ++ "./types.quicktype": "./types.quicktype.d.ts", + "./data.json": "./data.json", + "./data.schema.json": "./data.schema.json" + }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e0aa3e..994059e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + web-features@3.3.0: + hash: 9e31634375f7275a9f9f1a072864cc91eec6eda1d068e935fe6773d4463df310 + path: patches/web-features@3.3.0.patch + importers: .: @@ -14,13 +19,16 @@ importers: discord.js: specifier: ^14.22.1 version: 14.22.1 + web-features: + specifier: ^3.3.0 + version: 3.3.0(patch_hash=9e31634375f7275a9f9f1a072864cc91eec6eda1d068e935fe6773d4463df310) devDependencies: '@biomejs/biome': specifier: 2.2.4 version: 2.2.4 '@types/node': specifier: ^24.5.2 - version: 24.5.2 + version: 24.7.0 husky: specifier: ^9.1.7 version: 9.1.7 @@ -29,13 +37,13 @@ importers: version: 16.2.3 tsup: specifier: ^8.5.0 - version: 8.5.0(tsx@4.20.6)(typescript@5.9.2)(yaml@2.8.1) + version: 8.5.0(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) tsx: specifier: ^4.20.6 version: 4.20.6 typescript: specifier: ^5.9.2 - version: 5.9.2 + version: 5.9.3 packages: @@ -305,113 +313,113 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.52.3': - resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.3': - resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.3': - resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.3': - resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.3': - resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.3': - resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': - resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.3': - resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.3': - resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.3': - resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.3': - resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.3': - resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.3': - resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.3': - resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.3': - resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.3': - resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.3': - resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.3': - resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.3': - resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.3': - resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.3': - resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.3': - resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} cpu: [x64] os: [win32] @@ -434,14 +442,14 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/node@24.5.2': - resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@vladfrangu/async_event_emitter@2.4.6': - resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} acorn@8.15.0: @@ -542,8 +550,8 @@ packages: supports-color: optional: true - discord-api-types@0.38.26: - resolution: {integrity: sha512-xpmPviHjIJ6dFu1eNwNDIGQ3N6qmPUUYFVAx/YZ64h7ZgPkTcKjnciD8bZe8Vbeji7yS5uYljyciunpq0J5NSw==} + discord-api-types@0.38.28: + resolution: {integrity: sha512-QwgoJb+83O8Cx0bhHdI/Y9cQIHRvzy8lKXzSQOmzHEf8InuJMEWrzYk94f+OncHk3qWOqBdr9i0DjtXp4i+NHg==} discord.js@14.22.1: resolution: {integrity: sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==} @@ -795,8 +803,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.52.3: - resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -912,21 +920,24 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - undici-types@7.12.0: - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} undici@6.21.3: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + web-features@3.3.0: + resolution: {integrity: sha512-hg7CKhLUTKEi4zRrFXs1sEmu0kFK+hGCZHalw+nkyBN5X+lgNzs22q7WlNqZfe2avqB+AVuEhONytMgaq/SgxA==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -1009,7 +1020,7 @@ snapshots: '@discordjs/formatters': 0.6.1 '@discordjs/util': 1.1.1 '@sapphire/shapeshift': 4.0.0 - discord-api-types: 0.38.26 + discord-api-types: 0.38.28 fast-deep-equal: 3.1.3 ts-mixer: 6.0.4 tslib: 2.8.1 @@ -1024,15 +1035,15 @@ snapshots: '@discordjs/util': 1.1.1 '@discordjs/ws': 2.0.3 '@sapphire/snowflake': 3.5.5 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.26 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.28 transitivePeerDependencies: - bufferutil - utf-8-validate '@discordjs/formatters@0.6.1': dependencies: - discord-api-types: 0.38.26 + discord-api-types: 0.38.28 '@discordjs/rest@2.6.0': dependencies: @@ -1040,8 +1051,8 @@ snapshots: '@discordjs/util': 1.1.1 '@sapphire/async-queue': 1.5.5 '@sapphire/snowflake': 3.5.5 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.26 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.28 magic-bytes.js: 1.12.1 tslib: 2.8.1 undici: 6.21.3 @@ -1055,8 +1066,8 @@ snapshots: '@discordjs/util': 1.1.1 '@sapphire/async-queue': 1.5.5 '@types/ws': 8.18.1 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.26 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.28 tslib: 2.8.1 ws: 8.18.3 transitivePeerDependencies: @@ -1070,8 +1081,8 @@ snapshots: '@discordjs/util': 1.1.1 '@sapphire/async-queue': 1.5.5 '@types/ws': 8.18.1 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.26 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.28 tslib: 2.8.1 ws: 8.18.3 transitivePeerDependencies: @@ -1182,70 +1193,70 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.52.3': + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true - '@rollup/rollup-android-arm64@4.52.3': + '@rollup/rollup-android-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-arm64@4.52.3': + '@rollup/rollup-darwin-arm64@4.52.4': optional: true - '@rollup/rollup-darwin-x64@4.52.3': + '@rollup/rollup-darwin-x64@4.52.4': optional: true - '@rollup/rollup-freebsd-arm64@4.52.3': + '@rollup/rollup-freebsd-arm64@4.52.4': optional: true - '@rollup/rollup-freebsd-x64@4.52.3': + '@rollup/rollup-freebsd-x64@4.52.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.3': + '@rollup/rollup-linux-arm-musleabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.3': + '@rollup/rollup-linux-arm64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.3': + '@rollup/rollup-linux-arm64-musl@4.52.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.3': + '@rollup/rollup-linux-loong64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.3': + '@rollup/rollup-linux-ppc64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.3': + '@rollup/rollup-linux-riscv64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.3': + '@rollup/rollup-linux-riscv64-musl@4.52.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.3': + '@rollup/rollup-linux-s390x-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.3': + '@rollup/rollup-linux-x64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-x64-musl@4.52.3': + '@rollup/rollup-linux-x64-musl@4.52.4': optional: true - '@rollup/rollup-openharmony-arm64@4.52.3': + '@rollup/rollup-openharmony-arm64@4.52.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.3': + '@rollup/rollup-win32-arm64-msvc@4.52.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.3': + '@rollup/rollup-win32-ia32-msvc@4.52.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.3': + '@rollup/rollup-win32-x64-gnu@4.52.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.3': + '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true '@sapphire/async-queue@1.5.5': {} @@ -1261,15 +1272,15 @@ snapshots: '@types/estree@1.0.8': {} - '@types/node@24.5.2': + '@types/node@24.7.0': dependencies: - undici-types: 7.12.0 + undici-types: 7.14.0 '@types/ws@8.18.1': dependencies: - '@types/node': 24.5.2 + '@types/node': 24.7.0 - '@vladfrangu/async_event_emitter@2.4.6': {} + '@vladfrangu/async_event_emitter@2.4.7': {} acorn@8.15.0: {} @@ -1345,7 +1356,7 @@ snapshots: dependencies: ms: 2.1.3 - discord-api-types@0.38.26: {} + discord-api-types@0.38.28: {} discord.js@14.22.1: dependencies: @@ -1356,7 +1367,7 @@ snapshots: '@discordjs/util': 1.1.1 '@discordjs/ws': 1.2.3 '@sapphire/snowflake': 3.5.3 - discord-api-types: 0.38.26 + discord-api-types: 0.38.28 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 magic-bytes.js: 1.12.1 @@ -1421,7 +1432,7 @@ snapshots: dependencies: magic-string: 0.30.19 mlly: 1.8.0 - rollup: 4.52.3 + rollup: 4.52.4 foreground-child@3.3.1: dependencies: @@ -1598,32 +1609,32 @@ snapshots: rfdc@1.4.1: {} - rollup@4.52.3: + rollup@4.52.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.3 - '@rollup/rollup-android-arm64': 4.52.3 - '@rollup/rollup-darwin-arm64': 4.52.3 - '@rollup/rollup-darwin-x64': 4.52.3 - '@rollup/rollup-freebsd-arm64': 4.52.3 - '@rollup/rollup-freebsd-x64': 4.52.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 - '@rollup/rollup-linux-arm-musleabihf': 4.52.3 - '@rollup/rollup-linux-arm64-gnu': 4.52.3 - '@rollup/rollup-linux-arm64-musl': 4.52.3 - '@rollup/rollup-linux-loong64-gnu': 4.52.3 - '@rollup/rollup-linux-ppc64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-musl': 4.52.3 - '@rollup/rollup-linux-s390x-gnu': 4.52.3 - '@rollup/rollup-linux-x64-gnu': 4.52.3 - '@rollup/rollup-linux-x64-musl': 4.52.3 - '@rollup/rollup-openharmony-arm64': 4.52.3 - '@rollup/rollup-win32-arm64-msvc': 4.52.3 - '@rollup/rollup-win32-ia32-msvc': 4.52.3 - '@rollup/rollup-win32-x64-gnu': 4.52.3 - '@rollup/rollup-win32-x64-msvc': 4.52.3 + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 shebang-command@2.0.0: @@ -1717,7 +1728,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(tsx@4.20.6)(typescript@5.9.2)(yaml@2.8.1): + tsup@8.5.0(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.10) cac: 6.7.14 @@ -1730,14 +1741,14 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(tsx@4.20.6)(yaml@2.8.1) resolve-from: 5.0.0 - rollup: 4.52.3 + rollup: 4.52.4 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - typescript: 5.9.2 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color @@ -1751,14 +1762,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - typescript@5.9.2: {} + typescript@5.9.3: {} ufo@1.6.1: {} - undici-types@7.12.0: {} + undici-types@7.14.0: {} undici@6.21.3: {} + web-features@3.3.0(patch_hash=9e31634375f7275a9f9f1a072864cc91eec6eda1d068e935fe6773d4463df310): {} + webidl-conversions@4.0.2: {} whatwg-url@7.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5e75a6d --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +patchedDependencies: + web-features@3.3.0: patches/web-features@3.3.0.patch From 03e774785aadf93a3c6f16c2ac5fea4547eb7053 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 16:37:53 +0300 Subject: [PATCH 18/43] refactor: remove web-features pnpm patch --- patches/web-features@3.3.0.patch | 13 ------------- pnpm-lock.yaml | 9 ++------- pnpm-workspace.yaml | 2 -- 3 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 patches/web-features@3.3.0.patch delete mode 100644 pnpm-workspace.yaml diff --git a/patches/web-features@3.3.0.patch b/patches/web-features@3.3.0.patch deleted file mode 100644 index 9ffecd3..0000000 --- a/patches/web-features@3.3.0.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/package.json b/package.json -index ab2c3c3cf87d40fd6d0f4c1735a528059f719e7e..d020595138c719be06e1c663a91b9607bdcbb9c4 100644 ---- a/package.json -+++ b/package.json -@@ -12,6 +12,8 @@ - "main": "index.js", - "exports": { - ".": "./index.js", -+ "./types": "./types.d.ts", -+ "./types.quicktype": "./types.quicktype.d.ts", - "./data.json": "./data.json", - "./data.schema.json": "./data.schema.json" - }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 994059e..51e6809 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,11 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -patchedDependencies: - web-features@3.3.0: - hash: 9e31634375f7275a9f9f1a072864cc91eec6eda1d068e935fe6773d4463df310 - path: patches/web-features@3.3.0.patch - importers: .: @@ -21,7 +16,7 @@ importers: version: 14.22.1 web-features: specifier: ^3.3.0 - version: 3.3.0(patch_hash=9e31634375f7275a9f9f1a072864cc91eec6eda1d068e935fe6773d4463df310) + version: 3.3.0 devDependencies: '@biomejs/biome': specifier: 2.2.4 @@ -1770,7 +1765,7 @@ snapshots: undici@6.21.3: {} - web-features@3.3.0(patch_hash=9e31634375f7275a9f9f1a072864cc91eec6eda1d068e935fe6773d4463df310): {} + web-features@3.3.0: {} webidl-conversions@4.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 5e75a6d..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -patchedDependencies: - web-features@3.3.0: patches/web-features@3.3.0.patch From 946ca4c452f7b357875770c70d2cf174fdb89796 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 16:46:17 +0300 Subject: [PATCH 19/43] feat: Set TS's version in vscode settings choose the version from node_modules instead of vscode's version --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d722df4..9affadd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ }, "[json]": { "editor.defaultFormatter": "biomejs.biome" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } From f1a92dbc0b6ad02f7efea52b23df7140b530c46d Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 16:48:19 +0300 Subject: [PATCH 20/43] feat: Add fuzzy search util --- src/util/fuzzy-search.ts | 111 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/util/fuzzy-search.ts diff --git a/src/util/fuzzy-search.ts b/src/util/fuzzy-search.ts new file mode 100644 index 0000000..8e29a63 --- /dev/null +++ b/src/util/fuzzy-search.ts @@ -0,0 +1,111 @@ +export const levenshtein = (a: string, b: string) => { + const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0)); + + for (let i = 0; i <= a.length; i++) dp[i][0] = i; + for (let j = 0; j <= b.length; j++) dp[0][j] = j; + + for (let i = 1; i <= a.length; i++) { + for (let j = 1; j <= b.length; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + dp[i][j] = Math.min( + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + ); + } + } + + return dp[a.length][b.length]; +}; + +const bestSubstringDistance = (query: string, text: string): number => { + const queryLen = query.length; + const textLen = text.length; + let minDistance = Infinity; + + // The range of substring lengths to check (e.g., query length +/- 3 characters) + const range = 3; + + // Iterate over all possible starting points in the text + for (let i = 0; i < textLen; i++) { + // Iterate over possible ending points, limiting the substring length + for ( + let j = Math.max(i + queryLen - range, i + 1); + j <= Math.min(i + queryLen + range, textLen); + j++ + ) { + const sub = text.substring(i, j); + const distance = levenshtein(query, sub); + minDistance = Math.min(minDistance, distance); + } + } + + return minDistance; +}; + +export function fuzzySearch({ + query, + items, + findIn, + limit = 10, +}: { + query?: string; + items: T[]; + findIn: Array<(item: T) => string>; + limit?: number; +}) { + if (!query || query.trim() === '') { + return items.slice(0, limit); + } + + const threshold = 0.3; // minimum score to consider a match + const EXACT_MATCH_BOOST = 100; // Large boost for perfect match + const PREFIX_BOOST = 10; // Smaller boost for prefix match + + query = query.trim().toLowerCase(); + const queryLen = query.length; + + const scored = items.map((item) => { + let maxFuzzyScore = 0; + let titleMatchScore = 0; + + // 1. Calculate Fuzzy Score (Max Levenshtein Score from all keys) + const scores = findIn.map((fn) => { + const text = fn(item).toLowerCase(); + + // Apply a large, non-normalized boost for exact or prefix matches + if (text === query) { + titleMatchScore = EXACT_MATCH_BOOST; // Query "array" matches title "Array" + } else if (titleMatchScore < EXACT_MATCH_BOOST && text.startsWith(query)) { + // If not a perfect match, check for prefix match + titleMatchScore = Math.max(titleMatchScore, PREFIX_BOOST); + } else if (titleMatchScore === 0 && text.includes(` ${query} `)) { + // A minor boost if it's a whole word in the middle (optional) + titleMatchScore = Math.max(titleMatchScore, 1); + } + + // Fuzzy match using normalized Levenshtein distance + const distance = bestSubstringDistance(query, text); + const fuzzyScore = 1 - distance / queryLen; // normalized to 0–1 + return fuzzyScore; + }); + + maxFuzzyScore = Math.max(...scores); + + // Combine Scores + // The final score is the max fuzzy score plus the title match boost. + // The high boost (100) ensures it ranks above any normalized fuzzy score (max 1.0). + const finalScore = maxFuzzyScore + titleMatchScore; + + return { item, score: finalScore }; + }); + + return ( + scored + // Filter by the original fuzzy score threshold (optional, you could use a lower threshold) + .filter((s) => s.score >= threshold) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((s) => s.item) + ); +} From 0f834d74dd0fb8048267e388b634578039c145e7 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:40:03 +0300 Subject: [PATCH 21/43] refactor: Use `Command` type in `createCommand` util --- src/util/commands.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/util/commands.ts b/src/util/commands.ts index 219f806..2985b40 100644 --- a/src/util/commands.ts +++ b/src/util/commands.ts @@ -1,24 +1,12 @@ -import type { - Client, - CommandInteraction, - RESTPostAPIApplicationCommandsJSONBody, -} from 'discord.js'; +import type { Client } from 'discord.js'; import type { Command } from '../commands/types.js'; -export const createCommand = ( - data: RESTPostAPIApplicationCommandsJSONBody, - execute: (interaction: CommandInteraction) => Promise | void -): Command => { - return { data, execute } satisfies Command; +export const createCommand = (command: Command): Command => { + return command; }; -export const createCommands = ( - commands: Array<{ - data: RESTPostAPIApplicationCommandsJSONBody; - execute: (interaction: CommandInteraction) => Promise | void; - }> -): Command[] => { - return commands.map(({ data, execute }) => createCommand(data, execute)); +export const createCommands = (commands: Array): Command[] => { + return commands.map(createCommand); }; export const registerCommands = async ( From 95568810e1b1b2ad6c78cef1d3abb336eee0e6ba Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:41:40 +0300 Subject: [PATCH 22/43] refactor: Remove unnecessary `satisfies` from return type --- src/util/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/events.ts b/src/util/events.ts index 3636182..74af4ce 100644 --- a/src/util/events.ts +++ b/src/util/events.ts @@ -8,7 +8,7 @@ export const createEvent = ( }, execute: (...args: ClientEvents[T]) => Promise | void ): DiscordEvent => { - return { ...data, execute } satisfies DiscordEvent; + return { ...data, execute }; }; export const createEvents = ( From 34cc21cf99fc3f4948d179e5846455a936d47524 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:43:50 +0300 Subject: [PATCH 23/43] feat: Add emoji constants --- src/constants/emoji.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/constants/emoji.ts diff --git a/src/constants/emoji.ts b/src/constants/emoji.ts new file mode 100644 index 0000000..3a9a6c6 --- /dev/null +++ b/src/constants/emoji.ts @@ -0,0 +1 @@ +export const CHAIN_EMOJI = '🔗'; From 1adba444d5fc248fd1c35386736c4a684f68e98a Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:46:53 +0300 Subject: [PATCH 24/43] refactor: Update command consumers to use object arg --- src/commands/guides/index.ts | 11 ++++++----- src/commands/ping.ts | 10 +++++----- src/commands/tips/index.ts | 21 +++++++++++---------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/commands/guides/index.ts b/src/commands/guides/index.ts index a02ca39..9d71ab4 100644 --- a/src/commands/guides/index.ts +++ b/src/commands/guides/index.ts @@ -4,6 +4,7 @@ import { createCommand } from '../../util/commands.js'; import { loadMarkdownOptions } from '../../util/markdown.js'; const subjectsDir = new URL('./subjects/', import.meta.url); + const subjectChoices = new Map(); const loadChoices = async (): Promise => { @@ -18,8 +19,8 @@ const loadChoices = async (): Promise => { await loadChoices(); -export const guidesCommand = createCommand( - { +export const guidesCommand = createCommand({ + data: { name: 'guides', description: 'Get a guide on a specific subject', type: ApplicationCommandType.ChatInput, @@ -42,7 +43,7 @@ export const guidesCommand = createCommand( }, ], }, - async (interaction) => { + execute: async (interaction) => { if (!interaction.isChatInputCommand()) return; const subject = interaction.options.getString('subject', true); const user = interaction.options.getUser('user'); @@ -69,5 +70,5 @@ export const guidesCommand = createCommand( await interaction.reply({ content: 'Guide sent!', flags: MessageFlags.Ephemeral }); return; - } -); + }, +}); diff --git a/src/commands/ping.ts b/src/commands/ping.ts index d0657d3..4151c72 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,12 +1,12 @@ import { createCommand } from '../util/commands.js'; -export const pingCommand = createCommand( - { +export const pingCommand = createCommand({ + data: { name: 'ping', description: 'Replies with Pong!', }, - async (interaction) => { + execute: async (interaction) => { const user = interaction.user; await interaction.reply(`<@${user.id}> Pong!`); - } -); + }, +}); diff --git a/src/commands/tips/index.ts b/src/commands/tips/index.ts index 0970fd1..b5f7677 100644 --- a/src/commands/tips/index.ts +++ b/src/commands/tips/index.ts @@ -4,6 +4,7 @@ import { createCommand } from '../../util/commands.js'; import { loadMarkdownOptions } from '../../util/markdown.js'; const subjectsDir = new URL('./subjects/', import.meta.url); + const subjectChoices = new Map(); const loadChoices = async (): Promise => { @@ -18,8 +19,8 @@ const loadChoices = async (): Promise => { await loadChoices(); -const slashCommand = createCommand( - { +const slashCommand = createCommand({ + data: { name: 'tips', description: 'Provide a helpful tip on a given subject', options: [ @@ -40,7 +41,7 @@ const slashCommand = createCommand( }, ], }, - async (interaction) => { + execute: async (interaction) => { if (!interaction.isChatInputCommand()) return; const subject = interaction.options.getString('subject', true); @@ -66,16 +67,16 @@ const slashCommand = createCommand( }); await interaction.reply({ content: 'Tip sent!', ephemeral: true }); - } -); + }, +}); const contextMenuCommands = Array.from(subjectChoices).map(([key, value]) => - createCommand( - { + createCommand({ + data: { type: ApplicationCommandType.Message, name: `Tip: ${key}`, }, - async (interaction) => { + execute: async (interaction) => { if (!interaction.isMessageContextMenuCommand()) return; const message = interaction.targetMessage; @@ -88,8 +89,8 @@ const contextMenuCommands = Array.from(subjectChoices).map(([key, value]) => await interaction.editReply({ content: 'Tip sent!' }); return; - } - ) + }, + }) ); export const tipsCommands = [slashCommand, ...contextMenuCommands]; From ddf520e4ed22298bf1731d99c653981f7ec4f07c Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:47:42 +0300 Subject: [PATCH 25/43] feat: Add shared docs command types --- src/commands/docs/types.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/commands/docs/types.ts diff --git a/src/commands/docs/types.ts b/src/commands/docs/types.ts new file mode 100644 index 0000000..bccbb24 --- /dev/null +++ b/src/commands/docs/types.ts @@ -0,0 +1,36 @@ +import type { + ActionRowBuilder, + Collection, + EmbedBuilder, + MessageActionRowComponentBuilder, +} from 'discord.js'; + +export type ActionBuilders = { + selectRow: ActionRowBuilder; + buttonRow: ActionRowBuilder; +}; + +export type ProviderConfig = { + color: number; + icon: string; + commandDescription: string; + directUrl?: string; + + getFilteredData: (query: string) => Promise | Item[]; + + createCollection: (items: Array) => Collection; + + createActionBuilders: (data: Collection) => ActionBuilders; + + // Create result embeds to show after selection + createResultEmbeds: (data: Collection) => EmbedBuilder | EmbedBuilder[]; + + // Get display title for an item + getDisplayTitle: (item: Item) => string; + + // Get selection content message + getSelectionMessage: (query: string) => string; + + // Get display message after selection + getDisplayMessage: (selectedTitles: string[]) => string; +}; From cfb5c031580290813a8b73d96885c4853fb7c3a3 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:48:22 +0300 Subject: [PATCH 26/43] feat: Add shared docs commands utils --- src/commands/docs/utils.ts | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/commands/docs/utils.ts diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts new file mode 100644 index 0000000..cc445fa --- /dev/null +++ b/src/commands/docs/utils.ts @@ -0,0 +1,90 @@ +import { type CommandInteraction, MessageFlags } from 'discord.js'; +import { logToChannel } from '../../util/channel-logging.js'; +import type { ProviderConfig } from './types.js'; + +export const SEARCH_TERM = '%SEARCH%'; +export const TERM = '%TERM%'; + +// Utility functions +export const getSearchUrl = (url: string, search: string) => + url.replace(SEARCH_TERM, encodeURIComponent(search)); + +export const createBaseConfig = (options: { + color: number; + icon: string; + commandDescription: string; + directUrl?: string; +}) => ({ + color: options.color, + icon: options.icon, + commandDescription: options.commandDescription, + directUrl: options.directUrl, +}); + +export const executeDocCommand = async ( + config: ProviderConfig, + interaction: CommandInteraction +): Promise => { + if (!interaction.isChatInputCommand()) return; + + const query = interaction.options.getString('query', true).trim(); + + try { + const items = await config.getFilteredData(query); + + if (items.length === 0) { + await interaction.reply({ + content: `No results found for "${query}"`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + const collection = config.createCollection(items); + const { selectRow, buttonRow } = config.createActionBuilders(collection); + + const choiceInteraction = await interaction.reply({ + content: config.getSelectionMessage(query), + components: [selectRow, buttonRow], + flags: MessageFlags.Ephemeral, + }); + + const collector = choiceInteraction.createMessageComponentCollector({ + filter: (i) => i.user.id === interaction.user.id, + }); + + collector.once('collect', async (i) => { + if (i.isStringSelectMenu()) { + const selectedSet = new Set(i.values); + const selectedItems = collection.filter((_, key) => selectedSet.has(key)); + const selectedTitles = selectedItems.map(config.getDisplayTitle); + + await interaction.editReply({ + content: config.getDisplayMessage(selectedTitles), + components: [], + }); + + const embeds = config.createResultEmbeds(selectedItems); + + logToChannel({ + channel: interaction.channel, + content: { + type: 'embed', + embed: embeds, + content: interaction.options.getUser('user') + ? `<@${interaction.options.getUser('user')?.id}>` + : undefined, + }, + }); + } else if (i.isButton()) { + await choiceInteraction.delete(); + } + }); + } catch (error) { + console.error('Error executing doc command:', error); + await interaction.reply({ + content: `Error: ${error}`, + flags: MessageFlags.Ephemeral, + }); + } +}; From ca7e127f1ed979f22949433f36577902c9b2b2f5 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Wed, 8 Oct 2025 21:51:59 +0300 Subject: [PATCH 27/43] feat: Restructure docs commands - add baseline command - delete old providers file - extract proviers to seperate files - add baseline command --- src/commands/docs/index.ts | 15 +- src/commands/docs/providers.ts | 305 ------------------------ src/commands/docs/providers/baseline.ts | 125 ++++++++++ src/commands/docs/providers/mdn.ts | 88 +++++++ src/commands/docs/providers/npm.ts | 100 ++++++++ 5 files changed, 326 insertions(+), 307 deletions(-) delete mode 100644 src/commands/docs/providers.ts create mode 100644 src/commands/docs/providers/baseline.ts create mode 100644 src/commands/docs/providers/mdn.ts create mode 100644 src/commands/docs/providers/npm.ts diff --git a/src/commands/docs/index.ts b/src/commands/docs/index.ts index f120800..d996fb0 100644 --- a/src/commands/docs/index.ts +++ b/src/commands/docs/index.ts @@ -1,6 +1,16 @@ import { ApplicationCommandOptionType } from 'discord.js'; import { createCommands } from '../../util/commands.js'; -import { type DocProvider, docProviders, executeDocCommand } from './providers.js'; +import { baselineProvider } from './providers/baseline.js'; +import { mdnProvider } from './providers/mdn.js'; +import { npmProvider } from './providers/npm.js'; +import type { ProviderConfig } from './types.js'; +import { executeDocCommand } from './utils.js'; + +const docProviders = { + mdn: mdnProvider, + npm: npmProvider, + baseline: baselineProvider, +}; export const docsCommands = createCommands( Object.entries(docProviders).map(([providerKey, providerConfig]) => ({ @@ -22,6 +32,7 @@ export const docsCommands = createCommands( }, ], }, - execute: async (interaction) => executeDocCommand(providerKey as DocProvider, interaction), + execute: async (interaction) => + executeDocCommand(providerConfig as ProviderConfig, interaction), })) ); diff --git a/src/commands/docs/providers.ts b/src/commands/docs/providers.ts deleted file mode 100644 index 3f022b3..0000000 --- a/src/commands/docs/providers.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - Collection, - type CommandInteraction, - EmbedBuilder, - type MessageActionRowComponentBuilder, - MessageFlags, - StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, -} from 'discord.js'; -import { logToChannel } from '../../util/channel-logging.js'; - -const SEARCH_TERM = '%SEARCH%'; -const TERM = '%TERM%'; - -export type NPMSearchResult = { - objects: Array<{ - package: { - name: string; - version: string; - description: string; - license: string; - links: { - npm: string; - homepage?: string; - repository?: string; - }; - }; - }>; -}; - -export type MDNSearchResult = { - documents: Array<{ - mdn_url: string; - title: string; - slug: string; - summary: string; - }>; -}; - -export type ActionBuilders = { - selectRow: ActionRowBuilder; - buttonRow: ActionRowBuilder; -}; - -export type DocProvider = 'mdn' | 'npm'; - -export type ProviderData = T extends 'mdn' - ? MDNSearchResult - : T extends 'npm' - ? NPMSearchResult - : never; - -export type ProviderItem = T extends 'mdn' - ? MDNSearchResult['documents'][number] - : T extends 'npm' - ? NPMSearchResult['objects'][number]['package'] - : never; - -export type ProviderKey = T extends 'mdn' - ? MDNSearchResult['documents'][number]['slug'] - : T extends 'npm' - ? NPMSearchResult['objects'][number]['package']['name'] - : never; - -export type DocProviderConfig = { - searchUrl: string; - directUrl?: string; - icon: string; - color: number; - commandDescription: string; - - // Transform raw API response to array of items - transformResponse: (data: ProviderData) => Array>; - - // Create collection from items - createCollection: (items: Array>) => Collection, ProviderItem>; - - // Create action builders (select menu and buttons) - createActionBuilders: (data: Collection, ProviderItem>) => ActionBuilders; - - // Create result embeds to show after selection - createResultEmbeds: ( - data: Collection, ProviderItem> - ) => EmbedBuilder | EmbedBuilder[]; - - // Get display title for an item - getDisplayTitle: (item: ProviderItem) => string; - - // Get selection content message - getSelectionMessage: (query: string) => string; - - // Get display message after selection - getDisplayMessage: (selectedTitles: string[]) => string; -}; - -export type DocProviders = { - [K in DocProvider]: DocProviderConfig; -}; - -export const docProviders: DocProviders = { - mdn: { - color: 0x83_d0_f2, - icon: 'https://avatars0.githubusercontent.com/u/7565578', - searchUrl: `https://developer.mozilla.org/api/v1/search?q=${SEARCH_TERM}&locale=en-US`, - directUrl: `https://developer.mozilla.org${TERM}`, - commandDescription: 'Search MDN for documentation on web development topics', - transformResponse: (data) => data.documents, - createCollection: (items) => new Collection(items.map((item) => [item.slug, item])), - createActionBuilders: (data) => { - const selectRow = new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId('mdn-select') - .setPlaceholder('Select 1 to 5 results') - .setMinValues(1) - .setMaxValues(Math.min(5, data.size)) - .addOptions( - ...data.map((doc) => ({ - label: doc.title.length > 100 ? `${doc.title.slice(0, 97)}...` : doc.title, - description: - doc.summary.length > 100 ? `${doc.summary.slice(0, 97)}...` : doc.summary, - value: doc.slug, - })) - ) - ); - - const buttonRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('Cancel') - .setStyle(ButtonStyle.Danger) - .setCustomId('mdn-cancel') - ); - - return { selectRow, buttonRow }; - }, - createResultEmbeds: (selectedItems) => - selectedItems.map((doc) => - new EmbedBuilder() - .setTitle(doc.title) - .setURL(getDirectUrl('mdn', doc.mdn_url) ?? '') - .setDescription(doc.summary) - .setColor(getColor('mdn')) - .setFooter({ text: 'Powered by MDN' }) - .setTimestamp() - ), - getDisplayTitle: (item) => item.title, - getSelectionMessage: (query) => `Select 1 to 5 results for **${query}**:`, - getDisplayMessage: (selectedTitles) => - `Displaying Result for **${new Intl.ListFormat('en-US').format(selectedTitles)}**:`, - }, - npm: { - color: 0xfb_3e_44, - icon: 'https://avatars0.githubusercontent.com/u/6078720', - searchUrl: `https://registry.npmjs.org/-/v1/search?text=${SEARCH_TERM}&size=10`, - commandDescription: 'Search NPM for JavaScript packages', - transformResponse: (data) => data.objects.map((obj) => obj.package), - createCollection: (items) => new Collection(items.map((item) => [item.name, item])), - createActionBuilders: (data) => { - const selectRow = new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId('npm-select') - .setPlaceholder('Select a package') - .setMinValues(1) - .setMaxValues(1) - .addOptions( - ...data.map((pkg) => - new StringSelectMenuOptionBuilder() - .setLabel(pkg.name) - .setDescription(pkg.description) - .setValue(pkg.name) - ) - ) - ); - const buttonRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('npm-cancel') - .setLabel('Cancel') - .setStyle(ButtonStyle.Danger) - ); - return { selectRow, buttonRow }; - }, - createResultEmbeds: (selectedItems) => - selectedItems.map((pkg) => - new EmbedBuilder() - .setTitle(pkg.name) - .setThumbnail(getIconUrl('npm')) - .setURL(pkg.links.npm) - .setColor(getColor('npm')) - .setDescription(pkg.description) - .setFields( - Object.entries(pkg.links) - .filter(([key, value]) => key !== 'npm' && value !== undefined) - .map(([key, value]) => ({ - name: key, - value, - })) - ) - .setFooter({ text: `Version ${pkg.version} | License: ${pkg.license ?? 'N/A'}` }) - .setTimestamp() - ), - getDisplayTitle: (item) => item.name, - getSelectionMessage: (query) => `Select a package for **${query}**:`, - getDisplayMessage: (selectedTitles) => - `Displaying Result for **${new Intl.ListFormat('en-US').format(selectedTitles)}**:`, - }, -}; - -// Utility functions -export const getSearchUrl = (provider: DocProvider, search: string) => - docProviders[provider].searchUrl.replace(SEARCH_TERM, encodeURIComponent(search)); - -export const getDirectUrl = ( - provider: T, - term: string -): DocProviders[T]['directUrl'] => { - const direct = docProviders[provider].directUrl; - if (!direct) return undefined; - return direct.replace(TERM, term); -}; - -export const getIconUrl = (provider: DocProvider): string => docProviders[provider].icon; - -export const getColor = (provider: DocProvider): number => docProviders[provider].color; - -export const executeDocCommand = async ( - provider: T, - interaction: CommandInteraction -): Promise => { - if (!interaction.isChatInputCommand()) return; - - const query = interaction.options.getString('query', true).trim(); - const config = docProviders[provider]; - - try { - const url = getSearchUrl(provider, query); - const response = await fetch(url); - if (!response.ok) { - await interaction.reply({ - content: `Error: ${response.status} ${response.statusText}`, - flags: MessageFlags.Ephemeral, - }); - return; - } - - const data = (await response.json()) as ProviderData; - const items = config.transformResponse(data); - - if (items.length === 0) { - await interaction.reply({ - content: `No results found for "${query}"`, - flags: MessageFlags.Ephemeral, - }); - return; - } - - const collection = config.createCollection(items); - const { selectRow, buttonRow } = config.createActionBuilders(collection); - - const choiceInteraction = await interaction.reply({ - content: config.getSelectionMessage(query), - components: [selectRow, buttonRow], - flags: MessageFlags.Ephemeral, - }); - - const collector = choiceInteraction.createMessageComponentCollector({ - filter: (i) => i.user.id === interaction.user.id, - }); - - collector.once('collect', async (i) => { - if (i.isStringSelectMenu()) { - const selectedSet = new Set(i.values); - const selectedItems = collection.filter((_, key) => selectedSet.has(key)); - const selectedTitles = selectedItems.map(config.getDisplayTitle); - - await interaction.editReply({ - content: config.getDisplayMessage(selectedTitles), - components: [], - }); - - const embeds = config.createResultEmbeds(selectedItems); - - logToChannel({ - channel: interaction.channel, - content: { - type: 'embed', - embed: embeds, - content: interaction.options.getUser('user') - ? `<@${interaction.options.getUser('user')?.id}>` - : undefined, - }, - }); - } else if (i.isButton()) { - await choiceInteraction.delete(); - } - }); - } catch (error) { - console.error('Error executing doc command:', error); - await interaction.reply({ - content: `Error: ${error}`, - flags: MessageFlags.Ephemeral, - }); - } -}; diff --git a/src/commands/docs/providers/baseline.ts b/src/commands/docs/providers/baseline.ts new file mode 100644 index 0000000..a50322d --- /dev/null +++ b/src/commands/docs/providers/baseline.ts @@ -0,0 +1,125 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Collection, + EmbedBuilder, + type MessageActionRowComponentBuilder, + StringSelectMenuBuilder, +} from 'discord.js'; +import { features as data } from 'web-features'; +import { fuzzySearch } from '../../../util/fuzzy-search.js'; +import type { ProviderConfig } from '../types.js'; +import { createBaseConfig } from '../utils.js'; + +export type FeatureData = { + name: string; + description: string; + status: { + baseline: 'high' | 'low' | false; + support: Record; + }; +}; + +const features = Object.fromEntries( + Object.entries(data).filter(([, feature]) => feature.kind === 'feature') +) as Record; + +const baselines = { + high: { + image: + 'https://web-platform-dx.github.io/web-features/assets/img/baseline-widely-word-dark.png', + description: 'Widely supported', + }, + low: { + image: 'https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word-dark.png', + description: 'Newly supported', + }, + none: { + image: + 'https://web-platform-dx.github.io/web-features/assets/img/baseline-limited-word-dark.png', + description: 'Not supported', + }, +}; + +type Item = FeatureData; + +const baseConfig = createBaseConfig({ + color: 0x4e_8c_2f, + icon: '', + commandDescription: 'Get baseline support information for web platform features', +}); + +export const baselineProvider: ProviderConfig = { + ...baseConfig, + getFilteredData: (query: string) => { + return fuzzySearch({ + items: Object.entries(features), + query, + findIn: [([key]) => key], + limit: 20, + }).map(([, feature]) => feature); + }, + createCollection: (items) => new Collection(items.map((item) => [item.name, item])), + createActionBuilders: (data) => { + const selectRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('baseline-select') + .setPlaceholder('Select one feature') + .setMinValues(1) + .setMaxValues(1) + .addOptions( + ...data.map((feature) => ({ + label: feature.name.length > 100 ? `${feature.name.slice(0, 97)}...` : feature.name, + description: + feature.description.length > 100 + ? `${feature.description.slice(0, 97)}...` + : feature.description, + value: feature.name, + })) + ) + ); + + const buttonRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('Cancel') + .setStyle(ButtonStyle.Danger) + .setCustomId('baseline-cancel') + ); + + return { selectRow, buttonRow }; + }, + createResultEmbeds: (selectedItems) => + selectedItems.map((feature) => { + const support = Object.entries(feature.status.support) + .map(([browser, data]) => `${browser}: **${data}**`) + .join('\n'); + return new EmbedBuilder() + .setTitle(feature.name) + .setColor(baseConfig.color) + .setDescription(` + ${feature.description}\n + ${support} + `) + .setImage( + typeof feature.status.baseline === 'string' + ? baselines[feature.status.baseline].image + : baselines.none.image + ) + .addFields({ + name: 'Baseline status', + value: + typeof feature.status.baseline === 'string' + ? baselines[feature.status.baseline].description + : baselines.none.description, + }) + .setFooter({ + text: 'Powered by web-features (web-platform-dx.github.io/web-features)', + }) + .setTimestamp(); + }), + getDisplayMessage: (selectedTitles) => + `Displaying Result for **${new Intl.ListFormat('en-US').format(selectedTitles)}**:`, + getDisplayTitle: (feature) => feature.name, + getSelectionMessage: (query) => `Select a feature for **${query}**:`, +}; diff --git a/src/commands/docs/providers/mdn.ts b/src/commands/docs/providers/mdn.ts new file mode 100644 index 0000000..44b8a46 --- /dev/null +++ b/src/commands/docs/providers/mdn.ts @@ -0,0 +1,88 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Collection, + EmbedBuilder, + type MessageActionRowComponentBuilder, + StringSelectMenuBuilder, +} from 'discord.js'; +import { CHAIN_EMOJI } from '../../../constants/emoji.js'; +import { clampText } from '../../../util/text.js'; +import type { ProviderConfig } from '../types.js'; +import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from '../utils.js'; + +type SearchResult = { + documents: Array<{ + mdn_url: string; + title: string; + slug: string; + summary: string; + }>; +}; + +type SearchItem = SearchResult['documents'][number]; + +const baseConfig = createBaseConfig({ + color: 0x83_d0_f2, + icon: 'https://avatars0.githubusercontent.com/u/7565578', + directUrl: `https://developer.mozilla.org${TERM}`, + commandDescription: 'Search MDN for documentation on web development topics', +}); + +export const mdnProvider: ProviderConfig = { + ...baseConfig, + getFilteredData: async (query: string) => { + const response = await fetch( + getSearchUrl( + `https://developer.mozilla.org/api/v1/search?q=${SEARCH_TERM}&locale=en-US`, + query + ) + ); + + if (!response.ok) { + throw new Error(`Error fetching MDN data: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as SearchResult; + return data.documents; + }, + createCollection: (items) => new Collection(items.map((item) => [item.slug, item])), + createActionBuilders: (data) => { + const selectRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('mdn-select') + .setPlaceholder('Select 1 to 5 results') + .setMinValues(1) + .setMaxValues(Math.min(5, data.size)) + .addOptions( + ...data.map((doc) => ({ + label: doc.title.length > 100 ? `${doc.title.slice(0, 97)}...` : doc.title, + description: doc.summary.length > 100 ? `${doc.summary.slice(0, 97)}...` : doc.summary, + value: doc.slug, + })) + ) + ); + + const buttonRow = new ActionRowBuilder().addComponents( + new ButtonBuilder().setLabel('Cancel').setStyle(ButtonStyle.Danger).setCustomId('mdn-cancel') + ); + + return { selectRow, buttonRow }; + }, + createResultEmbeds: (selectedItems) => + selectedItems.map((doc) => + new EmbedBuilder() + .setTitle(`${CHAIN_EMOJI} ${doc.title}`) + .setURL(baseConfig.directUrl!.replace(TERM, doc.mdn_url)) + .setDescription(clampText(doc.summary, 200)) + .setColor(baseConfig.color) + .setThumbnail(baseConfig.icon) + .setFooter({ text: 'Powered by MDN' }) + .setTimestamp() + ), + getDisplayTitle: (item) => item.title, + getSelectionMessage: (query) => `Select 1 to 5 results for **${query}**:`, + getDisplayMessage: (selectedTitles) => + `Displaying Result for **${new Intl.ListFormat('en-US').format(selectedTitles)}**:`, +}; diff --git a/src/commands/docs/providers/npm.ts b/src/commands/docs/providers/npm.ts new file mode 100644 index 0000000..bb1e55c --- /dev/null +++ b/src/commands/docs/providers/npm.ts @@ -0,0 +1,100 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Collection, + EmbedBuilder, + type MessageActionRowComponentBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, +} from 'discord.js'; +import { CHAIN_EMOJI } from '../../../constants/emoji.js'; +import { clampText } from '../../../util/text.js'; +import type { ProviderConfig } from '../types.js'; +import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from '../utils.js'; + +type SearchResult = { + objects: Array<{ + package: { + name: string; + version: string; + description: string; + license: string; + links: { + npm: string; + homepage?: string; + repository?: string; + }; + }; + }>; +}; + +type Item = SearchResult['objects'][number]['package']; + +const baseConfig = createBaseConfig({ + color: 0xfb_3e_44, + icon: 'https://avatars0.githubusercontent.com/u/6078720', + directUrl: `https://www.npmjs.com/package/${TERM}`, + commandDescription: 'Search NPM for JavaScript packages', +}); + +export const npmProvider: ProviderConfig = { + ...baseConfig, + getFilteredData: async (query: string) => { + const response = await fetch( + getSearchUrl(`https://registry.npmjs.org/-/v1/search?text=${SEARCH_TERM}&size=10`, query) + ); + + if (!response.ok) { + throw new Error(`Error fetching NPM data: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as SearchResult; + return data.objects.map((obj) => obj.package); + }, + createCollection: (items) => new Collection(items.map((item) => [item.name, item])), + createActionBuilders: (data) => { + const selectRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('npm-select') + .setPlaceholder('Select a package') + .setMinValues(1) + .setMaxValues(1) + .addOptions( + ...data.map((pkg) => + new StringSelectMenuOptionBuilder() + .setLabel(pkg.name) + .setDescription(pkg.description) + .setValue(pkg.name) + ) + ) + ); + const buttonRow = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('npm-cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger) + ); + return { selectRow, buttonRow }; + }, + createResultEmbeds: (selectedItems) => + selectedItems.map((pkg) => + new EmbedBuilder() + .setTitle(`${CHAIN_EMOJI} ${pkg.name}`) + .setThumbnail(baseConfig.icon) + .setURL(pkg.links.npm) + .setColor(baseConfig.color) + .setDescription(clampText(pkg.description, 200)) + .setFields( + Object.entries(pkg.links) + .filter(([key, value]) => key !== 'npm' && value !== undefined) + .map(([key, value]) => ({ + name: key, + value, + })) + ) + .setFooter({ text: `Version ${pkg.version} | License: ${pkg.license ?? 'N/A'}` }) + .setTimestamp() + ), + getDisplayTitle: (item) => item.name, + getSelectionMessage: (query) => `Select a package for **${query}**:`, + getDisplayMessage: (selectedTitles) => + `Displaying Result for **${new Intl.ListFormat('en-US').format(selectedTitles)}**:`, +}; From c966730dcf5b7a2da2b7893cce49f26462b913d8 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Fri, 10 Oct 2025 17:56:30 +0300 Subject: [PATCH 28/43] feat: add `getBaselineFeatures` util --- src/commands/docs/utils.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts index cc445fa..34ee22e 100644 --- a/src/commands/docs/utils.ts +++ b/src/commands/docs/utils.ts @@ -1,13 +1,15 @@ import { type CommandInteraction, MessageFlags } from 'discord.js'; import { logToChannel } from '../../util/channel-logging.js'; +import type { FeatureData } from './baseline.js'; import type { ProviderConfig } from './types.js'; export const SEARCH_TERM = '%SEARCH%'; export const TERM = '%TERM%'; // Utility functions -export const getSearchUrl = (url: string, search: string) => - url.replace(SEARCH_TERM, encodeURIComponent(search)); +export const getSearchUrl = (url: string, search: string) => { + return url.replace(SEARCH_TERM, encodeURIComponent(search)); +}; export const createBaseConfig = (options: { color: number; @@ -25,7 +27,9 @@ export const executeDocCommand = async ( config: ProviderConfig, interaction: CommandInteraction ): Promise => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand()) { + return; + } const query = interaction.options.getString('query', true).trim(); @@ -88,3 +92,15 @@ export const executeDocCommand = async ( }); } }; + +export const NON_BASELINE_FEATURES = ['numeric-seperators', 'single-color-gradients']; +export const getBaselineFeatures = ( + originalFeatures: Record, + nonFeatureKeys: string[] = NON_BASELINE_FEATURES +): Record => { + const features = { ...originalFeatures }; + for (const nonFeature of nonFeatureKeys) { + delete features[nonFeature]; + } + return features as Record; +}; From cc7067f706429a18b01b933cdda13c5296501b16 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Fri, 10 Oct 2025 17:57:25 +0300 Subject: [PATCH 29/43] fix: Fix package.json test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e11205..60a3b32 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "check": "biome check .", "check:fix": "biome check --write .", "typecheck": "tsc --noEmit", - "test": "node --test", + "test": "pnpm run build:dev && node --test dist/**/*.test.js", "prepare": "husky", "pre-commit": "lint-staged" }, From 33fe1a8f6d4694a41319e84589b671f0b0509398 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Fri, 10 Oct 2025 18:00:52 +0300 Subject: [PATCH 30/43] refactor: move providers to the root of docs/ folder --- src/commands/docs/{providers => }/baseline.ts | 6 +++--- src/commands/docs/index.ts | 6 +++--- src/commands/docs/{providers => }/mdn.ts | 8 ++++---- src/commands/docs/{providers => }/npm.ts | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) rename src/commands/docs/{providers => }/baseline.ts (95%) rename src/commands/docs/{providers => }/mdn.ts (94%) rename src/commands/docs/{providers => }/npm.ts (94%) diff --git a/src/commands/docs/providers/baseline.ts b/src/commands/docs/baseline.ts similarity index 95% rename from src/commands/docs/providers/baseline.ts rename to src/commands/docs/baseline.ts index a50322d..8686259 100644 --- a/src/commands/docs/providers/baseline.ts +++ b/src/commands/docs/baseline.ts @@ -8,9 +8,9 @@ import { StringSelectMenuBuilder, } from 'discord.js'; import { features as data } from 'web-features'; -import { fuzzySearch } from '../../../util/fuzzy-search.js'; -import type { ProviderConfig } from '../types.js'; -import { createBaseConfig } from '../utils.js'; +import { fuzzySearch } from '../../util/fuzzy-search.js'; +import type { ProviderConfig } from './types.js'; +import { createBaseConfig } from './utils.js'; export type FeatureData = { name: string; diff --git a/src/commands/docs/index.ts b/src/commands/docs/index.ts index d996fb0..20b3da7 100644 --- a/src/commands/docs/index.ts +++ b/src/commands/docs/index.ts @@ -1,8 +1,8 @@ import { ApplicationCommandOptionType } from 'discord.js'; import { createCommands } from '../../util/commands.js'; -import { baselineProvider } from './providers/baseline.js'; -import { mdnProvider } from './providers/mdn.js'; -import { npmProvider } from './providers/npm.js'; +import { baselineProvider } from './baseline.js'; +import { mdnProvider } from './mdn.js'; +import { npmProvider } from './npm.js'; import type { ProviderConfig } from './types.js'; import { executeDocCommand } from './utils.js'; diff --git a/src/commands/docs/providers/mdn.ts b/src/commands/docs/mdn.ts similarity index 94% rename from src/commands/docs/providers/mdn.ts rename to src/commands/docs/mdn.ts index 44b8a46..5f684a4 100644 --- a/src/commands/docs/providers/mdn.ts +++ b/src/commands/docs/mdn.ts @@ -7,10 +7,10 @@ import { type MessageActionRowComponentBuilder, StringSelectMenuBuilder, } from 'discord.js'; -import { CHAIN_EMOJI } from '../../../constants/emoji.js'; -import { clampText } from '../../../util/text.js'; -import type { ProviderConfig } from '../types.js'; -import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from '../utils.js'; +import { CHAIN_EMOJI } from '../../constants/emoji.js'; +import { clampText } from '../../util/text.js'; +import type { ProviderConfig } from './types.js'; +import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from './utils.js'; type SearchResult = { documents: Array<{ diff --git a/src/commands/docs/providers/npm.ts b/src/commands/docs/npm.ts similarity index 94% rename from src/commands/docs/providers/npm.ts rename to src/commands/docs/npm.ts index bb1e55c..8ab9da9 100644 --- a/src/commands/docs/providers/npm.ts +++ b/src/commands/docs/npm.ts @@ -8,10 +8,10 @@ import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder, } from 'discord.js'; -import { CHAIN_EMOJI } from '../../../constants/emoji.js'; -import { clampText } from '../../../util/text.js'; -import type { ProviderConfig } from '../types.js'; -import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from '../utils.js'; +import { CHAIN_EMOJI } from '../../constants/emoji.js'; +import { clampText } from '../../util/text.js'; +import type { ProviderConfig } from './types.js'; +import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from './utils.js'; type SearchResult = { objects: Array<{ From 08995779302822c94101fab6d8924664c19da38a Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 00:39:24 +0300 Subject: [PATCH 31/43] test: Add baseline unit tests --- src/commands/docs/utils.test.ts | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/commands/docs/utils.test.ts diff --git a/src/commands/docs/utils.test.ts b/src/commands/docs/utils.test.ts new file mode 100644 index 0000000..b10f6f3 --- /dev/null +++ b/src/commands/docs/utils.test.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { features } from 'web-features'; +import { getBaselineFeatures, NON_BASELINE_FEATURES } from './utils.js'; + +describe('getBaselineFeatures', () => { + it('should return the correct baseline features when provided with non-features key array', () => { + const originalFeatures = { + 'feature-1': { + name: 'Feature 1', + description: 'Description 1', + status: { support: {}, baseline: 'full' }, + }, + 'feature-2': { + name: 'Feature 2', + description: 'Description 2', + status: { support: {}, baseline: 'partial' }, + }, + 'numeric-seperators': { + name: 'Numeric Separators', + description: 'Description NS', + status: { support: {}, baseline: 'none' }, + }, + 'single-color-gradient': { + name: 'Single Color Gradient', + description: 'Description SCG', + status: { support: {}, baseline: 'none' }, + }, + }; + + const expectedFeatures = { + 'feature-1': { + name: 'Feature 1', + description: 'Description 1', + status: { support: {}, baseline: 'full' }, + }, + 'feature-2': { + name: 'Feature 2', + description: 'Description 2', + status: { support: {}, baseline: 'partial' }, + }, + }; + + const result = getBaselineFeatures(originalFeatures, [ + 'numeric-seperators', + 'single-color-gradient', + ]); + + assert.deepStrictEqual(result, expectedFeatures); + }); + + it('NON_BASELINE_FEATURES should contain the correct features to exclude', () => { + const expectedNonBaselineFeatures = Object.entries(features) + .filter(([, feature]) => feature.kind !== 'feature') + .map(([key]) => key); + + assert.deepStrictEqual(NON_BASELINE_FEATURES, expectedNonBaselineFeatures); + }); +}); From b6ba4dd2b31eb1174f2deaae5ff2a4be3a57d790 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 00:48:32 +0300 Subject: [PATCH 32/43] feat: add checks before running docs interaction and defer the reply --- src/commands/docs/utils.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts index 34ee22e..db55a6b 100644 --- a/src/commands/docs/utils.ts +++ b/src/commands/docs/utils.ts @@ -27,6 +27,17 @@ export const executeDocCommand = async ( config: ProviderConfig, interaction: CommandInteraction ): Promise => { + if (interaction.replied || interaction.deferred) { + return; + } + + try { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + } catch (error) { + console.error(`deferReply FAILED:`, error); + return; + } + if (!interaction.isChatInputCommand()) { return; } @@ -37,20 +48,16 @@ export const executeDocCommand = async ( const items = await config.getFilteredData(query); if (items.length === 0) { - await interaction.reply({ - content: `No results found for "${query}"`, - flags: MessageFlags.Ephemeral, - }); + await interaction.editReply({ content: `No results found for "${query}"` }); return; } const collection = config.createCollection(items); const { selectRow, buttonRow } = config.createActionBuilders(collection); - const choiceInteraction = await interaction.reply({ + const choiceInteraction = await interaction.editReply({ content: config.getSelectionMessage(query), components: [selectRow, buttonRow], - flags: MessageFlags.Ephemeral, }); const collector = choiceInteraction.createMessageComponentCollector({ @@ -85,11 +92,8 @@ export const executeDocCommand = async ( } }); } catch (error) { - console.error('Error executing doc command:', error); - await interaction.reply({ - content: `Error: ${error}`, - flags: MessageFlags.Ephemeral, - }); + console.error(`executeDocCommand FAILED:`, error); + await interaction.editReply({ content: `Error: ${error}` }); } }; From 55b1efac6d1e1861cc30f9c29eb09a4ba3b48547 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 00:49:31 +0300 Subject: [PATCH 33/43] feat: use new `getBaselineFeatures` --- src/commands/docs/baseline.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/commands/docs/baseline.ts b/src/commands/docs/baseline.ts index 8686259..79d6542 100644 --- a/src/commands/docs/baseline.ts +++ b/src/commands/docs/baseline.ts @@ -10,20 +10,24 @@ import { import { features as data } from 'web-features'; import { fuzzySearch } from '../../util/fuzzy-search.js'; import type { ProviderConfig } from './types.js'; -import { createBaseConfig } from './utils.js'; +import { createBaseConfig, getBaselineFeatures } from './utils.js'; export type FeatureData = { name: string; + kind: 'feature'; description: string; status: { baseline: 'high' | 'low' | false; support: Record; }; }; +type FeatureItem = FeatureData & { key: string }; -const features = Object.fromEntries( - Object.entries(data).filter(([, feature]) => feature.kind === 'feature') -) as Record; +// Prepare baseline features by excluding non-feature entries and converting to array +const features: FeatureItem[] = Object.entries(getBaselineFeatures(data)).map(([key, feature]) => ({ + ...feature, + key, +})); const baselines = { high: { @@ -42,25 +46,23 @@ const baselines = { }, }; -type Item = FeatureData; - const baseConfig = createBaseConfig({ color: 0x4e_8c_2f, icon: '', commandDescription: 'Get baseline support information for web platform features', }); -export const baselineProvider: ProviderConfig = { +export const baselineProvider: ProviderConfig = { ...baseConfig, getFilteredData: (query: string) => { return fuzzySearch({ - items: Object.entries(features), + items: features, query, - findIn: [([key]) => key], + findIn: [(feature) => feature.name], limit: 20, - }).map(([, feature]) => feature); + }); }, - createCollection: (items) => new Collection(items.map((item) => [item.name, item])), + createCollection: (items) => new Collection(items.map((item) => [item.key, item])), createActionBuilders: (data) => { const selectRow = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder() @@ -75,7 +77,7 @@ export const baselineProvider: ProviderConfig = { feature.description.length > 100 ? `${feature.description.slice(0, 97)}...` : feature.description, - value: feature.name, + value: feature.key, })) ) ); From 4939517adc4ff70b3a08e9cfbc65e970f168868f Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 00:51:40 +0300 Subject: [PATCH 34/43] chore(tools): require following curly brace conventions in biome config --- biome.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 06b3bb0..b7ae8e1 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,8 @@ "style": { "noNonNullAssertion": "off", "useConst": "error", - "useTemplate": "error" + "useTemplate": "error", + "useBlockStatements": "error" }, "suspicious": { "noExplicitAny": "warn", From 03ec2974059aee432bc17a25ffe4615d7e3ec69e Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 00:57:18 +0300 Subject: [PATCH 35/43] chore: use curly braces instead of one line functions --- src/commands/guides/index.ts | 4 +++- src/commands/tips/index.ts | 8 ++++++-- src/events/has-var.ts | 6 +++--- src/events/just-ask.ts | 10 +++++++--- src/util/fuzzy-search.ts | 8 ++++++-- src/util/text.ts | 4 +++- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/commands/guides/index.ts b/src/commands/guides/index.ts index 9d71ab4..24e2e67 100644 --- a/src/commands/guides/index.ts +++ b/src/commands/guides/index.ts @@ -44,7 +44,9 @@ export const guidesCommand = createCommand({ ], }, execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand()) { + return; + } const subject = interaction.options.getString('subject', true); const user = interaction.options.getUser('user'); if (!subjectChoices.has(subject)) { diff --git a/src/commands/tips/index.ts b/src/commands/tips/index.ts index b5f7677..cb86175 100644 --- a/src/commands/tips/index.ts +++ b/src/commands/tips/index.ts @@ -42,7 +42,9 @@ const slashCommand = createCommand({ ], }, execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand()) { + return; + } const subject = interaction.options.getString('subject', true); const user = interaction.options.getUser('user'); @@ -77,7 +79,9 @@ const contextMenuCommands = Array.from(subjectChoices).map(([key, value]) => name: `Tip: ${key}`, }, execute: async (interaction) => { - if (!interaction.isMessageContextMenuCommand()) return; + if (!interaction.isMessageContextMenuCommand()) { + return; + } const message = interaction.targetMessage; await interaction.reply({ content: 'Fetching tip...', flags: MessageFlags.Ephemeral }); diff --git a/src/events/has-var.ts b/src/events/has-var.ts index 951ee1a..64447d8 100644 --- a/src/events/has-var.ts +++ b/src/events/has-var.ts @@ -43,9 +43,9 @@ export const hasVarEvent = createEvent( once: false, }, async (message) => { - if (message.author.bot) return; - - if (!canRun()) return; + if (message.author.bot || !canRun()) { + return; + } const codeBlocks = Array.from(message.content.match(codeBlockRegex) || []); diff --git a/src/events/just-ask.ts b/src/events/just-ask.ts index 8029c52..331859d 100644 --- a/src/events/just-ask.ts +++ b/src/events/just-ask.ts @@ -38,10 +38,14 @@ export const justAskEvent = createEvent( name: Events.MessageCreate, }, async (message) => { - if (!canRun()) return; - if (message.author.bot) return; + if (!canRun() || message.author.bot) { + return; + } - if (message.content.split(' ').length > 10) return; + // Ignore long messages, likely user provided more context + if (message.content.split(' ').length > 10) { + return; + } if (isAskingToAsk(message.content)) { await message.reply({ diff --git a/src/util/fuzzy-search.ts b/src/util/fuzzy-search.ts index 8e29a63..1ecffb1 100644 --- a/src/util/fuzzy-search.ts +++ b/src/util/fuzzy-search.ts @@ -1,8 +1,12 @@ export const levenshtein = (a: string, b: string) => { const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0)); - for (let i = 0; i <= a.length; i++) dp[i][0] = i; - for (let j = 0; j <= b.length; j++) dp[0][j] = j; + for (let i = 0; i <= a.length; i++) { + dp[i][0] = i; + } + for (let j = 0; j <= b.length; j++) { + dp[0][j] = j; + } for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { diff --git a/src/util/text.ts b/src/util/text.ts index 5e49272..04360f5 100644 --- a/src/util/text.ts +++ b/src/util/text.ts @@ -1,4 +1,6 @@ export const clampText = (text: string, maxLength: number): string => { - if (text.length <= maxLength) return text; + if (text.length <= maxLength) { + return text; + } return `${text.slice(0, maxLength - 3)}...`; }; From 37842311a11b7086d6e4059ec2f15d623eacb643 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 01:03:26 +0300 Subject: [PATCH 36/43] refactor: simplify SearchItem type definition in mdn and npm providers --- src/commands/docs/mdn.ts | 15 +++++++-------- src/commands/docs/npm.ts | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/commands/docs/mdn.ts b/src/commands/docs/mdn.ts index 5f684a4..a759d3c 100644 --- a/src/commands/docs/mdn.ts +++ b/src/commands/docs/mdn.ts @@ -12,17 +12,16 @@ import { clampText } from '../../util/text.js'; import type { ProviderConfig } from './types.js'; import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from './utils.js'; +type SearchItem = { + mdn_url: string; + title: string; + slug: string; + summary: string; +}; type SearchResult = { - documents: Array<{ - mdn_url: string; - title: string; - slug: string; - summary: string; - }>; + documents: SearchItem[]; }; -type SearchItem = SearchResult['documents'][number]; - const baseConfig = createBaseConfig({ color: 0x83_d0_f2, icon: 'https://avatars0.githubusercontent.com/u/7565578', diff --git a/src/commands/docs/npm.ts b/src/commands/docs/npm.ts index 8ab9da9..1461b9c 100644 --- a/src/commands/docs/npm.ts +++ b/src/commands/docs/npm.ts @@ -13,24 +13,24 @@ import { clampText } from '../../util/text.js'; import type { ProviderConfig } from './types.js'; import { createBaseConfig, getSearchUrl, SEARCH_TERM, TERM } from './utils.js'; +type SearchItem = { + name: string; + version: string; + description: string; + license: string; + links: { + npm: string; + homepage?: string; + repository?: string; + }; +}; + type SearchResult = { objects: Array<{ - package: { - name: string; - version: string; - description: string; - license: string; - links: { - npm: string; - homepage?: string; - repository?: string; - }; - }; + package: SearchItem; }>; }; -type Item = SearchResult['objects'][number]['package']; - const baseConfig = createBaseConfig({ color: 0xfb_3e_44, icon: 'https://avatars0.githubusercontent.com/u/6078720', @@ -38,7 +38,7 @@ const baseConfig = createBaseConfig({ commandDescription: 'Search NPM for JavaScript packages', }); -export const npmProvider: ProviderConfig = { +export const npmProvider: ProviderConfig = { ...baseConfig, getFilteredData: async (query: string) => { const response = await fetch( From 21c61473ecc916a76bf32fedf07713225d60b320 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 01:07:02 +0300 Subject: [PATCH 37/43] refactor: simplify createBaseConfig function implementation and add explicit return type --- src/commands/docs/utils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts index db55a6b..09c338f 100644 --- a/src/commands/docs/utils.ts +++ b/src/commands/docs/utils.ts @@ -16,12 +16,7 @@ export const createBaseConfig = (options: { icon: string; commandDescription: string; directUrl?: string; -}) => ({ - color: options.color, - icon: options.icon, - commandDescription: options.commandDescription, - directUrl: options.directUrl, -}); +}): Pick => options; export const executeDocCommand = async ( config: ProviderConfig, From b7cacff2892368afd5f509cba6fbd153334ee610 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 01:20:42 +0300 Subject: [PATCH 38/43] chore(tools): fix test script in ci --- .github/workflows/deploy.yml | 2 +- .github/workflows/test.yml | 2 +- package.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a950248..c7ee80d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,7 +44,7 @@ jobs: CLIENT_ID: ${{ secrets.CLIENT_ID }} - name: Run tests - run: npm test + run: npm test:ci - name: Package artifact run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4cc4865..7517784 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,5 +42,5 @@ jobs: CLIENT_ID: ${{ secrets.CLIENT_ID }} - name: Run tests - run: npm test + run: npm test:ci diff --git a/package.json b/package.json index 60a3b32..5f0755e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "check:fix": "biome check --write .", "typecheck": "tsc --noEmit", "test": "pnpm run build:dev && node --test dist/**/*.test.js", + "test:ci": "npm run build:ci && node --test dist/**/*.test.js", "prepare": "husky", "pre-commit": "lint-staged" }, From 3288f41cd5fa6b4b24bb400d804aff48763e98eb Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 01:22:36 +0300 Subject: [PATCH 39/43] chore(tools): fix test scripts #2 --- .github/workflows/deploy.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c7ee80d..56efb46 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,7 +44,7 @@ jobs: CLIENT_ID: ${{ secrets.CLIENT_ID }} - name: Run tests - run: npm test:ci + run: npm run test:ci - name: Package artifact run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7517784..c0b882c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,5 +42,5 @@ jobs: CLIENT_ID: ${{ secrets.CLIENT_ID }} - name: Run tests - run: npm test:ci + run: npm run test:ci From df144356c1032352fbc0fbc5283e5e4194164fe6 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 01:26:27 +0300 Subject: [PATCH 40/43] chore(tools): simplify ci scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5f0755e..08719e7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Web Dev & Web Design discord bot", "type": "module", "scripts": { - "build:ci": "tsup && node scripts/copy-assets.js", + "build:ci": "npm run build:tc && npm run build:copy", "build:dev": "pnpm run build:ts && pnpm run build:copy", "build:ts": "tsup", "build:copy": "node scripts/copy-assets.js", @@ -18,7 +18,7 @@ "check:fix": "biome check --write .", "typecheck": "tsc --noEmit", "test": "pnpm run build:dev && node --test dist/**/*.test.js", - "test:ci": "npm run build:ci && node --test dist/**/*.test.js", + "test:ci": "node --test dist/**/*.test.js", "prepare": "husky", "pre-commit": "lint-staged" }, From d0544ccbc7c3c13f3f5a9343fa16209b49244a6a Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 01:28:04 +0300 Subject: [PATCH 41/43] chore(tools): Fix typo in script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08719e7..c66397c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Web Dev & Web Design discord bot", "type": "module", "scripts": { - "build:ci": "npm run build:tc && npm run build:copy", + "build:ci": "npm run build:ts && npm run build:copy", "build:dev": "pnpm run build:ts && pnpm run build:copy", "build:ts": "tsup", "build:copy": "node scripts/copy-assets.js", From 2fad4b20fd4db60ec946dcf82c8f03474345a732 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 20:18:58 +0300 Subject: [PATCH 42/43] refactor: pick a better name for collection filter function arg --- src/commands/docs/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts index 09c338f..024e352 100644 --- a/src/commands/docs/utils.ts +++ b/src/commands/docs/utils.ts @@ -56,7 +56,7 @@ export const executeDocCommand = async ( }); const collector = choiceInteraction.createMessageComponentCollector({ - filter: (i) => i.user.id === interaction.user.id, + filter: (componentInteraction) => componentInteraction.user.id === interaction.user.id, }); collector.once('collect', async (i) => { From cfca3b7089f8c9065e256a052108dff035c6f855 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 11 Oct 2025 20:26:56 +0300 Subject: [PATCH 43/43] refactor: rename variable in component collector for clarity --- src/commands/docs/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/docs/utils.ts b/src/commands/docs/utils.ts index 024e352..1d55053 100644 --- a/src/commands/docs/utils.ts +++ b/src/commands/docs/utils.ts @@ -59,9 +59,9 @@ export const executeDocCommand = async ( filter: (componentInteraction) => componentInteraction.user.id === interaction.user.id, }); - collector.once('collect', async (i) => { - if (i.isStringSelectMenu()) { - const selectedSet = new Set(i.values); + collector.once('collect', async (componentInteraction) => { + if (componentInteraction.isStringSelectMenu()) { + const selectedSet = new Set(componentInteraction.values); const selectedItems = collection.filter((_, key) => selectedSet.has(key)); const selectedTitles = selectedItems.map(config.getDisplayTitle); @@ -82,7 +82,7 @@ export const executeDocCommand = async ( : undefined, }, }); - } else if (i.isButton()) { + } else if (componentInteraction.isButton()) { await choiceInteraction.delete(); } });