@minesa-org/mini-interaction 0.4.4 → 0.4.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.
|
@@ -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) {
|
|
@@ -271,6 +356,13 @@ export class MiniInteraction {
|
|
|
271
356
|
}
|
|
272
357
|
return data.name;
|
|
273
358
|
}
|
|
359
|
+
resolveCommandPayload(command) {
|
|
360
|
+
const data = command.data;
|
|
361
|
+
if (typeof data.toJSON === "function") {
|
|
362
|
+
return data.toJSON();
|
|
363
|
+
}
|
|
364
|
+
return command.data;
|
|
365
|
+
}
|
|
274
366
|
isCustomIdHandler(value) {
|
|
275
367
|
return (typeof value === "object" &&
|
|
276
368
|
value !== null &&
|
|
@@ -332,5 +424,44 @@ export class MiniInteraction {
|
|
|
332
424
|
res.setHeader?.("Content-Type", "application/json; charset=utf-8");
|
|
333
425
|
res.end(JSON.stringify(body));
|
|
334
426
|
}
|
|
427
|
+
async loadHtmlFile(htmlFile) {
|
|
428
|
+
const absolutePath = path.resolve(this.projectRoot, htmlFile);
|
|
429
|
+
return readFile(absolutePath, "utf8");
|
|
430
|
+
}
|
|
431
|
+
async renderOAuthTemplate(res, template) {
|
|
432
|
+
const html = await this.loadHtmlFile(template.htmlFile);
|
|
433
|
+
this.sendHtml(res, 200, html);
|
|
434
|
+
}
|
|
435
|
+
sendHtml(res, statusCode, html) {
|
|
436
|
+
if (typeof res.status === "function" && typeof res.end === "function") {
|
|
437
|
+
res.status(statusCode);
|
|
438
|
+
}
|
|
439
|
+
res.statusCode = statusCode;
|
|
440
|
+
res.setHeader?.("Content-Type", "text/html; charset=utf-8");
|
|
441
|
+
res.end(html);
|
|
442
|
+
}
|
|
443
|
+
getOAuthConfig() {
|
|
444
|
+
const appId = this.options.applicationId ??
|
|
445
|
+
process.env.DISCORD_APPLICATION_ID ??
|
|
446
|
+
process.env.DISCORD_APP_ID;
|
|
447
|
+
const appSecret = process.env.DISCORD_CLIENT_SECRET ?? process.env.DISCORD_APPLICATION_SECRET;
|
|
448
|
+
const redirectUri = process.env.DISCORD_REDIRECT_URI;
|
|
449
|
+
if (!appId || !appSecret || !redirectUri) {
|
|
450
|
+
throw new Error("[MiniInteraction] Missing OAuth config. Expected DISCORD_APPLICATION_ID, DISCORD_CLIENT_SECRET and DISCORD_REDIRECT_URI.");
|
|
451
|
+
}
|
|
452
|
+
return { appId, appSecret, redirectUri };
|
|
453
|
+
}
|
|
454
|
+
getCookie(req, name) {
|
|
455
|
+
const cookieHeader = this.getHeader(req.headers, "cookie");
|
|
456
|
+
if (!cookieHeader)
|
|
457
|
+
return undefined;
|
|
458
|
+
for (const rawPart of cookieHeader.split(";")) {
|
|
459
|
+
const [rawKey, ...rawValue] = rawPart.trim().split("=");
|
|
460
|
+
if (rawKey === name) {
|
|
461
|
+
return decodeURIComponent(rawValue.join("="));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
335
466
|
}
|
|
336
467
|
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