Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "crownutils",
"version": "1.0.0",
"version": "0.1.0",
"description": "A Discord bot",
"main": "dist/index.js",
"type": "module",
Expand Down
24 changes: 16 additions & 8 deletions src/commands/slash/ping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { SlashCommandBuilder } from 'discord.js';
import type { SlashCommand } from '@/types/command.js';
import { pingMessages } from '@/lang/index.js';
Comment on lines 1 to 2
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()
Expand All @@ -9,15 +13,19 @@ 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 Container()
.color('info')
.add(
new Title(pingMessages.title),
new Separator(),
new Text(pingMessages.result(totalLatency, discordLatency)),
)
.build();

await interaction.editReply(message);
Comment on lines 15 to +29
},
};
7 changes: 4 additions & 3 deletions src/handlers/base-loader.ts
Original file line number Diff line number Diff line change
@@ -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));

Expand All @@ -11,7 +11,7 @@ export async function loadModules<T>(
isValid: (obj: unknown) => obj is T,
): Promise<T[]> {
const items: T[] = [];
const dirPath = dirname(join(__dirname, '..', directory));
const dirPath = join(__dirname, '..', directory);

let files: string[];
try {
Expand All @@ -25,7 +25,8 @@ export async function loadModules<T>(
(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<string, unknown>;
const candidate = module[exportName];
Expand Down
4 changes: 2 additions & 2 deletions src/lang/ping.ts
Original file line number Diff line number Diff line change
@@ -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;
30 changes: 30 additions & 0 deletions src/lib/components/component.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Comment on lines +23 to +26
export interface RawComponent {
kind: 'raw';
apply(container: ContainerBuilder): void;
}
62 changes: 62 additions & 0 deletions src/lib/components/container.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
17 changes: 17 additions & 0 deletions src/lib/components/footer.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
10 changes: 10 additions & 0 deletions src/lib/components/separator.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
12 changes: 12 additions & 0 deletions src/lib/components/text.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 17 additions & 0 deletions src/lib/components/title.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}