@hyperneutrino/djs-lite 1.6.1 → 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 +169 -12
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -46,6 +46,10 @@ type WrapperConfig<R extends Record<string, Handleable>> = {
46
46
  [K in keyof R]?: Wrapper<Handler<R[K]>>;
47
47
  };
48
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
+ */
49
53
  export interface CommandWrappers extends WrapperConfig<{
50
54
  slashCommands: ChatInputCommandInteraction;
51
55
  userCommands: UserContextMenuCommandInteraction;
@@ -53,6 +57,10 @@ export interface CommandWrappers extends WrapperConfig<{
53
57
  autocompletes: AutocompleteInteraction;
54
58
  }> {}
55
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
+ */
56
64
  export interface InteractionWrappers extends WrapperConfig<{
57
65
  modal: ModalSubmitInteraction;
58
66
  button: ButtonInteraction;
@@ -63,13 +71,26 @@ export interface InteractionWrappers extends WrapperConfig<{
63
71
  channelSelectMenu: ChannelSelectMenuInteraction;
64
72
  }> {}
65
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
+ */
66
77
  export interface Wrappers extends CommandWrappers, InteractionWrappers {}
67
78
 
68
79
  abstract class Command<T extends BaseApplicationCommandData, U extends CommandInteraction, Z extends boolean = false> {
80
+ /** This is the command data for the configured command. */
69
81
  data: T;
82
+
83
+ /** This is the handler for the configured command. */
70
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). */
71
87
  autocomplete: Handler<AutocompleteInteraction> | null;
72
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
+ */
73
94
  constructor({ handler, ...data }: T & { handler: Handler<U> } & (Z extends true ? { autocomplete?: Handler<AutocompleteInteraction> } : {})) {
74
95
  if ("autocomplete" in data) {
75
96
  const { autocomplete, ...rest } = data;
@@ -84,56 +105,116 @@ abstract class Command<T extends BaseApplicationCommandData, U extends CommandIn
84
105
  }
85
106
  }
86
107
 
108
+ /** This is a slash command configuration object. Prefer {@link SlashCommandWithSubcommands} unless you are sure you want to do subcommand routing manually. */
87
109
  export class SlashCommand extends Command<Omit<ChatInputApplicationCommandData, "type">, ChatInputCommandInteraction, true> {}
110
+
111
+ /** This is a user context menu command configuration object. */
88
112
  export class UserCommand extends Command<Omit<UserApplicationCommandData, "type">, UserContextMenuCommandInteraction> {}
113
+
114
+ /** This is a message context menu command configuration object. */
89
115
  export class MessageCommand extends Command<Omit<MessageApplicationCommandData, "type">, MessageContextMenuCommandInteraction> {}
90
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
+ */
91
121
  export class SlashCommandWithSubcommands {
122
+ /** This is the command data for the root command with the options omitted. */
92
123
  data: Omit<ChatInputApplicationCommandData, "options" | "type">;
93
124
 
125
+ /**
126
+ * @param data This is the command data for the root command with the options ommitted.
127
+ */
94
128
  constructor(data: typeof this.data) {
95
129
  this.data = data;
96
130
  }
97
131
  }
98
132
 
133
+ /** This is a slash command subcommand group configuration object. There is no handler here; the handler is specified in each subcommand. */
99
134
  export class SubcommandGroup {
135
+ /** This is the subcommand group data with the options omitted. */
100
136
  data: Omit<ApplicationCommandSubGroupData, "options" | "type">;
101
137
 
138
+ /**
139
+ * @param data This is the subcommand group data with the options omitted.
140
+ */
102
141
  constructor(data: typeof this.data) {
103
142
  this.data = data;
104
143
  }
105
144
  }
106
145
 
146
+ /** This is a slash command subcommand configuration object. */
107
147
  export class Subcommand {
148
+ /** This is the subcommand data for the configured subcommand. */
108
149
  data: Omit<ApplicationCommandSubCommandData, "type">;
150
+
151
+ /** This is the handler for the configured subcommand. */
109
152
  handler: Handler<ChatInputCommandInteraction>;
110
153
 
111
- 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"] }) {
112
163
  this.data = data;
113
164
  this.handler = handler;
165
+ this.autocomplete = autocomplete ?? null;
114
166
  }
115
167
  }
116
168
 
117
169
  abstract class ComponentHandler<T extends ModalSubmitInteraction | MessageComponentInteraction> {
170
+ /** This is the handler for the configured interaction. */
118
171
  handler: Handler<T>;
119
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
+ */
120
177
  constructor(handler: Handler<T>) {
121
178
  this.handler = handler;
122
179
  }
123
180
  }
124
181
 
182
+ /** This is a modal submit interaction configuration object. */
125
183
  export class ModalHandler extends ComponentHandler<ModalSubmitInteraction> {}
184
+
185
+ /** This is a button component interaction configuration object. */
126
186
  export class ButtonHandler extends ComponentHandler<ButtonInteraction> {}
187
+
188
+ /** This is a string select menu component interaction configuration object. */
127
189
  export class StringSelectMenuHandler extends ComponentHandler<StringSelectMenuInteraction> {}
190
+
191
+ /** This is a user select menu component interaction configuration object. */
128
192
  export class UserSelectMenuHandler extends ComponentHandler<UserSelectMenuInteraction> {}
193
+
194
+ /** This is a role select menu component interaction configuration object. */
129
195
  export class RoleSelectMenuHandler extends ComponentHandler<RoleSelectMenuInteraction> {}
196
+
197
+ /** This is a mentionable select menu component interaction configuration object. */
130
198
  export class MentionableSelectMenuHandler extends ComponentHandler<MentionableSelectMenuInteraction> {}
199
+
200
+ /** This is a channel select menu component interaction configuration object. */
131
201
  export class ChannelSelectMenuHandler extends ComponentHandler<ChannelSelectMenuInteraction> {}
132
202
 
203
+ /**
204
+ * This is an event handler configuration object.
205
+ */
133
206
  export class EventHandler<T extends keyof ClientEvents> {
207
+ /** This is the event to watch. */
134
208
  event: T;
209
+
210
+ /** This is the handler for the configured event type. */
135
211
  handler: (...args: ClientEvents[T]) => unknown;
136
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
+ */
137
218
  constructor({ event, handler }: { event: T; handler: (...args: ClientEvents[T]) => unknown }) {
138
219
  this.event = event;
139
220
  this.handler = handler;
@@ -161,45 +242,44 @@ async function importAll(
161
242
  );
162
243
  }
163
244
 
164
- async function loadSubcommands(directory: string): Promise<{
165
- options: ApplicationCommandSubCommandData[];
166
- handlers: Map<string, Handler<ChatInputCommandInteraction>>;
167
- }> {
245
+ async function loadSubcommands(directory: string) {
168
246
  if (!(await fs.exists(directory))) throw new Error(`Loading subcommands within a group failed: ${directory} is required but could not be found.`);
169
247
 
170
248
  const options: ApplicationCommandSubCommandData[] = [];
171
249
  const handlers = new Map<string, Handler<ChatInputCommandInteraction>>();
250
+ const autocompletes = new Map<string, Handler<AutocompleteInteraction>>();
172
251
 
173
252
  await importAll({ directory, recursive: false }, async ({ file, absolutePath, item }) => {
174
253
  if (item instanceof Subcommand) {
175
254
  options.push({ ...item.data, type: ApplicationCommandOptionType.Subcommand });
176
255
  handlers.set(item.data.name, item.handler);
256
+ if (item.autocomplete) autocompletes.set(item.data.name, item.autocomplete);
177
257
  } else throw new Error(`Loading commands failed: export from ${absolutePath} (third-level in commands folder) was not an instance of Subcommand.`);
178
258
 
179
259
  if (item.data.name !== file.name.replace(/.[^/.]+$/, ""))
180
260
  throw new Error(`Code style enforcement: name exported from ${absolutePath} does not match the filename`);
181
261
  });
182
262
 
183
- return { options, handlers };
263
+ return { options, handlers, autocompletes };
184
264
  }
185
265
 
186
- async function loadSubcommandsAndGroups(directory: string): Promise<{
187
- options: (ApplicationCommandSubGroupData | ApplicationCommandSubCommandData)[];
188
- handler: Handler<ChatInputCommandInteraction>;
189
- }> {
266
+ async function loadSubcommandsAndGroups(directory: string) {
190
267
  if (!(await fs.exists(directory))) throw new Error(`Loading subcommands/groups failed: ${directory} is required but could not be found.`);
191
268
 
192
269
  const options: (ApplicationCommandSubGroupData | ApplicationCommandSubCommandData)[] = [];
193
270
  const handlers = new Map<string, Handler<ChatInputCommandInteraction>>();
271
+ const autocompletes = new Map<string, Handler<AutocompleteInteraction>>();
194
272
 
195
273
  await importAll({ directory, recursive: false }, async ({ file, absolutePath, relativePath, item }) => {
196
274
  if (item instanceof Subcommand) {
197
275
  options.push({ ...item.data, type: ApplicationCommandOptionType.Subcommand });
198
276
  handlers.set(`/${item.data.name}`, item.handler);
277
+ if (item.autocomplete) autocompletes.set(`/${item.data.name}`, item.autocomplete);
199
278
  } else if (item instanceof SubcommandGroup) {
200
279
  const subcommands = await loadSubcommands(relativePath.replace(/\.[^/.]+$/, ""));
201
280
  options.push({ ...item.data, type: ApplicationCommandOptionType.SubcommandGroup, options: subcommands.options });
202
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));
203
283
  } else
204
284
  throw new Error(
205
285
  `Loading commands failed: export from ${absolutePath} (second-level in commands folder) was not an instance of SubcommandGroup or Subcommand.`,
@@ -211,10 +291,37 @@ async function loadSubcommandsAndGroups(directory: string): Promise<{
211
291
 
212
292
  return {
213
293
  options,
214
- 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),
215
297
  };
216
298
  }
217
299
 
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
+ */
218
325
  export async function loadCommands(client: Client<true>, directory: string, config?: { guildId?: string; wrappers?: CommandWrappers }) {
219
326
  const commandData: (ChatInputApplicationCommandData | UserApplicationCommandData | MessageApplicationCommandData)[] = [];
220
327
 
@@ -236,9 +343,10 @@ export async function loadCommands(client: Client<true>, directory: string, conf
236
343
  commandData.push({ ...item.data, type: ApplicationCommandType.Message });
237
344
  messageCommandHandlers.set(item.data.name, (config?.wrappers?.messageCommands ?? identity)(item.handler));
238
345
  } else if (item instanceof SlashCommandWithSubcommands) {
239
- const { options, handler } = await loadSubcommandsAndGroups(relativePath.replace(/\.[^/.]+$/, ""));
346
+ const { options, handler, autocomplete } = await loadSubcommandsAndGroups(relativePath.replace(/\.[^/.]+$/, ""));
240
347
  commandData.push({ ...item.data, options });
241
348
  slashCommandHandlers.set(item.data.name, (config?.wrappers?.slashCommands ?? identity)(handler));
349
+ slashCommandAutocompletes.set(item.data.name, (config?.wrappers?.autocompletes ?? identity)(autocomplete));
242
350
  } else {
243
351
  throw new Error(`Loading commands failed: export from ${absolutePath} was not an instance of <Type>Command.`);
244
352
  }
@@ -279,6 +387,38 @@ export async function loadCommands(client: Client<true>, directory: string, conf
279
387
  return { commands: commandData, slashCommandHandlers, userCommandHandlers, messageCommandHandlers, setCommands: commandDataHasBeenUpdated };
280
388
  }
281
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
+ */
282
422
  export async function loadInteractions(
283
423
  client: Client,
284
424
  directory: string,
@@ -340,6 +480,23 @@ export async function loadInteractions(
340
480
  };
341
481
  }
342
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
+ */
343
500
  export async function loadEvents(client: Client, directory: string, recursive: boolean = false) {
344
501
  const handlers: Partial<{ [K in keyof ClientEvents]: ((...args: ClientEvents[K]) => unknown)[] }> = {};
345
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.1",
4
+ "version": "1.7.0",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {