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.
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/Client.d.ts +192 -0
- package/dist/Client.js +904 -0
- package/dist/attachments.d.ts +41 -0
- package/dist/attachments.js +63 -0
- package/dist/collectors.d.ts +41 -0
- package/dist/collectors.js +52 -0
- package/dist/commands.d.ts +108 -0
- package/dist/commands.js +216 -0
- package/dist/compat.d.ts +72 -0
- package/dist/compat.js +75 -0
- package/dist/components.d.ts +137 -0
- package/dist/components.js +226 -0
- package/dist/embeds.d.ts +97 -0
- package/dist/embeds.js +125 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +48 -0
- package/dist/formatters.d.ts +16 -0
- package/dist/formatters.js +43 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +101 -0
- package/dist/intents.d.ts +28 -0
- package/dist/intents.js +33 -0
- package/dist/modals.d.ts +96 -0
- package/dist/modals.js +162 -0
- package/dist/rest.d.ts +208 -0
- package/dist/rest.js +356 -0
- package/dist/structures/Channel.d.ts +25 -0
- package/dist/structures/Channel.js +34 -0
- package/dist/structures/Interaction.d.ts +149 -0
- package/dist/structures/Interaction.js +170 -0
- package/dist/structures/Member.d.ts +35 -0
- package/dist/structures/Member.js +55 -0
- package/dist/structures/Message.d.ts +181 -0
- package/dist/structures/Message.js +191 -0
- package/dist/structures/Role.d.ts +17 -0
- package/dist/structures/Role.js +20 -0
- package/dist/structures/Server.d.ts +29 -0
- package/dist/structures/Server.js +36 -0
- package/dist/structures/ServerChannels.d.ts +24 -0
- package/dist/structures/ServerChannels.js +50 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.js +2 -0
- 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.
|