Combine commands, arguments, embeds, events, guards, cooldowns and shutdown in one file
This bot fits in one file and combines what most bots need: Commands with an argument, a help command, an embed, a welcome message for new members, a permission check, a cooldown, error reporting and a clean stop. It builds on the quick start, so start there if the SDK is not installed yet
The Complete Bot
Save this as bot.js, or bot.ts for TypeScript, in the quick start's folder. Like the JavaScript and TypeScript quick start bots, it exports its options as bot, so the test that fluxerly init writes for those starters still runs it. A project from --template effect needs the Effect version instead, saved as bot.ts:
import { builders, defineBot, describeError, format, guards, runBot } from "@neontechspace/fluxerly"
// Optional channel that receives welcome messages
const welcomeChannelId = process.env.WELCOME_CHANNEL_ID
export const bot = defineBot({
// Handler and command failures arrive here instead of stopping the bot
onError: (report) => {
const source = report.command ? `Command ${report.command}` : `Handler ${report.event ?? report.kind}`
console.error(`${source} failed:\n${describeError(report.error)}`)
},
setup: () => {
if (!welcomeChannelId) console.info("WELCOME_CHANNEL_ID is not set, so the bot sends no welcome messages")
},
events: {
guildMemberAdd: async ({ event: member, client, signal }) => {
if (!welcomeChannelId || member.isBot) return
const channel = await client.channels.fetch(welcomeChannelId, { signal })
// A returned Err is reported to onError like a thrown error
if (channel.isErr()) return channel
if (channel.value.guildId !== member.guildId) return
return client.messages.send(
welcomeChannelId,
{
content: `Welcome, ${format.userMention(member.userId)}!`,
allowedMentions: { users: [member.userId] },
},
{ signal },
)
},
},
commands: {
prefix: "!",
commands: {
help: {
description: "List the commands",
// The first page holds up to 2,000 characters, enough for this bot's commands
execute: ({ help, reply }) => reply(help()[0] ?? "No commands"),
},
ping: {
description: "Check that the bot is online",
execute: ({ reply }) => reply("Pong!"),
},
greet: {
description: "Greet someone by name",
arguments: { name: { type: "text", rest: true } },
cooldown: { durationMs: 5_000, per: "user" },
execute: ({ values, reply }) => reply(`Hello, ${values.name}!`),
},
announce: {
description: "Post an announcement card",
arguments: { text: { type: "text", rest: true } },
guard: guards.requirePermissions(["ManageMessages"]),
execute: ({ message, values, reply }) => {
const card = builders
.embed()
.title("Announcement")
.description(values.text)
.color("#3b82f6")
.footer({ text: `Posted by ${message.author.username}` })
return reply({ embeds: [card] })
},
},
},
},
})
// Running this file starts the bot, while importing it from a test does not.
// A failure that stops the bot, such as a rejected token, is logged and sets a failing exit code
if (import.meta.main) await runBot({ token: process.env.FLUXER_BOT_TOKEN, ...bot })
import { builders, defineBot, describeError, format, guards, runBot } from "@neontechspace/fluxerly"
// Optional channel that receives welcome messages
const welcomeChannelId = process.env.WELCOME_CHANNEL_ID
export const bot = defineBot({
// Handler and command failures arrive here instead of stopping the bot
onError: (report) => {
const source = report.command ? `Command ${report.command}` : `Handler ${report.event ?? report.kind}`
console.error(`${source} failed:\n${describeError(report.error)}`)
},
setup: () => {
if (!welcomeChannelId) console.info("WELCOME_CHANNEL_ID is not set, so the bot sends no welcome messages")
},
events: {
guildMemberAdd: async ({ event: member, client, signal }) => {
if (!welcomeChannelId || member.isBot) return
const channel = await client.channels.fetch(welcomeChannelId, { signal })
// A returned Err is reported to onError like a thrown error
if (channel.isErr()) return channel
if (channel.value.guildId !== member.guildId) return
return client.messages.send(
welcomeChannelId,
{
content: `Welcome, ${format.userMention(member.userId)}!`,
allowedMentions: { users: [member.userId] },
},
{ signal },
)
},
},
commands: {
prefix: "!",
commands: {
help: {
description: "List the commands",
// The first page holds up to 2,000 characters, enough for this bot's commands
execute: ({ help, reply }) => reply(help()[0] ?? "No commands"),
},
ping: {
description: "Check that the bot is online",
execute: ({ reply }) => reply("Pong!"),
},
greet: {
description: "Greet someone by name",
arguments: { name: { type: "text", rest: true } },
cooldown: { durationMs: 5_000, per: "user" },
execute: ({ values, reply }) => reply(`Hello, ${values.name}!`),
},
announce: {
description: "Post an announcement card",
arguments: { text: { type: "text", rest: true } },
guard: guards.requirePermissions(["ManageMessages"]),
execute: ({ message, values, reply }) => {
const card = builders
.embed()
.title("Announcement")
.description(values.text)
.color("#3b82f6")
.footer({ text: `Posted by ${message.author.username}` })
return reply({ embeds: [card] })
},
},
},
},
})
// Running this file starts the bot, while importing it from a test does not.
// A failure that stops the bot, such as a rejected token, is logged and sets a failing exit code
if (import.meta.main) await runBot({ token: process.env.FLUXER_BOT_TOKEN, ...bot })
Run It
Start the bot with the .env file from the quick start, or with the project's start script:
node --env-file=.env bot.jsnode --env-file=.env bot.tsFor welcome messages, add the ID of the channel that should receive them to .env. A channel's ID is the last number in its link, which has the form https://:
FLUXER_BOT_TOKEN=paste-the-token-here
WELCOME_CHANNEL_ID=paste-the-channel-id-here
Without WELCOME_, the bot prints a note at startup and runs everything else. Then try it in a channel the bot can read and reply to:
- Send
!helpto list the commands - Send
!pingto get Pong! - Send
!greet Ada Lovelaceto get Hello, Ada Lovelace! - Send
!greetalone to see what is missing and how to use the command - Send
!announce Movie night on Fridayto post an announcement card. Only members who can manage messages may use it - A new member joining the community gets a welcome message in the configured channel
Press Ctrl+C to stop the bot
How It Works
As in the quick start, define holds the bot's options: The handlers, the commands and the settings. The last line passes them to run together with the token, which connects and runs until the bot stops. That line runs only when Node.js runs the file, so a test can import bot and pass it to create instead, as Testing a bot shows
Commands
The commands option of run turns messages that start with the prefix ! into commands. Each key of the inner commands object is a command name, and description feeds generated help. The reply function answers the message that ran the command, and it accepts a plain string
Each command returns the result of its reply. If the reply fails, the SDK reports that failure to on, the error hook described below, like a thrown error. The commands guide covers aliases, groups, help pages and more argument types
Generated Help
The help command calls help() from its context, which lists every command with its description and usage. It returns pages of at most 2,000 characters, so each page fits in one message, and this bot needs only the first one
A Typed Argument
The greet command declares one argument, name, as text. With rest: true, it takes everything after the command name, so a name can contain spaces. The router converts the argument before execute runs, and in TypeScript values is a string
A command used wrongly gets a short explanation, such as Missing name. Usage: !greet <name...>. The run function answers rejected commands this way by default, and on turns the answers off
Mentions in a reply do not notify anyone unless the message allows it, so !greet @everyone notifies nobody
A Cooldown
The cooldown setting lets each user run greet once every 5 seconds. A second attempt within that time is rejected, and the bot says once how long to wait. Use per: "channel" or per: "guild" to share the limit across a channel or a whole community
The router keeps cooldowns in memory, so they reset when the bot restarts. The commands guide explains how to share cooldowns between processes
A Permission Guard
A guard decides whether a command may run. The announce command uses guards, which allows it only for members who have the Manage permission in that channel. It checks the person who sent the command, not the bot, and it denies the command in direct messages
The guard looks up the community, member, roles and channel, reading them from Fluxer when they are not cached. A denied member gets a reply such as Using this command requires these permissions: Manage messages, at most once every 5 seconds. The communities and permissions guide explains how permissions are calculated
An Embed Reply
An embed is a formatted card with a title, text, color and footer. The builders chain sets its parts, and the finished builder goes into the embeds list of a reply. Building sends nothing, and limits such as the title length are checked when the message is sent. The bot needs the Embed permission in the channel, which new communities grant to everyone. Without it, Fluxer rejects the whole reply, and the failure reaches on. The messages guide shows more message options
A Welcome Message
The events option registers a handler for each event name. The guild event arrives when someone joins a community the bot is in, and its handler receives the member as event, the client and a cancellation signal
The handler skips bots. It then reads the welcome channel with client. Like every request, it returns a Result: Either a success, whose value holds the channel, or a failure, whose error says what went wrong, and is tells them apart. The handler uses the channel to check that it belongs to the community the member joined, so a bot in several communities never announces one community's members in another. Passing signal lets the SDK cancel these requests when the bot stops
The mention in the welcome text notifies the new member because allowed lists them. If the channel read fails, the handler returns the failed Result, which the SDK reports to on
Error Reporting
The on function receives every failure that has no caller to return it to: A failed command, event handler or reply. The report names the command or event, and describe prints the full error with its code, hint and causes, with credentials masked. The bot keeps running after a failure. Without on, the SDK logs each failure itself
The logging guide shows how to change what the SDK prints
Clean Shutdown
By default, Ctrl+C or a stop request from a process manager stops the bot. The SDK gives running work up to 5 seconds to finish, cancels the rest, closes the connection and finishes its cleanup, and then run returns
When the bot could not connect or stopped for another reason, run logs that failure once and sets the process exit code to 1, so a shell or process manager sees a failed run. A missing token is reported the same way before the bot connects, and other invalid settings throw an error at once. The reliability guide covers what happens between start and stop
The Effect Version
The same bot with the Effect API returns an Effect from each handler and command. A failed channel read fails the handler's Effect, which the SDK reports to on. With process, Ctrl+C completes the program successfully. A failure that stops the bot is logged and sets a failing exit code, as with the default API
import { Effect } from "effect"
import { builders, defineBot, describeError, format, guards, runBot } from "@neontechspace/fluxerly/effect"
// Optional channel that receives welcome messages
const welcomeChannelId = process.env.WELCOME_CHANNEL_ID
export const bot = defineBot({
// Handler and command failures arrive here instead of stopping the bot
onError: (report) => {
const source = report.command ? `Command ${report.command}` : `Handler ${report.event ?? report.kind}`
return Effect.logError(`${source} failed:\n${describeError(report.error)}`)
},
setup: () =>
welcomeChannelId
? Effect.void
: Effect.logInfo("WELCOME_CHANNEL_ID is not set, so the bot sends no welcome messages"),
events: {
guildMemberAdd: ({ event: member, client }) =>
Effect.gen(function* () {
if (!welcomeChannelId || member.isBot) return
const channel = yield* client.channels.fetch(welcomeChannelId)
if (channel.guildId !== member.guildId) return
yield* client.messages.send(welcomeChannelId, {
content: `Welcome, ${format.userMention(member.userId)}!`,
allowedMentions: { users: [member.userId] },
})
}),
},
commands: {
prefix: "!",
commands: {
help: {
description: "List the commands",
// The first page holds up to 2,000 characters, enough for this bot's commands
execute: ({ help, reply }) => reply(help()[0] ?? "No commands"),
},
ping: {
description: "Check that the bot is online",
execute: ({ reply }) => reply("Pong!"),
},
greet: {
description: "Greet someone by name",
arguments: { name: { type: "text", rest: true } },
cooldown: { durationMs: 5_000, per: "user" },
execute: ({ values, reply }) => reply(`Hello, ${values.name}!`),
},
announce: {
description: "Post an announcement card",
arguments: { text: { type: "text", rest: true } },
guard: guards.requirePermissions(["ManageMessages"]),
execute: ({ message, values, reply }) => {
const card = builders
.embed()
.title("Announcement")
.description(values.text)
.color("#3b82f6")
.footer({ text: `Posted by ${message.author.username}` })
return reply({ embeds: [card] })
},
},
},
},
})
// Running this file starts the bot, while importing it from a test does not.
// A failure that stops the bot, such as a rejected token, is logged and sets a failing exit code
if (import.meta.main)
await Effect.runPromiseExit(runBot({ token: process.env.FLUXER_BOT_TOKEN, processSignals: true, ...bot }))
All three versions are in the small-bot example folder. The Effect learning path introduces the Effect concepts used here