From c82e3950dd736096398eccd21a593934148cca7b Mon Sep 17 00:00:00 2001 From: Ntalcme <57364571+Ntalcme@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:44:15 +0200 Subject: [PATCH 1/4] fix(handlers): the dirPath was not build properly --- src/handlers/base-loader.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/handlers/base-loader.ts b/src/handlers/base-loader.ts index 0373f10..a9da5d2 100644 --- a/src/handlers/base-loader.ts +++ b/src/handlers/base-loader.ts @@ -1,7 +1,7 @@ -import { logger } from '@/lib/logger.js'; import { readdir } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { logger } from '@/lib/logger.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -11,7 +11,7 @@ export async function loadModules( isValid: (obj: unknown) => obj is T, ): Promise { const items: T[] = []; - const dirPath = dirname(join(__dirname, '..', directory)); + const dirPath = join(__dirname, '..', directory); let files: string[]; try { @@ -25,7 +25,8 @@ export async function loadModules( (file) => (file.endsWith('.js') || file.endsWith('.ts')) && !file.endsWith('.d.ts'), ); - for (const file of files) { + + for (const file of moduleFiles) { const fileUrl = pathToFileURL(join(dirPath, file)).href; const module = (await import(fileUrl)) as Record; const candidate = module[exportName]; From 051332ac32ea54dbe17592c7c17e7f2c01704773 Mon Sep 17 00:00:00 2001 From: Ntalcme <57364571+Ntalcme@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:45:01 +0200 Subject: [PATCH 2/4] Add MessageBuilder class for discord componentV2 #4 --- src/commands/slash/ping.ts | 19 +++++----- src/lang/ping.ts | 4 +-- src/lib/message-builder.ts | 71 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 src/lib/message-builder.ts diff --git a/src/commands/slash/ping.ts b/src/commands/slash/ping.ts index 1cca459..e435b0a 100644 --- a/src/commands/slash/ping.ts +++ b/src/commands/slash/ping.ts @@ -1,6 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; -import type { SlashCommand } from '@/types/command.js'; +import { MessageBuilder } from '@/lib/message-builder.js'; import { pingMessages } from '@/lang/index.js'; +import type { SlashCommand } from '@/types/command.js'; export const command: SlashCommand = { data: new SlashCommandBuilder() @@ -9,15 +10,17 @@ export const command: SlashCommand = { async execute(interaction) { const before = Date.now(); - - await interaction.reply({ content: pingMessages.calculating }); - + await interaction.deferReply(); const totalLatency = Date.now() - before; - const discordLatency = Math.round(interaction.client.ws.ping); - await interaction.editReply( - pingMessages.result(totalLatency, discordLatency), - ); + const message = new MessageBuilder() + .color('info') + .title('Pong 🏓') + .separator() + .text(pingMessages.result(totalLatency, discordLatency)) + .build(); + + await interaction.editReply(message); }, }; diff --git a/src/lang/ping.ts b/src/lang/ping.ts index 037f242..452e0e2 100644 --- a/src/lang/ping.ts +++ b/src/lang/ping.ts @@ -1,5 +1,5 @@ export const pingMessages = { - calculating: '🏓 Pong', + title: '🏓 Pong', result: (totalMs: number, discordMs: number): string => - `🏓 Pong\nLatence totale : ${totalMs} ms\nLatence Discord : ${discordMs} ms`, + `Latence totale : ${totalMs} ms\nLatence Discord : ${discordMs} ms`, } as const; diff --git a/src/lib/message-builder.ts b/src/lib/message-builder.ts new file mode 100644 index 0000000..4154e3f --- /dev/null +++ b/src/lib/message-builder.ts @@ -0,0 +1,71 @@ +import { + ContainerBuilder, + MessageFlags, + SeparatorBuilder, + TextDisplayBuilder, +} from 'discord.js'; + +/** + * Reusable accent colors for containers. Use a name for anything + * recurring; pass a raw number to .color() for one-off custom colors. + */ +const COLORS = { + success: 0x57f287, + error: 0xed4245, + info: 0x5865f2, + warning: 0xfee75c, +} as const; + +type ColorName = keyof typeof COLORS; + +export class MessageBuilder { + private readonly container = new ContainerBuilder(); + + private flagBits: number = MessageFlags.IsComponentsV2; + + public color(color: ColorName | number): this { + const value = typeof color === 'number' ? color : COLORS[color]; + this.container.setAccentColor(value); + return this; + } + + public title(content: string): this { + this.container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(`## ${content}`), + ); + return this; + } + + public text(content: string): this { + this.container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(content), + ); + return this; + } + + public separator(): this { + this.container.addSeparatorComponents(new SeparatorBuilder()); + return this; + } + + public flags(flag: number): this { + this.flagBits |= flag; + return this; + } + + public ephemeral(): this { + this.flagBits |= MessageFlags.Ephemeral; + return this; + } + + public get raw(): ContainerBuilder { + return this.container; + } + + public build(): { components: ContainerBuilder[]; flags: number } { + return { + components: [this.container], + flags: this.flagBits, + }; + } +} From 8516a6582a870e63eba0071c49be6056c669ef1b Mon Sep 17 00:00:00 2001 From: Ntalcme <57364571+Ntalcme@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:29:16 +0200 Subject: [PATCH 3/4] feat(components): update V2 component model (Container, Text, Title, Footer) --- src/commands/slash/ping.ts | 15 ++++--- src/lib/components/component.ts | 30 ++++++++++++++ src/lib/components/container.ts | 62 ++++++++++++++++++++++++++++ src/lib/components/footer.ts | 17 ++++++++ src/lib/components/separator.ts | 10 +++++ src/lib/components/text.ts | 12 ++++++ src/lib/components/title.ts | 17 ++++++++ src/lib/message-builder.ts | 71 --------------------------------- 8 files changed, 158 insertions(+), 76 deletions(-) create mode 100644 src/lib/components/component.ts create mode 100644 src/lib/components/container.ts create mode 100644 src/lib/components/footer.ts create mode 100644 src/lib/components/separator.ts create mode 100644 src/lib/components/text.ts create mode 100644 src/lib/components/title.ts delete mode 100644 src/lib/message-builder.ts diff --git a/src/commands/slash/ping.ts b/src/commands/slash/ping.ts index e435b0a..b4bab7b 100644 --- a/src/commands/slash/ping.ts +++ b/src/commands/slash/ping.ts @@ -1,7 +1,10 @@ import { SlashCommandBuilder } from 'discord.js'; -import { MessageBuilder } from '@/lib/message-builder.js'; import { pingMessages } from '@/lang/index.js'; import type { SlashCommand } from '@/types/command.js'; +import { Container } from '@/lib/components/container.js'; +import { Text } from '@/lib/components/text.js'; +import { Separator } from '@/lib/components/separator.js'; +import { Title } from '@/lib/components/title.js'; export const command: SlashCommand = { data: new SlashCommandBuilder() @@ -14,11 +17,13 @@ export const command: SlashCommand = { const totalLatency = Date.now() - before; const discordLatency = Math.round(interaction.client.ws.ping); - const message = new MessageBuilder() + const message = new Container() .color('info') - .title('Pong 🏓') - .separator() - .text(pingMessages.result(totalLatency, discordLatency)) + .add( + new Title(pingMessages.title), + new Separator(), + new Text(pingMessages.result(totalLatency, discordLatency)), + ) .build(); await interaction.editReply(message); diff --git a/src/lib/components/component.ts b/src/lib/components/component.ts new file mode 100644 index 0000000..b924ba7 --- /dev/null +++ b/src/lib/components/component.ts @@ -0,0 +1,30 @@ +import type { + ContainerBuilder, + SeparatorBuilder, + TextDisplayBuilder, +} from 'discord.js'; + +/** The discriminated union of all V2 components. + * "kind" is the discriminant: it tells Container which native + * discord.js method to call when assembling the component. + */ +export type V2Component = TextComponent | SeparatorComponent | RawComponent; + +export interface TextComponent { + kind: 'text'; + toBuilder(): TextDisplayBuilder; +} + +export interface SeparatorComponent { + kind: 'separator'; + toBuilder(): SeparatorBuilder; +} + +/** Escape hatch: wraps any native component the model doesn't cover + * (sections, galleries, buttons...). "apply" adds it to the container + *directly, since each native type needs its own add method. + */ +export interface RawComponent { + kind: 'raw'; + apply(container: ContainerBuilder): void; +} diff --git a/src/lib/components/container.ts b/src/lib/components/container.ts new file mode 100644 index 0000000..694672e --- /dev/null +++ b/src/lib/components/container.ts @@ -0,0 +1,62 @@ +import { ContainerBuilder, MessageFlags, TextDisplayBuilder } from 'discord.js'; +import type { TextComponent, V2Component } from './component.js'; + +const COLORS = { + success: 0x57f287, + error: 0xed4245, + info: 0x5865f2, + warning: 0xfee75c, +} as const; + +type ColorName = keyof typeof COLORS; + +export class Container { + private readonly components: V2Component[] = []; + private accentColor?: number; + + public color(color: ColorName | number): this { + this.accentColor = typeof color === 'number' ? color : COLORS[color]; + return this; + } + + public add(...components: V2Component[]): this { + this.components.push(...components); + return this; + } + + public build(): { components: ContainerBuilder[]; flags: number } { + const container = new ContainerBuilder(); + if (this.accentColor !== undefined) { + container.setAccentColor(this.accentColor); + } + + for (const component of this.components) { + switch (component.kind) { + case 'text': + container.addTextDisplayComponents(component.toBuilder()); + break; + case 'separator': + container.addSeparatorComponents(component.toBuilder()); + break; + case 'raw': + component.apply(container); + break; + } + } + + return { + components: [container], + flags: MessageFlags.IsComponentsV2, + }; + } +} + +export function render(component: TextComponent): { + components: TextDisplayBuilder[]; + flags: number; +} { + return { + components: [component.toBuilder()], + flags: MessageFlags.IsComponentsV2, + }; +} diff --git a/src/lib/components/footer.ts b/src/lib/components/footer.ts new file mode 100644 index 0000000..f1ed47e --- /dev/null +++ b/src/lib/components/footer.ts @@ -0,0 +1,17 @@ +import { TextDisplayBuilder } from 'discord.js'; +import type { TextComponent } from './component.js'; + +/** + * A footer-like component: small, subtle text (markdown "-#"). + * V2 has no real footer field, so this is a Text under the hood + * (kind: 'text') rendered with the subtle "-#" markdown. + */ +export class Footer implements TextComponent { + public readonly kind = 'text'; + + public constructor(private readonly content: string) {} + + public toBuilder(): TextDisplayBuilder { + return new TextDisplayBuilder().setContent(`-# ${this.content}`); + } +} diff --git a/src/lib/components/separator.ts b/src/lib/components/separator.ts new file mode 100644 index 0000000..dcf3836 --- /dev/null +++ b/src/lib/components/separator.ts @@ -0,0 +1,10 @@ +import { SeparatorBuilder } from 'discord.js'; +import type { SeparatorComponent } from './component.js'; + +export class Separator implements SeparatorComponent { + public readonly kind = 'separator'; + + public toBuilder(): SeparatorBuilder { + return new SeparatorBuilder(); + } +} diff --git a/src/lib/components/text.ts b/src/lib/components/text.ts new file mode 100644 index 0000000..e901ed8 --- /dev/null +++ b/src/lib/components/text.ts @@ -0,0 +1,12 @@ +import { TextDisplayBuilder } from 'discord.js'; +import type { TextComponent } from './component.js'; + +export class Text implements TextComponent { + public readonly kind = 'text'; + + public constructor(private readonly content: string) {} + + public toBuilder(): TextDisplayBuilder { + return new TextDisplayBuilder().setContent(this.content); + } +} diff --git a/src/lib/components/title.ts b/src/lib/components/title.ts new file mode 100644 index 0000000..08d8bd0 --- /dev/null +++ b/src/lib/components/title.ts @@ -0,0 +1,17 @@ +import { TextDisplayBuilder } from 'discord.js'; +import type { TextComponent } from './component.js'; + +/** + * A heading component. It is a Text under the hood (kind: 'text') + * since Discord renders titles as markdown headers inside a + * TextDisplay — there is no dedicated "title" component in V2. + */ +export class Title implements TextComponent { + public readonly kind = 'text'; + + public constructor(private readonly content: string) {} + + public toBuilder(): TextDisplayBuilder { + return new TextDisplayBuilder().setContent(`## ${this.content}`); + } +} diff --git a/src/lib/message-builder.ts b/src/lib/message-builder.ts deleted file mode 100644 index 4154e3f..0000000 --- a/src/lib/message-builder.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - ContainerBuilder, - MessageFlags, - SeparatorBuilder, - TextDisplayBuilder, -} from 'discord.js'; - -/** - * Reusable accent colors for containers. Use a name for anything - * recurring; pass a raw number to .color() for one-off custom colors. - */ -const COLORS = { - success: 0x57f287, - error: 0xed4245, - info: 0x5865f2, - warning: 0xfee75c, -} as const; - -type ColorName = keyof typeof COLORS; - -export class MessageBuilder { - private readonly container = new ContainerBuilder(); - - private flagBits: number = MessageFlags.IsComponentsV2; - - public color(color: ColorName | number): this { - const value = typeof color === 'number' ? color : COLORS[color]; - this.container.setAccentColor(value); - return this; - } - - public title(content: string): this { - this.container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(`## ${content}`), - ); - return this; - } - - public text(content: string): this { - this.container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(content), - ); - return this; - } - - public separator(): this { - this.container.addSeparatorComponents(new SeparatorBuilder()); - return this; - } - - public flags(flag: number): this { - this.flagBits |= flag; - return this; - } - - public ephemeral(): this { - this.flagBits |= MessageFlags.Ephemeral; - return this; - } - - public get raw(): ContainerBuilder { - return this.container; - } - - public build(): { components: ContainerBuilder[]; flags: number } { - return { - components: [this.container], - flags: this.flagBits, - }; - } -} From a968e5e49237c94d3e43cec407ae6c4188652f2d Mon Sep 17 00:00:00 2001 From: Ntalcme <57364571+Ntalcme@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:26:05 +0200 Subject: [PATCH 4/4] Update the package.json #4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48d6957..198d0af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crownutils", - "version": "1.0.0", + "version": "0.1.0", "description": "A Discord bot", "main": "dist/index.js", "type": "module",