@minesa-org/mini-interaction 0.2.9 → 0.2.11

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.
@@ -1303,6 +1303,8 @@ export class MiniInteraction {
1303
1303
  const interactionWithHelpers = createMessageContextMenuInteraction(commandInteraction, {
1304
1304
  onAck: (response) => ackResolver?.(response),
1305
1305
  sendFollowUp,
1306
+ canRespond: (id) => this.canRespond(id),
1307
+ trackResponse: (id, token, state) => this.trackInteractionState(id, token, state),
1306
1308
  });
1307
1309
  response = await command.handler(interactionWithHelpers);
1308
1310
  resolvedResponse =
@@ -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;
@@ -153,21 +55,14 @@ export interface CommandInteraction extends Omit<APIChatInputApplicationCommandI
153
55
  }): APIModalInteractionResponse;
154
56
  withTimeoutProtection<T>(operation: () => Promise<T>, deferOptions?: DeferReplyOptions): Promise<T>;
155
57
  canRespond?(interactionId: string): boolean;
156
- trackResponse?(interactionId: string, token: string, state: 'responded' | 'deferred'): void;
58
+ trackResponse?(interactionId: string, token: string, state: "responded" | "deferred"): void;
157
59
  onAck?(response: APIInteractionResponse): void;
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
- trackResponse?: (interactionId: string, token: string, state: 'responded' | 'deferred') => void;
65
+ trackResponse?: (interactionId: string, token: string, state: "responded" | "deferred") => void;
171
66
  logTiming?: (interactionId: string, operation: string, startTime: number, success: boolean) => void;
172
67
  onAck?: (response: APIInteractionResponse) => void;
173
68
  sendFollowUp?: (token: string, response: APIInteractionResponse, messageId?: string) => Promise<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,23 +252,26 @@ 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)) {
382
- throw new Error('Interaction cannot respond: already responded or expired');
255
+ if (this.canRespond && !this.canRespond(this.id)) {
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
- this.trackResponse?.(this.id, this.token, 'responded');
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
- await this.sendFollowUp(this.token, response, '');
274
+ await this.sendFollowUp(this.token, response, "");
397
275
  }
398
276
  return response;
399
277
  },
@@ -401,46 +279,39 @@ export function createCommandInteraction(interaction, helpers) {
401
279
  return createMessageResponse(InteractionResponseType.UpdateMessage, data);
402
280
  },
403
281
  async editReply(data) {
404
- // Validate interaction can respond
405
- if (!this.canRespond?.(this.id)) {
406
- throw new Error('Interaction cannot edit reply: expired');
282
+ if (this.canRespond && !this.canRespond(this.id)) {
283
+ throw new Error("Interaction cannot edit reply: expired");
407
284
  }
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);
419
- }
420
- // If it's already deferred or responded, we MUST use a webhook
421
- if (this.sendFollowUp && (isDeferred || hasResponded)) {
422
- 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;
285
+ if (isDeferred) {
286
+ const normalisedData = normaliseInteractionMessageData(data);
287
+ if (!normalisedData) {
288
+ throw new Error("[MiniInteraction] editReply requires data when deferred");
289
+ }
290
+ const response = {
291
+ type: InteractionResponseType.ChannelMessageWithSource,
292
+ data: normalisedData,
293
+ };
294
+ if (this.sendFollowUp) {
295
+ await this.sendFollowUp(this.token, response, "@original");
296
+ }
297
+ this.trackResponse?.(this.id, this.token, "responded");
298
+ hasResponded = true;
299
+ return response;
426
300
  }
427
- // Track response
428
- this.trackResponse?.(this.id, this.token, 'responded');
301
+ const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
302
+ this.trackResponse?.(this.id, this.token, "responded");
429
303
  hasResponded = true;
430
304
  return response;
431
305
  },
432
306
  deferReply(options) {
433
- // Validate interaction can respond
434
- if (!this.canRespond?.(this.id)) {
435
- throw new Error('Interaction cannot defer: already responded or expired');
307
+ if (this.canRespond && !this.canRespond(this.id)) {
308
+ throw new Error("Interaction cannot defer: already responded or expired");
436
309
  }
437
310
  const response = createDeferredResponse(options?.flags !== undefined
438
311
  ? { flags: options.flags }
439
312
  : undefined);
440
- // Track deferred state
441
- this.trackResponse?.(this.id, this.token, 'deferred');
313
+ this.trackResponse?.(this.id, this.token, "deferred");
442
314
  isDeferred = true;
443
- // Notify acknowledgment
444
315
  this.onAck?.(response);
445
316
  return response;
446
317
  },
@@ -455,17 +326,9 @@ export function createCommandInteraction(interaction, helpers) {
455
326
  data: resolvedData,
456
327
  });
457
328
  },
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
329
  async withTimeoutProtection(operation, deferOptions) {
466
330
  const startTime = Date.now();
467
331
  let deferred = false;
468
- // Set up a timer to auto-defer after 2.5 seconds
469
332
  const deferTimer = setTimeout(async () => {
470
333
  if (!deferred) {
471
334
  console.warn("[MiniInteraction] Auto-deferring interaction due to slow operation. " +
@@ -489,7 +352,6 @@ export function createCommandInteraction(interaction, helpers) {
489
352
  throw error;
490
353
  }
491
354
  },
492
- // Helper methods for state management
493
355
  canRespond: helpers?.canRespond,
494
356
  trackResponse: helpers?.trackResponse,
495
357
  onAck: helpers?.onAck,
@@ -28,8 +28,12 @@ function createContextMenuInteractionHelpers(interaction, helpers) {
28
28
  return captureResponse({ type });
29
29
  }
30
30
  const reply = (data) => {
31
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
32
+ throw new Error("[MiniInteraction] Interaction cannot respond: already responded or expired");
33
+ }
31
34
  const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
32
35
  hasResponded = true;
36
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'responded');
33
37
  helpers?.onAck?.(response);
34
38
  return response;
35
39
  };
@@ -41,21 +45,31 @@ function createContextMenuInteractionHelpers(interaction, helpers) {
41
45
  return response;
42
46
  };
43
47
  const editReply = async (data) => {
44
- const response = createMessageResponse(InteractionResponseType.UpdateMessage, data);
48
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
49
+ throw new Error("[MiniInteraction] Interaction cannot edit reply: expired");
50
+ }
51
+ // Context menu commands (User/Message) MUST use ChannelMessageWithSource (4)
52
+ // for their initial response if it's the first response.
53
+ const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
45
54
  if (helpers?.sendFollowUp && (isDeferred || hasResponded)) {
46
55
  await helpers.sendFollowUp(interaction.token, response, '@original');
47
56
  return capturedResponse;
48
57
  }
49
58
  hasResponded = true;
59
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'responded');
50
60
  return response;
51
61
  };
52
62
  const deferReply = (options = {}) => {
63
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
64
+ throw new Error("[MiniInteraction] Interaction cannot defer: already responded or expired");
65
+ }
53
66
  const flags = normaliseMessageFlags(options.flags);
54
67
  const response = captureResponse({
55
68
  type: InteractionResponseType.DeferredChannelMessageWithSource,
56
69
  data: flags ? { flags } : undefined,
57
70
  });
58
71
  isDeferred = true;
72
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'deferred');
59
73
  helpers?.onAck?.(response);
60
74
  return response;
61
75
  };
@@ -71,7 +85,7 @@ function createContextMenuInteractionHelpers(interaction, helpers) {
71
85
  getResponse,
72
86
  reply,
73
87
  followUp,
74
- editReply,
88
+ editReply: editReply,
75
89
  deferReply,
76
90
  showModal,
77
91
  onAck: helpers?.onAck,
@@ -24,6 +24,9 @@ export function createMessageComponentInteraction(interaction, helpers) {
24
24
  return response;
25
25
  };
26
26
  const reply = async (data) => {
27
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
28
+ throw new Error("[MiniInteraction] Interaction cannot respond: already responded or expired");
29
+ }
27
30
  const normalisedData = normaliseInteractionMessageData(data);
28
31
  if (!normalisedData) {
29
32
  throw new Error("[MiniInteraction] Component replies require response data to be provided.");
@@ -38,9 +41,13 @@ export function createMessageComponentInteraction(interaction, helpers) {
38
41
  else {
39
42
  helpers?.onAck?.(response);
40
43
  }
44
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'responded');
41
45
  return response;
42
46
  };
43
47
  const deferReply = (options) => {
48
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
49
+ throw new Error("[MiniInteraction] Interaction cannot defer: already responded or expired");
50
+ }
44
51
  const flags = normaliseMessageFlags(options?.flags);
45
52
  const response = flags !== undefined
46
53
  ? {
@@ -52,10 +59,14 @@ export function createMessageComponentInteraction(interaction, helpers) {
52
59
  };
53
60
  captureResponse(response);
54
61
  isDeferred = true;
62
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'deferred');
55
63
  helpers?.onAck?.(response);
56
64
  return response;
57
65
  };
58
66
  const update = async (data) => {
67
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
68
+ throw new Error("[MiniInteraction] Interaction cannot update: already responded or expired");
69
+ }
59
70
  const normalisedData = normaliseInteractionMessageData(data);
60
71
  const response = captureResponse(normalisedData
61
72
  ? {
@@ -71,13 +82,18 @@ export function createMessageComponentInteraction(interaction, helpers) {
71
82
  else {
72
83
  helpers?.onAck?.(response);
73
84
  }
85
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'responded');
74
86
  return response;
75
87
  };
76
88
  const deferUpdate = () => {
89
+ if (helpers?.canRespond && !helpers.canRespond(interaction.id)) {
90
+ throw new Error("[MiniInteraction] Interaction cannot defer update: already responded or expired");
91
+ }
77
92
  const response = captureResponse({
78
93
  type: InteractionResponseType.DeferredMessageUpdate,
79
94
  });
80
95
  isDeferred = true;
96
+ helpers?.trackResponse?.(interaction.id, interaction.token, 'deferred');
81
97
  helpers?.onAck?.(response);
82
98
  return response;
83
99
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",