# Build a small bot

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](https://fluxerly.neontechspace.com/docs/nightly/quick-start.md), 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](https://fluxerly.neontechspace.com/docs/nightly/small-bot.md#the-effect-version) instead, saved as `bot.ts`:

```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 })
```

## Run it

Start the bot with the `.env` file from the [quick start](https://fluxerly.neontechspace.com/docs/nightly/quick-start.md), or with the project's start script:

```sh
node --env-file=.env bot.js
```

For 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.app/channels/<community ID>/<channel ID>`:

```text
FLUXER_BOT_TOKEN=paste-the-token-here
WELCOME_CHANNEL_ID=paste-the-channel-id-here
```

Without `WELCOME_CHANNEL_ID`, 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 `!help` to list the commands
- Send `!ping` to get **Pong!**
- Send `!greet Ada Lovelace` to get **Hello, Ada Lovelace!**
- Send `!greet` alone to see what is missing and how to use the command
- Send `!announce Movie night on Friday` to 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, `defineBot` holds the bot's options: The handlers, the commands and the settings. The last line passes them to `runBot` 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 `createTestBot` instead, as [Testing a bot](https://fluxerly.neontechspace.com/docs/nightly/testing.md) shows

### Commands

The `commands` option of `runBot` 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 `onError`, the [error hook](https://fluxerly.neontechspace.com/docs/nightly/small-bot.md#error-reporting) described below, like a thrown error. The [commands guide](https://fluxerly.neontechspace.com/docs/nightly/commands.md) 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.name` is a string

A command used wrongly gets a short explanation, such as `Missing name. Usage: !greet <name...>`. The `runBot` function answers rejected commands this way by default, and `onReject: "silent"` 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](https://fluxerly.neontechspace.com/docs/nightly/commands.md) explains how to share [cooldowns](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#cooldown) between processes

### A permission guard

A [guard](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#guard) decides whether a command may run. The `announce` command uses `guards.requirePermissions(["ManageMessages"])`, which allows it only for members who have the `ManageMessages` 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](https://fluxerly.neontechspace.com/docs/nightly/guilds-and-permissions.md) explains how permissions are calculated

### An embed reply

An embed is a formatted card with a title, text, color and footer. The `builders.embed()` 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 `EmbedLinks` permission in the channel, which new communities grant to everyone. Without it, Fluxer rejects the whole reply, and the failure reaches `onError`. The [messages guide](https://fluxerly.neontechspace.com/docs/nightly/messages.md) shows more message options

### A welcome message

The `events` option registers a handler for each event name. The `guildMemberAdd` 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.channels.fetch`. Like every request, it returns a [Result](https://fluxerly.neontechspace.com/docs/nightly/core-concepts.md#results): Either a success, whose `value` holds the channel, or a failure, whose `error` says what went wrong, and `isErr()` 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 `allowedMentions` lists them. If the channel read fails, the handler returns the failed Result, which the SDK reports to `onError`

### Error reporting

The `onError` 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 `describeError` prints the full error with its code, hint and causes, with credentials masked. The bot keeps running after a failure. Without `onError`, the SDK logs each failure itself

The [logging guide](https://fluxerly.neontechspace.com/docs/nightly/logging.md) 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 `runBot` returns

When the bot could not connect or stopped for another reason, `runBot` 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](https://fluxerly.neontechspace.com/docs/nightly/reliability.md) covers what happens between start and stop

## The Effect version

The same bot with the [Effect](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#effect) API returns an Effect from each handler and command. A failed channel read fails the handler's Effect, which the SDK reports to `onError`. With `processSignals: true`, 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

```ts
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](https://github.com/NeonTechSpace/Fluxerly.js/tree/main/projects/sdk/examples/small-bot). The [Effect learning path](https://fluxerly.neontechspace.com/docs/nightly/effect-first-bot.md) introduces the Effect concepts used here
