@lilsnibbi/discord 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/package.json +45 -0
- package/src/DiscordClient.ts +598 -0
- package/src/DiscordCommand.ts +93 -0
- package/src/DiscordEvent.ts +126 -0
- package/src/DiscordModuleLoader.ts +49 -0
- package/src/DiscordPagination.ts +626 -0
- package/src/DiscordShutdown.ts +110 -0
- package/src/container.ts +65 -0
- package/src/index.ts +18 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteInteraction,
|
|
3
|
+
ChatInputCommandInteraction,
|
|
4
|
+
Client,
|
|
5
|
+
ContextMenuCommandBuilder,
|
|
6
|
+
ContextMenuCommandInteraction,
|
|
7
|
+
SlashCommandBuilder,
|
|
8
|
+
SlashCommandOptionsOnlyBuilder,
|
|
9
|
+
SlashCommandSubcommandsOnlyBuilder,
|
|
10
|
+
} from "discord.js";
|
|
11
|
+
|
|
12
|
+
/** Any builder discord.js accepts as an application command definition. */
|
|
13
|
+
export type CommandData =
|
|
14
|
+
| SlashCommandBuilder
|
|
15
|
+
| SlashCommandOptionsOnlyBuilder
|
|
16
|
+
| SlashCommandSubcommandsOnlyBuilder
|
|
17
|
+
| ContextMenuCommandBuilder;
|
|
18
|
+
|
|
19
|
+
/** Interactions a command's `execute` handler can receive. */
|
|
20
|
+
export type CommandInteraction =
|
|
21
|
+
| ChatInputCommandInteraction
|
|
22
|
+
| ContextMenuCommandInteraction;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Arbitrary per-command data, empty by default.
|
|
26
|
+
*
|
|
27
|
+
* Augment it via module declaration to give your own fields types across every
|
|
28
|
+
* command in the project.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* declare module "@lilsnibbi/discord" {
|
|
33
|
+
* interface DiscordCommandMetadata {
|
|
34
|
+
* cooldown?: number;
|
|
35
|
+
* category?: string;
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export interface DiscordCommandMetadata extends Record<string, unknown> {}
|
|
41
|
+
|
|
42
|
+
/** Constructor options for {@link DiscordCommand}. */
|
|
43
|
+
export interface DiscordCommandOptions<C extends Client = Client> {
|
|
44
|
+
/** The slash or context menu command definition. */
|
|
45
|
+
data: CommandData;
|
|
46
|
+
/** Consumer-defined data — see {@link DiscordCommandMetadata}. */
|
|
47
|
+
metadata: DiscordCommandMetadata;
|
|
48
|
+
/** Runs when the command is invoked. */
|
|
49
|
+
execute: (client: C, interaction: CommandInteraction) => void | Promise<void>;
|
|
50
|
+
/** Runs when an option with autocomplete enabled is focused. */
|
|
51
|
+
autocomplete?: (
|
|
52
|
+
client: C,
|
|
53
|
+
interaction: AutocompleteInteraction,
|
|
54
|
+
) => void | Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A discord.js slash or context menu command bundled with its handlers.
|
|
59
|
+
*
|
|
60
|
+
* Holding the definition and its handlers together lets a command loader read
|
|
61
|
+
* `data` for registration and call `execute` for dispatch without a separate
|
|
62
|
+
* lookup table.
|
|
63
|
+
*
|
|
64
|
+
* @typeParam C - The bot's `Client` type, forwarded to every handler.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* export default new DiscordCommand({
|
|
69
|
+
* data: new SlashCommandBuilder().setName("ping").setDescription("Pong."),
|
|
70
|
+
* metadata: {},
|
|
71
|
+
* execute: async (_client, interaction) => {
|
|
72
|
+
* await interaction.reply("Pong!");
|
|
73
|
+
* },
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export class DiscordCommand<C extends Client = Client> {
|
|
78
|
+
/** The slash or context menu command definition. */
|
|
79
|
+
public readonly data: CommandData;
|
|
80
|
+
/** Runs when the command is invoked. */
|
|
81
|
+
public readonly execute: DiscordCommandOptions<C>["execute"];
|
|
82
|
+
/** Runs when an option with autocomplete enabled is focused. */
|
|
83
|
+
public readonly autocomplete?: DiscordCommandOptions<C>["autocomplete"];
|
|
84
|
+
/** Consumer-defined data — see {@link DiscordCommandMetadata}. */
|
|
85
|
+
public metadata: DiscordCommandMetadata;
|
|
86
|
+
|
|
87
|
+
constructor(options: DiscordCommandOptions<C>) {
|
|
88
|
+
this.data = options.data;
|
|
89
|
+
this.metadata = options.metadata;
|
|
90
|
+
this.execute = options.execute;
|
|
91
|
+
this.autocomplete = options.autocomplete;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Client, ClientEvents, RestEvents } from "discord.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom events, empty by default.
|
|
5
|
+
*
|
|
6
|
+
* Augment it via module declaration to make your own events available to
|
|
7
|
+
* {@link DiscordEvent} under `type: "custom"`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* declare module "@lilsnibbi/discord" {
|
|
12
|
+
* interface DiscordEventCustomType {
|
|
13
|
+
* myEvent: [data: string];
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
// biome-ignore lint/suspicious/noEmptyInterface: the empty shape is the extension point
|
|
19
|
+
export interface DiscordEventCustomType {}
|
|
20
|
+
|
|
21
|
+
/** Which discord.js emitter an event belongs to. */
|
|
22
|
+
export type DiscordEventSource = "client" | "rest" | "custom";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shown instead of `never` when `type: "custom"` is used before any custom
|
|
26
|
+
* event has been declared, so autocomplete explains the missing augmentation
|
|
27
|
+
* rather than offering nothing.
|
|
28
|
+
*/
|
|
29
|
+
interface NoCustomEventsDeclared {
|
|
30
|
+
"augment DiscordEventCustomType to declare custom events": [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolves an event source to the event map it emits. */
|
|
34
|
+
export type EventMap<T extends DiscordEventSource> = T extends "client"
|
|
35
|
+
? ClientEvents
|
|
36
|
+
: T extends "rest"
|
|
37
|
+
? RestEvents
|
|
38
|
+
: keyof DiscordEventCustomType extends never
|
|
39
|
+
? NoCustomEventsDeclared
|
|
40
|
+
: DiscordEventCustomType;
|
|
41
|
+
|
|
42
|
+
/** Resolves the listener argument tuple for a source and event name. */
|
|
43
|
+
export type EventArgs<
|
|
44
|
+
T extends DiscordEventSource,
|
|
45
|
+
K extends keyof EventMap<T>,
|
|
46
|
+
> = Extract<EventMap<T>[K], unknown[]>;
|
|
47
|
+
|
|
48
|
+
/** Constructor options for {@link DiscordEvent}. */
|
|
49
|
+
export interface DiscordEventOptions<
|
|
50
|
+
T extends DiscordEventSource,
|
|
51
|
+
K extends keyof EventMap<T>,
|
|
52
|
+
C extends Client = Client,
|
|
53
|
+
> {
|
|
54
|
+
/** Which emitter the event comes from. */
|
|
55
|
+
type: T;
|
|
56
|
+
/** The event name, checked against the map `type` selects. */
|
|
57
|
+
name: K;
|
|
58
|
+
/** Detach the listener after its first call. Defaults to `false`. */
|
|
59
|
+
once?: boolean;
|
|
60
|
+
/** Runs when the event fires. */
|
|
61
|
+
method: (client: C, ...args: EventArgs<T, K>) => void | Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A discord.js event listener bundled with the metadata needed to register it.
|
|
66
|
+
*
|
|
67
|
+
* The `type` discriminant selects the event map `name` and the handler
|
|
68
|
+
* arguments are checked against, covering the gateway client, the REST manager,
|
|
69
|
+
* and any events declared through {@link DiscordEventCustomType}.
|
|
70
|
+
*
|
|
71
|
+
* @typeParam T - The event source.
|
|
72
|
+
* @typeParam K - The event name within that source's map.
|
|
73
|
+
* @typeParam C - The bot's `Client` type, forwarded to the handler.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* export default new DiscordEvent({
|
|
78
|
+
* type: "client",
|
|
79
|
+
* name: "messageCreate",
|
|
80
|
+
* method: async (_client, message) => {
|
|
81
|
+
* if (!message.author.bot) await message.react("👋");
|
|
82
|
+
* },
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export class DiscordEvent<
|
|
87
|
+
T extends DiscordEventSource,
|
|
88
|
+
K extends keyof EventMap<T> = keyof EventMap<T>,
|
|
89
|
+
C extends Client = Client,
|
|
90
|
+
> {
|
|
91
|
+
/** Which emitter the event comes from. */
|
|
92
|
+
public readonly type: T;
|
|
93
|
+
/** The event name. */
|
|
94
|
+
public readonly name: K;
|
|
95
|
+
/** Whether the listener detaches after its first call. */
|
|
96
|
+
public readonly once: boolean;
|
|
97
|
+
/** Runs when the event fires. */
|
|
98
|
+
public readonly method: DiscordEventOptions<T, K, C>["method"];
|
|
99
|
+
|
|
100
|
+
constructor(options: DiscordEventOptions<T, K, C>) {
|
|
101
|
+
this.type = options.type;
|
|
102
|
+
this.name = options.name;
|
|
103
|
+
this.once = options.once ?? false;
|
|
104
|
+
this.method = options.method;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Any event, whatever its source — the shape a registry stores.
|
|
110
|
+
*
|
|
111
|
+
* {@link DiscordEvent} cannot be widened by passing the union to `T`:
|
|
112
|
+
* `keyof EventMap<DiscordEventSource>` is the *intersection* of the three
|
|
113
|
+
* maps' keys, which is `never`, so nothing would be assignable. Naming each
|
|
114
|
+
* source separately keeps `type` narrowable, and `any` for `K` collapses the
|
|
115
|
+
* per-event argument tuple that would otherwise make a concrete event
|
|
116
|
+
* incompatible under `strictFunctionTypes`.
|
|
117
|
+
*
|
|
118
|
+
* @typeParam C - The bot's `Client` type, forwarded to every handler.
|
|
119
|
+
*/
|
|
120
|
+
export type AnyDiscordEvent<C extends Client = Client> =
|
|
121
|
+
// biome-ignore lint/suspicious/noExplicitAny: widening K is the point
|
|
122
|
+
| DiscordEvent<"client", any, C>
|
|
123
|
+
// biome-ignore lint/suspicious/noExplicitAny: widening K is the point
|
|
124
|
+
| DiscordEvent<"rest", any, C>
|
|
125
|
+
// biome-ignore lint/suspicious/noExplicitAny: widening K is the point
|
|
126
|
+
| DiscordEvent<"custom", any, C>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/** Discovery options, resolved relative to `custom.root`. */
|
|
6
|
+
export interface DiscordModuleOptions {
|
|
7
|
+
/** Directory containing feature folders. Defaults to `custom.root`. */
|
|
8
|
+
directory?: string;
|
|
9
|
+
/** Explicit entrypoints only; no discovery happens when patterns are omitted. */
|
|
10
|
+
patterns?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Discovers sorted, unique module entrypoints. Tests, declarations and
|
|
15
|
+
* node_modules are excluded even when a broad pattern is supplied.
|
|
16
|
+
*/
|
|
17
|
+
export async function discoverDiscordModules(
|
|
18
|
+
root: string,
|
|
19
|
+
options: DiscordModuleOptions,
|
|
20
|
+
): Promise<string[]> {
|
|
21
|
+
const paths = new Set<string>();
|
|
22
|
+
const directory = resolve(root, options.directory ?? ".");
|
|
23
|
+
if (options.patterns?.length && !(await stat(directory)).isDirectory()) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Discord module directory is not a directory: ${directory}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
for (const pattern of options.patterns ?? []) {
|
|
29
|
+
for await (const path of new Bun.Glob(pattern).scan({
|
|
30
|
+
cwd: directory,
|
|
31
|
+
absolute: true,
|
|
32
|
+
onlyFiles: true,
|
|
33
|
+
})) {
|
|
34
|
+
const normalized = path.replaceAll("\\", "/");
|
|
35
|
+
if (
|
|
36
|
+
/\.(?:test|spec|d)\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
37
|
+
/(?:^|\/)(?:node_modules|__tests__|__mocks__)(?:\/|$)/.test(normalized)
|
|
38
|
+
)
|
|
39
|
+
continue;
|
|
40
|
+
paths.add(resolve(path));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...paths].sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Imports via file URLs so Windows paths and reserved URL characters work. */
|
|
47
|
+
export async function importDiscordModule(path: string): Promise<unknown> {
|
|
48
|
+
return (await import(pathToFileURL(path).href)).default;
|
|
49
|
+
}
|