@hyperneutrino/djs-lite 1.6.0 → 1.7.0

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.
Files changed (2) hide show
  1. package/index.ts +197 -35
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -35,16 +35,62 @@ const identity = <T>(value: T): T => value;
35
35
 
36
36
  type Wrapper<T extends Function> = (fn: T) => T;
37
37
 
38
- type Handler<T extends CommandInteraction | AutocompleteInteraction | MessageComponentInteraction | ModalSubmitInteraction> = (
38
+ type Handleable = CommandInteraction | AutocompleteInteraction | MessageComponentInteraction | ModalSubmitInteraction;
39
+
40
+ type Handler<T extends Handleable> = (
39
41
  interaction: T,
40
42
  ...args: T extends MessageComponentInteraction | ModalSubmitInteraction ? (string | undefined)[] : []
41
43
  ) => Awaitable<unknown>;
42
44
 
45
+ type WrapperConfig<R extends Record<string, Handleable>> = {
46
+ [K in keyof R]?: Wrapper<Handler<R[K]>>;
47
+ };
48
+
49
+ /**
50
+ * This is the type of `config.wrappers` in `loadCommands`. This type is exported so that you can expose the wrappers upstream if you are making a library
51
+ * surrounding this library.
52
+ */
53
+ export interface CommandWrappers extends WrapperConfig<{
54
+ slashCommands: ChatInputCommandInteraction;
55
+ userCommands: UserContextMenuCommandInteraction;
56
+ messageCommands: MessageContextMenuCommandInteraction;
57
+ autocompletes: AutocompleteInteraction;
58
+ }> {}
59
+
60
+ /**
61
+ * This is the type of `config.wrappers` in `loadInteractions`. This type is exported so that you can expose the wrappers upstream if you are making a library
62
+ * surrounding this library.
63
+ */
64
+ export interface InteractionWrappers extends WrapperConfig<{
65
+ modal: ModalSubmitInteraction;
66
+ button: ButtonInteraction;
67
+ stringSelectMenu: StringSelectMenuInteraction;
68
+ userSelectMenu: UserSelectMenuInteraction;
69
+ roleSelectMenu: RoleSelectMenuInteraction;
70
+ mentionableSelectMenu: MentionableSelectMenuInteraction;
71
+ channelSelectMenu: ChannelSelectMenuInteraction;
72
+ }> {}
73
+
74
+ /**
75
+ * This is a union type of `CommandWrappers` and `InteractionWrappers` so you can expose all of them at once (since there is no overlap in the field names).
76
+ */
77
+ export interface Wrappers extends CommandWrappers, InteractionWrappers {}
78
+
43
79
  abstract class Command<T extends BaseApplicationCommandData, U extends CommandInteraction, Z extends boolean = false> {
80
+ /** This is the command data for the configured command. */
44
81
  data: T;
82
+
83
+ /** This is the handler for the configured command. */
45
84
  handler: Handler<U>;
85
+
86
+ /** This is the handler for the configured command's autocomplete fields (always null if not on a slash command). */
46
87
  autocomplete: Handler<AutocompleteInteraction> | null;
47
88
 
89
+ /**
90
+ * @param object This object should contain all of the fields that would be present in this command type's command data except for the `type`.
91
+ * @param object.handler This parameter must be a function that takes the interaction as the sole parameter and processes it.
92
+ * @param object.autocomplete If this is a slash command, this parameter may be a function that takes an autocomplete interaction as the sole parameter.
93
+ */
48
94
  constructor({ handler, ...data }: T & { handler: Handler<U> } & (Z extends true ? { autocomplete?: Handler<AutocompleteInteraction> } : {})) {
49
95
  if ("autocomplete" in data) {
50
96
  const { autocomplete, ...rest } = data;
@@ -59,56 +105,116 @@ abstract class Command<T extends BaseApplicationCommandData, U extends CommandIn
59
105
  }
60
106
  }
61
107
 
108
+ /** This is a slash command configuration object. Prefer {@link SlashCommandWithSubcommands} unless you are sure you want to do subcommand routing manually. */
62
109
  export class SlashCommand extends Command<Omit<ChatInputApplicationCommandData, "type">, ChatInputCommandInteraction, true> {}
110
+
111
+ /** This is a user context menu command configuration object. */
63
112
  export class UserCommand extends Command<Omit<UserApplicationCommandData, "type">, UserContextMenuCommandInteraction> {}
113
+
114
+ /** This is a message context menu command configuration object. */
64
115
  export class MessageCommand extends Command<Omit<MessageApplicationCommandData, "type">, MessageContextMenuCommandInteraction> {}
65
116
 
117
+ /**
118
+ * This is a slash command configuration object when the slash command has subcommands/subcommand groups.
119
+ * There is no handler here; the handler is specified in each subcommand.
120
+ */
66
121
  export class SlashCommandWithSubcommands {
122
+ /** This is the command data for the root command with the options omitted. */
67
123
  data: Omit<ChatInputApplicationCommandData, "options" | "type">;
68
124
 
125
+ /**
126
+ * @param data This is the command data for the root command with the options ommitted.
127
+ */
69
128
  constructor(data: typeof this.data) {
70
129
  this.data = data;
71
130
  }
72
131
  }
73
132
 
133
+ /** This is a slash command subcommand group configuration object. There is no handler here; the handler is specified in each subcommand. */
74
134
  export class SubcommandGroup {
135
+ /** This is the subcommand group data with the options omitted. */
75
136
  data: Omit<ApplicationCommandSubGroupData, "options" | "type">;
76
137
 
138
+ /**
139
+ * @param data This is the subcommand group data with the options omitted.
140
+ */
77
141
  constructor(data: typeof this.data) {
78
142
  this.data = data;
79
143
  }
80
144
  }
81
145
 
146
+ /** This is a slash command subcommand configuration object. */
82
147
  export class Subcommand {
148
+ /** This is the subcommand data for the configured subcommand. */
83
149
  data: Omit<ApplicationCommandSubCommandData, "type">;
150
+
151
+ /** This is the handler for the configured subcommand. */
84
152
  handler: Handler<ChatInputCommandInteraction>;
85
153
 
86
- constructor({ handler, ...data }: typeof this.data & { handler: Subcommand["handler"] }) {
154
+ /** This is the handler for the configured subcommand's autocomplete fields. */
155
+ autocomplete: Handler<AutocompleteInteraction> | null;
156
+
157
+ /**
158
+ * @param object This object should contain all of the fields that would be present in this command type's command data except for the `type`.
159
+ * @param object.handler This parameter must be a function that takes the interaction as the sole parameter and processes it.
160
+ * @param object.autocomplete If this is a slash command, this parameter may be a function that takes an autocomplete interaction as the sole parameter.
161
+ */
162
+ constructor({ handler, autocomplete, ...data }: typeof this.data & { handler: Subcommand["handler"]; autocomplete?: Subcommand["autocomplete"] }) {
87
163
  this.data = data;
88
164
  this.handler = handler;
165
+ this.autocomplete = autocomplete ?? null;
89
166
  }
90
167
  }
91
168
 
92
169
  abstract class ComponentHandler<T extends ModalSubmitInteraction | MessageComponentInteraction> {
170
+ /** This is the handler for the configured interaction. */
93
171
  handler: Handler<T>;
94
172
 
173
+ /**
174
+ * @param handler This parameter must be a function that takes the interaction as the first parameter and processes it. Subsequent parameters are possible
175
+ * and will all be strings if present. These are derived from the interaction's `customId`; see {@link loadInteractions} for instructions.
176
+ */
95
177
  constructor(handler: Handler<T>) {
96
178
  this.handler = handler;
97
179
  }
98
180
  }
99
181
 
182
+ /** This is a modal submit interaction configuration object. */
100
183
  export class ModalHandler extends ComponentHandler<ModalSubmitInteraction> {}
184
+
185
+ /** This is a button component interaction configuration object. */
101
186
  export class ButtonHandler extends ComponentHandler<ButtonInteraction> {}
187
+
188
+ /** This is a string select menu component interaction configuration object. */
102
189
  export class StringSelectMenuHandler extends ComponentHandler<StringSelectMenuInteraction> {}
190
+
191
+ /** This is a user select menu component interaction configuration object. */
103
192
  export class UserSelectMenuHandler extends ComponentHandler<UserSelectMenuInteraction> {}
193
+
194
+ /** This is a role select menu component interaction configuration object. */
104
195
  export class RoleSelectMenuHandler extends ComponentHandler<RoleSelectMenuInteraction> {}
196
+
197
+ /** This is a mentionable select menu component interaction configuration object. */
105
198
  export class MentionableSelectMenuHandler extends ComponentHandler<MentionableSelectMenuInteraction> {}
199
+
200
+ /** This is a channel select menu component interaction configuration object. */
106
201
  export class ChannelSelectMenuHandler extends ComponentHandler<ChannelSelectMenuInteraction> {}
107
202
 
203
+ /**
204
+ * This is an event handler configuration object.
205
+ */
108
206
  export class EventHandler<T extends keyof ClientEvents> {
207
+ /** This is the event to watch. */
109
208
  event: T;
209
+
210
+ /** This is the handler for the configured event type. */
110
211
  handler: (...args: ClientEvents[T]) => unknown;
111
212
 
213
+ /**
214
+ * @param object.event This is the event to watch. This automatically configures the generic type argument.
215
+ * @param object.handler This is the handler for the configured event type. The parameter types are automatically configured from the generic type
216
+ * argument.
217
+ */
112
218
  constructor({ event, handler }: { event: T; handler: (...args: ClientEvents[T]) => unknown }) {
113
219
  this.event = event;
114
220
  this.handler = handler;
@@ -136,45 +242,44 @@ async function importAll(
136
242
  );
137
243
  }
138
244
 
139
- async function loadSubcommands(directory: string): Promise<{
140
- options: ApplicationCommandSubCommandData[];
141
- handlers: Map<string, Handler<ChatInputCommandInteraction>>;
142
- }> {
245
+ async function loadSubcommands(directory: string) {
143
246
  if (!(await fs.exists(directory))) throw new Error(`Loading subcommands within a group failed: ${directory} is required but could not be found.`);
144
247
 
145
248
  const options: ApplicationCommandSubCommandData[] = [];
146
249
  const handlers = new Map<string, Handler<ChatInputCommandInteraction>>();
250
+ const autocompletes = new Map<string, Handler<AutocompleteInteraction>>();
147
251
 
148
252
  await importAll({ directory, recursive: false }, async ({ file, absolutePath, item }) => {
149
253
  if (item instanceof Subcommand) {
150
254
  options.push({ ...item.data, type: ApplicationCommandOptionType.Subcommand });
151
255
  handlers.set(item.data.name, item.handler);
256
+ if (item.autocomplete) autocompletes.set(item.data.name, item.autocomplete);
152
257
  } else throw new Error(`Loading commands failed: export from ${absolutePath} (third-level in commands folder) was not an instance of Subcommand.`);
153
258
 
154
259
  if (item.data.name !== file.name.replace(/.[^/.]+$/, ""))
155
260
  throw new Error(`Code style enforcement: name exported from ${absolutePath} does not match the filename`);
156
261
  });
157
262
 
158
- return { options, handlers };
263
+ return { options, handlers, autocompletes };
159
264
  }
160
265
 
161
- async function loadSubcommandsAndGroups(directory: string): Promise<{
162
- options: (ApplicationCommandSubGroupData | ApplicationCommandSubCommandData)[];
163
- handler: Handler<ChatInputCommandInteraction>;
164
- }> {
266
+ async function loadSubcommandsAndGroups(directory: string) {
165
267
  if (!(await fs.exists(directory))) throw new Error(`Loading subcommands/groups failed: ${directory} is required but could not be found.`);
166
268
 
167
269
  const options: (ApplicationCommandSubGroupData | ApplicationCommandSubCommandData)[] = [];
168
270
  const handlers = new Map<string, Handler<ChatInputCommandInteraction>>();
271
+ const autocompletes = new Map<string, Handler<AutocompleteInteraction>>();
169
272
 
170
273
  await importAll({ directory, recursive: false }, async ({ file, absolutePath, relativePath, item }) => {
171
274
  if (item instanceof Subcommand) {
172
275
  options.push({ ...item.data, type: ApplicationCommandOptionType.Subcommand });
173
276
  handlers.set(`/${item.data.name}`, item.handler);
277
+ if (item.autocomplete) autocompletes.set(`/${item.data.name}`, item.autocomplete);
174
278
  } else if (item instanceof SubcommandGroup) {
175
279
  const subcommands = await loadSubcommands(relativePath.replace(/\.[^/.]+$/, ""));
176
280
  options.push({ ...item.data, type: ApplicationCommandOptionType.SubcommandGroup, options: subcommands.options });
177
281
  subcommands.handlers.entries().forEach(([key, handler]) => handlers.set(`${item.data.name}/${key}`, handler));
282
+ subcommands.autocompletes.entries().forEach(([key, autocomplete]) => autocompletes.set(`${item.data.name}/${key}`, autocomplete));
178
283
  } else
179
284
  throw new Error(
180
285
  `Loading commands failed: export from ${absolutePath} (second-level in commands folder) was not an instance of SubcommandGroup or Subcommand.`,
@@ -186,23 +291,38 @@ async function loadSubcommandsAndGroups(directory: string): Promise<{
186
291
 
187
292
  return {
188
293
  options,
189
- handler: (cmd) => handlers.get(`${cmd.options.getSubcommandGroup(false) ?? ""}/${cmd.options.getSubcommand(true)}`)?.(cmd),
294
+ handler: (cmd: ChatInputCommandInteraction) => handlers.get(`${cmd.options.getSubcommandGroup(false) ?? ""}/${cmd.options.getSubcommand(true)}`)?.(cmd),
295
+ autocomplete: (cmd: AutocompleteInteraction) =>
296
+ autocompletes.get(`${cmd.options.getSubcommandGroup(false) ?? ""}/${cmd.options.getSubcommand(true)}`)?.(cmd),
190
297
  };
191
298
  }
192
299
 
193
- export async function loadCommands(
194
- client: Client<true>,
195
- directory: string,
196
- config?: {
197
- guildId?: string;
198
- wrappers?: {
199
- slashCommands?: Wrapper<Handler<ChatInputCommandInteraction>>;
200
- userCommands?: Wrapper<Handler<UserContextMenuCommandInteraction>>;
201
- messageCommands?: Wrapper<Handler<MessageContextMenuCommandInteraction>>;
202
- autocompletes?: Wrapper<Handler<AutocompleteInteraction>>;
203
- };
204
- },
205
- ) {
300
+ /**
301
+ * Load commands from a directory to a client. This will attach the command listeners. If the command data has been changed (based on the local .cache/
302
+ * directory), then the commands will be automatically set on the client (or the guild if `config.guildId` is set).
303
+ *
304
+ * Each file within the directory should be `command-name.{ts,js}` exporting a {@link SlashCommand}, {@link UserCommand}, {@link MessageCommand}, or
305
+ * {@link SlashCommandWithSubcommands}. If the file exports {@link SlashCommandWithSubcommands}, there should be a directory with the same command name.
306
+ *
307
+ * Each file within that subdirectory should be `subcommand-name.{ts,js}` exporting a {@link Subcommand} or `subcommand-group-name.{ts,js}` exporting a
308
+ * {@link SubcommandGroup}. If it exports a {@link SubcommandGroup}, there should be a directory with the same subcommand group name.
309
+ *
310
+ * Each file within the third directory level should be `subcommand-name.{ts,js}` exporting a {@link Subcommand}.
311
+ *
312
+ * The file name must match the command (or subcommand group, or subcommand) name for all types.
313
+ *
314
+ * @param client The post-login client into which to load the commands.
315
+ * @param directory The relative path (from your execution point) containing your command handlers.
316
+ * @param config Command-related configuration values.
317
+ * @param config.guildId The ID of the guild to which to set the commands.
318
+ * @param config.wrappers A set of wrappers which transform all handler functions for each handler type.
319
+ * @returns `commands`, the loaded command data (an array of objects).
320
+ * @returns `slashCommandHandlers`, the map from each slash command name to its handler.
321
+ * @returns `userCommandHandlers`, the map from each user context menu command name to its handler.
322
+ * @returns `messageCommandHandlers`, the map from each message context menu command name to its handler.
323
+ * @returns `setCommands`, indicating whether or not the commands were pushed to the Discord API.
324
+ */
325
+ export async function loadCommands(client: Client<true>, directory: string, config?: { guildId?: string; wrappers?: CommandWrappers }) {
206
326
  const commandData: (ChatInputApplicationCommandData | UserApplicationCommandData | MessageApplicationCommandData)[] = [];
207
327
 
208
328
  const slashCommandHandlers = new Map<string, Handler<ChatInputCommandInteraction>>();
@@ -223,9 +343,10 @@ export async function loadCommands(
223
343
  commandData.push({ ...item.data, type: ApplicationCommandType.Message });
224
344
  messageCommandHandlers.set(item.data.name, (config?.wrappers?.messageCommands ?? identity)(item.handler));
225
345
  } else if (item instanceof SlashCommandWithSubcommands) {
226
- const { options, handler } = await loadSubcommandsAndGroups(relativePath.replace(/\.[^/.]+$/, ""));
346
+ const { options, handler, autocomplete } = await loadSubcommandsAndGroups(relativePath.replace(/\.[^/.]+$/, ""));
227
347
  commandData.push({ ...item.data, options });
228
348
  slashCommandHandlers.set(item.data.name, (config?.wrappers?.slashCommands ?? identity)(handler));
349
+ slashCommandAutocompletes.set(item.data.name, (config?.wrappers?.autocompletes ?? identity)(autocomplete));
229
350
  } else {
230
351
  throw new Error(`Loading commands failed: export from ${absolutePath} was not an instance of <Type>Command.`);
231
352
  }
@@ -266,20 +387,44 @@ export async function loadCommands(
266
387
  return { commands: commandData, slashCommandHandlers, userCommandHandlers, messageCommandHandlers, setCommands: commandDataHasBeenUpdated };
267
388
  }
268
389
 
390
+ /**
391
+ * Load interactions from a directory to a client. This will attach the interaction (modal submit and message component) listeners.
392
+ *
393
+ * Each file within the directory (recursively) should be `*.{ts,js}` exporting a {@link ModalHandler}, {@link ButtonHandler}, {@link StringSelectMenuHandler},
394
+ * {@link UserSelectMenuHandler}, {@link RoleSelectMenuHandler}, {@link MentionableSelectMenuHandler}, or {@link ChannelSelectMenuHandler}.
395
+ *
396
+ * To use these interactions, the `customId` on the modal/message component must start with the argument separator (`config.argumentSeparator`, which defaults
397
+ * to `:`). You can optionally put a user ID after this to restrict who can interact with that component. Otherwise, place another argument separator
398
+ * immediately. Since modals are only submittable by the user to whom they're shown, setting a user ID is not recommended as you should control who sees the
399
+ * modal at all instead, but it will technically work. If this ID is set, any other users will simply be ignored and your handler will not be invoked.
400
+ *
401
+ * After the second argument separator, specify the path (separated by slashes regardless of your operating system). This will be looked up relative to the
402
+ * provided directory, so if you provide `src/interactions` and the path argument of `customId` is `pages/next`, then this will point to
403
+ * `src/interactions/pages/next.{ts,js}`.
404
+ *
405
+ * You can specify any number of additional string arguments, separating each with the argument separator. The `customId` will be naively `.split()` on this
406
+ * separator, so it is impossible to include the separator symbol/sequence anywhere in any of the arguments. Your handler will receive these as additional
407
+ * arguments.
408
+ *
409
+ * @param client The client (may be pre-login) into which to load the interaction handlers.
410
+ * @param directory The relative path (from your execution point) containing your interaction handlers.
411
+ * @param config Interaction-related configuration values.
412
+ * @param config.argumentSeparator The separator used to split custom IDs. This defaults to `:`.
413
+ * @param config.wrappers A set of wrappers which transform all handler functions for each handler type.
414
+ * @returns `modalHandlers`, the map from each modal submit handler's configuration file path to its handler function.
415
+ * @returns `buttonHandlers`, the map from each button handler's configuration file path to its handler function.
416
+ * @returns `stringSelectMenuHandlers`, the map from each string select menu handler's file path to its handler function.
417
+ * @returns `userSelectMenuHandlers`, the map from each user select menu handler's file path to its handler function.
418
+ * @returns `roleSelectMenuHandlers`, the map from each role select menu handler's file path to its handler function.
419
+ * @returns `mentionableSelectMenuHandlers`, the map from each mentionable select menu handler's file path to its handler function.
420
+ * @returns `channelSelectMenuHandlers`, the map from each channel select menu handler's file path to its handler function.
421
+ */
269
422
  export async function loadInteractions(
270
423
  client: Client,
271
424
  directory: string,
272
425
  config?: {
273
426
  argumentSeparator?: string;
274
- wrappers?: {
275
- modal?: Wrapper<Handler<ModalSubmitInteraction>>;
276
- button?: Wrapper<Handler<ButtonInteraction>>;
277
- stringSelectMenu?: Wrapper<Handler<StringSelectMenuInteraction>>;
278
- userSelectMenu?: Wrapper<Handler<UserSelectMenuInteraction>>;
279
- roleSelectMenu?: Wrapper<Handler<RoleSelectMenuInteraction>>;
280
- mentionableSelectMenu?: Wrapper<Handler<MentionableSelectMenuInteraction>>;
281
- channelSelectMenu?: Wrapper<Handler<ChannelSelectMenuInteraction>>;
282
- };
427
+ wrappers?: InteractionWrappers;
283
428
  },
284
429
  ) {
285
430
  const modalHandlers = new Map<string, Handler<ModalSubmitInteraction>>();
@@ -335,6 +480,23 @@ export async function loadInteractions(
335
480
  };
336
481
  }
337
482
 
483
+ /**
484
+ * Load event listeners from a directory to a client.
485
+ *
486
+ * Each file within the directory should be `*.{ts,js}` exporting an {@link EventHandler}. The generic type argument of {@link EventHandler} will automatically
487
+ * be inferred by its {@link EventHandler.event} property, which will automatically establish the type for the handler.
488
+ *
489
+ * Whether the files are loaded recursively depends on {@link recursive} which is `false` by default. If it is `false`, then subdirectories will not be
490
+ * scanned, allowing you to place utility functions and similar files within them.
491
+ *
492
+ * Unlike with {@link loadCommands} and {@link loadInteractions}, event handler files can be named anything and the name is purely cosmetic.
493
+ *
494
+ * @param client The client (may be pre-login) into which to load the event handlers.
495
+ * @param directory The relative path (from your execution point) containing your event handlers.
496
+ * @param recursive Whether to load recursively from the directory (defaults to false).
497
+ * @returns `handlers`, a partial record from each event type to an array of its handler functions.
498
+ * @returns `filenames`, a partial record from each event type to an array of its files' paths (relative from `directory`).
499
+ */
338
500
  export async function loadEvents(client: Client, directory: string, recursive: boolean = false) {
339
501
  const handlers: Partial<{ [K in keyof ClientEvents]: ((...args: ClientEvents[K]) => unknown)[] }> = {};
340
502
  const filenames: Partial<{ [K in keyof ClientEvents]: string[] }> = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperneutrino/djs-lite",
3
3
  "private": false,
4
- "version": "1.6.0",
4
+ "version": "1.7.0",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {