@minesa-org/mini-interaction 0.4.12 → 0.4.17

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.
package/README.md CHANGED
@@ -139,6 +139,53 @@ try {
139
139
 
140
140
  ---
141
141
 
142
+ ## 🔗 Linked Role Metadata
143
+
144
+ Register application role connection metadata with `mini.registerMetadata(...)`.
145
+
146
+ ```ts
147
+ import {
148
+ MiniInteraction,
149
+ RoleConnectionMetadataTypes,
150
+ } from '@minesa-org/mini-interaction';
151
+
152
+ const mini = new MiniInteraction({
153
+ applicationId: process.env.DISCORD_APPLICATION_ID,
154
+ });
155
+
156
+ await mini.registerMetadata(process.env.DISCORD_BOT_TOKEN!, [
157
+ {
158
+ key: 'is_miniapp',
159
+ name: 'Is Mini App?',
160
+ description: 'Is the user an assistant?',
161
+ type: RoleConnectionMetadataTypes.BooleanEqual,
162
+ },
163
+ ]);
164
+ ```
165
+
166
+ Localization maps use `locale -> string` objects for `name_localizations` and `description_localizations`.
167
+
168
+ ```ts
169
+ await mini.registerMetadata(process.env.DISCORD_BOT_TOKEN!, [
170
+ {
171
+ key: 'is_miniapp',
172
+ name: 'Is Mini App?',
173
+ description: 'Is the user an assistant?',
174
+ type: RoleConnectionMetadataTypes.BooleanEqual,
175
+ name_localizations: {
176
+ tr: 'Mini Uygulama mi?',
177
+ de: 'Ist Mini-App?',
178
+ },
179
+ description_localizations: {
180
+ tr: 'Kullanici bir assistant mi?',
181
+ de: 'Benutzer ist ein Assistent?',
182
+ },
183
+ },
184
+ ]);
185
+ ```
186
+
187
+ ---
188
+
142
189
  ## 📜 License
143
190
 
144
191
  MIT © [Minesa](https://github.com/minesa-org)
@@ -0,0 +1,14 @@
1
+ import { type DiscordRestClientOptions } from "../core/http/DiscordRestClient.js";
2
+ import type { RegisterMetadataResult, RoleConnectionMetadataInput } from "../types/RoleConnectionMetadata.js";
3
+ export type MiniInteractionOptions = {
4
+ applicationId?: string;
5
+ apiBaseUrl?: DiscordRestClientOptions["apiBaseUrl"];
6
+ maxRetries?: DiscordRestClientOptions["maxRetries"];
7
+ fetchImplementation?: DiscordRestClientOptions["fetchImplementation"];
8
+ };
9
+ export declare class MiniInteraction {
10
+ private readonly options;
11
+ constructor(options?: MiniInteractionOptions);
12
+ get applicationId(): string;
13
+ registerMetadata(botToken: string, metadata: RoleConnectionMetadataInput[]): Promise<RegisterMetadataResult>;
14
+ }
@@ -0,0 +1,41 @@
1
+ import { DiscordRestClient } from "../core/http/DiscordRestClient.js";
2
+ export class MiniInteraction {
3
+ options;
4
+ constructor(options = {}) {
5
+ this.options = options;
6
+ const fetchImpl = options.fetchImplementation ?? globalThis.fetch;
7
+ if (typeof fetchImpl !== "function") {
8
+ throw new Error("[MiniInteraction] fetch is not available. Provide a global fetch implementation.");
9
+ }
10
+ }
11
+ get applicationId() {
12
+ const resolvedApplicationId = this.options.applicationId ??
13
+ (typeof process !== "undefined"
14
+ ? process.env.DISCORD_APPLICATION_ID
15
+ : undefined);
16
+ if (!resolvedApplicationId) {
17
+ throw new Error("[MiniInteraction] Missing Discord application ID. Set options.applicationId or DISCORD_APPLICATION_ID.");
18
+ }
19
+ return resolvedApplicationId;
20
+ }
21
+ async registerMetadata(botToken, metadata) {
22
+ if (!botToken) {
23
+ throw new Error("[MiniInteraction] botToken is required");
24
+ }
25
+ if (!Array.isArray(metadata) || metadata.length === 0) {
26
+ throw new Error("[MiniInteraction] metadata must be a non-empty array payload");
27
+ }
28
+ const rest = new DiscordRestClient({
29
+ token: botToken,
30
+ applicationId: this.applicationId,
31
+ apiBaseUrl: this.options.apiBaseUrl,
32
+ maxRetries: this.options.maxRetries,
33
+ fetchImplementation: this.options.fetchImplementation,
34
+ });
35
+ const payload = metadata.map((field) => ({
36
+ ...field,
37
+ type: field.type,
38
+ }));
39
+ return rest.putApplicationRoleConnectionMetadata(payload);
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { MiniInteraction } from "../MiniInteraction.js";
4
+ import { RoleConnectionMetadataTypes } from "../../types/RoleConnectionMetadataTypes.js";
5
+ test("registerMetadata sends the Discord payload and returns the response", async () => {
6
+ const originalAppId = process.env.DISCORD_APPLICATION_ID;
7
+ process.env.DISCORD_APPLICATION_ID = "app_123";
8
+ const calls = [];
9
+ const fetchImpl = (async (input, init) => {
10
+ calls.push({ input: String(input), init });
11
+ return new Response(JSON.stringify([
12
+ {
13
+ key: "is_miniapp",
14
+ name: "Is Mini App?",
15
+ description: "Is the user an assistant?",
16
+ type: RoleConnectionMetadataTypes.BooleanEqual,
17
+ name_localizations: { tr: "Mini Uygulama mi?" },
18
+ description_localizations: { tr: "Kullanici bir assistant mi?" },
19
+ },
20
+ ]), { status: 200 });
21
+ });
22
+ try {
23
+ const mini = new MiniInteraction({ fetchImplementation: fetchImpl });
24
+ const metadata = await mini.registerMetadata("bot-token", [
25
+ {
26
+ key: "is_miniapp",
27
+ name: "Is Mini App?",
28
+ description: "Is the user an assistant?",
29
+ type: RoleConnectionMetadataTypes.BooleanEqual,
30
+ name_localizations: { tr: "Mini Uygulama mi?" },
31
+ description_localizations: { tr: "Kullanici bir assistant mi?" },
32
+ },
33
+ ]);
34
+ assert.equal(calls.length, 1);
35
+ assert.equal(calls[0]?.input, "https://discord.com/api/v10/applications/app_123/role-connections/metadata");
36
+ assert.equal(calls[0]?.init?.method, "PUT");
37
+ assert.deepEqual(JSON.parse(String(calls[0]?.init?.body)), [
38
+ {
39
+ key: "is_miniapp",
40
+ name: "Is Mini App?",
41
+ description: "Is the user an assistant?",
42
+ type: RoleConnectionMetadataTypes.BooleanEqual,
43
+ name_localizations: { tr: "Mini Uygulama mi?" },
44
+ description_localizations: { tr: "Kullanici bir assistant mi?" },
45
+ },
46
+ ]);
47
+ assert.deepEqual(metadata, [
48
+ {
49
+ key: "is_miniapp",
50
+ name: "Is Mini App?",
51
+ description: "Is the user an assistant?",
52
+ type: RoleConnectionMetadataTypes.BooleanEqual,
53
+ name_localizations: { tr: "Mini Uygulama mi?" },
54
+ description_localizations: { tr: "Kullanici bir assistant mi?" },
55
+ },
56
+ ]);
57
+ }
58
+ finally {
59
+ if (originalAppId === undefined) {
60
+ delete process.env.DISCORD_APPLICATION_ID;
61
+ }
62
+ else {
63
+ process.env.DISCORD_APPLICATION_ID = originalAppId;
64
+ }
65
+ }
66
+ });
67
+ test("registerMetadata throws when metadata is empty", async () => {
68
+ const mini = new MiniInteraction({ applicationId: "app_123" });
69
+ await assert.rejects(() => mini.registerMetadata("bot-token", []), /\[MiniInteraction\] metadata must be a non-empty array payload/);
70
+ });
71
+ test("registerMetadata throws when application id cannot be resolved", async () => {
72
+ const originalAppId = process.env.DISCORD_APPLICATION_ID;
73
+ delete process.env.DISCORD_APPLICATION_ID;
74
+ try {
75
+ const mini = new MiniInteraction();
76
+ await assert.rejects(() => mini.registerMetadata("bot-token", [
77
+ {
78
+ key: "is_miniapp",
79
+ name: "Is Mini App?",
80
+ description: "Is the user an assistant?",
81
+ type: RoleConnectionMetadataTypes.BooleanEqual,
82
+ },
83
+ ]), /\[MiniInteraction\] Missing Discord application ID/);
84
+ }
85
+ finally {
86
+ if (originalAppId === undefined) {
87
+ delete process.env.DISCORD_APPLICATION_ID;
88
+ }
89
+ else {
90
+ process.env.DISCORD_APPLICATION_ID = originalAppId;
91
+ }
92
+ }
93
+ });
94
+ test("registerMetadata includes the response body in thrown errors", async () => {
95
+ const fetchImpl = (async () => new Response(JSON.stringify({ message: "Bad metadata" }), {
96
+ status: 400,
97
+ }));
98
+ const mini = new MiniInteraction({
99
+ applicationId: "app_123",
100
+ fetchImplementation: fetchImpl,
101
+ });
102
+ await assert.rejects(() => mini.registerMetadata("bot-token", [
103
+ {
104
+ key: "is_miniapp",
105
+ name: "Is Mini App?",
106
+ description: "Is the user an assistant?",
107
+ type: RoleConnectionMetadataTypes.BooleanEqual,
108
+ },
109
+ ]), /\[DiscordRestClient\] PUT \/applications\/app_123\/role-connections\/metadata failed: 400 {"message":"Bad metadata"}/);
110
+ });
@@ -1,3 +1,7 @@
1
+ import type { APIChannel, RESTPutAPIApplicationRoleConnectionMetadataJSONBody, RESTPutAPIApplicationRoleConnectionMetadataResult } from 'discord-api-types/v10';
2
+ import { DiscordSentMessage } from '../messages/DiscordSentMessage.js';
3
+ import { type BaseDiscordMessageOptions, type DiscordReaction, type DiscordSendMessageOptions, type DiscordStartThreadOptions } from '../messages/message-payloads.js';
4
+ import { DiscordWebhook } from '../webhooks/DiscordWebhook.js';
1
5
  type FetchLike = typeof fetch;
2
6
  export type DiscordRestClientOptions = {
3
7
  token: string;
@@ -18,5 +22,13 @@ export declare class DiscordRestClient {
18
22
  private createRequestError;
19
23
  createFollowup(interactionToken: string, body: unknown): Promise<unknown>;
20
24
  editOriginal(interactionToken: string, body: unknown): Promise<unknown>;
25
+ createFollowupMessage(interactionToken: string, options: BaseDiscordMessageOptions): Promise<DiscordSentMessage>;
26
+ editOriginalMessage(interactionToken: string, options: BaseDiscordMessageOptions): Promise<DiscordSentMessage>;
27
+ sendMessage(options: DiscordSendMessageOptions): Promise<DiscordSentMessage>;
28
+ send(options: DiscordSendMessageOptions): Promise<DiscordSentMessage>;
29
+ startThread(options: DiscordStartThreadOptions): Promise<APIChannel>;
30
+ addReaction(channelId: string, messageId: string, reaction: DiscordReaction): Promise<void>;
31
+ webhook(id: string, token: string): DiscordWebhook;
32
+ putApplicationRoleConnectionMetadata(body: RESTPutAPIApplicationRoleConnectionMetadataJSONBody): Promise<RESTPutAPIApplicationRoleConnectionMetadataResult>;
21
33
  }
22
34
  export {};
@@ -1,4 +1,7 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { DiscordSentMessage } from '../messages/DiscordSentMessage.js';
3
+ import { createMessageRequestInit, } from '../messages/message-payloads.js';
4
+ import { DiscordWebhook } from '../webhooks/DiscordWebhook.js';
2
5
  export class DiscordRestClient {
3
6
  options;
4
7
  fetchImpl;
@@ -20,7 +23,7 @@ export class DiscordRestClient {
20
23
  ...requestInit,
21
24
  headers: {
22
25
  ...(authenticated ? { Authorization: `Bot ${this.options.token}` } : {}),
23
- 'Content-Type': 'application/json',
26
+ ...getDefaultContentTypeHeader(requestInit.body),
24
27
  ...(requestInit.headers ?? {}),
25
28
  },
26
29
  });
@@ -45,13 +48,17 @@ export class DiscordRestClient {
45
48
  if (response.ok) {
46
49
  if (response.status === 204)
47
50
  return undefined;
48
- return (await response.json());
51
+ const responseText = await response.text();
52
+ if (!responseText)
53
+ return undefined;
54
+ return JSON.parse(responseText);
49
55
  }
50
56
  if (response.status >= 500 && attempt < this.maxRetries) {
51
57
  await sleep(150 * (attempt + 1));
52
58
  continue;
53
59
  }
54
- lastError = new Error(`[DiscordRestClient] ${requestInit.method ?? 'GET'} ${path} failed: ${response.status}`);
60
+ const errorBody = await response.text();
61
+ lastError = new Error(`[DiscordRestClient] ${requestInit.method ?? 'GET'} ${path} failed: ${response.status}${errorBody ? ` ${errorBody}` : ''}`);
55
62
  break;
56
63
  }
57
64
  throw lastError instanceof Error ? lastError : new Error('[DiscordRestClient] unknown request failure');
@@ -74,4 +81,78 @@ export class DiscordRestClient {
74
81
  authenticated: false,
75
82
  });
76
83
  }
84
+ async createFollowupMessage(interactionToken, options) {
85
+ const requestInit = createMessageRequestInit(options);
86
+ const message = await this.request(`/webhooks/${this.options.applicationId}/${interactionToken}`, {
87
+ method: 'POST',
88
+ ...requestInit,
89
+ authenticated: false,
90
+ });
91
+ return new DiscordSentMessage(this, message);
92
+ }
93
+ async editOriginalMessage(interactionToken, options) {
94
+ const requestInit = createMessageRequestInit(options);
95
+ const message = await this.request(`/webhooks/${this.options.applicationId}/${interactionToken}/messages/@original`, {
96
+ method: 'PATCH',
97
+ ...requestInit,
98
+ authenticated: false,
99
+ });
100
+ return new DiscordSentMessage(this, message);
101
+ }
102
+ async sendMessage(options) {
103
+ const { channelId, ...messageOptions } = options;
104
+ const requestInit = createMessageRequestInit(messageOptions);
105
+ const message = await this.request(`/channels/${channelId}/messages`, {
106
+ method: 'POST',
107
+ ...requestInit,
108
+ });
109
+ return new DiscordSentMessage(this, message);
110
+ }
111
+ send(options) {
112
+ return this.sendMessage(options);
113
+ }
114
+ async startThread(options) {
115
+ const { channelId, messageId, reason, ...body } = options;
116
+ return this.request(`/channels/${channelId}/messages/${messageId}/threads`, {
117
+ method: 'POST',
118
+ body: JSON.stringify({
119
+ auto_archive_duration: body.autoArchiveDuration,
120
+ rate_limit_per_user: body.rateLimitPerUser,
121
+ name: body.name,
122
+ }),
123
+ headers: reason ? { 'X-Audit-Log-Reason': reason } : undefined,
124
+ });
125
+ }
126
+ addReaction(channelId, messageId, reaction) {
127
+ return this.request(`/channels/${channelId}/messages/${messageId}/reactions/${encodeDiscordReaction(reaction)}/@me`, {
128
+ method: 'PUT',
129
+ });
130
+ }
131
+ webhook(id, token) {
132
+ return new DiscordWebhook(this, id, token);
133
+ }
134
+ putApplicationRoleConnectionMetadata(body) {
135
+ return this.request(`/applications/${this.options.applicationId}/role-connections/metadata`, {
136
+ method: 'PUT',
137
+ body: JSON.stringify(body),
138
+ });
139
+ }
140
+ }
141
+ function getDefaultContentTypeHeader(body) {
142
+ return body instanceof FormData ? {} : { 'Content-Type': 'application/json' };
143
+ }
144
+ function encodeDiscordReaction(reaction) {
145
+ if (typeof reaction !== 'string') {
146
+ return encodeURIComponent(reaction.id ? `${reaction.name}:${reaction.id}` : reaction.name);
147
+ }
148
+ const trimmed = reaction.trim();
149
+ const customEmojiMatch = trimmed.match(/^<a?:([^:>]+):(\d+)>$/);
150
+ if (customEmojiMatch) {
151
+ const [, name, id] = customEmojiMatch;
152
+ return encodeURIComponent(`${name}:${id}`);
153
+ }
154
+ if (/^[^:\s]+:\d+$/.test(trimmed)) {
155
+ return encodeURIComponent(trimmed);
156
+ }
157
+ return encodeURIComponent(trimmed);
77
158
  }
@@ -0,0 +1,122 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { MessageFlags } from "discord-api-types/v10";
4
+ import { ContainerBuilder, TextDisplayBuilder } from "../../../builders/index.js";
5
+ import { DiscordRestClient } from "../DiscordRestClient.js";
6
+ test("sendMessage sends channel messages and returns a thread-capable wrapper", async () => {
7
+ const calls = [];
8
+ const fetchImpl = (async (input, init) => {
9
+ calls.push({ input: String(input), init: init ?? {} });
10
+ if (calls.length === 1) {
11
+ return new Response(JSON.stringify({ id: "msg_1", channel_id: "chan_1" }), { status: 200 });
12
+ }
13
+ return new Response(JSON.stringify({ id: "thread_1", type: 11, parent_id: "chan_1" }), { status: 200 });
14
+ });
15
+ const rest = new DiscordRestClient({
16
+ token: "token",
17
+ applicationId: "app",
18
+ fetchImplementation: fetchImpl,
19
+ });
20
+ const container = new ContainerBuilder().addComponent(new TextDisplayBuilder().setContent("Hello"));
21
+ const sentMessage = await rest.send({
22
+ channelId: "chan_1",
23
+ components: [container],
24
+ flags: MessageFlags.IsComponentsV2,
25
+ });
26
+ assert.equal(sentMessage.id, "msg_1");
27
+ await sentMessage.startThread({
28
+ name: "Thread name",
29
+ autoArchiveDuration: 60,
30
+ reason: "Testing",
31
+ });
32
+ assert.equal(calls.length, 2);
33
+ assert.match(calls[0].input, /\/channels\/chan_1\/messages$/);
34
+ assert.match(calls[1].input, /\/channels\/chan_1\/messages\/msg_1\/threads$/);
35
+ assert.match(String(calls[0].init.body), /payload|components|flags/);
36
+ });
37
+ test("sentMessage.react supports custom emoji strings and emoji objects", async () => {
38
+ const calls = [];
39
+ const fetchImpl = (async (input, init) => {
40
+ calls.push({ input: String(input), init: init ?? {} });
41
+ if (calls.length === 1) {
42
+ return new Response(JSON.stringify({ id: "msg_react", channel_id: "chan_react" }), { status: 200 });
43
+ }
44
+ return new Response(null, { status: 204 });
45
+ });
46
+ const rest = new DiscordRestClient({
47
+ token: "token",
48
+ applicationId: "app",
49
+ fetchImplementation: fetchImpl,
50
+ });
51
+ const sentMessage = await rest.sendMessage({
52
+ channelId: "chan_react",
53
+ content: "React to me",
54
+ });
55
+ await sentMessage.react("<:wave:1234567890>");
56
+ await sentMessage.react({ name: "thumbsup", id: "999" });
57
+ assert.equal(calls.length, 3);
58
+ assert.match(calls[1].input, /\/channels\/chan_react\/messages\/msg_react\/reactions\/wave%3A1234567890\/@me$/);
59
+ assert.match(calls[2].input, /\/channels\/chan_react\/messages\/msg_react\/reactions\/thumbsup%3A999\/@me$/);
60
+ });
61
+ test("sendMessage uses multipart form-data when files are provided", async () => {
62
+ let capturedBody;
63
+ const fetchImpl = (async (_input, init) => {
64
+ capturedBody = init?.body;
65
+ return new Response(JSON.stringify({ id: "msg_file", channel_id: "chan_file" }), { status: 200 });
66
+ });
67
+ const rest = new DiscordRestClient({
68
+ token: "token",
69
+ applicationId: "app",
70
+ fetchImplementation: fetchImpl,
71
+ });
72
+ await rest.sendMessage({
73
+ channelId: "chan_file",
74
+ content: "with file",
75
+ files: [
76
+ {
77
+ name: "hello.txt",
78
+ data: new TextEncoder().encode("hello world"),
79
+ contentType: "text/plain",
80
+ },
81
+ ],
82
+ });
83
+ assert.ok(capturedBody instanceof FormData);
84
+ const payloadJson = capturedBody.get("payload_json");
85
+ assert.equal(typeof payloadJson, "string");
86
+ assert.match(String(payloadJson), /hello\.txt/);
87
+ });
88
+ test("webhook.send returns a sent message wrapper", async () => {
89
+ const calls = [];
90
+ const fetchImpl = (async (input, init) => {
91
+ calls.push({ input: String(input), init: init ?? {} });
92
+ return new Response(JSON.stringify({ id: "msg_hook", channel_id: "chan_hook" }), { status: 200 });
93
+ });
94
+ const rest = new DiscordRestClient({
95
+ token: "token",
96
+ applicationId: "app",
97
+ fetchImplementation: fetchImpl,
98
+ });
99
+ const webhook = rest.webhook("wh_1", "wh_token");
100
+ const sentMessage = await webhook.send({
101
+ content: "hello",
102
+ threadId: "thread_123",
103
+ username: "Mini",
104
+ });
105
+ assert.equal(sentMessage.id, "msg_hook");
106
+ assert.match(calls[0].input, /\/webhooks\/wh_1\/wh_token\?wait=true&thread_id=thread_123$/);
107
+ assert.match(String(calls[0].init.body), /"username":"Mini"/);
108
+ });
109
+ test("sendMessage rejects ephemeral flags for regular messages", async () => {
110
+ const rest = new DiscordRestClient({
111
+ token: "token",
112
+ applicationId: "app",
113
+ fetchImplementation: (async () => new Response(JSON.stringify({ id: "x", channel_id: "y" }), {
114
+ status: 200,
115
+ })),
116
+ });
117
+ await assert.rejects(() => rest.sendMessage({
118
+ channelId: "chan",
119
+ content: "nope",
120
+ flags: MessageFlags.Ephemeral,
121
+ }), /regular channel or webhook messages/);
122
+ });
@@ -1,6 +1,8 @@
1
1
  import type { APIInteractionResponse, APIInteractionResponseCallbackData, APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
2
2
  import type { ParsedInteraction } from '../../types/discord.js';
3
3
  import { DiscordRestClient } from '../http/DiscordRestClient.js';
4
+ import type { DiscordSentMessage } from '../messages/DiscordSentMessage.js';
5
+ import type { BaseDiscordMessageOptions, DiscordSendMessageOptions } from '../messages/message-payloads.js';
4
6
  export type InteractionContextOptions = {
5
7
  interaction: ParsedInteraction;
6
8
  rest: DiscordRestClient;
@@ -18,8 +20,9 @@ export declare class InteractionContext {
18
20
  reply(data: APIInteractionResponseCallbackData): APIInteractionResponse;
19
21
  deferReply(ephemeral?: boolean): APIInteractionResponse;
20
22
  showModal(data: APIModalInteractionResponseCallbackData): APIInteractionResponse;
21
- editReply(body: unknown): Promise<unknown>;
22
- followUp(body: unknown): Promise<unknown>;
23
+ editReply(body: BaseDiscordMessageOptions): Promise<DiscordSentMessage>;
24
+ followUp(body: BaseDiscordMessageOptions): Promise<DiscordSentMessage>;
25
+ send(body: DiscordSendMessageOptions): Promise<DiscordSentMessage>;
23
26
  get hasResponded(): boolean;
24
27
  private clearAutoAck;
25
28
  }
@@ -29,10 +29,13 @@ export class InteractionContext {
29
29
  return { type: 9, data };
30
30
  }
31
31
  editReply(body) {
32
- return this.options.rest.editOriginal(this.options.interaction.token, body);
32
+ return this.options.rest.editOriginalMessage(this.options.interaction.token, body);
33
33
  }
34
34
  followUp(body) {
35
- return this.options.rest.createFollowup(this.options.interaction.token, body);
35
+ return this.options.rest.createFollowupMessage(this.options.interaction.token, body);
36
+ }
37
+ send(body) {
38
+ return this.options.rest.sendMessage(body);
36
39
  }
37
40
  get hasResponded() {
38
41
  return this.responded;
@@ -0,0 +1,13 @@
1
+ import type { APIChannel, APIMessage } from "discord-api-types/v10";
2
+ import type { DiscordRestClient } from "../http/DiscordRestClient.js";
3
+ import type { DiscordReaction, DiscordStartThreadOptions } from "./message-payloads.js";
4
+ export declare class DiscordSentMessage {
5
+ private readonly rest;
6
+ readonly raw: APIMessage;
7
+ constructor(rest: DiscordRestClient, raw: APIMessage);
8
+ get id(): string;
9
+ get channelId(): string;
10
+ startThread(options: Omit<DiscordStartThreadOptions, "channelId" | "messageId">): Promise<APIChannel>;
11
+ react(reaction: DiscordReaction): Promise<this>;
12
+ toJSON(): APIMessage;
13
+ }
@@ -0,0 +1,28 @@
1
+ export class DiscordSentMessage {
2
+ rest;
3
+ raw;
4
+ constructor(rest, raw) {
5
+ this.rest = rest;
6
+ this.raw = raw;
7
+ }
8
+ get id() {
9
+ return this.raw.id;
10
+ }
11
+ get channelId() {
12
+ return this.raw.channel_id;
13
+ }
14
+ async startThread(options) {
15
+ return this.rest.startThread({
16
+ channelId: this.channelId,
17
+ messageId: this.id,
18
+ ...options,
19
+ });
20
+ }
21
+ async react(reaction) {
22
+ await this.rest.addReaction(this.channelId, this.id, reaction);
23
+ return this;
24
+ }
25
+ toJSON() {
26
+ return this.raw;
27
+ }
28
+ }
@@ -0,0 +1,40 @@
1
+ import type { APIAllowedMentions } from "discord-api-types/v10";
2
+ import { type InteractionMessageData, type MessageFlagLike } from "../../utils/interactionMessageHelpers.js";
3
+ export type DiscordMessageFile = {
4
+ name: string;
5
+ data: ArrayBuffer | Blob | Buffer | Uint8Array;
6
+ contentType?: string;
7
+ };
8
+ export type BaseDiscordMessageOptions = Omit<InteractionMessageData, "flags"> & {
9
+ flags?: MessageFlagLike | MessageFlagLike[];
10
+ allowedMentions?: APIAllowedMentions;
11
+ attachments?: Array<Record<string, unknown>>;
12
+ stickerIds?: string[];
13
+ files?: DiscordMessageFile[];
14
+ };
15
+ export type DiscordSendMessageOptions = BaseDiscordMessageOptions & {
16
+ channelId: string;
17
+ };
18
+ export type DiscordStartThreadOptions = {
19
+ channelId: string;
20
+ messageId: string;
21
+ name: string;
22
+ autoArchiveDuration?: number;
23
+ rateLimitPerUser?: number;
24
+ reason?: string;
25
+ };
26
+ export type DiscordWebhookSendOptions = BaseDiscordMessageOptions & {
27
+ threadId?: string;
28
+ username?: string;
29
+ avatarUrl?: string;
30
+ };
31
+ export type DiscordReaction = string | {
32
+ name: string;
33
+ id?: string;
34
+ animated?: boolean;
35
+ };
36
+ export declare function normaliseDiscordMessagePayload(options: BaseDiscordMessageOptions): Record<string, unknown>;
37
+ export declare function createMessageRequestInit(options: BaseDiscordMessageOptions): {
38
+ body: BodyInit;
39
+ headers?: HeadersInit;
40
+ };
@@ -0,0 +1,60 @@
1
+ import { MessageFlags } from "discord-api-types/v10";
2
+ import { normaliseInteractionMessageData, } from "../../utils/interactionMessageHelpers.js";
3
+ export function normaliseDiscordMessagePayload(options) {
4
+ const payload = normaliseInteractionMessageData({
5
+ content: options.content,
6
+ components: options.components,
7
+ embeds: options.embeds,
8
+ flags: options.flags,
9
+ });
10
+ const resolvedPayload = payload ? { ...payload } : {};
11
+ const flags = resolvedPayload.flags;
12
+ if (typeof flags === "number" && (flags & MessageFlags.Ephemeral) === MessageFlags.Ephemeral) {
13
+ throw new Error("[MiniInteraction] Ephemeral flags are not supported for regular channel or webhook messages.");
14
+ }
15
+ if (options.allowedMentions) {
16
+ resolvedPayload.allowed_mentions = options.allowedMentions;
17
+ }
18
+ if (options.stickerIds && options.stickerIds.length > 0) {
19
+ resolvedPayload.sticker_ids = options.stickerIds;
20
+ }
21
+ const files = options.files ?? [];
22
+ if (options.attachments && options.attachments.length > 0) {
23
+ resolvedPayload.attachments = options.attachments;
24
+ }
25
+ else if (files.length > 0) {
26
+ resolvedPayload.attachments = files.map((file, index) => ({
27
+ id: String(index),
28
+ filename: file.name,
29
+ }));
30
+ }
31
+ return resolvedPayload;
32
+ }
33
+ export function createMessageRequestInit(options) {
34
+ const payload = normaliseDiscordMessagePayload(options);
35
+ const files = options.files ?? [];
36
+ if (files.length === 0) {
37
+ return {
38
+ body: JSON.stringify(payload),
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ },
42
+ };
43
+ }
44
+ const formData = new FormData();
45
+ formData.set("payload_json", JSON.stringify(payload));
46
+ files.forEach((file, index) => {
47
+ formData.append(`files[${index}]`, toBlob(file), file.name);
48
+ });
49
+ return { body: formData };
50
+ }
51
+ function toBlob(file) {
52
+ if (file.data instanceof Blob) {
53
+ return file.data;
54
+ }
55
+ if (file.data instanceof ArrayBuffer) {
56
+ return new Blob([file.data], file.contentType ? { type: file.contentType } : undefined);
57
+ }
58
+ const bytes = Uint8Array.from(file.data);
59
+ return new Blob([bytes.buffer], file.contentType ? { type: file.contentType } : undefined);
60
+ }
@@ -0,0 +1,10 @@
1
+ import type { DiscordRestClient } from "../http/DiscordRestClient.js";
2
+ import { DiscordSentMessage } from "../messages/DiscordSentMessage.js";
3
+ import { type DiscordWebhookSendOptions } from "../messages/message-payloads.js";
4
+ export declare class DiscordWebhook {
5
+ private readonly rest;
6
+ readonly id: string;
7
+ readonly token: string;
8
+ constructor(rest: DiscordRestClient, id: string, token: string);
9
+ send(options: DiscordWebhookSendOptions): Promise<DiscordSentMessage>;
10
+ }
@@ -0,0 +1,47 @@
1
+ import { DiscordSentMessage } from "../messages/DiscordSentMessage.js";
2
+ import { createMessageRequestInit, } from "../messages/message-payloads.js";
3
+ export class DiscordWebhook {
4
+ rest;
5
+ id;
6
+ token;
7
+ constructor(rest, id, token) {
8
+ this.rest = rest;
9
+ this.id = id;
10
+ this.token = token;
11
+ }
12
+ async send(options) {
13
+ const { threadId, username, avatarUrl, ...messageOptions } = options;
14
+ const requestInit = createMessageRequestInit(messageOptions);
15
+ const searchParams = new URLSearchParams({ wait: "true" });
16
+ if (threadId) {
17
+ searchParams.set("thread_id", threadId);
18
+ }
19
+ const message = await this.rest.request(`/webhooks/${this.id}/${this.token}?${searchParams.toString()}`, {
20
+ method: "POST",
21
+ body: appendWebhookPayload(requestInit.body, {
22
+ username,
23
+ avatar_url: avatarUrl,
24
+ }),
25
+ headers: requestInit.headers,
26
+ authenticated: false,
27
+ });
28
+ return new DiscordSentMessage(this.rest, message);
29
+ }
30
+ }
31
+ function appendWebhookPayload(body, webhookOverrides) {
32
+ if (body instanceof FormData) {
33
+ const payload = JSON.parse(String(body.get("payload_json") ?? "{}"));
34
+ if (webhookOverrides.username)
35
+ payload.username = webhookOverrides.username;
36
+ if (webhookOverrides.avatar_url)
37
+ payload.avatar_url = webhookOverrides.avatar_url;
38
+ body.set("payload_json", JSON.stringify(payload));
39
+ return body;
40
+ }
41
+ const payload = JSON.parse(String(body));
42
+ if (webhookOverrides.username)
43
+ payload.username = webhookOverrides.username;
44
+ if (webhookOverrides.avatar_url)
45
+ payload.avatar_url = webhookOverrides.avatar_url;
46
+ return JSON.stringify(payload);
47
+ }
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export { ChannelType } from "./types/ChannelType.js";
12
12
  export { InteractionFlags, } from "./types/InteractionFlags.js";
13
13
  export { ButtonStyle } from "./types/ButtonStyle.js";
14
14
  export { SeparatorSpacingSize } from "./types/SeparatorSpacingSize.js";
15
+ export { MessageFlags, type APIAllowedMentions } from "discord-api-types/v10";
15
16
  export { TextInputStyle } from "discord-api-types/v10";
16
17
  export { MiniPermFlags } from "./types/PermissionFlags.js";
17
18
  export type { ActionRowComponent, MessageActionRowComponent, InteractionComponentData, } from "./types/ComponentTypes.js";
@@ -24,8 +25,12 @@ export { MiniDatabase } from "./database/MiniDatabase.js";
24
25
  export { generateOAuthUrl, getOAuthTokens, refreshAccessToken, getDiscordUser, ensureValidToken, } from "./oauth/DiscordOAuth.js";
25
26
  export type { OAuthConfig, OAuthTokens, DiscordUser, } from "./oauth/DiscordOAuth.js";
26
27
  export { OAuthTokenStorage } from "./oauth/OAuthTokenStorage.js";
28
+ export type { DiscordLocale, LocalizationMap, RegisterMetadataResult, RoleConnectionMetadata, RoleConnectionMetadataInput, } from "./types/RoleConnectionMetadata.js";
27
29
  export { DiscordRestClient } from "./core/http/DiscordRestClient.js";
28
30
  export type { DiscordRestClientOptions } from "./core/http/DiscordRestClient.js";
31
+ export { DiscordSentMessage } from "./core/messages/DiscordSentMessage.js";
32
+ export type { DiscordMessageFile, DiscordReaction, DiscordSendMessageOptions, DiscordStartThreadOptions, DiscordWebhookSendOptions, } from "./core/messages/message-payloads.js";
33
+ export { DiscordWebhook } from "./core/webhooks/DiscordWebhook.js";
29
34
  export { InteractionContext } from "./core/interactions/InteractionContext.js";
30
35
  export type { InteractionContextOptions } from "./core/interactions/InteractionContext.js";
31
36
  export { verifyAndParseInteraction } from "./core/interactions/InteractionVerifier.js";
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { ChannelType } from "./types/ChannelType.js";
10
10
  export { InteractionFlags, } from "./types/InteractionFlags.js";
11
11
  export { ButtonStyle } from "./types/ButtonStyle.js";
12
12
  export { SeparatorSpacingSize } from "./types/SeparatorSpacingSize.js";
13
+ export { MessageFlags } from "discord-api-types/v10";
13
14
  export { TextInputStyle } from "discord-api-types/v10";
14
15
  export { MiniPermFlags } from "./types/PermissionFlags.js";
15
16
  export * from "./builders/index.js";
@@ -20,6 +21,8 @@ export { generateOAuthUrl, getOAuthTokens, refreshAccessToken, getDiscordUser, e
20
21
  export { OAuthTokenStorage } from "./oauth/OAuthTokenStorage.js";
21
22
  // New v10 core modules
22
23
  export { DiscordRestClient } from "./core/http/DiscordRestClient.js";
24
+ export { DiscordSentMessage } from "./core/messages/DiscordSentMessage.js";
25
+ export { DiscordWebhook } from "./core/webhooks/DiscordWebhook.js";
23
26
  export { InteractionContext } from "./core/interactions/InteractionContext.js";
24
27
  export { verifyAndParseInteraction } from "./core/interactions/InteractionVerifier.js";
25
28
  export { InteractionRouter } from "./router/InteractionRouter.js";
@@ -0,0 +1,14 @@
1
+ import type { APIApplicationRoleConnectionMetadata, Locale, RESTPutAPIApplicationRoleConnectionMetadataResult } from "discord-api-types/v10";
2
+ import type { RoleConnectionMetadataTypes } from "./RoleConnectionMetadataTypes.js";
3
+ export type DiscordLocale = `${Locale}`;
4
+ export type LocalizationMap = Partial<Record<DiscordLocale, string>>;
5
+ export type RoleConnectionMetadataInput = {
6
+ key: string;
7
+ name: string;
8
+ description: string;
9
+ type: RoleConnectionMetadataTypes;
10
+ name_localizations?: LocalizationMap;
11
+ description_localizations?: LocalizationMap;
12
+ };
13
+ export type RoleConnectionMetadata = APIApplicationRoleConnectionMetadata;
14
+ export type RegisterMetadataResult = RESTPutAPIApplicationRoleConnectionMetadataResult;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.4.12",
3
+ "version": "0.4.17",
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",