@minesa-org/mini-interaction 0.4.4 → 0.4.6
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DiscordUser, type OAuthTokens } from "../oauth/DiscordOAuth.js";
|
|
1
2
|
type TimeoutConfig = {
|
|
2
3
|
initialResponseTimeout?: number;
|
|
3
4
|
autoDeferSlowOperations?: boolean;
|
|
@@ -14,6 +15,17 @@ export type MiniInteractionOptions = {
|
|
|
14
15
|
publicKey?: string;
|
|
15
16
|
applicationId?: string;
|
|
16
17
|
token?: string;
|
|
18
|
+
guildId?: string;
|
|
19
|
+
};
|
|
20
|
+
type OAuthPageTemplate = {
|
|
21
|
+
htmlFile: string;
|
|
22
|
+
};
|
|
23
|
+
type OAuthCallbackTemplates = {
|
|
24
|
+
success: OAuthPageTemplate;
|
|
25
|
+
missingCode: OAuthPageTemplate;
|
|
26
|
+
oauthError: OAuthPageTemplate;
|
|
27
|
+
invalidState: OAuthPageTemplate;
|
|
28
|
+
serverError: OAuthPageTemplate;
|
|
17
29
|
};
|
|
18
30
|
type NodeRequest = {
|
|
19
31
|
body?: unknown;
|
|
@@ -22,6 +34,7 @@ type NodeRequest = {
|
|
|
22
34
|
get(name: string): string | null;
|
|
23
35
|
};
|
|
24
36
|
method?: string;
|
|
37
|
+
url?: string;
|
|
25
38
|
[Symbol.asyncIterator]?: () => AsyncIterableIterator<Uint8Array>;
|
|
26
39
|
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
27
40
|
};
|
|
@@ -40,6 +53,22 @@ export declare class MiniInteraction {
|
|
|
40
53
|
private loadedModulesPromise?;
|
|
41
54
|
constructor(options?: MiniInteractionOptions);
|
|
42
55
|
createNodeHandler(): (req: NodeRequest, res: NodeResponse) => Promise<void>;
|
|
56
|
+
registerCommands(tokenOverride?: string): Promise<unknown>;
|
|
57
|
+
discordOAuthVerificationPage(options: {
|
|
58
|
+
htmlFile: string;
|
|
59
|
+
scopes?: string[];
|
|
60
|
+
}): (req: NodeRequest, res: NodeResponse) => Promise<void>;
|
|
61
|
+
connectedOAuthPage(htmlFile: string): OAuthPageTemplate;
|
|
62
|
+
failedOAuthPage(htmlFile: string): OAuthPageTemplate;
|
|
63
|
+
discordOAuthCallback(options: {
|
|
64
|
+
templates: OAuthCallbackTemplates;
|
|
65
|
+
onAuthorize?: (payload: {
|
|
66
|
+
user: DiscordUser;
|
|
67
|
+
tokens: OAuthTokens;
|
|
68
|
+
req: NodeRequest;
|
|
69
|
+
res: NodeResponse;
|
|
70
|
+
}) => Promise<void> | void;
|
|
71
|
+
}): (req: NodeRequest, res: NodeResponse) => Promise<void>;
|
|
43
72
|
private dispatch;
|
|
44
73
|
private executeCommandHandler;
|
|
45
74
|
private executeComponentHandler;
|
|
@@ -54,11 +83,17 @@ export declare class MiniInteraction {
|
|
|
54
83
|
private isImportableModule;
|
|
55
84
|
private isInteractionCommand;
|
|
56
85
|
private getCommandName;
|
|
86
|
+
private resolveCommandPayload;
|
|
57
87
|
private isCustomIdHandler;
|
|
58
88
|
private looksLikeModalFile;
|
|
59
89
|
private readRawBody;
|
|
60
90
|
private getHeader;
|
|
61
91
|
private sendJson;
|
|
92
|
+
private loadHtmlFile;
|
|
93
|
+
private renderOAuthTemplate;
|
|
94
|
+
private sendHtml;
|
|
95
|
+
private getOAuthConfig;
|
|
96
|
+
private getCookie;
|
|
62
97
|
}
|
|
63
98
|
export declare const LegacyMiniInteractionAdapter: typeof MiniInteraction;
|
|
64
99
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { ApplicationCommandType, InteractionResponseType, InteractionType, } from "discord-api-types/v10";
|
|
@@ -8,6 +8,7 @@ import { createMessageComponentInteraction } from "../utils/MessageComponentInte
|
|
|
8
8
|
import { createModalSubmitInteraction } from "../utils/ModalSubmitInteraction.js";
|
|
9
9
|
import { DiscordRestClient } from "../core/http/DiscordRestClient.js";
|
|
10
10
|
import { verifyAndParseInteraction } from "../core/interactions/InteractionVerifier.js";
|
|
11
|
+
import { generateOAuthUrl, getDiscordUser, getOAuthTokens, } from "../oauth/DiscordOAuth.js";
|
|
11
12
|
export class MiniInteraction {
|
|
12
13
|
options;
|
|
13
14
|
projectRoot;
|
|
@@ -71,6 +72,90 @@ export class MiniInteraction {
|
|
|
71
72
|
}
|
|
72
73
|
};
|
|
73
74
|
}
|
|
75
|
+
async registerCommands(tokenOverride) {
|
|
76
|
+
const modules = await this.loadModules();
|
|
77
|
+
const payload = modules.commands.map((command) => this.resolveCommandPayload(command));
|
|
78
|
+
const applicationId = this.options.applicationId ??
|
|
79
|
+
process.env.DISCORD_APPLICATION_ID ??
|
|
80
|
+
process.env.DISCORD_APP_ID;
|
|
81
|
+
if (!applicationId) {
|
|
82
|
+
throw new Error("[MiniInteraction] Missing applicationId for command registration.");
|
|
83
|
+
}
|
|
84
|
+
const token = tokenOverride ??
|
|
85
|
+
this.options.token ??
|
|
86
|
+
process.env.DISCORD_BOT_TOKEN ??
|
|
87
|
+
process.env.DISCORD_TOKEN;
|
|
88
|
+
if (!token) {
|
|
89
|
+
throw new Error("[MiniInteraction] Missing bot token for command registration.");
|
|
90
|
+
}
|
|
91
|
+
const rest = new DiscordRestClient({ applicationId, token });
|
|
92
|
+
const guildId = this.options.guildId ?? process.env.DISCORD_GUILD_ID;
|
|
93
|
+
const route = guildId
|
|
94
|
+
? `/applications/${applicationId}/guilds/${guildId}/commands`
|
|
95
|
+
: `/applications/${applicationId}/commands`;
|
|
96
|
+
if (this.options.debug) {
|
|
97
|
+
console.debug(`[MiniInteraction] Registering ${payload.length} command(s) on ${guildId ? `guild ${guildId}` : "global"} scope.`);
|
|
98
|
+
}
|
|
99
|
+
return rest.request(route, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
body: JSON.stringify(payload),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
discordOAuthVerificationPage(options) {
|
|
105
|
+
return async (_req, res) => {
|
|
106
|
+
const oauthConfig = this.getOAuthConfig();
|
|
107
|
+
const { url, state } = generateOAuthUrl(oauthConfig, options.scopes ?? [
|
|
108
|
+
"applications.commands",
|
|
109
|
+
"identify",
|
|
110
|
+
"guilds",
|
|
111
|
+
"role_connections.write",
|
|
112
|
+
]);
|
|
113
|
+
const html = await this.loadHtmlFile(options.htmlFile);
|
|
114
|
+
const rendered = html.replaceAll("{{OAUTH_URL_RAW}}", url);
|
|
115
|
+
res.setHeader?.("Set-Cookie", `mini_oauth_state=${encodeURIComponent(state)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=900`);
|
|
116
|
+
this.sendHtml(res, 200, rendered);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
connectedOAuthPage(htmlFile) {
|
|
120
|
+
return { htmlFile };
|
|
121
|
+
}
|
|
122
|
+
failedOAuthPage(htmlFile) {
|
|
123
|
+
return { htmlFile };
|
|
124
|
+
}
|
|
125
|
+
discordOAuthCallback(options) {
|
|
126
|
+
return async (req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const requestUrl = new URL(req.url ?? "/", process.env.DISCORD_REDIRECT_URI ?? "http://localhost");
|
|
129
|
+
const error = requestUrl.searchParams.get("error");
|
|
130
|
+
const code = requestUrl.searchParams.get("code");
|
|
131
|
+
const state = requestUrl.searchParams.get("state");
|
|
132
|
+
const cookieState = this.getCookie(req, "mini_oauth_state");
|
|
133
|
+
if (error) {
|
|
134
|
+
await this.renderOAuthTemplate(res, options.templates.oauthError);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!code) {
|
|
138
|
+
await this.renderOAuthTemplate(res, options.templates.missingCode);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (state && cookieState && state !== cookieState) {
|
|
142
|
+
await this.renderOAuthTemplate(res, options.templates.invalidState);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const tokens = await getOAuthTokens(code, this.getOAuthConfig());
|
|
146
|
+
const user = await getDiscordUser(tokens.access_token);
|
|
147
|
+
await options.onAuthorize?.({ user, tokens, req, res });
|
|
148
|
+
res.setHeader?.("Set-Cookie", "mini_oauth_state=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
|
|
149
|
+
await this.renderOAuthTemplate(res, options.templates.success);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (this.options.debug) {
|
|
153
|
+
console.error("[MiniInteraction] discordOAuthCallback failed", error);
|
|
154
|
+
}
|
|
155
|
+
await this.renderOAuthTemplate(res, options.templates.serverError);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
74
159
|
async dispatch(interaction) {
|
|
75
160
|
const modules = await this.loadModules();
|
|
76
161
|
if (interaction.type === InteractionType.ApplicationCommand) {
|
|
@@ -118,8 +203,13 @@ export class MiniInteraction {
|
|
|
118
203
|
}
|
|
119
204
|
async runWithResponseLifecycle(interaction, executor) {
|
|
120
205
|
let ackResponse;
|
|
206
|
+
let initialResponseCommitted = false;
|
|
121
207
|
const helpers = {
|
|
122
|
-
|
|
208
|
+
// Legacy helper contracts use canRespond for both initial acknowledgements
|
|
209
|
+
// and later editReply/followUp calls. The compat layer does not currently
|
|
210
|
+
// track Discord token expiry, so we only block on real expiry outside of
|
|
211
|
+
// this helper and allow the wrapped interaction methods to complete.
|
|
212
|
+
canRespond: (_interactionId) => true,
|
|
123
213
|
trackResponse: (interactionId, _token, state) => {
|
|
124
214
|
this.responseStates.set(interactionId, state);
|
|
125
215
|
},
|
|
@@ -127,6 +217,14 @@ export class MiniInteraction {
|
|
|
127
217
|
ackResponse = response;
|
|
128
218
|
},
|
|
129
219
|
sendFollowUp: async (token, response, messageId) => {
|
|
220
|
+
// If the initial interaction response has not been sent yet, collapse the
|
|
221
|
+
// deferred/edit flow back into a single immediate response instead of
|
|
222
|
+
// calling the webhook endpoints early.
|
|
223
|
+
if (!initialResponseCommitted) {
|
|
224
|
+
ackResponse = response;
|
|
225
|
+
this.responseStates.set(interaction.id, "responded");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
130
228
|
const responseData = "data" in response ? response.data ?? {} : {};
|
|
131
229
|
if (messageId === "@original") {
|
|
132
230
|
await this.rest.editOriginal(token, responseData);
|
|
@@ -162,6 +260,7 @@ export class MiniInteraction {
|
|
|
162
260
|
if (this.options.debug || this.options.timeoutConfig?.enableResponseDebugLogging) {
|
|
163
261
|
console.debug(`[MiniInteraction] Interaction ${interaction.id} completed with ${result ? "explicit" : "fallback"} response.`);
|
|
164
262
|
}
|
|
263
|
+
initialResponseCommitted = true;
|
|
165
264
|
return result ?? ackResponse;
|
|
166
265
|
}
|
|
167
266
|
finally {
|
|
@@ -271,6 +370,13 @@ export class MiniInteraction {
|
|
|
271
370
|
}
|
|
272
371
|
return data.name;
|
|
273
372
|
}
|
|
373
|
+
resolveCommandPayload(command) {
|
|
374
|
+
const data = command.data;
|
|
375
|
+
if (typeof data.toJSON === "function") {
|
|
376
|
+
return data.toJSON();
|
|
377
|
+
}
|
|
378
|
+
return command.data;
|
|
379
|
+
}
|
|
274
380
|
isCustomIdHandler(value) {
|
|
275
381
|
return (typeof value === "object" &&
|
|
276
382
|
value !== null &&
|
|
@@ -332,5 +438,44 @@ export class MiniInteraction {
|
|
|
332
438
|
res.setHeader?.("Content-Type", "application/json; charset=utf-8");
|
|
333
439
|
res.end(JSON.stringify(body));
|
|
334
440
|
}
|
|
441
|
+
async loadHtmlFile(htmlFile) {
|
|
442
|
+
const absolutePath = path.resolve(this.projectRoot, htmlFile);
|
|
443
|
+
return readFile(absolutePath, "utf8");
|
|
444
|
+
}
|
|
445
|
+
async renderOAuthTemplate(res, template) {
|
|
446
|
+
const html = await this.loadHtmlFile(template.htmlFile);
|
|
447
|
+
this.sendHtml(res, 200, html);
|
|
448
|
+
}
|
|
449
|
+
sendHtml(res, statusCode, html) {
|
|
450
|
+
if (typeof res.status === "function" && typeof res.end === "function") {
|
|
451
|
+
res.status(statusCode);
|
|
452
|
+
}
|
|
453
|
+
res.statusCode = statusCode;
|
|
454
|
+
res.setHeader?.("Content-Type", "text/html; charset=utf-8");
|
|
455
|
+
res.end(html);
|
|
456
|
+
}
|
|
457
|
+
getOAuthConfig() {
|
|
458
|
+
const appId = this.options.applicationId ??
|
|
459
|
+
process.env.DISCORD_APPLICATION_ID ??
|
|
460
|
+
process.env.DISCORD_APP_ID;
|
|
461
|
+
const appSecret = process.env.DISCORD_CLIENT_SECRET ?? process.env.DISCORD_APPLICATION_SECRET;
|
|
462
|
+
const redirectUri = process.env.DISCORD_REDIRECT_URI;
|
|
463
|
+
if (!appId || !appSecret || !redirectUri) {
|
|
464
|
+
throw new Error("[MiniInteraction] Missing OAuth config. Expected DISCORD_APPLICATION_ID, DISCORD_CLIENT_SECRET and DISCORD_REDIRECT_URI.");
|
|
465
|
+
}
|
|
466
|
+
return { appId, appSecret, redirectUri };
|
|
467
|
+
}
|
|
468
|
+
getCookie(req, name) {
|
|
469
|
+
const cookieHeader = this.getHeader(req.headers, "cookie");
|
|
470
|
+
if (!cookieHeader)
|
|
471
|
+
return undefined;
|
|
472
|
+
for (const rawPart of cookieHeader.split(";")) {
|
|
473
|
+
const [rawKey, ...rawValue] = rawPart.trim().split("=");
|
|
474
|
+
if (rawKey === name) {
|
|
475
|
+
return decodeURIComponent(rawValue.join("="));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
335
480
|
}
|
|
336
481
|
export const LegacyMiniInteractionAdapter = MiniInteraction;
|
|
@@ -4,7 +4,7 @@ import type { APICheckboxComponent } from "./checkbox.js";
|
|
|
4
4
|
/** Defines a component structure for use in ActionRow builders. */
|
|
5
5
|
export type ActionRowComponent = APIComponentInActionRow | APIRadioComponent | APICheckboxComponent;
|
|
6
6
|
/** Defines a message component structure for use in message builders. */
|
|
7
|
-
export type MessageActionRowComponent = APIComponentInMessageActionRow
|
|
7
|
+
export type MessageActionRowComponent = APIComponentInMessageActionRow;
|
|
8
8
|
/** Structure for an action row containing mini-interaction components. */
|
|
9
9
|
export interface MiniActionRow<T extends ActionRowComponent = ActionRowComponent> {
|
|
10
10
|
type: 1;
|
package/package.json
CHANGED