@minesa-org/mini-interaction 0.2.4 → 0.2.5

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.
@@ -394,5 +394,10 @@ export declare class MiniInteraction {
394
394
  * Handles execution of an application command interaction.
395
395
  */
396
396
  private handleApplicationCommand;
397
+ /**
398
+ * Sends a follow-up response or edits an existing response via Discord's interaction webhooks.
399
+ * This is used for interactions that have already been acknowledged (e.g., via deferReply).
400
+ */
401
+ private sendFollowUp;
397
402
  }
398
403
  export {};
@@ -1125,8 +1125,11 @@ export class MiniInteraction {
1125
1125
  const ackPromise = new Promise((resolve) => {
1126
1126
  ackResolver = resolve;
1127
1127
  });
1128
+ // Helper to send follow-up responses via webhooks
1129
+ const sendFollowUp = (token, data) => this.sendFollowUp(token, data);
1128
1130
  const interactionWithHelpers = createMessageComponentInteraction(interaction, {
1129
1131
  onAck: (response) => ackResolver?.(response),
1132
+ sendFollowUp,
1130
1133
  });
1131
1134
  // Wrap component handler with timeout and acknowledgment
1132
1135
  const timeoutWrapper = createTimeoutWrapper(async () => {
@@ -1187,8 +1190,11 @@ export class MiniInteraction {
1187
1190
  const ackPromise = new Promise((resolve) => {
1188
1191
  ackResolver = resolve;
1189
1192
  });
1193
+ // Helper to send follow-up responses via webhooks
1194
+ const sendFollowUp = (token, data) => this.sendFollowUp(token, data);
1190
1195
  const interactionWithHelpers = createModalSubmitInteraction(interaction, {
1191
1196
  onAck: (response) => ackResolver?.(response),
1197
+ sendFollowUp,
1192
1198
  });
1193
1199
  // Wrap modal handler with timeout and acknowledgment
1194
1200
  const timeoutWrapper = createTimeoutWrapper(async () => {
@@ -1252,6 +1258,8 @@ export class MiniInteraction {
1252
1258
  const ackPromise = new Promise((resolve) => {
1253
1259
  ackResolver = resolve;
1254
1260
  });
1261
+ // Helper to send follow-up responses via webhooks
1262
+ const sendFollowUp = (token, data) => this.sendFollowUp(token, data);
1255
1263
  // Create a timeout wrapper for the command handler
1256
1264
  const timeoutWrapper = createTimeoutWrapper(async () => {
1257
1265
  // Check if it's a chat input (slash) command
@@ -1261,6 +1269,7 @@ export class MiniInteraction {
1261
1269
  canRespond: (id) => this.canRespond(id),
1262
1270
  trackResponse: (id, token, state) => this.trackInteractionState(id, token, state),
1263
1271
  onAck: (response) => ackResolver?.(response),
1272
+ sendFollowUp,
1264
1273
  });
1265
1274
  response = await command.handler(interactionWithHelpers);
1266
1275
  resolvedResponse =
@@ -1338,6 +1347,33 @@ export class MiniInteraction {
1338
1347
  };
1339
1348
  }
1340
1349
  }
1350
+ /**
1351
+ * Sends a follow-up response or edits an existing response via Discord's interaction webhooks.
1352
+ * This is used for interactions that have already been acknowledged (e.g., via deferReply).
1353
+ */
1354
+ async sendFollowUp(token, response) {
1355
+ const url = `${DISCORD_BASE_URL}/webhooks/${this.applicationId}/${token}/messages/@original`;
1356
+ // Only send follow-up if there is data to send
1357
+ if (!('data' in response) || !response.data) {
1358
+ return;
1359
+ }
1360
+ try {
1361
+ const fetchResponse = await this.fetchImpl(url, {
1362
+ method: "PATCH",
1363
+ headers: {
1364
+ "Content-Type": "application/json",
1365
+ },
1366
+ body: JSON.stringify(response.data),
1367
+ });
1368
+ if (!fetchResponse.ok) {
1369
+ const errorBody = await fetchResponse.text();
1370
+ console.error(`[MiniInteraction] Failed to send follow-up response: [${fetchResponse.status}] ${errorBody}`);
1371
+ }
1372
+ }
1373
+ catch (error) {
1374
+ console.error(`[MiniInteraction] Error sending follow-up response: ${error instanceof Error ? error.message : String(error)}`);
1375
+ }
1376
+ }
1341
1377
  }
1342
1378
  const DEFAULT_DISCORD_OAUTH_TEMPLATES = {
1343
1379
  success: ({ user }) => {
@@ -155,6 +155,7 @@ export interface CommandInteraction extends Omit<APIChatInputApplicationCommandI
155
155
  canRespond?(interactionId: string): boolean;
156
156
  trackResponse?(interactionId: string, token: string, state: 'responded' | 'deferred'): void;
157
157
  onAck?(response: APIInteractionResponse): void;
158
+ sendFollowUp?(token: string, response: APIInteractionResponse): void;
158
159
  }
159
160
  export declare const CommandInteraction: {};
160
161
  /**
@@ -169,4 +170,5 @@ export declare function createCommandInteraction(interaction: APIChatInputApplic
169
170
  trackResponse?: (interactionId: string, token: string, state: 'responded' | 'deferred') => void;
170
171
  logTiming?: (interactionId: string, operation: string, startTime: number, success: boolean) => void;
171
172
  onAck?: (response: APIInteractionResponse) => void;
173
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
172
174
  }): CommandInteraction;
@@ -379,17 +379,19 @@ export function createCommandInteraction(interaction, helpers) {
379
379
  if (!this.canRespond?.(this.id)) {
380
380
  throw new Error('Interaction cannot respond: already responded or expired');
381
381
  }
382
- const startTime = Date.now();
383
382
  const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
384
383
  // Track response
385
384
  this.trackResponse?.(this.id, this.token, 'responded');
386
385
  // Notify acknowledgment
387
386
  this.onAck?.(response);
388
- // Log timing if debug enabled
389
387
  return response;
390
388
  },
391
389
  followUp(data) {
392
- return createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
390
+ const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
391
+ if (this.sendFollowUp) {
392
+ this.sendFollowUp(this.token, response);
393
+ }
394
+ return response;
393
395
  },
394
396
  edit(data) {
395
397
  return createMessageResponse(InteractionResponseType.UpdateMessage, data);
@@ -399,11 +401,13 @@ export function createCommandInteraction(interaction, helpers) {
399
401
  if (!this.canRespond?.(this.id)) {
400
402
  throw new Error('Interaction cannot edit reply: already responded, expired, or not deferred');
401
403
  }
402
- const startTime = Date.now();
403
404
  const response = createMessageResponse(InteractionResponseType.UpdateMessage, data);
405
+ // If it's already deferred or responded, we MUST use a webhook
406
+ if (this.sendFollowUp) {
407
+ this.sendFollowUp(this.token, response);
408
+ }
404
409
  // Track response
405
410
  this.trackResponse?.(this.id, this.token, 'responded');
406
- // Log timing if debug enabled
407
411
  return response;
408
412
  },
409
413
  deferReply(options) {
@@ -411,7 +415,6 @@ export function createCommandInteraction(interaction, helpers) {
411
415
  if (!this.canRespond?.(this.id)) {
412
416
  throw new Error('Interaction cannot defer: already responded or expired');
413
417
  }
414
- const startTime = Date.now();
415
418
  const response = createDeferredResponse(options?.flags !== undefined
416
419
  ? { flags: options.flags }
417
420
  : undefined);
@@ -419,7 +422,6 @@ export function createCommandInteraction(interaction, helpers) {
419
422
  this.trackResponse?.(this.id, this.token, 'deferred');
420
423
  // Notify acknowledgment
421
424
  this.onAck?.(response);
422
- // Log timing if debug enabled
423
425
  return response;
424
426
  },
425
427
  showModal(data) {
@@ -28,6 +28,7 @@ type BaseComponentInteractionHelpers = {
28
28
  toJSON(): APIModalInteractionResponseCallbackData;
29
29
  }) => APIModalInteractionResponse;
30
30
  onAck?: (response: APIInteractionResponse) => void;
31
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
31
32
  };
32
33
  /**
33
34
  * Button interaction with helper methods.
@@ -97,6 +98,11 @@ export type MessageComponentInteraction = APIMessageComponentInteraction & {
97
98
  showModal: (data: APIModalInteractionResponseCallbackData | {
98
99
  toJSON(): APIModalInteractionResponseCallbackData;
99
100
  }) => APIModalInteractionResponse;
101
+ /**
102
+ * Finalise the interaction response via a webhook follow-up.
103
+ * This is automatically called by reply() and update() if the interaction is deferred.
104
+ */
105
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
100
106
  /**
101
107
  * The selected values from a select menu interaction.
102
108
  * This property is only present for select menu interactions.
@@ -138,5 +144,6 @@ export declare const MessageComponentInteraction: {};
138
144
  */
139
145
  export declare function createMessageComponentInteraction(interaction: APIMessageComponentInteraction, helpers?: {
140
146
  onAck?: (response: APIInteractionResponse) => void;
147
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
141
148
  }): MessageComponentInteraction;
142
149
  export {};
@@ -17,6 +17,7 @@ export const MessageComponentInteraction = {};
17
17
  */
18
18
  export function createMessageComponentInteraction(interaction, helpers) {
19
19
  let capturedResponse = null;
20
+ let isDeferred = false;
20
21
  const captureResponse = (response) => {
21
22
  capturedResponse = response;
22
23
  return response;
@@ -30,7 +31,12 @@ export function createMessageComponentInteraction(interaction, helpers) {
30
31
  type: InteractionResponseType.ChannelMessageWithSource,
31
32
  data: normalisedData,
32
33
  });
33
- helpers?.onAck?.(response);
34
+ if (isDeferred && helpers?.sendFollowUp) {
35
+ helpers.sendFollowUp(interaction.token, response);
36
+ }
37
+ else {
38
+ helpers?.onAck?.(response);
39
+ }
34
40
  return response;
35
41
  };
36
42
  const deferReply = (options) => {
@@ -44,6 +50,7 @@ export function createMessageComponentInteraction(interaction, helpers) {
44
50
  type: InteractionResponseType.DeferredChannelMessageWithSource,
45
51
  };
46
52
  captureResponse(response);
53
+ isDeferred = true;
47
54
  helpers?.onAck?.(response);
48
55
  return response;
49
56
  };
@@ -57,11 +64,19 @@ export function createMessageComponentInteraction(interaction, helpers) {
57
64
  : {
58
65
  type: InteractionResponseType.UpdateMessage,
59
66
  };
67
+ if (isDeferred && helpers?.sendFollowUp) {
68
+ helpers.sendFollowUp(interaction.token, response);
69
+ }
60
70
  return captureResponse(response);
61
71
  };
62
- const deferUpdate = () => captureResponse({
63
- type: InteractionResponseType.DeferredMessageUpdate,
64
- });
72
+ const deferUpdate = () => {
73
+ const response = captureResponse({
74
+ type: InteractionResponseType.DeferredMessageUpdate,
75
+ });
76
+ isDeferred = true;
77
+ helpers?.onAck?.(response);
78
+ return response;
79
+ };
65
80
  const showModal = (data) => {
66
81
  const resolvedData = typeof data === "object" &&
67
82
  "toJSON" in data &&
@@ -183,5 +198,6 @@ export function createMessageComponentInteraction(interaction, helpers) {
183
198
  getUsers,
184
199
  getMentionables,
185
200
  onAck: helpers?.onAck,
201
+ sendFollowUp: helpers?.sendFollowUp,
186
202
  });
187
203
  }
@@ -7,32 +7,25 @@ export type ModalSubmitInteraction = APIModalSubmitInteraction & {
7
7
  getResponse: () => APIInteractionResponse | null;
8
8
  reply: (data: InteractionMessageData) => APIInteractionResponseChannelMessageWithSource;
9
9
  deferReply: (options?: DeferReplyOptions) => APIInteractionResponseDeferredChannelMessageWithSource;
10
- onAck?: (response: APIInteractionResponse) => void;
11
- /**
12
- * Helper method to get the value of a text input component by custom_id.
13
- * @param customId - The custom_id of the text input component
14
- * @returns The value of the text input, or undefined if not found
15
- */
16
- getTextInputValue: (customId: string) => string | undefined;
17
10
  /**
18
- * Helper method to get all text input values as a map.
19
- * @returns A map of custom_id to value for all text inputs
11
+ * Helper method to get the value of a text input component by its custom ID.
20
12
  */
21
- getTextInputValues: () => Map<string, string>;
13
+ getTextFieldValue: (customId: string) => string | undefined;
22
14
  /**
23
- * Helper method to get the selected values of a select menu component by custom_id.
24
- * @param customId - The custom_id of the select menu component
25
- * @returns The selected values of the select menu, or undefined if not found
15
+ * Finalise the interaction response via a webhook follow-up.
16
+ * This is automatically called by reply() if the interaction is deferred.
26
17
  */
27
- getSelectMenuValues: (customId: string) => string[] | undefined;
18
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
28
19
  };
29
20
  export declare const ModalSubmitInteraction: {};
30
21
  /**
31
- * Wraps a raw modal submit interaction with helper methods.
22
+ * Wraps a raw modal submit interaction with helper methods mirroring Discord's expected responses.
32
23
  *
33
24
  * @param interaction - The raw interaction payload from Discord.
25
+ * @param helpers - Optional callback to capture the final interaction response.
34
26
  * @returns A helper-augmented interaction object.
35
27
  */
36
28
  export declare function createModalSubmitInteraction(interaction: APIModalSubmitInteraction, helpers?: {
37
29
  onAck?: (response: APIInteractionResponse) => void;
30
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
38
31
  }): ModalSubmitInteraction;
@@ -1,14 +1,16 @@
1
- import { InteractionResponseType, } from "discord-api-types/v10";
1
+ import { ComponentType, InteractionResponseType, } from "discord-api-types/v10";
2
2
  import { normaliseInteractionMessageData, normaliseMessageFlags, } from "./interactionMessageHelpers.js";
3
3
  export const ModalSubmitInteraction = {};
4
4
  /**
5
- * Wraps a raw modal submit interaction with helper methods.
5
+ * Wraps a raw modal submit interaction with helper methods mirroring Discord's expected responses.
6
6
  *
7
7
  * @param interaction - The raw interaction payload from Discord.
8
+ * @param helpers - Optional callback to capture the final interaction response.
8
9
  * @returns A helper-augmented interaction object.
9
10
  */
10
11
  export function createModalSubmitInteraction(interaction, helpers) {
11
12
  let capturedResponse = null;
13
+ let isDeferred = false;
12
14
  const captureResponse = (response) => {
13
15
  capturedResponse = response;
14
16
  return response;
@@ -16,13 +18,18 @@ export function createModalSubmitInteraction(interaction, helpers) {
16
18
  const reply = (data) => {
17
19
  const normalisedData = normaliseInteractionMessageData(data);
18
20
  if (!normalisedData) {
19
- throw new Error("[MiniInteraction] Modal submit replies require response data to be provided.");
21
+ throw new Error("[MiniInteraction] Modal replies require response data to be provided.");
20
22
  }
21
23
  const response = captureResponse({
22
24
  type: InteractionResponseType.ChannelMessageWithSource,
23
25
  data: normalisedData,
24
26
  });
25
- helpers?.onAck?.(response);
27
+ if (isDeferred && helpers?.sendFollowUp) {
28
+ helpers.sendFollowUp(interaction.token, response);
29
+ }
30
+ else {
31
+ helpers?.onAck?.(response);
32
+ }
26
33
  return response;
27
34
  };
28
35
  const deferReply = (options) => {
@@ -36,69 +43,29 @@ export function createModalSubmitInteraction(interaction, helpers) {
36
43
  type: InteractionResponseType.DeferredChannelMessageWithSource,
37
44
  };
38
45
  captureResponse(response);
46
+ isDeferred = true;
39
47
  helpers?.onAck?.(response);
40
48
  return response;
41
49
  };
42
50
  const getResponse = () => capturedResponse;
43
- // Helper to extract text input values from modal components
44
- const extractTextInputs = () => {
45
- const textInputs = new Map();
46
- for (const component of interaction.data.components) {
47
- // Handle action rows
48
- if ("components" in component && Array.isArray(component.components)) {
49
- for (const child of component.components) {
50
- if ("value" in child && "custom_id" in child) {
51
- textInputs.set(child.custom_id, child.value);
52
- }
53
- }
54
- }
55
- // Handle labeled components
56
- else if ("component" in component) {
57
- const labeledComponent = component.component;
58
- if ("value" in labeledComponent && "custom_id" in labeledComponent) {
59
- textInputs.set(labeledComponent.custom_id, labeledComponent.value);
60
- }
61
- }
62
- }
63
- return textInputs;
64
- };
65
- // Helper to extract select menu values from modal components
66
- const extractSelectMenuValues = () => {
67
- const selectMenuValues = new Map();
68
- for (const component of interaction.data.components) {
69
- // Handle action rows
70
- if ("components" in component && Array.isArray(component.components)) {
71
- for (const child of component.components) {
72
- if ("values" in child && "custom_id" in child && Array.isArray(child.values)) {
73
- selectMenuValues.set(child.custom_id, child.values);
51
+ const getTextFieldValue = (customId) => {
52
+ for (const actionRow of interaction.data.components) {
53
+ if ("components" in actionRow && Array.isArray(actionRow.components)) {
54
+ for (const component of actionRow.components) {
55
+ if (component.type === ComponentType.TextInput &&
56
+ component.custom_id === customId) {
57
+ return component.value;
74
58
  }
75
59
  }
76
60
  }
77
- // Handle labeled components (unlikely for select menus but good for completeness if spec allows)
78
- else if ("component" in component) {
79
- const labeledComponent = component.component; // Using any as ModalSubmitComponent might not cover select menus fully in types yet or strictness varies
80
- if ("values" in labeledComponent && "custom_id" in labeledComponent && Array.isArray(labeledComponent.values)) {
81
- selectMenuValues.set(labeledComponent.custom_id, labeledComponent.values);
82
- }
83
- }
84
61
  }
85
- return selectMenuValues;
86
- };
87
- const textInputValues = extractTextInputs();
88
- const selectMenuValues = extractSelectMenuValues();
89
- const getTextInputValue = (customId) => {
90
- return textInputValues.get(customId);
91
- };
92
- const getTextInputValues = () => {
93
- return new Map(textInputValues);
62
+ return undefined;
94
63
  };
95
64
  return Object.assign(interaction, {
96
65
  reply,
97
66
  deferReply,
98
67
  getResponse,
99
- getTextInputValue,
100
- getTextInputValues,
101
- getSelectMenuValues: (customId) => selectMenuValues.get(customId),
102
- onAck: helpers?.onAck,
68
+ getTextFieldValue,
69
+ sendFollowUp: helpers?.sendFollowUp,
103
70
  });
104
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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",