Make a bot
Roar.js reference
Roar.js is Roar's bot framework: one file with no dependencies that handles connecting, reconnecting, heartbeats, rate limits and slash commands, so your code only says what the bot does. Version 1.0.0.
Install
npm install https://bots.roarnode.com/roar.js.tgzIt needs Node.js 22 or newer (for its built-in WebSocket), with "type": "module" in your package.json. It also works in Deno straight from the web, and in Bun:
import { Bot } from "https://bots.roarnode.com/roar.js";Prefer a single file? Download roar.js (and roar.d.ts for autocomplete), put it next to your bot and import "./roar.js".
The Bot
import { Bot } from "roar.js";
const bot = new Bot({
token: process.env.ROAR_TOKEN, // the bot's token (this is the default)
presence: { activity: "for /help", activityType: "watching" },
});
bot.on("ready", () => console.log(`Online as ${bot.user.username}`));
await bot.start(); // resolves once the bot is ready| Option | What it does |
|---|---|
token | The bot's token. Left out, Roar.js reads the ROAR_TOKEN environment variable. |
node | Which node to join. Left out, it's the official node. See Which node. Defaults to ROAR_NODE. |
relay | The relay that finds nodes by id: https://roarnode.com. |
presence | { status, activity, activityType, customStatus }, shown from the moment it connects. See Presence. |
ownMessages | true also passes the bot's own messages to "message". Off by default, so a bot can't answer itself in a loop. |
What the bot has
| Property or method | |
|---|---|
bot.user | The bot's own User (once ready). |
bot.applicationId | The Application ID. |
bot.guilds | A Map of the servers it's in, by id. bot.guild("Bot Lab") finds one by id or name. |
bot.channels, bot.users | Maps of every channel and person the bot knows about. bot.channel(id) looks one up. |
bot.start() / bot.stop() | Connect (and stay connected, reconnecting by itself) / disconnect for good. start() rejects if the token is refused. |
bot.send(channel, content) | Send a message to a channel (a Channel or an id). See Sending messages. |
bot.dm(user, content) | Send a direct message. The bot and the person need to share a server. |
bot.fetchUser(id) | Look up anyone by id. |
bot.rest | Any API route not wrapped here: bot.rest.get("/users/@me"), .post, .put, .patch, .delete. |
Which node
A bot belongs to the node where it was made. Most bots live on the official node, which is the default. For any other node, pass node:
new Bot({ token }); // the official node, through roarnode.com
new Bot({ token, node: "mk3vancmfm" }); // a node by its id (through the relay)
new Bot({ token, node: "https://chat.example.com" }); // a node with its own address
new Bot({ token, node: "http://127.0.0.1:8080" }); // a node on this PC, for testingA node's id is on the Developer page of the Roar app, in Getting started.
Events
Listen with bot.on(event, handler), bot.once for just the next one, and bot.off to stop. Handlers can be async; if one throws, the error goes to "error" (or the console) and the bot keeps running.
| Event | Handler gets | When |
|---|---|---|
ready | the bot | Connected, with every server loaded. After a reconnect it fires again. |
message | Message | Someone sends a message the bot can see (not the bot's own, unless ownMessages). |
messageUpdate | Message | A message is edited. |
messageDelete | { id, channelId, guildId } | A message is deleted. |
reactionAdd, reactionRemove | Reaction | Someone else adds or removes a reaction. |
memberJoin | Member | Someone joins a server the bot is in. |
memberLeave | Member (or { id, user, guild }) | Someone leaves or is removed. |
memberUpdate | Member | Their roles, nickname or timeout change. |
guildJoin, guildLeave | Guild | The bot is added to, or removed from, a server. |
guildUpdate | Guild | A server's settings change. |
command | Interaction | Any slash command (as well as its own handler). |
typing | { userId, channelId, guildId } | Someone starts typing. |
presence | the raw presence | Someone's status or activity changes. |
raw | (type, data) | Every gateway event, unchanged: see the event list. |
disconnect, reconnecting | { code, reason }, { delay } | The connection dropped; it's trying again in delay ms. |
error, debug | Error, text | Something went wrong / what it's doing, for logs. |
Messages
bot.on("message", async (msg) => {
if (msg.author.isBot) return; // ignore other bots
if (msg.content.startsWith("!say ")) {
await msg.channel.send(msg.content.slice(5));
}
if (msg.mentionsMe) await msg.react("๐"); // someone pinged the bot
});| Message | |
|---|---|
id, content, createdAt, editedAt | The basics. content holds up to 4000 characters. |
author | Who sent it (a User). author.isBot, author.isWebhook. |
member, guild, channel | Their membership, the server and the channel. guild and member are null in DMs (msg.isDM). |
mentions, mentionsMe, mentionsEveryone | Ids of the people it pings, and whether it pings the bot or everyone. |
attachments | [{ id, name, size, contentType, width, height, url }], with full download addresses. |
embeds, reactions, replyTo, pinned | Its embeds, its reactions ({ emoji, count, me }), the message it replies to, and whether it's pinned. |
reply(content) | Reply to it (pings the author; pass { content, ping: false } not to). |
react(emoji), unreact(emoji) | "๐", or a server emoji as "name:id". |
edit(content), delete(), pin(), unpin() | Edit the bot's own messages; delete, pin and unpin need permission for other people's. |
Sending messages
Anything that sends (channel.send, msg.reply, bot.send, user.send) takes text, or an object:
await channel.send("Hello!");
await channel.send({
content: "Today's weather",
embed: { title: "Sunny", description: "22ยฐC, light wind", color: "#ffb86b" },
files: ["./forecast.png"], // paths, or { name, data }
silent: true, // no notification, like @silent
});| Field | |
|---|---|
content | Text, up to 4000 characters. Markdown works: **bold**, *italic*, `code`, ||spoiler||. |
embed / embeds | One card, or up to 10. See Embeds. |
files | Up to 10 files: paths on disk, or { name, data } with a Uint8Array, Buffer or Blob. The node's upload limit applies (25 MB by default; 8 MB through the relay). |
silent | Send without notifying anyone. |
replyTo, ping | Make it a reply to a message, and choose whether that pings its author. |
Mentions: put a user, member, channel or role in a template string and it turns into a ping, or use the helpers:
import { mention, channelMention, roleMention } from "roar.js";
await channel.send(`Hi ${msg.author}!`); // pings them
await channel.send(`Read ${channelMention(rulesId)} first, ${mention(userId)}`);Embeds
Embeds are cards with a coloured edge, a title, fields and pictures. Only bots and webhooks can send them.
await i.reply({
embed: {
title: "Server stats",
url: "https://roarnode.com",
description: "How we're doing this week",
color: "#5fd96a", // or a number like 0x5fd96a
author: "Stats Bot",
thumbnail: "https://example.com/logo.png",
fields: [
{ name: "Members", value: "128", inline: true },
{ name: "Messages", value: "4,210", inline: true },
],
image: "https://example.com/chart.png",
footer: "Updated every hour",
timestamp: new Date(),
},
});Up to 10 embeds per message, about 12 KB of text in total.
Slash commands
Add commands with bot.command. Roar.js sends them all to the node each time the bot connects, and runs the handler when someone uses one.
// The short way: a name, a description, a handler.
bot.command("ping", "Check the bot is awake", (i) => i.reply("Pong!"));
// With options.
bot.command({
name: "remind",
description: "Remind me later",
options: [
{ name: "minutes", description: "In how many minutes", type: "integer", required: true },
{ name: "what", description: "What to remind you about", type: "string", required: true },
{ name: "where", description: "Which channel", type: "channel" },
],
}, async (i) => {
await i.reply({ content: `OK, in ${i.options.minutes} min`, ephemeral: true });
});Options
type | i.options.name is |
|---|---|
"string" (the default) | text |
"integer", "number" | a whole number, any number |
"boolean" | true or false |
"user" | a user id; i.optionUser("name") gives the User |
"channel" | a channel id; i.optionChannel("name") gives the Channel |
"role" | a role id; i.optionRole("name") gives the Role |
Each option can have required: true and up to 25 choices, like choices: [4, 6, 20] or [{ name: "Heads", value: "h" }]. Rules the node checks, and Roar.js checks first: names are 1โ32 lowercase letters, numbers, - or _; descriptions are 1โ100 characters; at most 25 options; required options come first; at most 100 commands. An option left empty is missing from i.options.
Answering
| Interaction | |
|---|---|
reply(content) | Answer it: text, or { content, embed, embeds, ephemeral, silent }. ephemeral: true shows the answer only to the person who used the command. |
defer({ ephemeral }) | Show "Bot is thinking..." while you work. Then answer with reply or editReply, within 15 minutes. |
editReply(content) | Change the answer. |
user, member, guild, channel | Who used it, and where. |
commandName, options | The command, and the options by name. |
If your handler throws before answering, Roar.js answers "Something went wrong running that command." to that person only, and reports the error.
What isn't there yet: buttons, menus, pop-up forms, autocomplete, follow-up messages and commands for a single server. Commands are used in server channels from the Roar desktop app.
Servers, channels and roles
const guild = bot.guild("Bot Lab"); // or msg.guild, i.guild, member.guild
const welcome = guild.channel("welcome"); // by name or id
const mods = guild.role("Moderator");
await welcome.send(`We're ${guild.memberCount} now!`);
await guild.addRole(userId, "Member"); // a role by name, id or Role| Guild (a server) | |
|---|---|
id, name, ownerId, memberCount | The basics. |
channels, roles, members | Maps by id. channel(nameOrId), role(nameOrId) and member(userOrId) find one; systemChannel is where join messages go. |
addRole(user, role), removeRole(user, role) | Needs Manage Roles, and the bot's role above that role. |
kick(user), ban(user), unban(user) | Needs Kick Members / Ban Members, and to outrank them. |
timeout(user, minutes) | Stops them talking for a while (Timeout Members); 0 lifts it. |
fetchMember(user), leave() | Get a member fresh from the node / take the bot out of the server. |
Every moderation method takes an optional last argument, a reason, which shows in the server's audit log: guild.kick(id, "Spamming").
| Channel | |
|---|---|
id, name, type, topic, guild | type is "text", "voice", "announcement", "forum", "thread", "dm"... |
send(content), typing() | Send a message / show "Bot is typing..." for a few seconds. |
messages({ limit, before, after }), fetchMessage(id) | Read the channel's history (up to 100 at a time, newest first). |
createWebhook(name) | Make a webhook for this channel (Manage Webhooks). |
Users and members
A User is a person (or bot) anywhere on the node. A Member is that person in one server, with their nickname and roles.
| User | |
|---|---|
id, username, displayName, isBot, badges | Who they are. |
avatarUrl, createdAt, mention | Their picture's address (or null), when the account was made, and <@id>. |
send(content) | DM them. |
| Member | |
|---|---|
user, guild, displayName, nick, roles, joinedAt | Their nickname wins over the user's display name. roles holds role ids. |
hasRole(role), addRole(role), removeRole(role) | By name, id or Role. |
kick(), ban(), timeout(minutes), send(content) | The same as on the Guild. |
Reactions
bot.on("reactionAdd", async (r) => {
if (r.emoji === "โญ") {
const msg = await r.fetchMessage();
await bot.send(starboardId, `โญ ${msg.author.displayName}: ${msg.content}`);
}
});A reaction has emoji ("โญ", or "name:id" for a server emoji), userId, user, member, messageId, channelId, guild, roar (a Roar super reaction) and fetchMessage(). The bot's own reactions don't fire these events.
Presence
bot.setActivity("for /help", "watching"); // "Watching for /help"
bot.setPresence({ status: "idle", customStatus: "Taking a nap" });status is "online", "idle", "dnd" or "invisible". Activity types are playing, listening, watching, streaming and competing.
Webhooks
Webhook posts to a channel through a webhook link. It doesn't need a bot or a token. More about webhooks โ
import { Webhook } from "roar.js";
const hook = new Webhook(process.env.ROAR_WEBHOOK_URL);
await hook.send({ content: "Deploy finished โ
", username: "CI", embed: { title: "v1.4.2", color: "#5fd96a" } });Errors
When the node turns a request down, the promise rejects with a RoarError that has status (the HTTP status), code (Roar's error code) and a readable message:
import { RoarError } from "roar.js";
try {
await member.kick();
} catch (e) {
if (e instanceof RoarError && e.code === 50013) await msg.reply("I'm not allowed to kick them.");
else throw e;
}Rate limits (10 messages every 5 seconds per account, plus a channel's slowmode) are handled for you: Roar.js waits and tries again. Common error codes โ
Helpers
| Export | |
|---|---|
mention(user), channelMention(channel), roleMention(role) | <@id>, <#id>, <@&id> from an object or an id. |
timestampOf(id) | When anything with an id was made, as a Date. |
VERSION | The Roar.js version: "1.0.0". |
Everything in the cache also keeps the node's raw object as .raw, for fields Roar.js doesn't name yet.