privage.js 0.21.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +503 -0
  3. package/dist/Client.d.ts +192 -0
  4. package/dist/Client.js +904 -0
  5. package/dist/attachments.d.ts +41 -0
  6. package/dist/attachments.js +63 -0
  7. package/dist/collectors.d.ts +41 -0
  8. package/dist/collectors.js +52 -0
  9. package/dist/commands.d.ts +108 -0
  10. package/dist/commands.js +216 -0
  11. package/dist/compat.d.ts +72 -0
  12. package/dist/compat.js +75 -0
  13. package/dist/components.d.ts +137 -0
  14. package/dist/components.js +226 -0
  15. package/dist/embeds.d.ts +97 -0
  16. package/dist/embeds.js +125 -0
  17. package/dist/errors.d.ts +32 -0
  18. package/dist/errors.js +48 -0
  19. package/dist/formatters.d.ts +16 -0
  20. package/dist/formatters.js +43 -0
  21. package/dist/index.d.ts +33 -0
  22. package/dist/index.js +101 -0
  23. package/dist/intents.d.ts +28 -0
  24. package/dist/intents.js +33 -0
  25. package/dist/modals.d.ts +96 -0
  26. package/dist/modals.js +162 -0
  27. package/dist/rest.d.ts +208 -0
  28. package/dist/rest.js +356 -0
  29. package/dist/structures/Channel.d.ts +25 -0
  30. package/dist/structures/Channel.js +34 -0
  31. package/dist/structures/Interaction.d.ts +149 -0
  32. package/dist/structures/Interaction.js +170 -0
  33. package/dist/structures/Member.d.ts +35 -0
  34. package/dist/structures/Member.js +55 -0
  35. package/dist/structures/Message.d.ts +181 -0
  36. package/dist/structures/Message.js +191 -0
  37. package/dist/structures/Role.d.ts +17 -0
  38. package/dist/structures/Role.js +20 -0
  39. package/dist/structures/Server.d.ts +29 -0
  40. package/dist/structures/Server.js +36 -0
  41. package/dist/structures/ServerChannels.d.ts +24 -0
  42. package/dist/structures/ServerChannels.js +50 -0
  43. package/dist/types.d.ts +195 -0
  44. package/dist/types.js +2 -0
  45. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Privage
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,503 @@
1
+ # privage.js
2
+
3
+ The official SDK for building [Privage](https://privage.xyz) bots. It wraps the
4
+ bot gateway so you write simple, familiar event handlers instead of raw
5
+ WebSocket + opcodes.
6
+
7
+ Full documentation: [privage.xyz/docs](https://privage.xyz/docs)
8
+
9
+ > Feature-complete: messages (incl. threads), embeds and file attachments,
10
+ > buttons, select menus, slash commands, modals, ephemeral replies, polls,
11
+ > reactions, members/roles/moderation, presence, webhooks, collectors, a
12
+ > self-maintaining name cache, and auto reconnect + resume — all typed.
13
+ > Bot DMs are the one remaining backend-gated feature.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install privage.js
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ import { Client, Intents } from 'privage.js';
25
+
26
+ const client = new Client({ intents: [Intents.Messages] });
27
+
28
+ client.on('ready', ({ user }) => console.log(`Logged in as ${user.username}`));
29
+
30
+ client.on('messageCreate', async (msg) => {
31
+ if (msg.author.bot) return; // loop guard
32
+ if (msg.content === '!ping') await msg.reply('pong');
33
+ });
34
+
35
+ client.login(process.env.PRIVAGE_BOT_TOKEN);
36
+ ```
37
+
38
+ Create a bot in the Privage app (**User Settings → Bots**) and copy the
39
+ token — it's shown once, at creation. Add the bot to a server from the same
40
+ panel (needs **Manage Server** in the target). Full walkthrough:
41
+ [Create a bot](https://privage.xyz/docs/create-a-bot).
42
+
43
+ ## Configuration
44
+
45
+ The client connects to Privage production by default — no URLs needed.
46
+ Overrides are for self-hosted or local-dev backends:
47
+
48
+ ```ts
49
+ new Client({
50
+ intents: [Intents.Messages],
51
+ api: 'http://localhost:3001', // default: $PRIVAGE_API or https://api.privage.xyz
52
+ ws: 'ws://localhost:4000/socket', // default: $PRIVAGE_WS, derived from a custom `api`, or wss://ws.privage.xyz/socket
53
+ debug: false, // emit raw wire frames on the `debug` event
54
+ });
55
+ ```
56
+
57
+ ## Intents
58
+
59
+ Declare only what you handle. **Omitting intents entirely means you receive
60
+ everything** — the SDK warns when you do.
61
+
62
+ | Intent | Delivers |
63
+ |--------|----------|
64
+ | `Intents.Messages` | messages (including in-thread replies), edits, deletes, pins, polls, tip jars |
65
+ | `Intents.Reactions` | reaction updates |
66
+ | `Intents.Typing` | typing start/stop |
67
+ | `Intents.Members` | member joins/leaves/updates, roles, nicknames |
68
+ | `Intents.Moderation` | bans, kicks, timeouts |
69
+ | `Intents.Servers` | channel/role/emoji/config structure changes |
70
+ | `Intents.Interactions` | button clicks, select menus, slash commands, modal submits — delivered bot-direct |
71
+
72
+ ## Events
73
+
74
+ | Event | Payload |
75
+ |-------|---------|
76
+ | `ready` | `{ user }` — fired once, after the first connect |
77
+ | `messageCreate` | `Message` |
78
+ | `messageUpdate` | `Message` |
79
+ | `messageDelete` | `{ id, channelId, serverId }` |
80
+ | `messageDeleteBulk` | `{ ids, channelId, serverId }` |
81
+ | `reactionUpdate` | `{ messageId, channelId, reactions }` — full snapshot |
82
+ | `reactionAdd` | `{ messageId, channelId, emoji, userId }` — a specific user reacted |
83
+ | `reactionRemove` | `{ messageId, channelId, emoji, userId }` |
84
+ | `memberJoin` | `Member` |
85
+ | `memberLeave` | `{ userId, serverId }` |
86
+ | `roleAdd` / `roleRemove` | `{ serverId, userId, roleId }` |
87
+ | `memberUpdate` | `{ serverId, userId, displayName?, avatarUrl?, nickname? }` |
88
+ | `memberBan` / `memberUnban` | `{ serverId, userId, username, displayName }` |
89
+ | `memberTimeout` | `{ serverId, userId, until, moderatorId, reason }` |
90
+ | `memberUntimeout` | `{ serverId, userId }` |
91
+ | `messagePin` / `messageUnpin` | `{ messageId, channelId }` |
92
+ | `typingStart` / `typingStop` | `{ userId, channelId }` |
93
+ | `channelCreate` / `channelUpdate` | `Channel` |
94
+ | `channelDelete` | `{ channelId, serverId }` |
95
+ | `roleCreate` / `roleUpdate` | `Role` |
96
+ | `roleDelete` | `{ roleId, serverId }` |
97
+ | `pollVote` | `{ pollId, messageId, voterId, optionIdx, totalVotes }` |
98
+ | `pollClose` | `{ pollId, messageId, totalVotes }` |
99
+ | `interaction` | `Interaction` — button click, select choice, slash command, or modal submit (discriminate with `.kind`) |
100
+ | `connectionError` | `PrivageConnectionError` — recoverable; the SDK is reconnecting |
101
+ | `warn` | `string` |
102
+ | `debug` | `string` (only when `debug: true`) |
103
+
104
+ ## Reconnect & resume (automatic)
105
+
106
+ You don't handle any of this. On a dropped connection the client reconnects
107
+ with backoff, then **resumes** the event stream (replaying anything buffered
108
+ while you were away) and, if the gap is too large for the buffer, transparently
109
+ **REST-catches-up** the missed events — all deduplicated. Your handlers just
110
+ keep firing; there is no "you missed events" state to manage. If a gap is too
111
+ large even for catchup, you get a one-off `warn` suggesting a REST re-fetch.
112
+
113
+ ## Sending
114
+
115
+ ```ts
116
+ await msg.reply('pong'); // reply (threads via reply_to)
117
+ msg.reference; // reply/forward/pin pointer (+preview), null otherwise
118
+ msg.replyToId; // compat alias: reference.messageId for replies
119
+ await msg.channel.send('hi'); // plain send
120
+ await msg.channel.send({ content: 'rich', embeds: [ /* ... */ ] }); // embeds are bot-only
121
+
122
+ await client.send(channelId, 'hi'); // send to any channel by id — no server id needed
123
+ await client.channels.get(channelId)?.send('hi'); // same, via the cache
124
+ ```
125
+
126
+ In-thread replies arrive as normal `messageCreate` events (with the thread as
127
+ `channelId`), so a bot sees thread messages without any extra setup.
128
+
129
+ ## Embeds & attachments
130
+
131
+ Build rich embeds with `EmbedBuilder` — pass builders straight into
132
+ `embeds`, no `.toJSON()` call needed — and upload files with
133
+ `AttachmentBuilder` (a path, `Buffer`, or `Blob`):
134
+
135
+ ```ts
136
+ import { EmbedBuilder, AttachmentBuilder } from 'privage.js';
137
+
138
+ await msg.channel.send({
139
+ embeds: [
140
+ new EmbedBuilder()
141
+ .setTitle('Deploy finished')
142
+ .setColor(0x35b9e8) // or '#35b9e8'
143
+ .addFields({ name: 'Duration', value: '42s', inline: true })
144
+ .setTimestamp(),
145
+ ],
146
+ });
147
+
148
+ await msg.channel.send({
149
+ content: "this week's report",
150
+ files: [new AttachmentBuilder('./report.pdf')], // or Buffer/Blob + { name: '...' }
151
+ });
152
+
153
+ // Spoiler attachments render blurred until the viewer reveals them:
154
+ await msg.channel.send({
155
+ files: [new AttachmentBuilder('./ending.png').setSpoiler()],
156
+ });
157
+ ```
158
+
159
+ Limits mirror the server (10 embeds/message — title ≤256, description ≤4096,
160
+ ≤25 fields; 10 files, 25 MB each) and the builders throw at build time where
161
+ the server would silently truncate or drop. A message can carry embeds
162
+ **or** attachments, not both (v1). The client renders one image slot per
163
+ embed — `setThumbnail` wins over `setImage` if both are set.
164
+
165
+ ## Components & interactions
166
+
167
+ Attach buttons and select menus to messages and handle every interaction —
168
+ clicks, selections, slash commands, modal submits — as one `interaction`
169
+ event, discriminated by `kind`. No message reading required
170
+ (`Intents.Interactions` is delivered bot-direct, and covers all kinds):
171
+
172
+ ```ts
173
+ import { Client, Intents, ButtonBuilder, SelectMenuBuilder, ActionRowBuilder } from 'privage.js';
174
+
175
+ const client = new Client({ intents: [Intents.Interactions] });
176
+
177
+ await client.send(channelId, {
178
+ content: 'Ticket #42',
179
+ components: [
180
+ new ActionRowBuilder().addButtons(
181
+ new ButtonBuilder().setStyle('primary').setLabel('Claim').setCustomId('ticket:claim:42'),
182
+ new ButtonBuilder().setStyle('link').setLabel('Logs').setURL('https://logs.example.com/42'),
183
+ ),
184
+ new ActionRowBuilder().addSelect(
185
+ new SelectMenuBuilder().setCustomId('ticket:severity').setPlaceholder('Severity')
186
+ .addOptions({ label: 'Low', value: 'low' }, { label: 'High', value: 'high' }),
187
+ ),
188
+ ],
189
+ });
190
+
191
+ client.on('interaction', async (interaction) => {
192
+ if (interaction.kind === 'select' && interaction.customId === 'ticket:severity') {
193
+ await interaction.reply({ content: `Severity set to ${interaction.values![0]}`, ephemeral: true });
194
+ }
195
+ if (interaction.customId === 'ticket:claim:42') {
196
+ await interaction.update({ content: `Ticket #42 — claimed by <@${interaction.user.id}>`, components: [] });
197
+ }
198
+ });
199
+ ```
200
+
201
+ Respond within 5 minutes with exactly one terminal callback —
202
+ `interaction.update(...)` (edit the original; invalid for slash commands,
203
+ which have no source message) or `interaction.reply(...)` — optionally after
204
+ `interaction.defer()` (extends to 15 min). **Ephemeral replies**
205
+ (`reply({ ..., ephemeral: true })`) are visible only to the interacting user
206
+ and never persisted — great for confirmations and errors; valid for every
207
+ kind. Selected values arrive on `interaction.values` (selects only).
208
+
209
+ Limits: 5 rows/message; a row holds either ≤5 buttons or exactly one select
210
+ (≤25 interactive components/message); label ≤80, `custom_id` ≤100 and unique
211
+ per message; select options ≤25 with unique values. Link buttons take a URL
212
+ and fire no event. Components can't share a message with file attachments
213
+ (v1). `editMessage(id, { components })` replaces a message's rows (e.g.
214
+ disable a panel).
215
+
216
+ ## Slash commands
217
+
218
+ Register the bot's whole command set with a bulk overwrite (validated
219
+ locally against the same grammar the server enforces), and handle
220
+ invocations as `interaction` events with `kind: 'command'`:
221
+
222
+ ```ts
223
+ await client.commands.set([
224
+ {
225
+ name: 'ticket', // ^[a-z0-9_-]{1,32}$
226
+ description: 'Open a support ticket', // ≤100 chars
227
+ options: [
228
+ { type: 'string', name: 'subject', description: 'What broke', required: true },
229
+ { type: 'integer', name: 'severity', description: 'How bad',
230
+ choices: [{ name: 'Low', value: 1 }, { name: 'High', value: 3 }] },
231
+ ],
232
+ },
233
+ ]);
234
+
235
+ client.on('interaction', async (interaction) => {
236
+ if (interaction.kind !== 'command' || interaction.command?.name !== 'ticket') return;
237
+ const subject = interaction.command.getOption<string>('subject');
238
+ await interaction.reply({ content: `Ticket opened: ${subject}`, ephemeral: true });
239
+ });
240
+ ```
241
+
242
+ `client.commands.fetch()` lists the registered set; `set([])` clears it
243
+ (`set` replaces the whole registry each time — 5/60s rate limit). Option
244
+ types: `string | integer | number | boolean | user | channel | role`
245
+ (user/channel/role values arrive as id strings); required options must
246
+ precede optional ones; ≤50 commands, ≤10 options each, choices (≤25) only on
247
+ string/integer/number. Command interactions carry no source message —
248
+ respond with `reply(...)` (normal or ephemeral), never `update(...)`.
249
+
250
+ ## Modals
251
+
252
+ Open a form in response to a button/select/command interaction; the
253
+ submission comes back as a fresh `interaction` with `kind: 'modal_submit'`:
254
+
255
+ ```ts
256
+ import { ModalBuilder, TextInputBuilder } from 'privage.js';
257
+
258
+ client.on('interaction', async (interaction) => {
259
+ if (interaction.kind === 'button' && interaction.customId === 'ticket:open') {
260
+ await interaction.showModal(
261
+ new ModalBuilder().setTitle('Open a ticket').setCustomId('ticket-modal').addFields(
262
+ new TextInputBuilder().setCustomId('subject').setLabel('Subject').setStyle('short'),
263
+ new TextInputBuilder().setCustomId('details').setLabel('Details').setStyle('paragraph').setRequired(false),
264
+ ),
265
+ );
266
+ }
267
+ if (interaction.kind === 'modal_submit' && interaction.customId === 'ticket-modal') {
268
+ await interaction.reply({ content: `Got it: ${interaction.getField('subject')}`, ephemeral: true });
269
+ }
270
+ });
271
+ ```
272
+
273
+ `showModal(...)` is only valid from a *pending* interaction, once — not
274
+ after `defer()`, not in response to a modal submit (the SDK throws
275
+ client-side before the server would 400), and it does not extend the
276
+ 5-minute window. The submission is a brand-new interaction with a fresh
277
+ window: read values via `interaction.getField(customId)` (or the raw
278
+ `interaction.fields`), then `reply(...)` / `defer()` — `update(...)` works
279
+ only when the modal was opened from a message component. Limits: title and
280
+ labels ≤45, 1–5 fields, `short` inputs cap at 1024 chars, `paragraph` at
281
+ 4000.
282
+
283
+ ## Reactions, editing & fetching
284
+
285
+ ```ts
286
+ await msg.react('👍'); // add a reaction
287
+ await msg.removeReaction('👍'); // remove the bot's own
288
+ await msg.edit('updated text'); // edit the bot's own message
289
+ await msg.pin(); await msg.unpin();
290
+
291
+ const recent = await client.fetchMessages(channelId, { limit: 50 }); // Message[]
292
+ const members = await server.fetchMembers(); // Member[]
293
+ const roles = await server.fetchRoles(); // Role[] — e.g. roles.find(r => r.name === 'Mods')
294
+ await msg.member.setNickname('New Nick'); // null to clear
295
+ ```
296
+
297
+ REST calls automatically back off and retry on a 429 (up to 3 attempts,
298
+ respecting `retryAfter`).
299
+
300
+ ## Presence
301
+
302
+ Set the bot's own status and rich-presence activity (shown on its profile
303
+ and in member lists):
304
+
305
+ ```ts
306
+ import { ActivityType } from 'privage.js';
307
+
308
+ await client.setStatus('online'); // 'online' | 'idle' | 'dnd' | 'offline'
309
+ await client.setActivity({ type: ActivityType.Watching, name: 'the mod queue' });
310
+ await client.clearActivity();
311
+ ```
312
+
313
+ Activities are validated server-side (`name` required, ≤128 chars; optional
314
+ `details`, `state`, `timestamps`) and activity updates are rate-limited
315
+ (~10/min). Bots can **set** presence but do not **receive** presence events
316
+ (the `presence` intent is a no-op for bots in v1).
317
+
318
+ ## Polls
319
+
320
+ Create polls (the SDK already receives `pollVote` / `pollClose` events):
321
+
322
+ ```ts
323
+ await client.createPoll(channelId, {
324
+ question: 'Ship it?',
325
+ options: ['Yes', 'No', 'Needs work'], // 2–6 unique, ≤200 chars each
326
+ deadline: Date.now() + 24 * 3600e3, // required, within 7 days
327
+ });
328
+ await client.closePoll(pollId); // creator or MANAGE_MESSAGES
329
+ ```
330
+
331
+ ## Awaiting replies (collectors)
332
+
333
+ The standard confirmation/wizard building block — collect matching
334
+ `messageCreate`s until a count or time limit:
335
+
336
+ ```ts
337
+ await msg.reply('Really ban them? Reply `yes` within 30s.');
338
+ const [answer] = await client.awaitMessages({
339
+ channelId: msg.channelId,
340
+ filter: (m) => m.author.id === msg.author.id && m.content === 'yes',
341
+ timeMs: 30_000,
342
+ });
343
+ if (answer) await msg.member?.ban({ reason: 'confirmed' });
344
+
345
+ // or the event form:
346
+ const collector = client.createMessageCollector({ channelId, max: 5, timeMs: 60_000 });
347
+ collector.on('collect', (m) => console.log(m.content));
348
+ collector.on('end', (all, reason) => console.log(`${all.length} collected (${reason})`));
349
+ ```
350
+
351
+ ## Mentions & formatting
352
+
353
+ Mention syntax on the wire is `<@userId>` / `<@&roleId>` / `<#channelId>`:
354
+
355
+ ```ts
356
+ import { userMention, channelMention, bold, codeBlock } from 'privage.js';
357
+
358
+ await client.send(channelId, `${userMention(userId)} check ${channelMention(logsChannelId)} — ${bold('3 new alerts')}`);
359
+ ```
360
+
361
+ `@everyone` / `@here` work as plain text (broadcast mentions are
362
+ rate-limited server-side).
363
+
364
+ ## Webhooks
365
+
366
+ Bots with `MANAGE_WEBHOOKS` can provision channel webhooks:
367
+
368
+ ```ts
369
+ const { webhook, token } = await client.createWebhook(channelId, { name: 'Deploys' });
370
+ await client.fetchChannelWebhooks(channelId);
371
+ await client.editWebhook(webhook.id, { name: 'CI' });
372
+ await client.rotateWebhookToken(webhook.id); // old token dies immediately
373
+ await client.deleteWebhook(webhook.id);
374
+ ```
375
+
376
+ ## Servers & channels
377
+
378
+ The SDK maintains a server/channel name cache for you — no per-message REST
379
+ calls needed:
380
+
381
+ ```ts
382
+ client.on('messageCreate', (msg) => {
383
+ console.log(`[${msg.server?.name ?? 'DM'} / #${msg.channel.name}] ${msg.content}`);
384
+ });
385
+
386
+ client.channels.get(channelId); // Channel | undefined
387
+ client.servers.get(serverId); // Server | undefined
388
+ ```
389
+
390
+ The cache is **hydrated on connect** (`GET /servers`, then each server's
391
+ channel list in the background — `ready` never waits on it) and **lazily
392
+ filled on a cache miss** (a channel not yet seen resolves itself in the
393
+ background so the *next* message in it has a name). `msg.channel.name` is
394
+ `null` until resolved; `msg.channel.send(...)` always works regardless.
395
+
396
+ **Live updates (renames, new/deleted channels, server renames/deletes)
397
+ require `Intents.Servers`** — without it, the cache still hydrates on connect
398
+ and lazily fills on miss, but won't reflect changes made after that until the
399
+ next miss.
400
+
401
+ ## Managing roles
402
+
403
+ A bot with `MANAGE_ROLES` can grant/remove roles — the target role must sit
404
+ **below the bot's highest role**, and its permissions must be delegatable by
405
+ the bot (a 403 → `PrivagePermissionError` otherwise):
406
+
407
+ ```ts
408
+ // respond to "!role" by giving the author a role
409
+ client.on('messageCreate', async (msg) => {
410
+ if (msg.author.bot || !msg.member) return; // member is null in DMs
411
+ if (msg.content === '!role') await msg.member.addRole(ROLE_ID);
412
+ });
413
+
414
+ await msg.member.removeRole(ROLE_ID);
415
+ await client.addRole(serverId, userId, roleId); // anyone, anywhere
416
+ ```
417
+
418
+ `memberJoin` gives you a `Member` with the same helpers — handy for auto-roles:
419
+
420
+ ```ts
421
+ client.on('memberJoin', (m) => { if (!m.bot) m.addRole(WELCOME_ROLE_ID); });
422
+ ```
423
+
424
+ ## Moderation
425
+
426
+ Every mod action is on the `Member` (and mirrored on `Client`). Each needs the
427
+ matching permission (`KICK_MEMBERS` / `BAN_MEMBERS` / `TIMEOUT_MEMBERS` /
428
+ `MANAGE_MESSAGES`) and the bot must outrank the target:
429
+
430
+ ```ts
431
+ await msg.member.kick({ reason: 'spam' });
432
+ await msg.member.ban({ reason: 'raid', deleteHistory: true });
433
+ await msg.member.timeout({ durationSeconds: 600, reason: 'cool off' }); // or { until: new Date(...) }
434
+ await msg.member.removeTimeout();
435
+ await msg.delete(); // delete the offending message
436
+
437
+ // server-level / general form:
438
+ await client.unban(serverId, userId);
439
+ await client.kick(serverId, userId);
440
+ await client.deleteMessage(messageId);
441
+ ```
442
+
443
+ ## Migrating from discord.js
444
+
445
+ The SDK is deliberately discord.js-shaped, and ships compat constants so
446
+ common idioms port verbatim:
447
+
448
+ ```ts
449
+ import { Events, Colors, ButtonStyle, TextInputStyle, SlashCommandBuilder } from 'privage.js';
450
+
451
+ client.on(Events.InteractionCreate, async (interaction) => {
452
+ if (interaction.isChatInputCommand()) {
453
+ await interaction.deferReply(); // alias of defer()
454
+ const result = await slowWork();
455
+ await interaction.editReply(result); // alias of reply() — the deferred response IS the reply
456
+ }
457
+ });
458
+
459
+ // Ready-state accessors match discord.js (true once ready fires, across reconnects):
460
+ if (client.isReady()) console.log(`up since ${client.readyAt} (${client.uptime}ms)`);
461
+
462
+ await client.commands.set([
463
+ new SlashCommandBuilder()
464
+ .setName('ticket')
465
+ .setDescription('Open a support ticket')
466
+ .addStringOption(o => o.setName('subject').setDescription('What broke').setRequired(true)),
467
+ ]);
468
+
469
+ new EmbedBuilder().setColor(Colors.Blurple).setAuthor({ name: 'CI', iconURL: '...' });
470
+ // Colors.Blurple is an alias of Colors.Privage (brand purple) — ported bots go on-brand automatically.
471
+ new ButtonBuilder().setStyle(ButtonStyle.Primary).setLabel('Go').setCustomId('go');
472
+ ```
473
+
474
+ The honest differences: intents are coarser and semantically different (no
475
+ `GatewayIntentBits` alias on purpose — read the [intents docs](https://privage.xyz/docs/bot/reference/intents);
476
+ interaction-driven bots need no message intent at all), the token goes to
477
+ `login(token)` not the constructor, presence is `client.setStatus`/
478
+ `client.setActivity` (not `client.user.setActivity`), embeds/components
479
+ can't share a message with file attachments, interactions are not replayed
480
+ after reconnects, and there's no sharding (the collapsed pipe scales
481
+ without it). Full guide:
482
+ [Migrating from discord.js](https://privage.xyz/docs/bot/guides/migrating-from-discordjs).
483
+
484
+ ## Errors
485
+
486
+ `PrivageAuthError` (bad/rotated token — `login()` rejects, no retry; also
487
+ thrown from any REST call on a mid-session 401), `PrivageRateLimitError`
488
+ (`.retryAfter` ms), `PrivagePermissionError` (missing permission / kicked —
489
+ thrown from `reply`/`send`), `PrivageConnectionError` (emitted as
490
+ `connectionError` for gateway drops while the SDK auto-reconnects, and
491
+ thrown from REST calls that fail at the network level — those are not
492
+ retried). Full guide: [Error handling](https://privage.xyz/docs/guides/error-handling).
493
+
494
+ ## Roadmap
495
+
496
+ - ✅ Messages (incl. threads), reactions (+ granular add/remove), edits, deletes, polls
497
+ - ✅ Members, roles, nicknames; kick / ban / timeout moderation
498
+ - ✅ Reply / send, edit, pin, react; fetch messages / members / roles
499
+ - ✅ Server & channel name cache; auto reconnect + resume + REST catchup; 429 retry
500
+ - ✅ Interactions: buttons, select menus, slash commands, modals, ephemeral replies
501
+ - ⏳ Bot DMs (pending gateway support)
502
+
503
+ See `docs/bot-gateway-protocol.md` in the backend for the underlying wire spec.