@minesa-org/mini-interaction 0.2.10 → 0.2.12

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.
@@ -13,130 +13,32 @@ export type MentionableOption = {
13
13
  value: APIRole;
14
14
  };
15
15
  export declare const MentionableOption: {};
16
- /**
17
- * Provides ergonomic helpers for extracting typed command options from Discord interactions.
18
- */
19
16
  export declare class CommandInteractionOptionResolver {
20
17
  readonly raw: ReadonlyArray<APIApplicationCommandInteractionDataOption>;
21
18
  private readonly resolved;
22
19
  private readonly focusedOptions;
23
20
  private readonly subcommand;
24
21
  private readonly subcommandGroup;
25
- /**
26
- * Creates a new resolver from raw interaction options.
27
- *
28
- * @param options - The raw options provided by Discord.
29
- * @param resolved - The resolved data map included with the interaction payload.
30
- */
31
22
  constructor(options: APIApplicationCommandInteractionDataOption[] | undefined, resolved: APIInteractionDataResolved | undefined);
32
- /**
33
- * Retrieves the subcommand invoked by the user.
34
- *
35
- * @param required - Whether to throw when the subcommand is missing.
36
- */
37
23
  getSubcommand(required?: boolean): string | null;
38
- /**
39
- * Retrieves the subcommand group invoked by the user.
40
- *
41
- * @param required - Whether to throw when the subcommand group is missing.
42
- */
43
24
  getSubcommandGroup(required?: boolean): string | null;
44
- /**
45
- * Looks up a string option by name.
46
- *
47
- * @param name - The option name to resolve.
48
- * @param required - Whether to throw when the option is missing.
49
- */
50
25
  getString(name: string, required?: boolean): string | null;
51
- /**
52
- * Looks up an integer option by name.
53
- *
54
- * @param name - The option name to resolve.
55
- * @param required - Whether to throw when the option is missing.
56
- */
57
26
  getInteger(name: string, required?: boolean): number | null;
58
- /**
59
- * Looks up a numeric option by name.
60
- *
61
- * @param name - The option name to resolve.
62
- */
63
27
  getNumber(name: string, required?: boolean): number | null;
64
- /**
65
- * Looks up a boolean option by name.
66
- *
67
- * @param name - The option name to resolve.
68
- * @param required - Whether to throw when the option is missing.
69
- */
70
28
  getBoolean(name: string, required?: boolean): boolean | null;
71
- /**
72
- * Resolves a user option, including any guild member payload.
73
- *
74
- * @param name - The option name to resolve.
75
- * @param required - Whether to throw when the option is missing or cannot be resolved.
76
- */
77
29
  getUser(name: string, required?: boolean): ResolvedUserOption | null;
78
- /**
79
- * Resolves a role option to its resolved payload.
80
- *
81
- * @param name - The option name to resolve.
82
- * @param required - Whether to throw when the option is missing or cannot be resolved.
83
- */
84
30
  getRole(name: string, required?: boolean): APIRole | null;
85
- /**
86
- * Resolves a channel option to its resolved payload.
87
- *
88
- * @param name - The option name to resolve.
89
- * @param required - Whether to throw when the option is missing or cannot be resolved.
90
- */
91
31
  getChannel(name: string, required?: boolean): APIInteractionDataResolvedChannel | null;
92
- /**
93
- * Resolves an attachment option to its resolved payload.
94
- *
95
- * @param name - The option name to resolve.
96
- * @param required - Whether to throw when the option is missing or cannot be resolved.
97
- */
98
32
  getAttachment(name: string, required?: boolean): APIAttachment | null;
99
- /**
100
- * Resolves a mentionable option to either a role or user payload.
101
- *
102
- * @param name - The option name to resolve.
103
- * @param required - Whether to throw when the option is missing or cannot be resolved.
104
- */
105
33
  getMentionable(name: string, required?: boolean): MentionableOption | null;
106
- /**
107
- * Returns the raw option payload, bypassing type checks and casting.
108
- *
109
- * @param name - The option name to look up.
110
- */
111
34
  getRawOption(name: string): APIApplicationCommandInteractionDataOption | null;
112
- /**
113
- * Extracts a strongly typed option, ensuring it exists and matches the expected type.
114
- */
115
35
  private extractOption;
116
- /**
117
- * Resolves an attachment by identifier from the interaction's resolved payloads.
118
- */
119
36
  private resolveAttachment;
120
- /**
121
- * Resolves a channel by identifier from the interaction's resolved payloads.
122
- */
123
37
  private resolveChannel;
124
- /**
125
- * Resolves a role by identifier from the interaction's resolved payloads.
126
- */
127
38
  private resolveRole;
128
- /**
129
- * Resolves a user and optional member by identifier from the interaction's resolved payloads.
130
- */
131
39
  private resolveUser;
132
- /**
133
- * Retrieves a record from a resolved payload map by identifier.
134
- */
135
40
  private getResolvedRecord;
136
41
  }
137
- /**
138
- * Represents a command interaction augmented with helper methods for easy responses.
139
- */
140
42
  export interface CommandInteraction extends Omit<APIChatInputApplicationCommandInteraction, "data"> {
141
43
  data: Omit<APIChatInputApplicationCommandInteraction["data"], "options"> & {
142
44
  options: CommandInteractionOptionResolver;
@@ -144,9 +46,9 @@ export interface CommandInteraction extends Omit<APIChatInputApplicationCommandI
144
46
  options: CommandInteractionOptionResolver;
145
47
  getResponse(): APIInteractionResponse | null;
146
48
  reply(data: InteractionMessageData): APIInteractionResponseChannelMessageWithSource;
147
- followUp(data: InteractionMessageData): Promise<APIInteractionResponseChannelMessageWithSource>;
49
+ followUp(data: InteractionMessageData): Promise<void>;
148
50
  edit(data?: InteractionMessageData): APIInteractionResponseUpdateMessage;
149
- editReply(data?: InteractionMessageData): Promise<APIInteractionResponseChannelMessageWithSource | APIInteractionResponseUpdateMessage>;
51
+ editReply(data?: InteractionMessageData): Promise<void>;
150
52
  deferReply(options?: DeferReplyOptions): APIInteractionResponseDeferredChannelMessageWithSource;
151
53
  showModal(data: APIModalInteractionResponseCallbackData | {
152
54
  toJSON(): APIModalInteractionResponseCallbackData;
@@ -158,13 +60,6 @@ export interface CommandInteraction extends Omit<APIChatInputApplicationCommandI
158
60
  sendFollowUp?(token: string, response: APIInteractionResponse, messageId?: string): Promise<void>;
159
61
  }
160
62
  export declare const CommandInteraction: {};
161
- /**
162
- * Wraps a raw application command interaction with helper methods and option resolvers.
163
- *
164
- * @param interaction - The raw interaction payload from Discord.
165
- * @param helpers - Optional helper methods for state management and logging.
166
- * @returns A helper-augmented interaction object.
167
- */
168
63
  export declare function createCommandInteraction(interaction: APIChatInputApplicationCommandInteraction, helpers?: {
169
64
  canRespond?: (interactionId: string) => boolean;
170
65
  trackResponse?: (interactionId: string, token: string, state: 'responded' | 'deferred') => void;
@@ -1,8 +1,7 @@
1
- import { ApplicationCommandOptionType, InteractionResponseType, InteractionType, } from "discord-api-types/v10";
1
+ import { ApplicationCommandOptionType, InteractionResponseType, } from "discord-api-types/v10";
2
2
  import { normaliseInteractionMessageData, normaliseMessageFlags, } from "./interactionMessageHelpers.js";
3
3
  export const ResolvedUserOption = {};
4
4
  export const MentionableOption = {};
5
- /** Maps application command option types to human-readable labels for error messages. */
6
5
  const OPTION_TYPE_LABEL = {
7
6
  [ApplicationCommandOptionType.Subcommand]: "subcommand",
8
7
  [ApplicationCommandOptionType.SubcommandGroup]: "subcommand group",
@@ -16,28 +15,12 @@ const OPTION_TYPE_LABEL = {
16
15
  [ApplicationCommandOptionType.Number]: "number",
17
16
  [ApplicationCommandOptionType.Attachment]: "attachment",
18
17
  };
19
- /**
20
- * Determines whether the provided option represents a subcommand.
21
- *
22
- * @param option - The option to inspect.
23
- */
24
18
  function isSubcommandOption(option) {
25
19
  return option.type === ApplicationCommandOptionType.Subcommand;
26
20
  }
27
- /**
28
- * Determines whether the provided option represents a subcommand group.
29
- *
30
- * @param option - The option to inspect.
31
- */
32
21
  function isSubcommandGroupOption(option) {
33
22
  return option.type === ApplicationCommandOptionType.SubcommandGroup;
34
23
  }
35
- /**
36
- * Extracts the most relevant option set from the raw command options hierarchy.
37
- *
38
- * @param options - The raw options array from the interaction payload.
39
- * @returns The resolved subcommand context and focused options for convenience accessors.
40
- */
41
24
  function resolveFocusedOptions(options) {
42
25
  if (!options || options.length === 0) {
43
26
  return { subcommandGroup: null, subcommand: null, options: [] };
@@ -71,21 +54,12 @@ function resolveFocusedOptions(options) {
71
54
  options: options,
72
55
  };
73
56
  }
74
- /**
75
- * Provides ergonomic helpers for extracting typed command options from Discord interactions.
76
- */
77
57
  export class CommandInteractionOptionResolver {
78
58
  raw;
79
59
  resolved;
80
60
  focusedOptions;
81
61
  subcommand;
82
62
  subcommandGroup;
83
- /**
84
- * Creates a new resolver from raw interaction options.
85
- *
86
- * @param options - The raw options provided by Discord.
87
- * @param resolved - The resolved data map included with the interaction payload.
88
- */
89
63
  constructor(options, resolved) {
90
64
  this.raw = Object.freeze([...(options ?? [])]);
91
65
  this.resolved = resolved;
@@ -94,11 +68,6 @@ export class CommandInteractionOptionResolver {
94
68
  this.subcommand = focused.subcommand;
95
69
  this.subcommandGroup = focused.subcommandGroup;
96
70
  }
97
- /**
98
- * Retrieves the subcommand invoked by the user.
99
- *
100
- * @param required - Whether to throw when the subcommand is missing.
101
- */
102
71
  getSubcommand(required = true) {
103
72
  if (this.subcommand) {
104
73
  return this.subcommand;
@@ -108,11 +77,6 @@ export class CommandInteractionOptionResolver {
108
77
  }
109
78
  return null;
110
79
  }
111
- /**
112
- * Retrieves the subcommand group invoked by the user.
113
- *
114
- * @param required - Whether to throw when the subcommand group is missing.
115
- */
116
80
  getSubcommandGroup(required = true) {
117
81
  if (this.subcommandGroup) {
118
82
  return this.subcommandGroup;
@@ -122,51 +86,22 @@ export class CommandInteractionOptionResolver {
122
86
  }
123
87
  return null;
124
88
  }
125
- /**
126
- * Looks up a string option by name.
127
- *
128
- * @param name - The option name to resolve.
129
- * @param required - Whether to throw when the option is missing.
130
- */
131
89
  getString(name, required = false) {
132
90
  const option = this.extractOption(name, ApplicationCommandOptionType.String, required);
133
91
  return option ? option.value : null;
134
92
  }
135
- /**
136
- * Looks up an integer option by name.
137
- *
138
- * @param name - The option name to resolve.
139
- * @param required - Whether to throw when the option is missing.
140
- */
141
93
  getInteger(name, required = false) {
142
94
  const option = this.extractOption(name, ApplicationCommandOptionType.Integer, required);
143
95
  return option ? option.value : null;
144
96
  }
145
- /**
146
- * Looks up a numeric option by name.
147
- *
148
- * @param name - The option name to resolve.
149
- */
150
97
  getNumber(name, required = false) {
151
98
  const option = this.extractOption(name, ApplicationCommandOptionType.Number, required);
152
99
  return option ? option.value : null;
153
100
  }
154
- /**
155
- * Looks up a boolean option by name.
156
- *
157
- * @param name - The option name to resolve.
158
- * @param required - Whether to throw when the option is missing.
159
- */
160
101
  getBoolean(name, required = false) {
161
102
  const option = this.extractOption(name, ApplicationCommandOptionType.Boolean, required);
162
103
  return option ? option.value : null;
163
104
  }
164
- /**
165
- * Resolves a user option, including any guild member payload.
166
- *
167
- * @param name - The option name to resolve.
168
- * @param required - Whether to throw when the option is missing or cannot be resolved.
169
- */
170
105
  getUser(name, required = false) {
171
106
  const option = this.extractOption(name, ApplicationCommandOptionType.User, required);
172
107
  if (!option) {
@@ -181,12 +116,6 @@ export class CommandInteractionOptionResolver {
181
116
  }
182
117
  return resolvedUser;
183
118
  }
184
- /**
185
- * Resolves a role option to its resolved payload.
186
- *
187
- * @param name - The option name to resolve.
188
- * @param required - Whether to throw when the option is missing or cannot be resolved.
189
- */
190
119
  getRole(name, required = false) {
191
120
  const option = this.extractOption(name, ApplicationCommandOptionType.Role, required);
192
121
  if (!option) {
@@ -198,12 +127,6 @@ export class CommandInteractionOptionResolver {
198
127
  }
199
128
  return role ?? null;
200
129
  }
201
- /**
202
- * Resolves a channel option to its resolved payload.
203
- *
204
- * @param name - The option name to resolve.
205
- * @param required - Whether to throw when the option is missing or cannot be resolved.
206
- */
207
130
  getChannel(name, required = false) {
208
131
  const option = this.extractOption(name, ApplicationCommandOptionType.Channel, required);
209
132
  if (!option) {
@@ -215,12 +138,6 @@ export class CommandInteractionOptionResolver {
215
138
  }
216
139
  return channel ?? null;
217
140
  }
218
- /**
219
- * Resolves an attachment option to its resolved payload.
220
- *
221
- * @param name - The option name to resolve.
222
- * @param required - Whether to throw when the option is missing or cannot be resolved.
223
- */
224
141
  getAttachment(name, required = false) {
225
142
  const option = this.extractOption(name, ApplicationCommandOptionType.Attachment, required);
226
143
  if (!option) {
@@ -232,12 +149,6 @@ export class CommandInteractionOptionResolver {
232
149
  }
233
150
  return attachment ?? null;
234
151
  }
235
- /**
236
- * Resolves a mentionable option to either a role or user payload.
237
- *
238
- * @param name - The option name to resolve.
239
- * @param required - Whether to throw when the option is missing or cannot be resolved.
240
- */
241
152
  getMentionable(name, required = false) {
242
153
  const option = this.extractOption(name, ApplicationCommandOptionType.Mentionable, required);
243
154
  if (!option) {
@@ -256,17 +167,9 @@ export class CommandInteractionOptionResolver {
256
167
  }
257
168
  return null;
258
169
  }
259
- /**
260
- * Returns the raw option payload, bypassing type checks and casting.
261
- *
262
- * @param name - The option name to look up.
263
- */
264
170
  getRawOption(name) {
265
171
  return (this.focusedOptions.find((option) => option.name === name) ?? null);
266
172
  }
267
- /**
268
- * Extracts a strongly typed option, ensuring it exists and matches the expected type.
269
- */
270
173
  extractOption(name, type, required) {
271
174
  const option = this.focusedOptions.find((candidate) => candidate.name === name);
272
175
  if (!option) {
@@ -280,27 +183,15 @@ export class CommandInteractionOptionResolver {
280
183
  }
281
184
  return option;
282
185
  }
283
- /**
284
- * Resolves an attachment by identifier from the interaction's resolved payloads.
285
- */
286
186
  resolveAttachment(id) {
287
187
  return this.getResolvedRecord(this.resolved?.attachments, id);
288
188
  }
289
- /**
290
- * Resolves a channel by identifier from the interaction's resolved payloads.
291
- */
292
189
  resolveChannel(id) {
293
190
  return this.getResolvedRecord(this.resolved?.channels, id);
294
191
  }
295
- /**
296
- * Resolves a role by identifier from the interaction's resolved payloads.
297
- */
298
192
  resolveRole(id) {
299
193
  return this.getResolvedRecord(this.resolved?.roles, id);
300
194
  }
301
- /**
302
- * Resolves a user and optional member by identifier from the interaction's resolved payloads.
303
- */
304
195
  resolveUser(id) {
305
196
  const user = this.getResolvedRecord(this.resolved?.users, id);
306
197
  if (!user) {
@@ -309,29 +200,16 @@ export class CommandInteractionOptionResolver {
309
200
  const member = this.getResolvedRecord(this.resolved?.members, id);
310
201
  return { user, member: member ?? undefined };
311
202
  }
312
- /**
313
- * Retrieves a record from a resolved payload map by identifier.
314
- */
315
203
  getResolvedRecord(records, id) {
316
204
  return records?.[id];
317
205
  }
318
206
  }
319
207
  export const CommandInteraction = {};
320
- /**
321
- * Wraps a raw application command interaction with helper methods and option resolvers.
322
- *
323
- * @param interaction - The raw interaction payload from Discord.
324
- * @param helpers - Optional helper methods for state management and logging.
325
- * @returns A helper-augmented interaction object.
326
- */
327
208
  export function createCommandInteraction(interaction, helpers) {
328
209
  const options = new CommandInteractionOptionResolver(interaction.data.options, interaction.data.resolved);
329
210
  let capturedResponse = null;
330
211
  let isDeferred = false;
331
212
  let hasResponded = false;
332
- /**
333
- * Stores the most recent response helper payload for later retrieval.
334
- */
335
213
  const captureResponse = (response) => {
336
214
  capturedResponse = response;
337
215
  return response;
@@ -352,9 +230,6 @@ export function createCommandInteraction(interaction, helpers) {
352
230
  }
353
231
  return captureResponse({ type });
354
232
  }
355
- /**
356
- * Creates a deferred response while normalising any helper flag values.
357
- */
358
233
  const createDeferredResponse = (data) => {
359
234
  if (!data) {
360
235
  return captureResponse({
@@ -377,70 +252,58 @@ export function createCommandInteraction(interaction, helpers) {
377
252
  return capturedResponse;
378
253
  },
379
254
  reply(data) {
380
- // Validate interaction can respond
381
- if (!this.canRespond?.(this.id)) {
255
+ if (this.canRespond && !this.canRespond(this.id)) {
382
256
  throw new Error('Interaction cannot respond: already responded or expired');
383
257
  }
384
258
  const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
385
- // Track response
386
259
  this.trackResponse?.(this.id, this.token, 'responded');
387
260
  hasResponded = true;
388
- // Notify acknowledgment
389
261
  this.onAck?.(response);
390
262
  return response;
391
263
  },
392
264
  async followUp(data) {
393
- const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
265
+ const normalisedData = normaliseInteractionMessageData(data);
266
+ if (!normalisedData) {
267
+ throw new Error('[MiniInteraction] followUp requires data');
268
+ }
269
+ const response = {
270
+ type: InteractionResponseType.ChannelMessageWithSource,
271
+ data: normalisedData,
272
+ };
394
273
  if (this.sendFollowUp) {
395
- // Empty string for messageId means a new follow-up (POST)
396
274
  await this.sendFollowUp(this.token, response, '');
397
275
  }
398
- return response;
399
276
  },
400
277
  edit(data) {
401
278
  return createMessageResponse(InteractionResponseType.UpdateMessage, data);
402
279
  },
403
280
  async editReply(data) {
404
- // Validate interaction can respond
405
- if (!this.canRespond?.(this.id)) {
281
+ if (this.canRespond && !this.canRespond(this.id)) {
406
282
  throw new Error('Interaction cannot edit reply: expired');
407
283
  }
408
- // Slash commands (type 2) MUST use ChannelMessageWithSource (type 4) for their initial response,
409
- // or UpdateMessage (type 7) if they are updating a component interaction message.
410
- // However, for as-yet-unresponded slash commands, we need type 4.
411
- const interactionAny = interaction;
412
- const isComponent = interactionAny.type === InteractionType.MessageComponent;
413
- let response;
414
- if (isComponent) {
415
- response = createMessageResponse(InteractionResponseType.UpdateMessage, data);
416
- }
417
- else {
418
- response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
284
+ const normalisedData = normaliseInteractionMessageData(data);
285
+ if (!normalisedData) {
286
+ throw new Error('[MiniInteraction] editReply requires data');
419
287
  }
420
- // If it's already deferred or responded, we MUST use a webhook
421
- if (this.sendFollowUp && (isDeferred || hasResponded)) {
288
+ const response = {
289
+ type: InteractionResponseType.ChannelMessageWithSource,
290
+ data: normalisedData,
291
+ };
292
+ if (this.sendFollowUp) {
422
293
  await this.sendFollowUp(this.token, response, '@original');
423
- // If we already sent an ACK (like a DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE),
424
- // we return the original captured response to avoid sending type 4/7 back to the initial POST.
425
- return capturedResponse;
426
294
  }
427
- // Track response
428
295
  this.trackResponse?.(this.id, this.token, 'responded');
429
296
  hasResponded = true;
430
- return response;
431
297
  },
432
298
  deferReply(options) {
433
- // Validate interaction can respond
434
- if (!this.canRespond?.(this.id)) {
299
+ if (this.canRespond && !this.canRespond(this.id)) {
435
300
  throw new Error('Interaction cannot defer: already responded or expired');
436
301
  }
437
302
  const response = createDeferredResponse(options?.flags !== undefined
438
303
  ? { flags: options.flags }
439
304
  : undefined);
440
- // Track deferred state
441
305
  this.trackResponse?.(this.id, this.token, 'deferred');
442
306
  isDeferred = true;
443
- // Notify acknowledgment
444
307
  this.onAck?.(response);
445
308
  return response;
446
309
  },
@@ -455,17 +318,9 @@ export function createCommandInteraction(interaction, helpers) {
455
318
  data: resolvedData,
456
319
  });
457
320
  },
458
- /**
459
- * Creates a delayed response wrapper that automatically defers if the operation takes too long.
460
- * Use this for operations that might exceed Discord's 3-second limit.
461
- *
462
- * @param operation - The async operation to perform
463
- * @param deferOptions - Options for automatic deferral
464
- */
465
321
  async withTimeoutProtection(operation, deferOptions) {
466
322
  const startTime = Date.now();
467
323
  let deferred = false;
468
- // Set up a timer to auto-defer after 2.5 seconds
469
324
  const deferTimer = setTimeout(async () => {
470
325
  if (!deferred) {
471
326
  console.warn("[MiniInteraction] Auto-deferring interaction due to slow operation. " +
@@ -489,7 +344,6 @@ export function createCommandInteraction(interaction, helpers) {
489
344
  throw error;
490
345
  }
491
346
  },
492
- // Helper methods for state management
493
347
  canRespond: helpers?.canRespond,
494
348
  trackResponse: helpers?.trackResponse,
495
349
  onAck: helpers?.onAck,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",