novaapp-sdk
v1.4.2
Published
Official SDK for building bots on the Nova platform
Readme
novaapp-sdk
Official SDK for building bots on the Nova platform.
Current version: 1.3.0
Table of contents
- Installation
- Quick start
- Configuration
- Routing handlers
- Rich wrappers
- API reference
- Fluent builders
- Utilities
- Events
- WebSocket helpers
- Examples
Installation
npm install novaapp-sdkQuick start
import { NovaClient, EmbedBuilder } from 'novaapp-sdk'
const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })
client.on('ready', async (bot) => {
console.log(`Logged in as ${bot.botUser.username}`)
await client.commands.setSlash([
{ name: 'ping', description: 'Check if the bot is alive' },
])
})
// Slash command routing
client.command('ping', async (interaction) => {
await interaction.reply('Pong! 🏓')
})
// Button routing
client.button('confirm', async (interaction) => {
await interaction.replyEphemeral('Confirmed!')
})
// Modal submission routing
client.modal('report_modal', async (interaction) => {
const reason = interaction.modalData['reason']
await interaction.replyEphemeral(`Report received: ${reason}`)
})
await client.connect()Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| token | string | required | Your bot token (nova_bot_...) |
| baseUrl | string | https://novachatapp.com | Override for self-hosted Nova servers |
Routing handlers
Instead of handling everything inside interactionCreate, use the built-in router:
// /slash and !prefix commands
client.command('ban', async (interaction) => {
const userId = interaction.options.getUser('user', true)
const reason = interaction.options.getString('reason') ?? 'No reason given'
await client.members.ban(interaction.serverId!, userId, reason)
await interaction.reply(`✅ Banned <@${userId}>`)
})
// Button clicks
client.button('delete_confirm', async (interaction) => {
await interaction.replyEphemeral('Deleted.')
})
// Select menus
client.selectMenu('colour_pick', async (interaction) => {
const chosen = interaction.values[0]
await interaction.reply(`You picked: ${chosen}`)
})
// Modal submissions
client.modal('feedback_modal', async (interaction) => {
const text = interaction.modalData['feedback']
await interaction.replyEphemeral(`Thanks: ${text}`)
})Rich wrappers
NovaInteraction
Returned by every interaction event and routing handler.
client.on('interactionCreate', async (interaction) => {
interaction.id // string
interaction.type // 'SLASH_COMMAND' | 'BUTTON_CLICK' | 'SELECT_MENU' | 'MODAL_SUBMIT' | …
interaction.commandName // string | null
interaction.customId // string | null (buttons / selects / modals)
interaction.userId // string
interaction.channelId // string
interaction.serverId // string | null
interaction.values // string[] (select menu choices)
interaction.modalData // Record<string, string> (modal submitted values)
// Type guards
interaction.isSlashCommand()
interaction.isPrefixCommand()
interaction.isCommand() // slash OR prefix
interaction.isButton()
interaction.isSelectMenu()
interaction.isModalSubmit()
interaction.isAutocomplete()
interaction.isContextMenu()
// Typed option accessor
const userId = interaction.options.getUser('user', true)
const reason = interaction.options.getString('reason') ?? 'No reason'
const count = interaction.options.getInteger('count') // number | null
// Actions
await interaction.reply('Hello!')
await interaction.reply({ content: 'Hi', ephemeral: true, embed: { title: 'Stats' } })
await interaction.replyEphemeral('Only you can see this.')
await interaction.defer() // acknowledge — shows loading state
await interaction.editReply('Done!') // edit after defer()
// Open a modal and await its submission
const submitted = await interaction.openModal(
new ModalBuilder()
.setTitle('Report')
.setCustomId('report')
.addField(new TextInputBuilder().setCustomId('reason').setLabel('Reason').setRequired(true))
)
if (submitted) {
await submitted.replyEphemeral(`Received: ${submitted.modalData['reason']}`)
}
})NovaMessage
Returned by messageCreate / messageUpdate events and message fetch calls.
client.on('messageCreate', async (msg) => {
msg.id // string
msg.content // string
msg.channelId // string
msg.author // { id, username, displayName, avatar, isBot }
msg.embed // Embed | null
msg.components // MessageComponent[]
msg.attachments // Attachment[]
msg.reactions // Reaction[]
msg.replyToId // string | null
msg.createdAt // Date
msg.editedAt // Date | null
msg.isFromBot()
msg.hasEmbed()
msg.hasComponents()
msg.isEdited()
await msg.reply('Got it!')
await msg.edit('Updated')
await msg.delete()
await msg.pin()
await msg.unpin()
await msg.react('👍')
await msg.removeReaction('👍')
const fresh = await msg.fetch()
console.log(msg.url) // '/channels/<id>/messages/<id>'
})NovaChannel
Returned by client.fetchChannel() and client.fetchChannels().
const channel = await client.fetchChannel('channel-id')
channel.id // string
channel.name // string
channel.type // 'TEXT' | 'VOICE' | 'ANNOUNCEMENT' | 'FORUM' | 'STAGE'
channel.serverId // string | null
channel.topic // string | null
channel.position // number
channel.slowMode // number (seconds; 0 = disabled)
channel.createdAt // Date
channel.isText()
channel.isVoice()
channel.isAnnouncement()
channel.isForum()
channel.isStage()
channel.hasSlowMode()
await channel.send('Hello!')
await channel.send({ content: 'Hi', embed: { title: 'News' } })
const messages = await channel.fetchMessages({ limit: 50 })
const pins = await channel.fetchPins()
await channel.startTyping()
const updated = await channel.edit({ topic: 'New topic', slowMode: 5 })
await channel.delete()NovaMember
Returned by client.fetchMember() and client.fetchMembers().
const member = await client.fetchMember('server-id', 'user-id')
member.userId // string
member.serverId // string
member.username // string
member.displayName // string
member.avatar // string | null
member.role // 'OWNER' | 'ADMIN' | 'MEMBER'
member.status // 'ONLINE' | 'IDLE' | 'DND' | 'OFFLINE'
member.isBot // boolean
member.joinedAt // Date
member.isOwner()
member.isAdmin() // ADMIN *or* OWNER
member.isRegularMember()
member.isOnline()
member.isIdle()
member.isDND()
member.isOffline()
await member.kick()
await member.ban('Spamming')
await member.dm('Welcome!')
await member.addRole('role-id')
await member.removeRole('role-id')API reference
client.messages
await client.messages.send(channelId, { content: 'Hi!' })
await client.messages.send(channelId, { embed: { title: 'Report', color: '#5865F2' } })
await client.messages.send(channelId, {
content: 'Pick one:',
components: [
{ type: 'button', customId: 'yes', label: 'Yes', style: 'success' },
{ type: 'button', customId: 'no', label: 'No', style: 'danger' },
],
})
await client.messages.edit(messageId, { content: 'Updated' })
await client.messages.delete(messageId)
const msg = await client.messages.fetchOne(messageId)
const messages = await client.messages.fetch(channelId, { limit: 50, before: cursorId })
const pins = await client.messages.fetchPinned(channelId)
await client.messages.pin(messageId)
await client.messages.unpin(messageId)
await client.messages.typing(channelId)
await client.messages.addReaction(messageId, '👍')
await client.messages.removeReaction(messageId, '👍')
const reactions = await client.messages.fetchReactions(messageId)client.channels
const channels = await client.channels.list(serverId)
const channel = await client.channels.fetch(channelId)
const newChan = await client.channels.create(serverId, {
name: 'announcements', type: 'ANNOUNCEMENT', topic: 'Official news',
})
await client.channels.edit(channelId, { topic: 'New topic', slowMode: 10 })
await client.channels.delete(channelId)
const messages = await client.channels.fetchMessages(channelId, { limit: 20 })
const pins = await client.channels.fetchPins(channelId)
await client.channels.startTyping(channelId)Rich wrapper shortcuts (return NovaChannel):
const channel = await client.fetchChannel(channelId)
const channels = await client.fetchChannels(serverId)
const textOnly = channels.filter(c => c.isText())client.reactions
await client.reactions.add(messageId, '🎉')
await client.reactions.remove(messageId, '🎉')
await client.reactions.removeAll(messageId) // requires messages.manage
await client.reactions.removeEmoji(messageId, '🎉') // all of one emoji
const all = await client.reactions.fetch(messageId)
// [{ emoji: '🎉', count: 3, users: [{ id, username, displayName, avatar }] }, …]
const detail = await client.reactions.fetchEmoji(messageId, '🎉')
// { emoji: '🎉', count: 3, users: […] }client.members
const members = await client.members.list(serverId, { limit: 100 })
await client.members.kick(serverId, userId)
await client.members.ban(serverId, userId, 'Reason')
await client.members.unban(serverId, userId)
const bans = await client.members.listBans(serverId)
// [{ userId, username, displayName, avatar, reason, bannedAt, moderatorId }]
await client.members.dm(userId, 'Hello!')
await client.members.dm(userId, { content: 'Hi', embed: { title: 'Welcome' } })
await client.members.addRole(serverId, userId, roleId)
await client.members.removeRole(serverId, userId, roleId)Rich wrapper shortcuts (return NovaMember):
const member = await client.fetchMember(serverId, userId)
const members = await client.fetchMembers(serverId)
const bots = members.filter(m => m.isBot)client.servers
const servers = await client.servers.list()
const roles = await client.servers.listRoles(serverId)
// [{ id, name, color, position, serverId, hoist, createdAt }]client.commands
await client.commands.setSlash([
{ name: 'ban', description: 'Ban a user', options: [
{ name: 'user', description: 'User to ban', type: 'USER', required: true },
{ name: 'reason', description: 'Reason', type: 'STRING', required: false },
]},
])
const slash = await client.commands.getSlash()
await client.commands.deleteSlash('ban')
await client.commands.setPrefix([{ prefix: '!', name: 'help', description: 'Show help' }])
const prefix = await client.commands.getPrefix()
await client.commands.deletePrefix('!', 'help')
await client.commands.setContext([
{ name: 'Report message', target: 'MESSAGE' },
{ name: 'View profile', target: 'USER' },
])client.interactions
await client.interactions.ack(interaction.id) // acknowledge (show loading)
await client.interactions.respond(interaction.id, { content: 'Done!', ephemeral: true })
// Open a modal (low-level)
await client.interactions.respond(interaction.id, {
modal: {
title: 'Report', customId: 'report_modal',
fields: [{ customId: 'reason', label: 'Reason', type: 'paragraph', required: true }],
},
})
const pending = await client.interactions.poll({ limit: 20 })client.permissions
const result = await client.permissions.get({
serverId: 'server-id',
channelId: 'channel-id', // optional
roleId: 'role-id', // optional
})
result.permissions // merged Record<string, boolean>
result.records // raw permission recordsclient.cron() — recurring tasks
const cancel = client.cron(60_000, async () => {
const msgs = await client.messages.fetch(channelId, { limit: 1 })
console.log('Latest:', msgs[0]?.content)
})
cancel() // stop the taskclient.setStatus()
client.setStatus('ONLINE') // default
client.setStatus('IDLE') // away
client.setStatus('DND') // Do Not Disturb
client.setStatus('OFFLINE') // appear offlineclient.waitFor() — event fence
// Wait for a message in a specific channel (default 30 s timeout)
const msg = await client.waitFor(
'messageCreate',
(m) => m.channelId === channelId,
)
// Wait for a button click, throw after 60 s
const click = await client.waitFor(
'interactionCreate',
(i) => i.isButton() && i.customId === 'confirm',
60_000,
)Fluent builders
EmbedBuilder
const embed = new EmbedBuilder()
.setTitle('Server stats')
.setDescription('All systems operational.')
.setColor('#5865F2')
.setThumbnail('https://example.com/logo.png')
.addField('Members', '1,234', true)
.addField('Online', '456', true)
.setFooter('Updated just now')
.setTimestamp()
.toJSON()
await client.messages.send(channelId, { embed })ButtonBuilder + ActionRowBuilder
const row = new ActionRowBuilder()
.addComponent(
new ButtonBuilder().setCustomId('yes').setLabel('Yes').setStyle('success').toJSON()
)
.addComponent(
new ButtonBuilder().setCustomId('no').setLabel('No').setStyle('danger').toJSON()
)
.toJSON()
await client.messages.send(channelId, { content: 'Confirm?', components: [row] })SelectMenuBuilder
const row = new ActionRowBuilder()
.addComponent(
new SelectMenuBuilder()
.setCustomId('colour_pick')
.setPlaceholder('Choose a colour')
.addOption({ label: 'Red', value: 'red' })
.addOption({ label: 'Blue', value: 'blue' })
.addOption({ label: 'Green', value: 'green' })
.toJSON()
)
.toJSON()
await client.messages.send(channelId, { content: 'Pick:', components: [row] })ModalBuilder + TextInputBuilder
const modal = new ModalBuilder()
.setTitle('Submit a report')
.setCustomId('report_modal')
.addField(
new TextInputBuilder()
.setCustomId('subject')
.setLabel('Subject')
.setStyle('short')
.setRequired(true)
.setMaxLength(100)
)
.addField(
new TextInputBuilder()
.setCustomId('description')
.setLabel('Description')
.setStyle('paragraph')
.setRequired(true)
)
const submitted = await interaction.openModal(modal)
if (submitted) {
const subject = submitted.modalData['subject']
const desc = submitted.modalData['description']
await submitted.replyEphemeral(`Filed: ${subject}`)
}SlashCommandBuilder
const cmd = new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a member')
.addOption(
new SlashCommandOptionBuilder()
.setName('user').setDescription('Member to kick').setType('USER').setRequired(true)
)
.toJSON()
await client.commands.setSlash([cmd])PollBuilder
const poll = new PollBuilder()
.setQuestion('Best programming language?')
.addOption({ id: 'ts', label: 'TypeScript', emoji: '💙' })
.addOption({ id: 'rs', label: 'Rust', emoji: '🦀' })
.addOption({ id: 'py', label: 'Python', emoji: '🐍' })
.setMultipleChoice(false)
.toJSON()
await client.messages.send(channelId, { content: 'Vote below!', ...poll })MessageBuilder
Compose content, embeds, and components fluently:
const msg = new MessageBuilder()
.setContent('New announcement!')
.setEmbed(
new EmbedBuilder()
.setTitle('v2.0 Released')
.setDescription('Check the changelog for details.')
.setColor('#57F287')
.toJSON()
)
.addRow(
new ActionRowBuilder()
.addComponent(
new ButtonBuilder().setCustomId('changelog').setLabel('View changelog').setStyle('primary').toJSON()
)
.toJSON()
)
.build()
await client.messages.send(channelId, msg)Utilities
Collection<K, V>
A supercharged Map with array-style helpers:
const col = new Collection<string, User>()
col.set('1', { id: '1', name: 'Alice' })
col.set('2', { id: '2', name: 'Bob' })
col.first() // { id: '1', name: 'Alice' }
col.last() // { id: '2', name: 'Bob' }
col.random() // random value
col.find(v => v.name === 'Bob') // { id: '2', … }
col.filter(v => v.name.length > 3) // Collection with Alice
col.map(v => v.name) // ['Alice', 'Bob']
col.some(v => v.name === 'Alice') // true
col.every(v => v.id.length > 0) // true
col.toArray() // [{ … }, { … }]Paginator<T>
Cursor-based async paginator for any list API:
const paginator = new Paginator(async (cursor) => {
const messages = await client.messages.fetch(channelId, {
limit: 50, before: cursor ?? undefined,
})
return { items: messages, cursor: messages.at(-1)?.id ?? null }
})
// Async iteration
for await (const msg of paginator) {
console.log(msg.content)
}
// Collect everything at once
const all = await paginator.fetchAll()
// Collect first N
const first200 = await paginator.fetchN(200)
// Reset and iterate again from the start
paginator.reset()PermissionsBitfield
Work with Nova bot permissions as a typed bitfield:
import { PermissionsBitfield, Permissions } from 'novaapp-sdk'
const perms = new PermissionsBitfield({
[Permissions.MESSAGES_READ]: true,
[Permissions.MESSAGES_WRITE]: true,
})
perms.has(Permissions.MESSAGES_READ) // true
perms.has(Permissions.CHANNELS_MANAGE) // false
perms.hasAll(Permissions.MESSAGES_READ, Permissions.MESSAGES_WRITE) // true
perms.hasAny(Permissions.CHANNELS_MANAGE, Permissions.SERVERS_MANAGE) // false
perms.missing(Permissions.MESSAGES_MANAGE, Permissions.MEMBERS_BAN)
// ['messages.manage', 'members.ban']
const extended = perms.grant(Permissions.CHANNELS_MANAGE)
const restricted = perms.deny(Permissions.MESSAGES_WRITE)
const merged = serverPerms.merge(channelOverrides)
perms.toArray() // ['messages.read', 'messages.write']Available constants in Permissions:
| Constant | Value |
|---|---|
| MESSAGES_READ | 'messages.read' |
| MESSAGES_WRITE | 'messages.write' |
| MESSAGES_MANAGE | 'messages.manage' |
| CHANNELS_MANAGE | 'channels.manage' |
| MEMBERS_KICK | 'members.kick' |
| MEMBERS_BAN | 'members.ban' |
| MEMBERS_ROLES | 'members.roles' |
| SERVERS_MANAGE | 'servers.manage' |
CooldownManager
Per-user / per-command cooldown tracking:
const cooldowns = new CooldownManager()
client.command('daily', async (interaction) => {
if (cooldowns.isOnCooldown(interaction.userId, 'daily', 86_400_000)) {
const remaining = cooldowns.getRemaining(interaction.userId, 'daily', 86_400_000)
await interaction.replyEphemeral(`Try again in ${formatDuration(remaining)}.`)
return
}
cooldowns.set(interaction.userId, 'daily')
await interaction.reply('Here is your daily reward!')
})Logger
Coloured, namespaced console logger:
const log = new Logger('ModBot')
log.info('Bot started')
log.success('Command registered: ban')
log.warn('Rate limit approaching')
log.error('Failed to ban user', err)
log.debug('Payload', interaction.toJSON())Time utilities
import {
sleep, withTimeout,
formatDuration, formatRelative,
parseTimestamp, countdown,
} from 'novaapp-sdk'
await sleep(2_000) // wait 2 s
const data = await withTimeout(fetch(), 5_000) // throw if > 5 s
formatDuration(90_000) // '1m 30s'
formatDuration(3_661_000) // '1h 1m 1s'
formatRelative(Date.now() - 4_000) // 'just now'
formatRelative(Date.now() - 90_000) // '1 minute ago'
formatRelative(Date.now() + 60_000) // 'in 1 minute'
formatRelative(Date.now() - 86_400_000) // 'yesterday'
parseTimestamp('2026-01-01T00:00:00Z') // Date
parseTimestamp(null) // null
const { days, hours, minutes, seconds } = countdown(new Date('2027-01-01'))Events
// Connection
client.on('ready', (bot) => console.log('Ready!', bot.botUser.username))
client.on('disconnect', (reason) => console.log('Disconnected:', reason))
client.on('error', (err) => console.error(err))
// Interactions
client.on('interactionCreate', (interaction) => { /* NovaInteraction */ })
// Messages
client.on('messageCreate', (msg) => { /* NovaMessage */ })
client.on('messageUpdate', (msg) => { /* NovaMessage */ })
client.on('messageDelete', (data) => { /* { id, channelId } */ })
client.on('messagePinned', (data) => { /* { messageId, channelId, pinnedBy } */ })
// Reactions
client.on('reactionAdd', (data) => { /* { messageId, channelId, userId, emoji } */ })
client.on('reactionRemove', (data) => { /* { messageId, channelId, userId, emoji } */ })
// Members
client.on('memberAdd', (data) => { /* { serverId, userId, username } */ })
client.on('memberRemove', (data) => { /* { serverId, userId } */ })
client.on('memberUpdate', (data) => { /* { serverId, userId } */ })
client.on('memberBanned', (data) => { /* { userId, serverId, moderatorId, reason } */ })
client.on('memberUnbanned', (data) => { /* { userId, serverId } */ })
client.on('typingStart', (data) => { /* { channelId, userId } */ })
// Channels
client.on('channelCreate', (channel) => { /* Channel */ })
client.on('channelUpdate', (channel) => { /* Channel */ })
client.on('channelDelete', (data) => { /* { id, serverId } */ })
// Roles
client.on('roleCreate', (data) => { /* { id, name, color, serverId, position, hoist, createdAt } */ })
client.on('roleDelete', (data) => { /* { id, serverId } */ })
// Voice
client.on('voiceJoin', (data) => { /* { userId, channelId, serverId } */ })
client.on('voiceLeave', (data) => { /* { userId, channelId, serverId } */ })
// Raw event stream
client.on('event', (event) => {
console.log(event.type, event.data, event.timestamp)
})Raw event types
| event.type | Fires when |
|---|---|
| message.created | A message is sent |
| message.edited | A message is edited |
| message.deleted | A message is deleted |
| message.reaction_added | A reaction is added |
| message.reaction_removed | A reaction is removed |
| message.pinned | A message is pinned |
| user.joined_server | A user joins a server |
| user.left_server | A user leaves a server |
| user.updated_profile | A user updates their profile |
| user.banned | A user is banned |
| user.unbanned | A user is unbanned |
| user.role_added | A user receives a role |
| user.role_removed | A role is removed from a user |
| user.started_typing | A user starts typing |
| user.voice_joined | A user joins a voice channel |
| user.voice_left | A user leaves a voice channel |
| interaction.slash_command | A slash command is used |
| interaction.button_click | A button is clicked |
| interaction.select_menu | A select menu is used |
| interaction.modal_submit | A modal is submitted |
| interaction.autocomplete | An autocomplete request fires |
| server.updated | Server settings change |
| channel.created | A channel is created |
| channel.deleted | A channel is deleted |
| channel.updated | A channel is updated |
| role.created | A role is created |
| role.deleted | A role is deleted |
WebSocket helpers
// Send a message via WebSocket (lower latency than HTTP)
client.wsSend(channelId, 'Hello!')
// Typing indicators
client.wsTypingStart(channelId)
client.wsTypingStop(channelId)Examples
Moderation bot
import { NovaClient, EmbedBuilder, CooldownManager, Logger } from 'novaapp-sdk'
const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })
const log = new Logger('ModBot')
const cooldowns = new CooldownManager()
client.on('ready', async (bot) => {
log.success(`Logged in as ${bot.botUser.username}`)
await client.commands.setSlash([
{
name: 'ban',
description: 'Ban a user from the server',
options: [
{ name: 'user', type: 'USER', description: 'User to ban', required: true },
{ name: 'reason', type: 'STRING', description: 'Reason for ban', required: false },
],
},
])
})
client.command('ban', async (interaction) => {
if (cooldowns.isOnCooldown(interaction.userId, 'ban', 3_000)) {
await interaction.replyEphemeral('Slow down!')
return
}
cooldowns.set(interaction.userId, 'ban')
const userId = interaction.options.getUser('user', true)
const reason = interaction.options.getString('reason') ?? 'No reason provided'
try {
await client.members.ban(interaction.serverId!, userId, reason)
await interaction.reply({
embed: new EmbedBuilder()
.setTitle('🔨 User banned')
.setDescription(`<@${userId}> was banned.\n**Reason:** ${reason}`)
.setColor('#FF4444')
.setTimestamp()
.toJSON(),
})
} catch (err) {
log.error('Ban failed', err)
await interaction.replyEphemeral(`❌ Failed: ${(err as Error).message}`)
}
})
client.on('memberBanned', ({ userId, serverId, reason }) => {
log.warn(`${userId} banned from ${serverId} — ${reason ?? 'no reason'}`)
})
client.on('error', (err) => log.error('Gateway error', err))
await client.connect()Multi-step modal form
client.command('report', async (interaction) => {
const submitted = await interaction.openModal(
new ModalBuilder()
.setTitle('Submit a report')
.setCustomId('report_modal')
.addField(
new TextInputBuilder().setCustomId('subject').setLabel('Subject').setStyle('short').setRequired(true)
)
.addField(
new TextInputBuilder().setCustomId('details').setLabel('Details').setStyle('paragraph').setRequired(true)
)
)
if (!submitted) { await interaction.replyEphemeral('Cancelled.'); return }
await client.messages.send(logChannelId, {
embed: new EmbedBuilder()
.setTitle(`📋 ${submitted.modalData['subject']}`)
.setDescription(submitted.modalData['details'])
.addField('From', `<@${interaction.userId}>`, true)
.setTimestamp()
.toJSON(),
})
await submitted.replyEphemeral('Your report has been submitted.')
})Paginate all messages in a channel
import { Paginator, formatRelative } from 'novaapp-sdk'
const paginator = new Paginator(async (cursor) => {
const messages = await client.messages.fetch(channelId, {
limit: 100, before: cursor ?? undefined,
})
return { items: messages, cursor: messages.at(-1)?.id ?? null }
})
let total = 0
for await (const msg of paginator) {
console.log(`[${formatRelative(msg.createdAt)}] ${msg.author.username}: ${msg.content}`)
total++
}
console.log(`Scanned ${total} messages`)Self-hosted deployments
const client = new NovaClient({
token: 'nova_bot_...',
baseUrl: 'https://my-nova-server.example.com',
})Rate limits
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.
License
MIT
import { NovaClient } from 'nova-bot-sdk'
const client = new NovaClient({ token: 'nova_bot_your_token_here' })
client.on('ready', (bot) => {
console.log(`Logged in as ${bot.botUser.username}`)
})
client.on('interactionCreate', async (interaction) => {
if (interaction.commandName === 'ping') {
await client.interactions.respond(interaction.id, { content: 'Pong! 🏓' })
}
})
// Register commands once at startup
await client.connect()
await client.commands.setSlash([
{ name: 'ping', description: 'Check if the bot is alive' },
])Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| token | string | required | Your bot token (nova_bot_...) |
| baseUrl | string | https://novachatapp.com | Override for self-hosted deployments |
API Reference
client.messages
// Send a message
await client.messages.send(channelId, { content: 'Hello!' })
// Send with embed
await client.messages.send(channelId, {
embed: {
title: 'Report',
description: 'All systems operational.',
color: 0x5865f2,
},
})
// Send with action buttons
await client.messages.send(channelId, {
content: 'Confirm action?',
components: [
{ type: 'button', customId: 'confirm', label: 'Confirm', style: 'success' },
{ type: 'button', customId: 'cancel', label: 'Cancel', style: 'danger' },
],
})
// Edit a message
await client.messages.edit(messageId, { content: 'Updated!' })
// Delete a message
await client.messages.delete(messageId)
// Fetch recent messages
const messages = await client.messages.fetch(channelId, { limit: 50 })
// Show typing indicator
await client.messages.typing(channelId)client.commands
// Slash commands
await client.commands.setSlash([
{ name: 'ban', description: 'Ban a user', options: [
{ name: 'user', description: 'User to ban', type: 'USER', required: true },
{ name: 'reason', description: 'Reason', type: 'STRING' },
]},
])
const slash = await client.commands.getSlash()
await client.commands.deleteSlash('ban')
// Prefix commands
await client.commands.setPrefix([
{ prefix: '!', name: 'help', description: 'Show help' },
{ prefix: '!', name: 'stats', description: 'Show bot stats' },
])
const prefix = await client.commands.getPrefix()
await client.commands.deletePrefix('!', 'help')
// Context menu commands (right-click)
await client.commands.setContext([
{ name: 'Report message', target: 'MESSAGE' },
{ name: 'View profile', target: 'USER' },
])client.interactions
client.on('interactionCreate', async (interaction) => {
// Acknowledge immediately (shows loading state)
await client.interactions.ack(interaction.id)
// Do your work...
const result = await doExpensiveWork()
// Respond with result
await client.interactions.respond(interaction.id, {
content: result,
ephemeral: true, // only visible to the user who triggered it
})
})
// Poll for pending interactions (HTTP polling mode, no WebSocket)
const pending = await client.interactions.poll({ limit: 20 })client.members
// List server members
const members = await client.members.list(serverId, { limit: 100 })
// Kick a member (cannot kick OWNER or ADMIN)
await client.members.kick(serverId, userId)
// Ban a member
await client.members.ban(serverId, userId, 'Spamming')client.servers
// List all servers the bot is in
const servers = await client.servers.list()
console.log(`Active in ${servers.length} servers`)Events
// Connection
client.on('ready', (bot) => console.log('Ready!', bot.botUser.username))
client.on('disconnect', (reason) => console.log('Disconnected:', reason))
client.on('error', (err) => console.error('Error:', err.message))
// Interactions
client.on('interactionCreate', (interaction) => { /* ... */ })
// Messages (convenience events)
client.on('messageCreate', (data) => { /* ... */ })
client.on('messageUpdate', (data) => { /* ... */ })
client.on('messageDelete', (data) => { /* ... */ })
client.on('reactionAdd', (data) => { /* ... */ })
client.on('reactionRemove',(data) => { /* ... */ })
// Members
client.on('memberAdd', (data) => { /* ... */ })
client.on('memberRemove', (data) => { /* ... */ })
client.on('typingStart', (data) => { /* ... */ })
// All raw events
client.on('event', (event) => {
console.log(event.type, event.data, event.timestamp)
})All event types
| Event type | Fired when |
|---|---|
| message.created | A message is sent in a server the bot is in |
| message.edited | A message is edited |
| message.deleted | A message is deleted |
| message.reaction_added | A reaction is added |
| message.reaction_removed | A reaction is removed |
| message.pinned | A message is pinned |
| user.joined_server | A user joins a server |
| user.left_server | A user leaves a server |
| user.updated_profile | A user updates their profile |
| user.banned | A user is banned |
| user.unbanned | A user is unbanned |
| user.role_added | A user receives a role |
| user.role_removed | A role is removed from a user |
| user.started_typing | A user starts typing |
| user.voice_joined | A user joins a voice channel |
| user.voice_left | A user leaves a voice channel |
| interaction.slash_command | A slash command is used |
| interaction.button_click | A button is clicked |
| interaction.select_menu | A select menu item is chosen |
| interaction.modal_submit | A modal is submitted |
| interaction.autocomplete | An autocomplete request fires |
| server.updated | Server settings change |
| channel.created | A channel is created |
| channel.deleted | A channel is deleted |
| channel.updated | A channel is updated |
| role.created | A role is created |
| role.deleted | A role is deleted |
WebSocket helpers
For ultra-low-latency message sending without an HTTP round-trip:
// Send via WebSocket (fire and forget)
client.wsSend(channelId, 'Hello!')
// Typing indicators via WebSocket
client.wsTypingStart(channelId)
client.wsTypingStop(channelId)Full example — Moderation bot
import { NovaClient } from 'nova-bot-sdk'
const client = new NovaClient({ token: process.env.NOVA_BOT_TOKEN! })
client.on('ready', async (bot) => {
console.log(`[${bot.botUser.username}] Ready!`)
await client.commands.setSlash([
{
name: 'ban',
description: 'Ban a user from the server',
options: [
{ name: 'user', description: 'User to ban', type: 'USER', required: true },
{ name: 'reason', description: 'Reason for ban', type: 'STRING', required: false },
],
},
{
name: 'warn',
description: 'Warn a user',
options: [
{ name: 'user', description: 'User to warn', type: 'USER', required: true },
{ name: 'message', description: 'Warning message', type: 'STRING', required: true },
],
},
])
})
client.on('interactionCreate', async (interaction) => {
if (interaction.commandName === 'ban') {
await client.interactions.ack(interaction.id)
const data = interaction.data as { options?: Array<{ name: string; value: string }> }
const userId = data.options?.find((o) => o.name === 'user')?.value
const reason = data.options?.find((o) => o.name === 'reason')?.value ?? 'No reason provided'
if (!userId || !interaction.serverId) {
await client.interactions.respond(interaction.id, {
content: '❌ Missing user or server context.',
ephemeral: true,
})
return
}
try {
await client.members.ban(interaction.serverId, userId, reason)
await client.interactions.respond(interaction.id, {
embed: {
title: '🔨 User banned',
description: `<@${userId}> was banned. Reason: ${reason}`,
color: 0xff4444,
},
})
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error'
await client.interactions.respond(interaction.id, {
content: `❌ Failed to ban: ${message}`,
ephemeral: true,
})
}
}
if (interaction.commandName === 'warn') {
const data = interaction.data as { options?: Array<{ name: string; value: string }> }
const userId = data.options?.find((o) => o.name === 'user')?.value
const message = data.options?.find((o) => o.name === 'message')?.value
await client.interactions.respond(interaction.id, {
embed: {
title: '⚠️ Warning issued',
description: `<@${userId}> — ${message}`,
color: 0xffa500,
},
})
}
})
client.on('error', (err) => {
console.error('[Bot]', err.message)
})
client.connect().then(() => console.log('Connected to Nova gateway'))Self-hosted deployments
If you're running your own Nova server, pass the base URL:
const client = new NovaClient({
token: 'nova_bot_...',
baseUrl: 'https://my-nova-server.example.com',
})Rate limits
The Nova API enforces a limit of 50 requests/second per bot. The SDK will surface a 429 error if you exceed this. Build in exponential back-off for batch operations.
License
MIT
