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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}": [
Expand Down
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/commands/docs/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 default createCommands(
export const docsCommands = createCommands(
Object.entries(docProviders).map(([providerKey, providerConfig]) => ({
data: {
name: providerKey,
Expand Down
4 changes: 2 additions & 2 deletions src/commands/guides/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();
Expand All @@ -18,7 +18,7 @@ const loadChoices = async (): Promise<void> => {

await loadChoices();

export default createCommand(
export const guidesCommand = createCommand(
{
name: 'guides',
description: 'Get a guide on a specific subject',
Expand Down
72 changes: 9 additions & 63 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -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> | void;

__isCommand__: true;
};

/**
* Defines a schema for a command
*/
export const schema = z.object({
data: z.custom<RESTPostAPIApplicationCommandsJSONBody>(),
execute: z.function(),
__isCommand__: z.literal(true),
});

/**
* Defines the predicate to check if an object is a Command
*/
export const predicate: StructurePredicate<Command> = (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> | 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> | 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<string, Command>(
[pingCommand, guidesCommand, docsCommands, tipsCommands].flat().map((cmd) => [cmd.data.name, cmd])
);
4 changes: 2 additions & 2 deletions src/commands/ping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createCommand } from './index.js';
import { createCommand } from '../util/commands.js';

export default createCommand(
export const pingCommand = createCommand(
{
name: 'ping',
description: 'Replies with Pong!',
Expand Down
4 changes: 2 additions & 2 deletions src/commands/tips/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();
Expand Down Expand Up @@ -92,4 +92,4 @@ const contextMenuCommands = Array.from(subjectChoices).map(([key, value]) =>
)
);

export default [slashCommand, ...contextMenuCommands];
export const tipsCommands = [slashCommand, ...contextMenuCommands];
6 changes: 6 additions & 0 deletions src/commands/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { CommandInteraction, RESTPostAPIApplicationCommandsJSONBody } from 'discord.js';

export type Command = {
data: RESTPostAPIApplicationCommandsJSONBody;
execute: (interaction: CommandInteraction) => Promise<void> | void;
};
5 changes: 2 additions & 3 deletions src/events/has-var.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -37,7 +37,7 @@ const hasVarDeclaration = (code: string, language: string): boolean => {
}
};

export default createEvent(
export const hasVarEvent = createEvent(
{
name: Events.MessageCreate,
once: false,
Expand All @@ -64,7 +64,6 @@ export default createEvent(
}
}
}

return;
}
);
92 changes: 12 additions & 80 deletions src/events/index.ts
Original file line number Diff line number Diff line change
@@ -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<T extends keyof ClientEvents = keyof ClientEvents> = {
/**
* 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> | 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<DiscordEvent> = (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 = <T extends keyof ClientEvents = keyof ClientEvents>(
data: {
name: T;
once?: boolean;
},
execute: (...args: ClientEvents[T]) => Promise<void> | void
): DiscordEvent<T> => {
return { ...data, execute, __isEvent__: true } satisfies DiscordEvent<T>;
};

/**
*
* Creates multiple events
*
* @param events - An array of event data and execute functions
* @returns
*/
export const createEvents = <T extends keyof ClientEvents = keyof ClientEvents>(
events: Array<{
data: {
name: T;
once?: boolean;
};
execute: (...args: ClientEvents[T]) => Promise<void> | void;
}>
): DiscordEvent<T>[] => {
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[];
8 changes: 3 additions & 5 deletions src/events/interaction-create.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Events } from 'discord.js';
import { getCommands } from '../util/loaders.js';
import { createEvent } from './index.js';
import { commands } from '../commands/index.js';
import { createEvent } from '../util/events.js';

const commands = await getCommands(new URL('../commands/', import.meta.url));

export default createEvent(
export const interactionCreateEvent = createEvent(
{
name: Events.InteractionCreate,
},
Expand Down
4 changes: 2 additions & 2 deletions src/events/just-ask.ts
Original file line number Diff line number Diff line change
@@ -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)`;
Expand Down Expand Up @@ -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,
},
Expand Down
4 changes: 2 additions & 2 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Events } from 'discord.js';
import { createEvent } from './index.js';
import { createEvent } from '../util/events.js';

export default createEvent(
export const readyEvent = createEvent(
{
name: Events.ClientReady,
once: true,
Expand Down
7 changes: 7 additions & 0 deletions src/events/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ClientEvents } from 'discord.js';

export type DiscordEvent<T extends keyof ClientEvents = keyof ClientEvents> = {
name: T;
once?: boolean;
execute: (...args: ClientEvents[T]) => Promise<void> | void;
};
16 changes: 7 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ActivityType, Client, GatewayIntentBits } from 'discord.js';
import { commands } from './commands/index.js';
import { config } from './env.js';
import { getCommands, getEvents, loadCommands, loadEvents } 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({
Expand All @@ -22,13 +25,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);
Loading