novaapp-sdk 1.3.1 → 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.
Files changed (2) hide show
  1. package/README.md +966 -3
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,14 +1,977 @@
1
- # nova-bot-sdk
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 nova-bot-sdk
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
- ## Quick Start
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'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaapp-sdk",
3
- "version": "1.3.1",
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",