novaapp-sdk 1.3.0 → 1.3.2
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 +967 -4
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,14 +1,977 @@
|
|
|
1
|
-
#
|
|
1
|
+
# novaapp-sdk
|
|
2
2
|
|
|
3
3
|
Official SDK for building bots on the [Nova](https://novachatapp.com) platform.
|
|
4
4
|
|
|
5
|
+
> **Current version: 1.3.0**
|
|
6
|
+
|
|
7
|
+
## Table of contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick start](#quick-start)
|
|
11
|
+
- [Configuration](#configuration)
|
|
12
|
+
- [Routing handlers](#routing-handlers)
|
|
13
|
+
- [Rich wrappers](#rich-wrappers)
|
|
14
|
+
- [API reference](#api-reference)
|
|
15
|
+
- [Fluent builders](#fluent-builders)
|
|
16
|
+
- [Utilities](#utilities)
|
|
17
|
+
- [Events](#events)
|
|
18
|
+
- [WebSocket helpers](#websocket-helpers)
|
|
19
|
+
- [Examples](#examples)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
5
23
|
## Installation
|
|
6
24
|
|
|
7
25
|
```bash
|
|
8
|
-
npm install
|
|
26
|
+
npm install novaapp-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { NovaClient, EmbedBuilder } from 'novaapp-sdk'
|
|
35
|
+
|
|
36
|
+
const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })
|
|
37
|
+
|
|
38
|
+
client.on('ready', async (bot) => {
|
|
39
|
+
console.log(`Logged in as ${bot.botUser.username}`)
|
|
40
|
+
|
|
41
|
+
await client.commands.setSlash([
|
|
42
|
+
{ name: 'ping', description: 'Check if the bot is alive' },
|
|
43
|
+
])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Slash command routing
|
|
47
|
+
client.command('ping', async (interaction) => {
|
|
48
|
+
await interaction.reply('Pong! 🏓')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Button routing
|
|
52
|
+
client.button('confirm', async (interaction) => {
|
|
53
|
+
await interaction.replyEphemeral('Confirmed!')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Modal submission routing
|
|
57
|
+
client.modal('report_modal', async (interaction) => {
|
|
58
|
+
const reason = interaction.modalData['reason']
|
|
59
|
+
await interaction.replyEphemeral(`Report received: ${reason}`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await client.connect()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
| Option | Type | Default | Description |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `token` | `string` | **required** | Your bot token (`nova_bot_...`) |
|
|
72
|
+
| `baseUrl` | `string` | `https://novachatapp.com` | Override for self-hosted Nova servers |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Routing handlers
|
|
77
|
+
|
|
78
|
+
Instead of handling everything inside `interactionCreate`, use the built-in router:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// /slash and !prefix commands
|
|
82
|
+
client.command('ban', async (interaction) => {
|
|
83
|
+
const userId = interaction.options.getUser('user', true)
|
|
84
|
+
const reason = interaction.options.getString('reason') ?? 'No reason given'
|
|
85
|
+
await client.members.ban(interaction.serverId!, userId, reason)
|
|
86
|
+
await interaction.reply(`✅ Banned <@${userId}>`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Button clicks
|
|
90
|
+
client.button('delete_confirm', async (interaction) => {
|
|
91
|
+
await interaction.replyEphemeral('Deleted.')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Select menus
|
|
95
|
+
client.selectMenu('colour_pick', async (interaction) => {
|
|
96
|
+
const chosen = interaction.values[0]
|
|
97
|
+
await interaction.reply(`You picked: ${chosen}`)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Modal submissions
|
|
101
|
+
client.modal('feedback_modal', async (interaction) => {
|
|
102
|
+
const text = interaction.modalData['feedback']
|
|
103
|
+
await interaction.replyEphemeral(`Thanks: ${text}`)
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Rich wrappers
|
|
110
|
+
|
|
111
|
+
### `NovaInteraction`
|
|
112
|
+
|
|
113
|
+
Returned by every interaction event and routing handler.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
client.on('interactionCreate', async (interaction) => {
|
|
117
|
+
interaction.id // string
|
|
118
|
+
interaction.type // 'SLASH_COMMAND' | 'BUTTON_CLICK' | 'SELECT_MENU' | 'MODAL_SUBMIT' | …
|
|
119
|
+
interaction.commandName // string | null
|
|
120
|
+
interaction.customId // string | null (buttons / selects / modals)
|
|
121
|
+
interaction.userId // string
|
|
122
|
+
interaction.channelId // string
|
|
123
|
+
interaction.serverId // string | null
|
|
124
|
+
interaction.values // string[] (select menu choices)
|
|
125
|
+
interaction.modalData // Record<string, string> (modal submitted values)
|
|
126
|
+
|
|
127
|
+
// Type guards
|
|
128
|
+
interaction.isSlashCommand()
|
|
129
|
+
interaction.isPrefixCommand()
|
|
130
|
+
interaction.isCommand() // slash OR prefix
|
|
131
|
+
interaction.isButton()
|
|
132
|
+
interaction.isSelectMenu()
|
|
133
|
+
interaction.isModalSubmit()
|
|
134
|
+
interaction.isAutocomplete()
|
|
135
|
+
interaction.isContextMenu()
|
|
136
|
+
|
|
137
|
+
// Typed option accessor
|
|
138
|
+
const userId = interaction.options.getUser('user', true)
|
|
139
|
+
const reason = interaction.options.getString('reason') ?? 'No reason'
|
|
140
|
+
const count = interaction.options.getInteger('count') // number | null
|
|
141
|
+
|
|
142
|
+
// Actions
|
|
143
|
+
await interaction.reply('Hello!')
|
|
144
|
+
await interaction.reply({ content: 'Hi', ephemeral: true, embed: { title: 'Stats' } })
|
|
145
|
+
await interaction.replyEphemeral('Only you can see this.')
|
|
146
|
+
await interaction.defer() // acknowledge — shows loading state
|
|
147
|
+
await interaction.editReply('Done!') // edit after defer()
|
|
148
|
+
|
|
149
|
+
// Open a modal and await its submission
|
|
150
|
+
const submitted = await interaction.openModal(
|
|
151
|
+
new ModalBuilder()
|
|
152
|
+
.setTitle('Report')
|
|
153
|
+
.setCustomId('report')
|
|
154
|
+
.addField(new TextInputBuilder().setCustomId('reason').setLabel('Reason').setRequired(true))
|
|
155
|
+
)
|
|
156
|
+
if (submitted) {
|
|
157
|
+
await submitted.replyEphemeral(`Received: ${submitted.modalData['reason']}`)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `NovaMessage`
|
|
163
|
+
|
|
164
|
+
Returned by `messageCreate` / `messageUpdate` events and message fetch calls.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
client.on('messageCreate', async (msg) => {
|
|
168
|
+
msg.id // string
|
|
169
|
+
msg.content // string
|
|
170
|
+
msg.channelId // string
|
|
171
|
+
msg.author // { id, username, displayName, avatar, isBot }
|
|
172
|
+
msg.embed // Embed | null
|
|
173
|
+
msg.components // MessageComponent[]
|
|
174
|
+
msg.attachments // Attachment[]
|
|
175
|
+
msg.reactions // Reaction[]
|
|
176
|
+
msg.replyToId // string | null
|
|
177
|
+
msg.createdAt // Date
|
|
178
|
+
msg.editedAt // Date | null
|
|
179
|
+
|
|
180
|
+
msg.isFromBot()
|
|
181
|
+
msg.hasEmbed()
|
|
182
|
+
msg.hasComponents()
|
|
183
|
+
msg.isEdited()
|
|
184
|
+
|
|
185
|
+
await msg.reply('Got it!')
|
|
186
|
+
await msg.edit('Updated')
|
|
187
|
+
await msg.delete()
|
|
188
|
+
await msg.pin()
|
|
189
|
+
await msg.unpin()
|
|
190
|
+
await msg.react('👍')
|
|
191
|
+
await msg.removeReaction('👍')
|
|
192
|
+
const fresh = await msg.fetch()
|
|
193
|
+
|
|
194
|
+
console.log(msg.url) // '/channels/<id>/messages/<id>'
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `NovaChannel`
|
|
199
|
+
|
|
200
|
+
Returned by `client.fetchChannel()` and `client.fetchChannels()`.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const channel = await client.fetchChannel('channel-id')
|
|
204
|
+
|
|
205
|
+
channel.id // string
|
|
206
|
+
channel.name // string
|
|
207
|
+
channel.type // 'TEXT' | 'VOICE' | 'ANNOUNCEMENT' | 'FORUM' | 'STAGE'
|
|
208
|
+
channel.serverId // string | null
|
|
209
|
+
channel.topic // string | null
|
|
210
|
+
channel.position // number
|
|
211
|
+
channel.slowMode // number (seconds; 0 = disabled)
|
|
212
|
+
channel.createdAt // Date
|
|
213
|
+
|
|
214
|
+
channel.isText()
|
|
215
|
+
channel.isVoice()
|
|
216
|
+
channel.isAnnouncement()
|
|
217
|
+
channel.isForum()
|
|
218
|
+
channel.isStage()
|
|
219
|
+
channel.hasSlowMode()
|
|
220
|
+
|
|
221
|
+
await channel.send('Hello!')
|
|
222
|
+
await channel.send({ content: 'Hi', embed: { title: 'News' } })
|
|
223
|
+
const messages = await channel.fetchMessages({ limit: 50 })
|
|
224
|
+
const pins = await channel.fetchPins()
|
|
225
|
+
await channel.startTyping()
|
|
226
|
+
const updated = await channel.edit({ topic: 'New topic', slowMode: 5 })
|
|
227
|
+
await channel.delete()
|
|
9
228
|
```
|
|
10
229
|
|
|
11
|
-
|
|
230
|
+
### `NovaMember`
|
|
231
|
+
|
|
232
|
+
Returned by `client.fetchMember()` and `client.fetchMembers()`.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const member = await client.fetchMember('server-id', 'user-id')
|
|
236
|
+
|
|
237
|
+
member.userId // string
|
|
238
|
+
member.serverId // string
|
|
239
|
+
member.username // string
|
|
240
|
+
member.displayName // string
|
|
241
|
+
member.avatar // string | null
|
|
242
|
+
member.role // 'OWNER' | 'ADMIN' | 'MEMBER'
|
|
243
|
+
member.status // 'ONLINE' | 'IDLE' | 'DND' | 'OFFLINE'
|
|
244
|
+
member.isBot // boolean
|
|
245
|
+
member.joinedAt // Date
|
|
246
|
+
|
|
247
|
+
member.isOwner()
|
|
248
|
+
member.isAdmin() // ADMIN *or* OWNER
|
|
249
|
+
member.isRegularMember()
|
|
250
|
+
member.isOnline()
|
|
251
|
+
member.isIdle()
|
|
252
|
+
member.isDND()
|
|
253
|
+
member.isOffline()
|
|
254
|
+
|
|
255
|
+
await member.kick()
|
|
256
|
+
await member.ban('Spamming')
|
|
257
|
+
await member.dm('Welcome!')
|
|
258
|
+
await member.addRole('role-id')
|
|
259
|
+
await member.removeRole('role-id')
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## API reference
|
|
265
|
+
|
|
266
|
+
### `client.messages`
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
await client.messages.send(channelId, { content: 'Hi!' })
|
|
270
|
+
await client.messages.send(channelId, { embed: { title: 'Report', color: '#5865F2' } })
|
|
271
|
+
await client.messages.send(channelId, {
|
|
272
|
+
content: 'Pick one:',
|
|
273
|
+
components: [
|
|
274
|
+
{ type: 'button', customId: 'yes', label: 'Yes', style: 'success' },
|
|
275
|
+
{ type: 'button', customId: 'no', label: 'No', style: 'danger' },
|
|
276
|
+
],
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
await client.messages.edit(messageId, { content: 'Updated' })
|
|
280
|
+
await client.messages.delete(messageId)
|
|
281
|
+
|
|
282
|
+
const msg = await client.messages.fetchOne(messageId)
|
|
283
|
+
const messages = await client.messages.fetch(channelId, { limit: 50, before: cursorId })
|
|
284
|
+
const pins = await client.messages.fetchPinned(channelId)
|
|
285
|
+
|
|
286
|
+
await client.messages.pin(messageId)
|
|
287
|
+
await client.messages.unpin(messageId)
|
|
288
|
+
await client.messages.typing(channelId)
|
|
289
|
+
|
|
290
|
+
await client.messages.addReaction(messageId, '👍')
|
|
291
|
+
await client.messages.removeReaction(messageId, '👍')
|
|
292
|
+
const reactions = await client.messages.fetchReactions(messageId)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### `client.channels`
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const channels = await client.channels.list(serverId)
|
|
299
|
+
const channel = await client.channels.fetch(channelId)
|
|
300
|
+
|
|
301
|
+
const newChan = await client.channels.create(serverId, {
|
|
302
|
+
name: 'announcements', type: 'ANNOUNCEMENT', topic: 'Official news',
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
await client.channels.edit(channelId, { topic: 'New topic', slowMode: 10 })
|
|
306
|
+
await client.channels.delete(channelId)
|
|
307
|
+
|
|
308
|
+
const messages = await client.channels.fetchMessages(channelId, { limit: 20 })
|
|
309
|
+
const pins = await client.channels.fetchPins(channelId)
|
|
310
|
+
await client.channels.startTyping(channelId)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Rich wrapper shortcuts (return `NovaChannel`):
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const channel = await client.fetchChannel(channelId)
|
|
317
|
+
const channels = await client.fetchChannels(serverId)
|
|
318
|
+
const textOnly = channels.filter(c => c.isText())
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### `client.reactions`
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
await client.reactions.add(messageId, '🎉')
|
|
325
|
+
await client.reactions.remove(messageId, '🎉')
|
|
326
|
+
await client.reactions.removeAll(messageId) // requires messages.manage
|
|
327
|
+
await client.reactions.removeEmoji(messageId, '🎉') // all of one emoji
|
|
328
|
+
|
|
329
|
+
const all = await client.reactions.fetch(messageId)
|
|
330
|
+
// [{ emoji: '🎉', count: 3, users: [{ id, username, displayName, avatar }] }, …]
|
|
331
|
+
|
|
332
|
+
const detail = await client.reactions.fetchEmoji(messageId, '🎉')
|
|
333
|
+
// { emoji: '🎉', count: 3, users: […] }
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### `client.members`
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const members = await client.members.list(serverId, { limit: 100 })
|
|
340
|
+
await client.members.kick(serverId, userId)
|
|
341
|
+
await client.members.ban(serverId, userId, 'Reason')
|
|
342
|
+
await client.members.unban(serverId, userId)
|
|
343
|
+
|
|
344
|
+
const bans = await client.members.listBans(serverId)
|
|
345
|
+
// [{ userId, username, displayName, avatar, reason, bannedAt, moderatorId }]
|
|
346
|
+
|
|
347
|
+
await client.members.dm(userId, 'Hello!')
|
|
348
|
+
await client.members.dm(userId, { content: 'Hi', embed: { title: 'Welcome' } })
|
|
349
|
+
|
|
350
|
+
await client.members.addRole(serverId, userId, roleId)
|
|
351
|
+
await client.members.removeRole(serverId, userId, roleId)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Rich wrapper shortcuts (return `NovaMember`):
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const member = await client.fetchMember(serverId, userId)
|
|
358
|
+
const members = await client.fetchMembers(serverId)
|
|
359
|
+
const bots = members.filter(m => m.isBot)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `client.servers`
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
const servers = await client.servers.list()
|
|
366
|
+
const roles = await client.servers.listRoles(serverId)
|
|
367
|
+
// [{ id, name, color, position, serverId, hoist, createdAt }]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### `client.commands`
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
await client.commands.setSlash([
|
|
374
|
+
{ name: 'ban', description: 'Ban a user', options: [
|
|
375
|
+
{ name: 'user', description: 'User to ban', type: 'USER', required: true },
|
|
376
|
+
{ name: 'reason', description: 'Reason', type: 'STRING', required: false },
|
|
377
|
+
]},
|
|
378
|
+
])
|
|
379
|
+
const slash = await client.commands.getSlash()
|
|
380
|
+
await client.commands.deleteSlash('ban')
|
|
381
|
+
|
|
382
|
+
await client.commands.setPrefix([{ prefix: '!', name: 'help', description: 'Show help' }])
|
|
383
|
+
const prefix = await client.commands.getPrefix()
|
|
384
|
+
await client.commands.deletePrefix('!', 'help')
|
|
385
|
+
|
|
386
|
+
await client.commands.setContext([
|
|
387
|
+
{ name: 'Report message', target: 'MESSAGE' },
|
|
388
|
+
{ name: 'View profile', target: 'USER' },
|
|
389
|
+
])
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### `client.interactions`
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
await client.interactions.ack(interaction.id) // acknowledge (show loading)
|
|
396
|
+
await client.interactions.respond(interaction.id, { content: 'Done!', ephemeral: true })
|
|
397
|
+
|
|
398
|
+
// Open a modal (low-level)
|
|
399
|
+
await client.interactions.respond(interaction.id, {
|
|
400
|
+
modal: {
|
|
401
|
+
title: 'Report', customId: 'report_modal',
|
|
402
|
+
fields: [{ customId: 'reason', label: 'Reason', type: 'paragraph', required: true }],
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const pending = await client.interactions.poll({ limit: 20 })
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### `client.permissions`
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const result = await client.permissions.get({
|
|
413
|
+
serverId: 'server-id',
|
|
414
|
+
channelId: 'channel-id', // optional
|
|
415
|
+
roleId: 'role-id', // optional
|
|
416
|
+
})
|
|
417
|
+
result.permissions // merged Record<string, boolean>
|
|
418
|
+
result.records // raw permission records
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### `client.cron()` — recurring tasks
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const cancel = client.cron(60_000, async () => {
|
|
425
|
+
const msgs = await client.messages.fetch(channelId, { limit: 1 })
|
|
426
|
+
console.log('Latest:', msgs[0]?.content)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
cancel() // stop the task
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### `client.setStatus()`
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
client.setStatus('ONLINE') // default
|
|
436
|
+
client.setStatus('IDLE') // away
|
|
437
|
+
client.setStatus('DND') // Do Not Disturb
|
|
438
|
+
client.setStatus('OFFLINE') // appear offline
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### `client.waitFor()` — event fence
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
// Wait for a message in a specific channel (default 30 s timeout)
|
|
445
|
+
const msg = await client.waitFor(
|
|
446
|
+
'messageCreate',
|
|
447
|
+
(m) => m.channelId === channelId,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
// Wait for a button click, throw after 60 s
|
|
451
|
+
const click = await client.waitFor(
|
|
452
|
+
'interactionCreate',
|
|
453
|
+
(i) => i.isButton() && i.customId === 'confirm',
|
|
454
|
+
60_000,
|
|
455
|
+
)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Fluent builders
|
|
461
|
+
|
|
462
|
+
### `EmbedBuilder`
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
const embed = new EmbedBuilder()
|
|
466
|
+
.setTitle('Server stats')
|
|
467
|
+
.setDescription('All systems operational.')
|
|
468
|
+
.setColor('#5865F2')
|
|
469
|
+
.setThumbnail('https://example.com/logo.png')
|
|
470
|
+
.addField('Members', '1,234', true)
|
|
471
|
+
.addField('Online', '456', true)
|
|
472
|
+
.setFooter('Updated just now')
|
|
473
|
+
.setTimestamp()
|
|
474
|
+
.toJSON()
|
|
475
|
+
|
|
476
|
+
await client.messages.send(channelId, { embed })
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### `ButtonBuilder` + `ActionRowBuilder`
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
const row = new ActionRowBuilder()
|
|
483
|
+
.addComponent(
|
|
484
|
+
new ButtonBuilder().setCustomId('yes').setLabel('Yes').setStyle('success').toJSON()
|
|
485
|
+
)
|
|
486
|
+
.addComponent(
|
|
487
|
+
new ButtonBuilder().setCustomId('no').setLabel('No').setStyle('danger').toJSON()
|
|
488
|
+
)
|
|
489
|
+
.toJSON()
|
|
490
|
+
|
|
491
|
+
await client.messages.send(channelId, { content: 'Confirm?', components: [row] })
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### `SelectMenuBuilder`
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
const row = new ActionRowBuilder()
|
|
498
|
+
.addComponent(
|
|
499
|
+
new SelectMenuBuilder()
|
|
500
|
+
.setCustomId('colour_pick')
|
|
501
|
+
.setPlaceholder('Choose a colour')
|
|
502
|
+
.addOption({ label: 'Red', value: 'red' })
|
|
503
|
+
.addOption({ label: 'Blue', value: 'blue' })
|
|
504
|
+
.addOption({ label: 'Green', value: 'green' })
|
|
505
|
+
.toJSON()
|
|
506
|
+
)
|
|
507
|
+
.toJSON()
|
|
508
|
+
|
|
509
|
+
await client.messages.send(channelId, { content: 'Pick:', components: [row] })
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### `ModalBuilder` + `TextInputBuilder`
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
const modal = new ModalBuilder()
|
|
516
|
+
.setTitle('Submit a report')
|
|
517
|
+
.setCustomId('report_modal')
|
|
518
|
+
.addField(
|
|
519
|
+
new TextInputBuilder()
|
|
520
|
+
.setCustomId('subject')
|
|
521
|
+
.setLabel('Subject')
|
|
522
|
+
.setStyle('short')
|
|
523
|
+
.setRequired(true)
|
|
524
|
+
.setMaxLength(100)
|
|
525
|
+
)
|
|
526
|
+
.addField(
|
|
527
|
+
new TextInputBuilder()
|
|
528
|
+
.setCustomId('description')
|
|
529
|
+
.setLabel('Description')
|
|
530
|
+
.setStyle('paragraph')
|
|
531
|
+
.setRequired(true)
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
const submitted = await interaction.openModal(modal)
|
|
535
|
+
if (submitted) {
|
|
536
|
+
const subject = submitted.modalData['subject']
|
|
537
|
+
const desc = submitted.modalData['description']
|
|
538
|
+
await submitted.replyEphemeral(`Filed: ${subject}`)
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### `SlashCommandBuilder`
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
const cmd = new SlashCommandBuilder()
|
|
546
|
+
.setName('kick')
|
|
547
|
+
.setDescription('Kick a member')
|
|
548
|
+
.addOption(
|
|
549
|
+
new SlashCommandOptionBuilder()
|
|
550
|
+
.setName('user').setDescription('Member to kick').setType('USER').setRequired(true)
|
|
551
|
+
)
|
|
552
|
+
.toJSON()
|
|
553
|
+
|
|
554
|
+
await client.commands.setSlash([cmd])
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### `PollBuilder`
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
const poll = new PollBuilder()
|
|
561
|
+
.setQuestion('Best programming language?')
|
|
562
|
+
.addOption({ id: 'ts', label: 'TypeScript', emoji: '💙' })
|
|
563
|
+
.addOption({ id: 'rs', label: 'Rust', emoji: '🦀' })
|
|
564
|
+
.addOption({ id: 'py', label: 'Python', emoji: '🐍' })
|
|
565
|
+
.setMultipleChoice(false)
|
|
566
|
+
.toJSON()
|
|
567
|
+
|
|
568
|
+
await client.messages.send(channelId, { content: 'Vote below!', ...poll })
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### `MessageBuilder`
|
|
572
|
+
|
|
573
|
+
Compose content, embeds, and components fluently:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
const msg = new MessageBuilder()
|
|
577
|
+
.setContent('New announcement!')
|
|
578
|
+
.setEmbed(
|
|
579
|
+
new EmbedBuilder()
|
|
580
|
+
.setTitle('v2.0 Released')
|
|
581
|
+
.setDescription('Check the changelog for details.')
|
|
582
|
+
.setColor('#57F287')
|
|
583
|
+
.toJSON()
|
|
584
|
+
)
|
|
585
|
+
.addRow(
|
|
586
|
+
new ActionRowBuilder()
|
|
587
|
+
.addComponent(
|
|
588
|
+
new ButtonBuilder().setCustomId('changelog').setLabel('View changelog').setStyle('primary').toJSON()
|
|
589
|
+
)
|
|
590
|
+
.toJSON()
|
|
591
|
+
)
|
|
592
|
+
.build()
|
|
593
|
+
|
|
594
|
+
await client.messages.send(channelId, msg)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Utilities
|
|
600
|
+
|
|
601
|
+
### `Collection<K, V>`
|
|
602
|
+
|
|
603
|
+
A supercharged `Map` with array-style helpers:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
const col = new Collection<string, User>()
|
|
607
|
+
col.set('1', { id: '1', name: 'Alice' })
|
|
608
|
+
col.set('2', { id: '2', name: 'Bob' })
|
|
609
|
+
|
|
610
|
+
col.first() // { id: '1', name: 'Alice' }
|
|
611
|
+
col.last() // { id: '2', name: 'Bob' }
|
|
612
|
+
col.random() // random value
|
|
613
|
+
col.find(v => v.name === 'Bob') // { id: '2', … }
|
|
614
|
+
col.filter(v => v.name.length > 3) // Collection with Alice
|
|
615
|
+
col.map(v => v.name) // ['Alice', 'Bob']
|
|
616
|
+
col.some(v => v.name === 'Alice') // true
|
|
617
|
+
col.every(v => v.id.length > 0) // true
|
|
618
|
+
col.toArray() // [{ … }, { … }]
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### `Paginator<T>`
|
|
622
|
+
|
|
623
|
+
Cursor-based async paginator for any list API:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
const paginator = new Paginator(async (cursor) => {
|
|
627
|
+
const messages = await client.messages.fetch(channelId, {
|
|
628
|
+
limit: 50, before: cursor ?? undefined,
|
|
629
|
+
})
|
|
630
|
+
return { items: messages, cursor: messages.at(-1)?.id ?? null }
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
// Async iteration
|
|
634
|
+
for await (const msg of paginator) {
|
|
635
|
+
console.log(msg.content)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Collect everything at once
|
|
639
|
+
const all = await paginator.fetchAll()
|
|
640
|
+
|
|
641
|
+
// Collect first N
|
|
642
|
+
const first200 = await paginator.fetchN(200)
|
|
643
|
+
|
|
644
|
+
// Reset and iterate again from the start
|
|
645
|
+
paginator.reset()
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### `PermissionsBitfield`
|
|
649
|
+
|
|
650
|
+
Work with Nova bot permissions as a typed bitfield:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
import { PermissionsBitfield, Permissions } from 'novaapp-sdk'
|
|
654
|
+
|
|
655
|
+
const perms = new PermissionsBitfield({
|
|
656
|
+
[Permissions.MESSAGES_READ]: true,
|
|
657
|
+
[Permissions.MESSAGES_WRITE]: true,
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
perms.has(Permissions.MESSAGES_READ) // true
|
|
661
|
+
perms.has(Permissions.CHANNELS_MANAGE) // false
|
|
662
|
+
perms.hasAll(Permissions.MESSAGES_READ, Permissions.MESSAGES_WRITE) // true
|
|
663
|
+
perms.hasAny(Permissions.CHANNELS_MANAGE, Permissions.SERVERS_MANAGE) // false
|
|
664
|
+
perms.missing(Permissions.MESSAGES_MANAGE, Permissions.MEMBERS_BAN)
|
|
665
|
+
// ['messages.manage', 'members.ban']
|
|
666
|
+
|
|
667
|
+
const extended = perms.grant(Permissions.CHANNELS_MANAGE)
|
|
668
|
+
const restricted = perms.deny(Permissions.MESSAGES_WRITE)
|
|
669
|
+
const merged = serverPerms.merge(channelOverrides)
|
|
670
|
+
|
|
671
|
+
perms.toArray() // ['messages.read', 'messages.write']
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Available constants in `Permissions`:
|
|
675
|
+
|
|
676
|
+
| Constant | Value |
|
|
677
|
+
|---|---|
|
|
678
|
+
| `MESSAGES_READ` | `'messages.read'` |
|
|
679
|
+
| `MESSAGES_WRITE` | `'messages.write'` |
|
|
680
|
+
| `MESSAGES_MANAGE` | `'messages.manage'` |
|
|
681
|
+
| `CHANNELS_MANAGE` | `'channels.manage'` |
|
|
682
|
+
| `MEMBERS_KICK` | `'members.kick'` |
|
|
683
|
+
| `MEMBERS_BAN` | `'members.ban'` |
|
|
684
|
+
| `MEMBERS_ROLES` | `'members.roles'` |
|
|
685
|
+
| `SERVERS_MANAGE` | `'servers.manage'` |
|
|
686
|
+
|
|
687
|
+
### `CooldownManager`
|
|
688
|
+
|
|
689
|
+
Per-user / per-command cooldown tracking:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
const cooldowns = new CooldownManager()
|
|
693
|
+
|
|
694
|
+
client.command('daily', async (interaction) => {
|
|
695
|
+
if (cooldowns.isOnCooldown(interaction.userId, 'daily', 86_400_000)) {
|
|
696
|
+
const remaining = cooldowns.getRemaining(interaction.userId, 'daily', 86_400_000)
|
|
697
|
+
await interaction.replyEphemeral(`Try again in ${formatDuration(remaining)}.`)
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
cooldowns.set(interaction.userId, 'daily')
|
|
701
|
+
await interaction.reply('Here is your daily reward!')
|
|
702
|
+
})
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### `Logger`
|
|
706
|
+
|
|
707
|
+
Coloured, namespaced console logger:
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
const log = new Logger('ModBot')
|
|
711
|
+
|
|
712
|
+
log.info('Bot started')
|
|
713
|
+
log.success('Command registered: ban')
|
|
714
|
+
log.warn('Rate limit approaching')
|
|
715
|
+
log.error('Failed to ban user', err)
|
|
716
|
+
log.debug('Payload', interaction.toJSON())
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Time utilities
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
import {
|
|
723
|
+
sleep, withTimeout,
|
|
724
|
+
formatDuration, formatRelative,
|
|
725
|
+
parseTimestamp, countdown,
|
|
726
|
+
} from 'novaapp-sdk'
|
|
727
|
+
|
|
728
|
+
await sleep(2_000) // wait 2 s
|
|
729
|
+
const data = await withTimeout(fetch(), 5_000) // throw if > 5 s
|
|
730
|
+
|
|
731
|
+
formatDuration(90_000) // '1m 30s'
|
|
732
|
+
formatDuration(3_661_000) // '1h 1m 1s'
|
|
733
|
+
|
|
734
|
+
formatRelative(Date.now() - 4_000) // 'just now'
|
|
735
|
+
formatRelative(Date.now() - 90_000) // '1 minute ago'
|
|
736
|
+
formatRelative(Date.now() + 60_000) // 'in 1 minute'
|
|
737
|
+
formatRelative(Date.now() - 86_400_000) // 'yesterday'
|
|
738
|
+
|
|
739
|
+
parseTimestamp('2026-01-01T00:00:00Z') // Date
|
|
740
|
+
parseTimestamp(null) // null
|
|
741
|
+
|
|
742
|
+
const { days, hours, minutes, seconds } = countdown(new Date('2027-01-01'))
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## Events
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
// Connection
|
|
751
|
+
client.on('ready', (bot) => console.log('Ready!', bot.botUser.username))
|
|
752
|
+
client.on('disconnect', (reason) => console.log('Disconnected:', reason))
|
|
753
|
+
client.on('error', (err) => console.error(err))
|
|
754
|
+
|
|
755
|
+
// Interactions
|
|
756
|
+
client.on('interactionCreate', (interaction) => { /* NovaInteraction */ })
|
|
757
|
+
|
|
758
|
+
// Messages
|
|
759
|
+
client.on('messageCreate', (msg) => { /* NovaMessage */ })
|
|
760
|
+
client.on('messageUpdate', (msg) => { /* NovaMessage */ })
|
|
761
|
+
client.on('messageDelete', (data) => { /* { id, channelId } */ })
|
|
762
|
+
client.on('messagePinned', (data) => { /* { messageId, channelId, pinnedBy } */ })
|
|
763
|
+
|
|
764
|
+
// Reactions
|
|
765
|
+
client.on('reactionAdd', (data) => { /* { messageId, channelId, userId, emoji } */ })
|
|
766
|
+
client.on('reactionRemove', (data) => { /* { messageId, channelId, userId, emoji } */ })
|
|
767
|
+
|
|
768
|
+
// Members
|
|
769
|
+
client.on('memberAdd', (data) => { /* { serverId, userId, username } */ })
|
|
770
|
+
client.on('memberRemove', (data) => { /* { serverId, userId } */ })
|
|
771
|
+
client.on('memberUpdate', (data) => { /* { serverId, userId } */ })
|
|
772
|
+
client.on('memberBanned', (data) => { /* { userId, serverId, moderatorId, reason } */ })
|
|
773
|
+
client.on('memberUnbanned', (data) => { /* { userId, serverId } */ })
|
|
774
|
+
client.on('typingStart', (data) => { /* { channelId, userId } */ })
|
|
775
|
+
|
|
776
|
+
// Channels
|
|
777
|
+
client.on('channelCreate', (channel) => { /* Channel */ })
|
|
778
|
+
client.on('channelUpdate', (channel) => { /* Channel */ })
|
|
779
|
+
client.on('channelDelete', (data) => { /* { id, serverId } */ })
|
|
780
|
+
|
|
781
|
+
// Roles
|
|
782
|
+
client.on('roleCreate', (data) => { /* { id, name, color, serverId, position, hoist, createdAt } */ })
|
|
783
|
+
client.on('roleDelete', (data) => { /* { id, serverId } */ })
|
|
784
|
+
|
|
785
|
+
// Voice
|
|
786
|
+
client.on('voiceJoin', (data) => { /* { userId, channelId, serverId } */ })
|
|
787
|
+
client.on('voiceLeave', (data) => { /* { userId, channelId, serverId } */ })
|
|
788
|
+
|
|
789
|
+
// Raw event stream
|
|
790
|
+
client.on('event', (event) => {
|
|
791
|
+
console.log(event.type, event.data, event.timestamp)
|
|
792
|
+
})
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### Raw event types
|
|
796
|
+
|
|
797
|
+
| `event.type` | Fires when |
|
|
798
|
+
|---|---|
|
|
799
|
+
| `message.created` | A message is sent |
|
|
800
|
+
| `message.edited` | A message is edited |
|
|
801
|
+
| `message.deleted` | A message is deleted |
|
|
802
|
+
| `message.reaction_added` | A reaction is added |
|
|
803
|
+
| `message.reaction_removed` | A reaction is removed |
|
|
804
|
+
| `message.pinned` | A message is pinned |
|
|
805
|
+
| `user.joined_server` | A user joins a server |
|
|
806
|
+
| `user.left_server` | A user leaves a server |
|
|
807
|
+
| `user.updated_profile` | A user updates their profile |
|
|
808
|
+
| `user.banned` | A user is banned |
|
|
809
|
+
| `user.unbanned` | A user is unbanned |
|
|
810
|
+
| `user.role_added` | A user receives a role |
|
|
811
|
+
| `user.role_removed` | A role is removed from a user |
|
|
812
|
+
| `user.started_typing` | A user starts typing |
|
|
813
|
+
| `user.voice_joined` | A user joins a voice channel |
|
|
814
|
+
| `user.voice_left` | A user leaves a voice channel |
|
|
815
|
+
| `interaction.slash_command` | A slash command is used |
|
|
816
|
+
| `interaction.button_click` | A button is clicked |
|
|
817
|
+
| `interaction.select_menu` | A select menu is used |
|
|
818
|
+
| `interaction.modal_submit` | A modal is submitted |
|
|
819
|
+
| `interaction.autocomplete` | An autocomplete request fires |
|
|
820
|
+
| `server.updated` | Server settings change |
|
|
821
|
+
| `channel.created` | A channel is created |
|
|
822
|
+
| `channel.deleted` | A channel is deleted |
|
|
823
|
+
| `channel.updated` | A channel is updated |
|
|
824
|
+
| `role.created` | A role is created |
|
|
825
|
+
| `role.deleted` | A role is deleted |
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
## WebSocket helpers
|
|
830
|
+
|
|
831
|
+
```ts
|
|
832
|
+
// Send a message via WebSocket (lower latency than HTTP)
|
|
833
|
+
client.wsSend(channelId, 'Hello!')
|
|
834
|
+
|
|
835
|
+
// Typing indicators
|
|
836
|
+
client.wsTypingStart(channelId)
|
|
837
|
+
client.wsTypingStop(channelId)
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
## Examples
|
|
843
|
+
|
|
844
|
+
### Moderation bot
|
|
845
|
+
|
|
846
|
+
```ts
|
|
847
|
+
import { NovaClient, EmbedBuilder, CooldownManager, Logger } from 'novaapp-sdk'
|
|
848
|
+
|
|
849
|
+
const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })
|
|
850
|
+
const log = new Logger('ModBot')
|
|
851
|
+
const cooldowns = new CooldownManager()
|
|
852
|
+
|
|
853
|
+
client.on('ready', async (bot) => {
|
|
854
|
+
log.success(`Logged in as ${bot.botUser.username}`)
|
|
855
|
+
|
|
856
|
+
await client.commands.setSlash([
|
|
857
|
+
{
|
|
858
|
+
name: 'ban',
|
|
859
|
+
description: 'Ban a user from the server',
|
|
860
|
+
options: [
|
|
861
|
+
{ name: 'user', type: 'USER', description: 'User to ban', required: true },
|
|
862
|
+
{ name: 'reason', type: 'STRING', description: 'Reason for ban', required: false },
|
|
863
|
+
],
|
|
864
|
+
},
|
|
865
|
+
])
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
client.command('ban', async (interaction) => {
|
|
869
|
+
if (cooldowns.isOnCooldown(interaction.userId, 'ban', 3_000)) {
|
|
870
|
+
await interaction.replyEphemeral('Slow down!')
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
cooldowns.set(interaction.userId, 'ban')
|
|
874
|
+
|
|
875
|
+
const userId = interaction.options.getUser('user', true)
|
|
876
|
+
const reason = interaction.options.getString('reason') ?? 'No reason provided'
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
await client.members.ban(interaction.serverId!, userId, reason)
|
|
880
|
+
await interaction.reply({
|
|
881
|
+
embed: new EmbedBuilder()
|
|
882
|
+
.setTitle('🔨 User banned')
|
|
883
|
+
.setDescription(`<@${userId}> was banned.\n**Reason:** ${reason}`)
|
|
884
|
+
.setColor('#FF4444')
|
|
885
|
+
.setTimestamp()
|
|
886
|
+
.toJSON(),
|
|
887
|
+
})
|
|
888
|
+
} catch (err) {
|
|
889
|
+
log.error('Ban failed', err)
|
|
890
|
+
await interaction.replyEphemeral(`❌ Failed: ${(err as Error).message}`)
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
client.on('memberBanned', ({ userId, serverId, reason }) => {
|
|
895
|
+
log.warn(`${userId} banned from ${serverId} — ${reason ?? 'no reason'}`)
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
client.on('error', (err) => log.error('Gateway error', err))
|
|
899
|
+
await client.connect()
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Multi-step modal form
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
client.command('report', async (interaction) => {
|
|
906
|
+
const submitted = await interaction.openModal(
|
|
907
|
+
new ModalBuilder()
|
|
908
|
+
.setTitle('Submit a report')
|
|
909
|
+
.setCustomId('report_modal')
|
|
910
|
+
.addField(
|
|
911
|
+
new TextInputBuilder().setCustomId('subject').setLabel('Subject').setStyle('short').setRequired(true)
|
|
912
|
+
)
|
|
913
|
+
.addField(
|
|
914
|
+
new TextInputBuilder().setCustomId('details').setLabel('Details').setStyle('paragraph').setRequired(true)
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
if (!submitted) { await interaction.replyEphemeral('Cancelled.'); return }
|
|
919
|
+
|
|
920
|
+
await client.messages.send(logChannelId, {
|
|
921
|
+
embed: new EmbedBuilder()
|
|
922
|
+
.setTitle(`📋 ${submitted.modalData['subject']}`)
|
|
923
|
+
.setDescription(submitted.modalData['details'])
|
|
924
|
+
.addField('From', `<@${interaction.userId}>`, true)
|
|
925
|
+
.setTimestamp()
|
|
926
|
+
.toJSON(),
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
await submitted.replyEphemeral('Your report has been submitted.')
|
|
930
|
+
})
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### Paginate all messages in a channel
|
|
934
|
+
|
|
935
|
+
```ts
|
|
936
|
+
import { Paginator, formatRelative } from 'novaapp-sdk'
|
|
937
|
+
|
|
938
|
+
const paginator = new Paginator(async (cursor) => {
|
|
939
|
+
const messages = await client.messages.fetch(channelId, {
|
|
940
|
+
limit: 100, before: cursor ?? undefined,
|
|
941
|
+
})
|
|
942
|
+
return { items: messages, cursor: messages.at(-1)?.id ?? null }
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
let total = 0
|
|
946
|
+
for await (const msg of paginator) {
|
|
947
|
+
console.log(`[${formatRelative(msg.createdAt)}] ${msg.author.username}: ${msg.content}`)
|
|
948
|
+
total++
|
|
949
|
+
}
|
|
950
|
+
console.log(`Scanned ${total} messages`)
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
## Self-hosted deployments
|
|
956
|
+
|
|
957
|
+
```ts
|
|
958
|
+
const client = new NovaClient({
|
|
959
|
+
token: 'nova_bot_...',
|
|
960
|
+
baseUrl: 'https://my-nova-server.example.com',
|
|
961
|
+
})
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## Rate limits
|
|
967
|
+
|
|
968
|
+
The Nova API enforces a limit of **50 requests/second** per bot. The SDK surfaces a `429` error if exceeded. Use `sleep()` and exponential back-off for bulk operations.
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
## License
|
|
973
|
+
|
|
974
|
+
MIT
|
|
12
975
|
|
|
13
976
|
```ts
|
|
14
977
|
import { NovaClient } from 'nova-bot-sdk'
|
|
@@ -38,7 +1001,7 @@ await client.commands.setSlash([
|
|
|
38
1001
|
| Option | Type | Default | Description |
|
|
39
1002
|
|---|---|---|---|
|
|
40
1003
|
| `token` | `string` | **required** | Your bot token (`nova_bot_...`) |
|
|
41
|
-
| `baseUrl` | `string` | `https://
|
|
1004
|
+
| `baseUrl` | `string` | `https://novachatapp.com` | Override for self-hosted deployments |
|
|
42
1005
|
|
|
43
1006
|
## API Reference
|
|
44
1007
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novaapp-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Official SDK for building bots on the Nova platform",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"nova",
|
|
26
26
|
"bot",
|
|
27
27
|
"sdk",
|
|
28
|
-
"discord-like",
|
|
29
28
|
"chat"
|
|
30
29
|
],
|
|
31
30
|
"author": "Nova",
|