privage.js 0.21.0 → 0.25.0

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
@@ -205,6 +205,9 @@ which have no source message) or `interaction.reply(...)` — optionally after
205
205
  (`reply({ ..., ephemeral: true })`) are visible only to the interacting user
206
206
  and never persisted — great for confirmations and errors; valid for every
207
207
  kind. Selected values arrive on `interaction.values` (selects only).
208
+ Replies can carry uploads — `reply({ files: [...] })`, same limits and
209
+ `ATTACH_FILES` requirement as `client.send` (non-ephemeral only, no
210
+ embeds/components alongside in v1).
208
211
 
209
212
  Limits: 5 rows/message; a row holds either ≤5 buttons or exactly one select
210
213
  (≤25 interactive components/message); label ≤80, `custom_id` ≤100 and unique
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { Client } from './Client';
2
2
  export { Intents } from './intents';
3
3
  export type { Intent } from './intents';
4
4
  export { Message, TextChannel } from './structures/Message';
5
- export type { MessageThread, MessageReference, ReferencedMessage, MessageAttachment, MessageEmbed, } from './structures/Message';
5
+ export type { MessageThread, MessageReference, ReferencedMessage, MessageAttachment, MessageEmbed, MessageInteraction, } from './structures/Message';
6
6
  export { Member } from './structures/Member';
7
7
  export { Server } from './structures/Server';
8
8
  export { Channel } from './structures/Channel';
package/dist/rest.d.ts CHANGED
@@ -119,11 +119,11 @@ export declare class Rest {
119
119
  /**
120
120
  * Respond to an interaction. `type`: 'update' (terminal — edit the
121
121
  * original message), 'reply' (terminal — post a reply; `data.ephemeral:
122
- * true` for an only-you reply), 'defer' (extend the window; a terminal
123
- * callback must follow), or 'modal' (open a modal — pending interactions
124
- * only, once). Response: `{ success, message }` for update/reply (an
125
- * ephemeral reply's `message` is a reduced, never-persisted object);
126
- * defer/modal are 204s and resolve `null`.
122
+ * true` for an only-you reply, `data.files` for uploads), 'defer' (extend
123
+ * the window; a terminal callback must follow), or 'modal' (open a modal
124
+ * — pending interactions only, once). Response: `{ success, message }`
125
+ * for update/reply (an ephemeral reply's `message` is a reduced,
126
+ * never-persisted object); defer/modal are 204s and resolve `null`.
127
127
  */
128
128
  interactionCallback(interactionId: string, token: string, type: 'update' | 'reply' | 'defer' | 'modal', data?: object): Promise<any>;
129
129
  /** PUT /api/bots/commands — bulk overwrite the bot's slash commands (whole-or-400). Response: `{ success, commands }`. */
package/dist/rest.js CHANGED
@@ -144,14 +144,48 @@ class Rest {
144
144
  /**
145
145
  * Respond to an interaction. `type`: 'update' (terminal — edit the
146
146
  * original message), 'reply' (terminal — post a reply; `data.ephemeral:
147
- * true` for an only-you reply), 'defer' (extend the window; a terminal
148
- * callback must follow), or 'modal' (open a modal — pending interactions
149
- * only, once). Response: `{ success, message }` for update/reply (an
150
- * ephemeral reply's `message` is a reduced, never-persisted object);
151
- * defer/modal are 204s and resolve `null`.
147
+ * true` for an only-you reply, `data.files` for uploads), 'defer' (extend
148
+ * the window; a terminal callback must follow), or 'modal' (open a modal
149
+ * — pending interactions only, once). Response: `{ success, message }`
150
+ * for update/reply (an ephemeral reply's `message` is a reduced,
151
+ * never-persisted object); defer/modal are 204s and resolve `null`.
152
152
  */
153
- interactionCallback(interactionId, token, type, data) {
154
- return this.request(`/interactions/${interactionId}/${encodeURIComponent(token)}/callback`, { method: 'POST', body: data === undefined ? { type } : { type, data } });
153
+ async interactionCallback(interactionId, token, type, data) {
154
+ const path = `/interactions/${interactionId}/${encodeURIComponent(token)}/callback`;
155
+ const d = data;
156
+ if (d?.files?.length) {
157
+ // File replies upload as multipart; the server accepts them for
158
+ // non-ephemeral 'reply' only — fail loud and local on everything else.
159
+ if (type !== 'reply') {
160
+ throw new RangeError(`File attachments are only valid on reply callbacks (got '${type}').`);
161
+ }
162
+ if (d.ephemeral) {
163
+ throw new RangeError('Ephemeral replies cannot carry file attachments — nothing is persisted, so there is nowhere for the files to live.');
164
+ }
165
+ if (d.embeds?.length || d.components?.length) {
166
+ throw new RangeError('Attachments cannot be combined with embeds or components in one reply (v1) — send them separately.');
167
+ }
168
+ if (d.files.length > 10) {
169
+ throw new RangeError(`At most 10 attachments per reply (got ${d.files.length}).`);
170
+ }
171
+ const { files, ephemeral: _e, embeds: _em, components: _c, nonce, ...rest } = d;
172
+ const form = new FormData();
173
+ form.append('type', 'reply');
174
+ // Always carry a nonce: the server commits the message before the
175
+ // terminal state change, so a retried callback dedupes against it
176
+ // instead of inserting a second message.
177
+ form.append('data', JSON.stringify({ ...rest, nonce: nonce ?? (0, crypto_1.randomUUID)() }));
178
+ const parts = await (0, attachments_1.resolveFiles)(files);
179
+ for (const { blob, name } of parts) {
180
+ form.append('files', blob, name);
181
+ }
182
+ if (parts.some((p) => p.spoiler)) {
183
+ form.append('attachment_spoilers', JSON.stringify(parts.map((p) => p.spoiler)));
184
+ }
185
+ // Uploads get a longer budget than ordinary REST calls.
186
+ return this.request(path, { method: 'POST', body: form, timeoutMs: 60000 });
187
+ }
188
+ return this.request(path, { method: 'POST', body: data === undefined ? { type } : { type, data } });
155
189
  }
156
190
  /** PUT /api/bots/commands — bulk overwrite the bot's slash commands (whole-or-400). Response: `{ success, commands }`. */
157
191
  setCommands(commands) {
@@ -1,6 +1,7 @@
1
1
  import type { Client } from '../Client';
2
2
  import type { EmbedBuilder, Embed } from '../embeds';
3
3
  import type { ActionRow, ActionRowBuilder } from '../components';
4
+ import type { AttachmentBuilder, AttachmentSource } from '../attachments';
4
5
  import { ModalBuilder, type Modal } from '../modals';
5
6
  /** Discriminates what the user did. Pre-v2 envelopes (no `kind`) read as `'button'`. */
6
7
  export type InteractionKind = 'button' | 'select' | 'command' | 'modal_submit';
@@ -15,11 +16,27 @@ export interface InteractionReplyData {
15
16
  /**
16
17
  * Only-you reply: never persisted, delivered as an event to the
17
18
  * interacting user alone ("Only you can see this"). Valid for every
18
- * interaction kind. Still terminal.
19
+ * interaction kind. Still terminal. Cannot carry `files` (nothing is
20
+ * persisted, so there is nowhere for the bytes to live).
19
21
  */
20
22
  ephemeral?: boolean;
21
- /** Components on the reply — ephemeral replies only (they render, but aren't clickable v1). */
23
+ /**
24
+ * Components on the reply — ephemeral replies only. They are fully
25
+ * interactive: clicks arrive as fresh `interaction` events carrying
26
+ * `ephemeralMessageId`, recipient-locked to the user the ephemeral was
27
+ * delivered to, for 15 minutes after sending.
28
+ */
22
29
  components?: (ActionRow | ActionRowBuilder | Record<string, unknown>)[];
30
+ /**
31
+ * File attachments on the reply — `AttachmentBuilder`s, paths, raw bytes,
32
+ * or Blobs (max 10, 25 MB each). Non-ephemeral replies only; cannot be
33
+ * combined with `embeds` or `components` (v1). Uploads as multipart with
34
+ * an auto-generated `nonce`, so a retried callback never duplicates the
35
+ * message.
36
+ */
37
+ files?: (AttachmentBuilder | AttachmentSource)[];
38
+ /** Client-dedupe nonce — auto-generated for file replies when omitted. */
39
+ nonce?: string;
23
40
  }
24
41
  export type CommandOptionType = 'string' | 'integer' | 'number' | 'boolean' | 'user' | 'channel' | 'role';
25
42
  /** One resolved option of a slash-command invocation (already validated + coerced by the server). */
@@ -73,8 +90,18 @@ export declare class Interaction {
73
90
  readonly command: InteractionCommand | null;
74
91
  /** Submitted modal fields — `kind: 'modal_submit'` only, `null` otherwise. */
75
92
  readonly fields: ModalSubmitField[] | null;
76
- /** The source message. `null` for commands (and command-born modal submits) — `update()` is invalid then. */
93
+ /** The source message. `null` for commands, command-born modal submits, and ephemeral-born interactions — `update()` is invalid then. */
77
94
  readonly messageId: string | null;
95
+ /**
96
+ * Set when this interaction came from a component on an **ephemeral
97
+ * reply** — the synthetic ephemeral message's id. Recipient-locked:
98
+ * only the ephemeral's recipient can click, within the panel's
99
+ * 15-minute window (every click also re-checks membership/timeout).
100
+ * Respond with `update()` to morph the panel in place (the wizard
101
+ * pattern — updating refreshes the window), or `reply()` to post a
102
+ * new message.
103
+ */
104
+ readonly ephemeralMessageId: string | null;
78
105
  readonly channelId: string;
79
106
  readonly serverId: string | null;
80
107
  readonly user: {
@@ -103,16 +130,22 @@ export declare class Interaction {
103
130
  /** `kind === 'modal_submit'`. */
104
131
  isModalSubmit(): boolean;
105
132
  /**
106
- * Terminal: edit the original message (content/embeds/components).
107
- * Broadcasts a normal edit. Only valid when the interaction HAS a source
108
- * message throws client-side for `kind: 'command'` and for modal submits
109
- * born from a command (the server would 400).
133
+ * Terminal: edit the message this interaction came from
134
+ * (content/embeds/components). On a persisted message that's a normal
135
+ * edit broadcast. On an interaction born from an **ephemeral reply**
136
+ * (`ephemeralMessageId` set) it morphs the ephemeral panel in place —
137
+ * patch semantics (only the fields you send change), refreshing the
138
+ * panel's 15-minute window; clearing `components: []` ends the panel,
139
+ * and an expired panel rejects with a 404. Throws client-side for
140
+ * `kind: 'command'` and command-born modal submits (no source at all —
141
+ * the server would 400).
110
142
  */
111
143
  update(data: InteractionUpdateData): Promise<any>;
112
144
  /**
113
145
  * Terminal: post a bot message replying to the original (a plain channel
114
146
  * message for commands). Pass `{ ephemeral: true }` for an only-you reply
115
- * that is never persisted.
147
+ * that is never persisted, or `{ files: [...] }` to attach uploads
148
+ * (non-ephemeral only; needs `ATTACH_FILES` in the channel).
116
149
  */
117
150
  reply(data: string | InteractionReplyData): Promise<any>;
118
151
  /**
@@ -127,7 +160,12 @@ export declare class Interaction {
127
160
  * deferred reply and replying are the same terminal callback.
128
161
  */
129
162
  editReply(data: string | InteractionReplyData): Promise<any>;
130
- /** Extend the response window to 15 minutes; must still be followed by `update()` or `reply()`. */
163
+ /**
164
+ * Extend the response window to 15 minutes; must still be followed by
165
+ * `update()` or `reply()`. The invoker's client is told the bot needs
166
+ * more time, so a command's "thinking" row waits patiently instead of
167
+ * timing out at 15 seconds.
168
+ */
131
169
  defer(): Promise<any>;
132
170
  /**
133
171
  * Open a modal dialog for the interacting user; their submission arrives
@@ -49,6 +49,7 @@ class Interaction {
49
49
  ? data.modal.fields.map((f) => ({ customId: String(f?.custom_id ?? ''), value: String(f?.value ?? '') }))
50
50
  : null;
51
51
  this.messageId = data.message_id != null ? String(data.message_id) : null;
52
+ this.ephemeralMessageId = data.ephemeral_message_id != null ? String(data.ephemeral_message_id) : null;
52
53
  this.channelId = String(data.channel_id ?? '');
53
54
  this.serverId = data.server_id != null ? String(data.server_id) : null;
54
55
  this.user = {
@@ -87,24 +88,30 @@ class Interaction {
87
88
  return this.kind === 'modal_submit';
88
89
  }
89
90
  /**
90
- * Terminal: edit the original message (content/embeds/components).
91
- * Broadcasts a normal edit. Only valid when the interaction HAS a source
92
- * message throws client-side for `kind: 'command'` and for modal submits
93
- * born from a command (the server would 400).
91
+ * Terminal: edit the message this interaction came from
92
+ * (content/embeds/components). On a persisted message that's a normal
93
+ * edit broadcast. On an interaction born from an **ephemeral reply**
94
+ * (`ephemeralMessageId` set) it morphs the ephemeral panel in place —
95
+ * patch semantics (only the fields you send change), refreshing the
96
+ * panel's 15-minute window; clearing `components: []` ends the panel,
97
+ * and an expired panel rejects with a 404. Throws client-side for
98
+ * `kind: 'command'` and command-born modal submits (no source at all —
99
+ * the server would 400).
94
100
  */
95
101
  update(data) {
96
102
  if (this.kind === 'command') {
97
103
  throw new RangeError('update() is not valid for command interactions — there is no source message. Use reply() instead.');
98
104
  }
99
- if (this.kind === 'modal_submit' && !this.messageId) {
100
- throw new RangeError('update() is not valid for this modal submit its parent interaction had no source message. Use reply() instead.');
105
+ if (!this.messageId && !this.ephemeralMessageId) {
106
+ throw new RangeError('update() is not valid for this interactionit has no source message. Use reply() instead.');
101
107
  }
102
108
  return this.transition('responded', () => this.client.rest.interactionCallback(this.id, this.token, 'update', data));
103
109
  }
104
110
  /**
105
111
  * Terminal: post a bot message replying to the original (a plain channel
106
112
  * message for commands). Pass `{ ephemeral: true }` for an only-you reply
107
- * that is never persisted.
113
+ * that is never persisted, or `{ files: [...] }` to attach uploads
114
+ * (non-ephemeral only; needs `ATTACH_FILES` in the channel).
108
115
  */
109
116
  reply(data) {
110
117
  return this.transition('responded', () => this.client.rest.interactionCallback(this.id, this.token, 'reply', typeof data === 'string' ? { content: data } : data));
@@ -125,7 +132,12 @@ class Interaction {
125
132
  editReply(data) {
126
133
  return this.reply(data);
127
134
  }
128
- /** Extend the response window to 15 minutes; must still be followed by `update()` or `reply()`. */
135
+ /**
136
+ * Extend the response window to 15 minutes; must still be followed by
137
+ * `update()` or `reply()`. The invoker's client is told the bot needs
138
+ * more time, so a command's "thinking" row waits patiently instead of
139
+ * timing out at 15 seconds.
140
+ */
129
141
  defer() {
130
142
  if (this.state !== 'pending') {
131
143
  return this.client.rest.interactionCallback(this.id, this.token, 'defer');
@@ -76,6 +76,25 @@ export interface MessageEmbed {
76
76
  timestamp: string | null;
77
77
  createdAt: Date | null;
78
78
  }
79
+ /**
80
+ * Server-verified attribution on an interaction-born bot reply — who
81
+ * invoked the command/component that produced this message ("X used
82
+ * /price"). `user` is a frozen snapshot from interaction time; later
83
+ * renames never rewrite it. Ordinary messages carry `interaction: null`.
84
+ */
85
+ export interface MessageInteraction {
86
+ kind: 'command' | 'button' | 'select' | 'modal_submit';
87
+ /** Command name — `kind: 'command'` only, `null` otherwise. */
88
+ name: string | null;
89
+ /** Component/modal custom_id — component-born interactions only, `null` otherwise. */
90
+ customId: string | null;
91
+ /** The invoking user, snapshotted at interaction time. */
92
+ user: {
93
+ id: string;
94
+ username: string | null;
95
+ displayName: string | null;
96
+ };
97
+ }
79
98
  /** A frozen or live preview of the message a reference points at. */
80
99
  export interface ReferencedMessage {
81
100
  id: string;
@@ -144,6 +163,8 @@ export declare class Message {
144
163
  readonly reference: MessageReference | null;
145
164
  /** Thread badge — `null` unless this message has (or had) a thread. */
146
165
  readonly thread: MessageThread | null;
166
+ /** Attribution for interaction-born bot replies, or `null` (see {@link MessageInteraction}). */
167
+ readonly interaction: MessageInteraction | null;
147
168
  /** The raw wire payload, for anything the typed surface doesn't expose yet. */
148
169
  readonly raw: any;
149
170
  constructor(client: Client, data: any);
@@ -123,6 +123,22 @@ class Message {
123
123
  deletedAt: t.deleted_at ? new Date(t.deleted_at) : null,
124
124
  }
125
125
  : null;
126
+ const ix = data.interaction;
127
+ this.interaction =
128
+ ix && typeof ix === 'object'
129
+ ? {
130
+ kind: ix.kind === 'select' || ix.kind === 'command' || ix.kind === 'modal_submit'
131
+ ? ix.kind
132
+ : 'button',
133
+ name: ix.name != null ? String(ix.name) : null,
134
+ customId: ix.custom_id != null ? String(ix.custom_id) : null,
135
+ user: {
136
+ id: String(ix.user?.id ?? ''),
137
+ username: ix.user?.username ?? null,
138
+ displayName: ix.user?.display_name ?? null,
139
+ },
140
+ }
141
+ : null;
126
142
  }
127
143
  /**
128
144
  * Compat alias (discord.js-style): the id of the message this one replies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privage.js",
3
- "version": "0.21.0",
3
+ "version": "0.25.0",
4
4
  "description": "Official SDK for building Privage bots.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",