--- url: /concepts/bot.md description: >- The Bot object is the main component of the library. It is used to register event handlers, and provides the entry point of the bot. Learn how to instantiate a Bot object, customize it for your needs, and expose your bot to the fediverse. --- # Bot The `Bot` object is the main component of the library. It is used to register event handlers, and provides the entry point of the bot—the `~Bot.fetch()` method to be connected to the HTTP server. BotKit also exposes the bot actor's standard ActivityPub collections. The JSON-LD responses of the `outbox` and `followers` collections include the [FEP-5711] inverse properties `outboxOf` and `followersOf`. > \[!TIP] > A bot created by `createBot()` occupies its whole server. Since BotKit > 0.5.0, a single server can also host multiple bots; see the > [*Instance* concept document](./instance.md). [FEP-5711]: https://w3id.org/fep/5711 ## Instantiation You can instantiate a `Bot` instance by calling the `createBot()` function: ```typescript twoslash import { createBot } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const bot = createBot({ username: "my_bot", kv: new MemoryKvStore(), }); ``` It takes a type parameter [`TContextData`] which is the type of the [Fedify context data]. Usually, you don't have to mind this type parameter and can just use `void` as shown in the example above, unless you are familiar with Fedify and want to use the context data. It takes a `CreateBotOptions` object with the following required properties: [`TContextData`]: https://fedify.dev/manual/federation#tcontextdata [Fedify context data]: https://fedify.dev/manual/context ### `~CreateBotOptions.username` The username of the bot actor. This is used to be a part of the bot's fediverse handle. For example, if the username is `my_bot`, the bot's handle will be `@my_bot@your-domain`. > \[!TIP] > Although it is not recommended, you can change > the `~CreateBotOptions.username` after you have once deployed your bot. > However, in practice, as people would be confused about the username change, > it is recommended to stick with one good username forever. ### `~CreateBotOptions.kv` The key–value store to be used by the bot. There are several drivers available. See also the [related section][1] of the Fedify docs. During local development, you probably want to use the [`MemoryKvStore`], which stores data in memory. On production, you probably want to use one of the following drivers: * [`DenoKvStore`] (Deno only) * [`RedisKvStore`] * [`PostgresKvStore`] Or if you want to implement your own custom key–value store, [that is also possible!][2] [`MemoryKvStore`]: https://fedify.dev/manual/kv#memorykvstore [`DenoKvStore`]: https://fedify.dev/manual/kv#denokvstore-deno-only [`RedisKvStore`]: https://fedify.dev/manual/kv#rediskvstore [`PostgresKvStore`]: https://fedify.dev/manual/kv#postgreskvstore [1]: https://fedify.dev/manual/kv [2]: https://fedify.dev/manual/kv#implementing-a-custom-kvstore ## Additional options There are other options to customize your `Bot` instance: ### `~CreateBotOptions.repository` The [repository](./repository.md) (data access object) to be used by the bot. It is used to store and retrieve data from the bot's database. By default, it uses the [`KvRepository`](./repository.md#kvrepository) with the key–value store specified in the [`kv`](#createbotoptions-kv) option. For more information, see the [*Repository* section](./repository.md). If you want to use a SQL-backed repository directly instead of [`KvRepository`](./repository.md#kvrepository), BotKit also provides concrete repository packages such as [`SqliteRepository`](./repository.md#sqliterepository) and [`PostgresRepository`](./repository.md#postgresrepository). ### `~CreateBotOptions.identifier` The internal identifier of the bot actor. It is used for the URI of the bot actor, which is the unique identifier of ActivityPub objects. By default, it is just a `"bot"`. > \[!CAUTION] > Unlike [`username`](#createbotoptions-username), > the `~CreateBotOptions.identifier` *must not* be changed after once your > bot is deployed. If you change the identifier of your bot, other fediverse > software will not recognize your bot as the existing one but it will be > practically another distinct actor. ### `~CreateBotOptions.class` The type of the bot actor. It can be either `Application` or `Service`. `Service` by default. > \[!NOTE] > Since BotKit is a framework for creating bots, it does not support > `Person` or `Group` or `Organization`. ### `~CreateBotOptions.name` The display name of the bot. It can be changed after the bot is federated. If you change it after deployment and want followers to notice the change immediately, call [`Session.republishProfile()`](./session.md#republishing-the-bot-profile). ### `~CreateBotOptions.summary` The description of the bot. It will be displayed in the bio field of the profile. It can be changed after the bot is federated. Note that it does not take a string, but a `Text` object. See also the [*Text* chapter](./text.md). If you change it after deployment and want followers to notice the change immediately, call [`Session.republishProfile()`](./session.md#republishing-the-bot-profile). ### `~CreateBotOptions.icon` The avatar URL of the bot. It can be changed after the bot is federated. > \[!NOTE] > BotKit does not perform any modifications (cropping or resampling) on > the avatar image. The image as it is will be used. If you want to adjust > the avatar image so that it looks fine on the most fediverse platforms, > you need to change the image file itself. If you change it after deployment and want followers to notice the change immediately, call [`Session.republishProfile()`](./session.md#republishing-the-bot-profile). ### `~CreateBotOptions.image` The header image URL of the bot. It can be changed after the bot is federated. > \[!NOTE] > BotKit does not perform any modifications (cropping or resampling) on > the header image. The image as it is will be used. If you want to adjust > the header image so that it looks fine on the most fediverse platforms, > you need to change the image file itself. If you change it after deployment and want followers to notice the change immediately, call [`Session.republishProfile()`](./session.md#republishing-the-bot-profile). ### `~CreateBotOptions.properties` The custom properties of the bot. Usually you would like to put some metadata about the bot like the website URL, the source code repository URL, etc. here. Note that the property names should be human-readable and property values are of the `Text` type (see also the [*Text* chapter](./text.md)): ```typescript twoslash // @noErrors: 2345 import { createBot, link, mention } from "@fedify/botkit"; const bot = createBot({ // Omitted other options for brevity properties: { Website: link("https://botkit.fedify.dev/"), Repository: link("https://github.com/fedify-dev/botkit"), Creator: mention("@hongminhee@hollo.social"), }, }); ``` It can be changed after the bot is federated. ### `~CreateBotOptions.followerPolicy` How to handle incoming follow requests. Possible values are: `"accept"` (default) : Automatically accept all incoming follow requests. `"reject"` : Automatically reject all incoming follow requests. `"manual"` : Require manual handling of incoming follow requests. You can manually `~FollowRequest.accept()` or `~FollowRequest.reject()` in the [`Bot.onFollow`](./events.md#follow) event handler. It can be changed after the bot is federated. > \[!TIP] > This behavior can be overridden by manually invoking `FollowRequest.accept()` > or `FollowRequest.reject()` in the [`Bot.onFollow`](./events.md#follow) event > handler. ### `~CreateBotOptions.quotePolicy` Who can quote messages published by the bot by default. BotKit serializes the policy into each outgoing message and uses it as the fallback for older stored messages that do not have a quote policy yet. The shorthand values are: `"public"` (default) : Accept every quote request automatically. `"followers"` : Accept quote requests from followers automatically. `"manual"` : Leave every quote request pending for the [`Bot.onQuoteRequest`](./events.md#quote-request) event handler. `"nobody"` : Reject every quote request except the bot's own requests. You can also use the full two-axis policy object: ```typescript twoslash import type { KvStore } from "@fedify/fedify"; declare const kv: KvStore; // ---cut-before--- import { createBot } from "@fedify/botkit"; const bot = createBot({ username: "mybot", name: "My Bot", kv, quotePolicy: { automatic: "followers", manual: "public", }, }); ``` This accepts followers automatically and sends other quote requests to `Bot.onQuoteRequest` for manual moderation. A publish call can override the default with `Session.publish()`'s `quotePolicy` option. ### `~CreateBotOptions.queue` The message queue for handling incoming and outgoing activities. If omitted, incoming activities are processed immediately, and outgoing activities are sent immediately. There are several drivers available. See also the [related section][3] of the Fedify docs. During local development, you probably want to use the [`InProcessMessageQueue`], which processes messages in the same process. On production, you probably want to use one of the following drivers: * [`DenoKvMessageQueue`] (Deno only) * [`RedisMessageQueue`] * [`PostgresMessageQueue`] Or if you want to implement your own custom message queue, [that is also possible!][4] > \[!NOTE] > Although it can be unset for local development, in production, we highly > recommend to configure the `~CreateBotOptions.queue` to make your bot > performant and scalable. [`InProcessMessageQueue`]: https://fedify.dev/manual/mq#inprocessmessagequeue [`DenoKvMessageQueue`]: https://fedify.dev/manual/mq#denokvmessagequeue-deno-only [`RedisMessageQueue`]: https://fedify.dev/manual/mq#redismessagequeue [`PostgresMessageQueue`]: https://fedify.dev/manual/mq#postgresmessagequeue [3]: https://fedify.dev/manual/mq [4]: https://fedify.dev/manual/mq#implementing-a-custom-messagequeue ### `~CreateBotOptions.software` The metadata about the bot server. It is used for the NodeInfo protocol. It consists of the following properties: `name` (required) : The canonical name of the bot software. This must comply with pattern `/^[a-z0-9-]+$/`, i.e., it must consist of Latin lowercase letters, digits, and hyphens. `version` (required) : The version of the bot software. It should be a string: ``` ~~~~ typescript twoslash // @noErrors: 2345 import { createBot } from "@fedify/botkit"; const bot = createBot({ // Omitted other options for brevity software: { name: "my-bot", version: "1.0.0", // [!code highlight] } }); ~~~~ ``` `repository` (optional) : The [`URL`] of the source code repository of the bot software. `homepage` (optional) : the [`URL`] of the homepage of the bot software. [`URL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL ### `~CreateBotOptions.behindProxy` Whether to trust `X-Forwarded-*` headers. If the bot is behind an L7 reverse proxy (e.g., nginx, Caddy), you should turn it on. Turned off by default. > \[!TIP] > During local development, if you use tunneling services like > [`fedify tunnel`], [ngrok], [Tailscale Funnel], etc., you should turn it on. > These tunneling services practically act as an L7 reverse proxy. [`fedify tunnel`]: https://fedify.dev/cli#fedify-tunnel-exposing-a-local-http-server-to-the-public-internet [ngrok]: https://ngrok.com/ [Tailscale Funnel]: https://tailscale.com/kb/1223/funnel ### `~CreateBotOptions.pages` The options for the web pages of the bot. BotKit serves a bot's profile, posts, and follower list with a quiet, modern design that foregrounds the bot's own identity. The whole look is driven by a single accent color, adapts to light and dark automatically, and needs no build step or external CDN: the stylesheet and fonts are bundled with the package. See the [design language document][DESIGN.md] for the full system. `~PagesOptions.color` : The accent color of the bot's pages. It tints links, the follow button, and small highlights, and is used as the theme color for browser chrome. The default color is `"green"`. ``` Here's the list of available colors: - `"amber"` - `"azure"` - `"blue"` - `"cyan"` - `"fuchsia"` - `"green"` (default) - `"grey"` - `"indigo"` - `"jade"` - `"lime"` - `"orange"` - `"pink"` - `"pumpkin"` - `"purple"` - `"red"` - `"sand"` - `"slate"` - `"violet"` - `"yellow"` - `"zinc"` The accent values are taken from the [*Colors* section] of the Pico CSS docs, so each name looks the same as it does there. ``` `~PagesOptions.theme` : *This option is available since BotKit 0.5.0.* ``` The color scheme of the web pages: `"auto"` (the default) follows the visitor's operating system preference, while `"light"` and `"dark"` force a fixed scheme. ``` `~PagesOptions.css` : Custom CSS injected after BotKit's own stylesheet, so it can override any of the design system's tokens or rules. It should be a string of CSS code. For example, `:root { --bk-radius: 6px; }` gives the pages squarer corners. [DESIGN.md]: https://github.com/fedify-dev/botkit/blob/main/DESIGN.md [*Colors* section]: https://picocss.com/docs/colors ## Running the bot After you have instantiated a `Bot` object, you need to connect it to the HTTP server to run it. The `Bot` object comply with the [`fetch()`] API, so you can serve it with the [`deno serve`] command on Deno or the [srvx] CLI on Node.js. Both run the same `export default bot` entry. [`fetch()`]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API [`deno serve`]: https://docs.deno.com/runtime/reference/cli/serve/ [srvx]: https://srvx.h3.dev/ ### Deno For example, if you have a `Bot` object named `bot`, and `export` it as a default export: ```typescript [bot.ts] twoslash // @noErrors: 2345 import { createBot } from "@fedify/botkit"; const bot = createBot({ // Omitted other options for brevity }); export default bot; // [!code highlight] ``` You can run the following command to expose the bot to the fediverse: ```bash deno serve -A ./bot.ts ``` Then, it will show the following message: ``` deno serve: Listening on http://0.0.0.0:8000/ ``` And your bot will be available at . > \[!TIP] > You can change the listening port by specifying the `--port` option: > > ```bash > deno serve -A --port=3000 ./bot.ts > ``` ### Node.js On Node.js, `export` the `bot` as a default export exactly as on Deno: ```typescript [bot.ts] twoslash // @noErrors: 2345 import { createBot } from "@fedify/botkit"; const bot = createBot({ // Omitted other options for brevity }); export default bot; // [!code highlight] ``` Then serve it with the [srvx] CLI, which runs a fetch handler on Node.js with no glue code. It executes the TypeScript entry directly, so there is no build step: ::: code-group ```bash [npm] npx srvx serve --port 8000 --entry ./bot.ts ``` ```bash [pnpm] pnpx srvx serve --port 8000 --entry ./bot.ts ``` ```bash [Yarn] yarn dlx srvx serve --port 8000 --entry ./bot.ts ``` ::: Your bot will be available at . The CLI listens on port 3000 unless you pass `--port`. When you run the bot in production, add `--prod` to turn off watch mode and debug output. ### Exposing the bot to the public internet During local development, you probably want to use a tunneling service like [`fedify tunnel`], [ngrok], [Tailscale Funnel], etc. to expose your bot to the public internet so that other fediverse servers can interact with your bot. In such a case, you should turn on the [`behindProxy`](#createbotoptions-behindproxy) option to trust `X-Forwarded-*` headers from the tunneling service and recognize the public hostname of the server. It is recommended to have an environment variable to control the [`behindProxy`](#createbotoptions-behindproxy) option so that you can easily switch between local development and production: ::: code-group ```typescript [Deno] {3-4,8} twoslash // @noErrors: 2345 import { createBot } from "@fedify/botkit"; const BEHIND_PROXY = Deno.env.get("BEHIND_PROXY")?.trim()?.toLowerCase() === "true"; const bot = createBot({ // Omitted other options for brevity behindProxy: BEHIND_PROXY, }); ``` ```typescript [Node.js] {3-4,8} twoslash // @noErrors: 2345 import { createBot } from "@fedify/botkit"; const BEHIND_PROXY = process.env.BEHIND_PROXY?.trim()?.toLowerCase() === "true"; const bot = createBot({ // Omitted other options for brevity behindProxy: BEHIND_PROXY, }); ``` ::: Then, you can use the following command to run the bot with the `BEHIND_PROXY` environment variable set to `true`: ::: code-group ```bash [Deno] BEHIND_PROXY=true deno serve -A --port 8000 ./bot.ts ``` ```bash [Node.js] BEHIND_PROXY=true npx srvx serve --port 8000 --entry ./bot.ts ``` ::: Then, you can use the tunneling service to expose your bot to the public internet, for example, with [`fedify tunnel`]:\[^1] ```bash fedify tunnel 8000 ``` The above command will show a temporary public hostname that your bot can be accessed from the fediverse:\[^2] ``` ✔ Your local server at 8000 is now publicly accessible: https://8eba96f5416581.lhr.life/ Press ^C to close the tunnel. ``` \[^1]: You need to install the `fedify` command first. [There are multiple ways to install it.][5] \[^2]: The hostname will be different in your case. [5]: https://fedify.dev/cli#installation --- --- url: /deploy/cfworkers.md description: >- Deploy your BotKit bot to Cloudflare Workers with Workers KV and Cloudflare Queues, using Fedify's Cloudflare Workers adapter. --- # Cloudflare Workers [Cloudflare Workers] can run BotKit bots at the edge. BotKit itself stays the same: the Worker supplies Fedify's Cloudflare-specific key-value store and message queue adapters from [@fedify/cfworkers], then passes each request to `bot.fetch()`. Workers do not give your code long-lived process state or a startup hook with bindings, so create the bot from the `env` object inside each handler. Fedify's dispatchers are registered quickly, while keys and bot data live in Workers KV. [Cloudflare Workers]: https://developers.cloudflare.com/workers/ [@fedify/cfworkers]: https://jsr.io/@fedify/cfworkers ## Installing dependencies Install BotKit, Fedify's Cloudflare Workers adapter, and [Wrangler]: ::: code-group ```bash [npm] npm add @fedify/botkit @fedify/cfworkers npm add -D wrangler @cloudflare/workers-types typescript ``` ```bash [pnpm] pnpm add @fedify/botkit @fedify/cfworkers pnpm add -D wrangler @cloudflare/workers-types typescript ``` ```bash [Yarn] yarn add @fedify/botkit @fedify/cfworkers yarn add -D wrangler @cloudflare/workers-types typescript ``` ::: If your project uses TypeScript, use Workers types without the DOM library to avoid duplicate global declarations: ```json [tsconfig.json] { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "lib": ["ESNext"], "types": ["@cloudflare/workers-types"] } } ``` [Wrangler]: https://developers.cloudflare.com/workers/wrangler/ ## Configuring Wrangler Create a [Workers KV] namespace for Fedify and a [Cloudflare Queues] queue for background activity delivery: ```bash npx wrangler kv namespace create botkit npx wrangler queues create botkit ``` Copy the KV namespace ID printed by Wrangler into `wrangler.jsonc`: ```jsonc [wrangler.jsonc] { "name": "my-bot", "main": "src/worker.ts", "compatibility_date": "2026-07-08", "compatibility_flags": ["nodejs_compat"], "kv_namespaces": [ { "binding": "BOTKIT_KV", "id": "paste-your-kv-namespace-id-here" } ], "queues": { "producers": [ { "binding": "BOTKIT_QUEUE", "queue": "botkit" } ], "consumers": [ { "queue": "botkit", "max_retries": 3 } ] } } ``` The `nodejs_compat` flag is required because BotKit and its dependencies use [Node.js compatibility] when bundled for Workers. [Workers KV]: https://developers.cloudflare.com/kv/ [Cloudflare Queues]: https://developers.cloudflare.com/queues/ [Node.js compatibility]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ ## Writing the Worker Create the bot from the Worker bindings. Pass the `env` object as BotKit's context data so request handlers and queue tasks see the same bindings: ```typescript [src/worker.ts] import { WorkersKvStore, WorkersMessageQueue } from "@fedify/cfworkers"; import { createBot, text } from "@fedify/botkit"; import type { MessageBatch } from "@cloudflare/workers-types"; interface Env { readonly BOTKIT_KV: KVNamespace; readonly BOTKIT_QUEUE: Queue; } function createWorkerBot(env: Env) { const queue = new WorkersMessageQueue(env.BOTKIT_QUEUE, { orderingKv: env.BOTKIT_KV, }); const bot = createBot({ username: "mybot", kv: new WorkersKvStore(env.BOTKIT_KV), queue, }); bot.onMention = async (_session, message) => { await message.reply(text`Hello from Cloudflare Workers!`); }; return { bot, queue }; } export default { fetch(request: Request, env: Env): Promise { const { bot } = createWorkerBot(env); return bot.fetch(request, env); }, async queue(batch: MessageBatch, env: Env): Promise { const { bot, queue } = createWorkerBot(env); for (const message of batch.messages) { const result = await queue.processMessage(message.body); if (!result.shouldProcess) { message.retry(); continue; } try { await bot.federation.processQueuedTask(env, result.message); message.ack(); } catch { message.retry(); } finally { await result.release?.(); } } }, }; ``` Cloudflare Queues deliver messages to the Worker's `queue()` handler, not to Fedify's usual long-running queue listener. `WorkersMessageQueue.processMessage()` unwraps Fedify's message and handles ordering-key locks before `processQueuedTask()` runs the task. ## Deploying Deploy the Worker with Wrangler: ```bash npx wrangler deploy ``` Your bot is reachable at the Worker domain, for example `https://my-bot.your-subdomain.workers.dev/`. The fediverse handle includes that hostname, so choose the final domain before you announce the bot. You can attach a custom domain in the Cloudflare dashboard or with Wrangler. BotKit derives its public origin from incoming requests. For code that publishes without an incoming request, pass the origin explicitly: ```typescript const session = bot.getSession( env.ORIGIN ?? "https://my-bot.example.com", env, ); ``` Add `ORIGIN` to the `Env` interface and configure it as a Worker variable if your bot publishes on a schedule or from another request-less trigger. ## Operational notes Workers KV has eventual consistency and does not support atomic compare-and-set operations. This is fine for small bots, but high-traffic bots may need a repository backed by another service once BotKit has a Workers-compatible repository backend for that store. The web pages and BotKit stylesheet work on Workers because BotKit bundles those assets into the package. Custom emoji images registered from local files do not work on Workers, because there is no normal file system; use URL-backed custom emoji images instead. If you enable Cloudflare security products in front of the Worker, make sure ActivityPub JSON requests are not challenged. Other fediverse servers must be able to fetch actor documents, send inbox deliveries, and resolve WebFinger without a browser. --- --- url: /deploy/deno-deploy.md description: >- Deploy your BotKit bot to Deno Deploy, Deno's serverless platform, from a GitHub-connected app backed by a managed Deno KV database. --- # Deno Deploy [Deno Deploy] is Deno's serverless platform for running JavaScript and TypeScript in the cloud. You connect a GitHub repository, and it builds and runs your bot with no servers to manage. This guide targets the current Deno Deploy, whose dashboard lives at [console.deno.com]. > \[!IMPORTANT] > The original Deno Deploy is now called *Deno Deploy Classic* (at > `dash.deno.com`), and it shuts down on July 20, 2026 along with the > `deployctl` CLI. This guide covers the rebuilt Deno Deploy. If your bot > still runs on Classic, follow the [migration guide] to move it over. [Deno Deploy]: https://deno.com/deploy [console.deno.com]: https://console.deno.com/ [migration guide]: https://docs.deno.com/deploy/migration_guide/ ## Provisioning a Deno KV database BotKit needs a key–value store for Fedify's federation data. On Deno Deploy, [Deno KV] provides it, and once a KV database is assigned to your app, `Deno.openKv()` connects to it automatically with no credentials in your code: 1. In the [console][console.deno.com], open your organization and click *Databases*. 2. Click *Provision Database*, choose *Deno KV*, name it, and save. 3. Click *Assign* next to the database and pick your app. > \[!IMPORTANT] > The rebuilt Deno Deploy does not support Deno KV *queues*, so BotKit cannot > use `DenoKvMessageQueue` here. Without a message queue, BotKit processes > incoming and outgoing activities inline, which is fine for a bot with light to > moderate traffic. For background delivery, provision managed PostgreSQL and > use its queue; see the [*Message queues*](./store-mq.md#message-queues) > section. Deno Deploy also offers managed PostgreSQL for when you outgrow Deno KV's 64 KB per-value limit; see the [*Repositories*](./store-mq.md#repositories) section for moving the bot's own data onto a dedicated repository. [Deno KV]: https://docs.deno.com/deploy/reference/deno_kv/ ## Preparing the entrypoint Deno Deploy runs your entrypoint the way `deno run` does, so the file has to start an HTTP server itself with [`Deno.serve()`]. Pass the bot's `fetch()` method as the handler: ```typescript [bot.ts] twoslash import { createBot } from "@fedify/botkit"; import { DenoKvStore } from "@fedify/denokv"; const kv = await Deno.openKv(); const bot = createBot({ username: "mybot", kv: new DenoKvStore(kv), }); Deno.serve((request) => bot.fetch(request)); // [!code highlight] ``` This differs from the `export default bot` entrypoint that [*Running the bot*](../concepts/bot.md#running-the-bot) shows, because Deno Deploy executes the file directly instead of through the `deno serve` command. The `DenoKvStore` class comes from Fedify's *@fedify/denokv* package: ```sh deno add jsr:@fedify/denokv ``` [`Deno.serve()`]: https://docs.deno.com/api/deno/~/Deno.serve ## Creating the app Sign in to [console.deno.com] and create an organization if you don't have one. Click *+ New App* and choose the GitHub repository that holds your bot. > \[!NOTE] > Deno Deploy does not yet support a bot that lives in a subdirectory of > a monorepo; the repository root has to be the bot itself. Open *Edit build config* and set the fields that matter for a BotKit bot: *Framework preset* : *No Preset*. *Install command* : `deno install`, to cache the bot's dependencies. *Build command* : Leave it empty; a BotKit bot needs no build step. *Runtime configuration* : *Dynamic*, because the bot is a long-running server. *Dynamic entrypoint* : `bot.ts`, the file that calls `Deno.serve()`. Confirm the Deno KV database is assigned to the app, then start the first deployment. Deno Deploy rebuilds and ships a new version on every push to the connected branch. ## Custom domain and origin Deno Deploy serves each app from a default domain of the form `app-name.org-name.deno.net`. A bot's fediverse handle is `@username@domain`, where `domain` is that hostname, so settle on the final domain before you announce the bot: changing it later changes the handle and breaks existing follows. You can attach a custom domain to the app in the console. BotKit reads the domain from each incoming request, so most of the bot needs no configuration. The exception is code that publishes without a request to derive the origin from, such as a scheduled post. There, pass the origin to [`getSession()`](../concepts/session.md) yourself, usually from an environment variable: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- const session = bot.getSession( Deno.env.get("ORIGIN") ?? "https://mybot.myorg.deno.net", ); ``` ## Environment variables Set environment variables under the app's settings in the console. Each variable is either plain text or a secret, and applies to the *Production* context (your production domains), the *Development* context (preview and branch domains), or both. BotKit reads no environment variables of its own; these names are ones your bot code chooses. A common convention is: * `ORIGIN` (or `SERVER_NAME`): the bot's public origin, including the scheme (for example, `https://mybot.myorg.deno.net`), read when building a session for request-less publishing as shown above. * Any credentials your bot needs, such as API tokens. ## Deploying from the command line If you would rather not connect a repository, the `deno deploy` subcommand deploys straight from your machine. It replaces the old `deployctl`, which retires together with Deno Deploy Classic. See the [Deno Deploy documentation][Deno Deploy docs] for its usage. [Deno Deploy docs]: https://docs.deno.com/deploy/ --- --- url: /deploy/docker.md description: >- Learn how to package your BotKit bot as a Docker container and deploy it to platforms like Fly.io and Railway. --- # Docker A Docker image gives your bot a consistent runtime you can run anywhere, from your own host to a container platform like [Fly.io] or [Railway]. BotKit runs on both Deno and Node.js, so this guide shows a *Dockerfile* for each; pick the one that matches how you built your bot. [Fly.io]: https://fly.io/ [Railway]: https://railway.com/ ## Writing a *Dockerfile* Create a *Dockerfile* in your project root. ::: code-group ```dockerfile [Deno] FROM denoland/deno:2.9.1 WORKDIR /app # Cache dependencies first so this layer is reused when only the source changes COPY deno.json deno.lock ./ RUN deno install # Copy the rest of the source COPY . . # The bot listens on port 8000 EXPOSE 8000 # A bot uses `export default bot`, so it must be started with `deno serve` CMD ["deno", "serve", "-A", "--port", "8000", "bot.ts"] ``` ```dockerfile [Node.js] FROM node:24-bookworm-slim WORKDIR /app # Cache dependencies first so this layer is reused when only the source changes COPY package.json package-lock.json ./ RUN npm ci # Copy the rest of the source COPY . . # The bot listens on port 8000 EXPOSE 8000 # Serve the `export default bot` entry through the srvx CLI in production mode CMD ["node_modules/.bin/srvx", "serve", "--prod", "--port", "8000", "--entry", "bot.ts"] ``` ::: > \[!IMPORTANT] > Start a Deno bot with `deno serve`, not `deno run`. A bot is defined with > `export default bot`, and only `deno serve` connects that default export to > the HTTP server. `deno run` would evaluate the module and exit without > serving anything. See [*Running the bot*](../start.md#running-the-bot) for > the entrypoint each runtime expects. The Node.js image runs the bot through the [srvx] CLI, so add *srvx* to your project's dependencies (`npm add srvx`) and end *bot.ts* with `export default bot`, exactly as [*Running the bot*](../start.md#running-the-bot) shows. The CLI executes the TypeScript entry directly; if you would rather compile to JavaScript ahead of time, build in the image and point `--entry` at the compiled file. [srvx]: https://srvx.h3.dev/ ## Configuration and storage Pass your bot's configuration, such as its domain and any API tokens, as environment variables at run time rather than baking them into the image. That keeps secrets out of the image layers and lets one image run in several environments. A container's filesystem is ephemeral: anything written inside it is lost when the container is replaced. Do not rely on [`MemoryKvStore`](../start.md#creating-a-bot) or a SQLite file on the container filesystem for anything you want to keep. Point your bot at a managed key–value store, message queue, and [repository](./store-mq.md) instead, or mount a persistent volume for the SQLite file. ## Deploying to [Fly.io] 1. Install the [Fly.io CLI] 2. Launch the app, which detects the *Dockerfile* and creates a *fly.toml*: ```bash fly launch ``` 3. Set your configuration as secrets: ```bash fly secrets set SERVER_NAME=your-domain.com ``` 4. Deploy: ```bash fly deploy ``` Fly.io terminates TLS at its edge and forwards requests to your container over plain HTTP, so turn on [`behindProxy`](../concepts/bot.md#createbotoptions-behindproxy) in your bot for BotKit to reconstruct the public HTTPS origin from the forwarded headers. Make sure *fly.toml* forwards the public port to the container's port 8000. [Fly.io CLI]: https://fly.io/docs/flyctl/ ## Deploying to [Railway] 1. Create a new project on [Railway] and connect your GitHub repository. 2. Railway builds the *Dockerfile* and deploys the container automatically on each push. 3. Set your configuration under the service's *Variables*, and generate a domain (or attach a custom one) under *Settings → Networking*. Railway also fronts your container with a proxy that terminates TLS, so enable [`behindProxy`](../concepts/bot.md#createbotoptions-behindproxy) here as well. --- --- url: /concepts/events.md description: >- BotKit provides a way to handle events that are emitted from the fediverse. Learn how to handle events and what kinds of events are available. --- # Events BotKit provides a way to handle events that are emitted from the fediverse. You can handle events by setting event handlers on the bot instance. Event handlers are properties of the `Bot` instance with the `on` prefix followed by the event name. For example, to handle a mention event, you can set the `~Bot.onMention` property of the bot instance: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { await message.reply(text`Hi, ${message.actor}!`); }; ``` Every event handler receives a [session](./session.md) object as the first argument, and the event-specific object as the second argument. BotKit invokes these event handlers only for activities whose signatures were verified by Fedify. As of Fedify 2.1.0, BotKit also acknowledges certain unverified remote `Delete` activities from actors whose keys now return `410 Gone`, but those activities are not dispatched to `on*` handlers. The following is a list of events that BotKit supports: ## Follow The `~Bot.onFollow` event handler is called when someone follows your bot. It receives an `FollowRequest` object, which allows you to `~FollowRequest.accept()` or `~FollowRequest.reject()` the follow request, as the second argument. The following is an example of a follow event handler that accepts all follow requests and sends a direct message to new followers: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onFollow = async (session, followRequest) => { await followRequest.accept(); await session.publish( text`Thanks for following me, ${followRequest.follower}!`, { visibility: "direct" }, ); }; ``` > \[!TIP] > The manual invocation of `~FollowRequest.accept()` or > `~FollowRequest.reject()` is preceded by > the [`followerPolicy`](./bot.md#createbotoptions-followerpolicy) option. > Even if your bot has configured the `followerPolicy` option to > `"accept"` or `"reject"`, you can still manually `~FollowRequest.accept()` > or `~FollowRequest.reject()` in the `~Bot.onFollow` event handler, > and the configured policy is ignored for the specific follow request. > \[!TIP] > The passed `FollowRequest` object can outlive the event handler if your bot > is configured the [`followerPolicy`](./bot.md#createbotoptions-followerpolicy) > option to `"manual"`. The following example shows how to accept a follow > request after an hour: > > ```typescript {2-4} twoslash > import { type Bot, text } from "@fedify/botkit"; > const bot = {} as unknown as Bot; > // ---cut-before--- > bot.onFollow = async (session, followRequest) => { > setTimeout(async () => { > await followRequest.accept(); > }, 1000 * 60 * 60); > }; > ``` ## Unfollow The `~Bot.onUnfollow` event handler is called when some follower unfollows your bot. It receives an `Actor` object, who just unfollowed your bot, as the second argument. The following is an example of an unfollow event handler that sends a direct message when someone unfollows your bot: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onUnfollow = async (session, follower) => { await session.publish(text`Goodbye, ${follower}!`, { visibility: "direct", }); }; ``` ## Accept follow The `~Bot.onAcceptFollow` event handler is called when someone accepts your bot's follow request. It receives an `Actor` object, who just accepted your bot's follow request, as the second argument. The following is an example of an accept event handler that sends a direct message when someone accepts your bot's follow request: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onAcceptFollow = async (session, accepter) => { await session.publish( text`Thanks for accepting my follow request, ${accepter}!`, { visibility: "direct" }, ); }; ``` ## Reject follow The `~Bot.onRejectFollow` event handler is called when someone rejects your bot's follow request. It receives an `Actor` object, who just rejected your bot's follow request, as the second argument. The following is an example of a reject event handler that sends a direct message when someone rejects your bot's follow request: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onRejectFollow = async (session, rejecter) => { await session.publish( text`I'm sorry to hear that you rejected my follow request, ${rejecter}.`, { visibility: "direct" }, ); }; ``` ## Mention The `~Bot.onMention` event handler is called when someone mentions your bot. It receives a `Message` object, which represents the message that mentions your bot, as the second argument. The following is an example of a mention event handler that replies to a message that mentions your bot: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { await message.reply(text`You called me, ${message.actor}?`); }; ``` ## Reply The `~Bot.onReply` event handler is called when someone replies to your bot's message. It receives a `Message` object, which represents the reply message, as the second argument. The following is an example of a reply event handler that sends a second reply message when someone replies to your bot's message: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onReply = async (session, reply) => { await reply.reply(text`Thanks for your reply, ${reply.actor}!`); }; ``` If you want to get the parent message of a reply message, you can use the `~Message.replyTarget` property. See also the [*Traversing the conversation* section](./message.md#traversing-the-conversation) in the *Message* concept document. > \[!CAUTION] > In many ActivityPub implementations including Mastodon, reply messages > often contain the mention of the original author. In such cases, both > `~Bot.onMention` and `~Bot.onReply` event handlers are called. You should > be careful not to perform unexpected actions. > > The below example shows how to avoid the `~Bot.onMention` event handler from > being called when a reply message is received: > > ```typescript twoslash > import { type Bot, text } from "@fedify/botkit"; > const bot = {} as unknown as Bot; > // ---cut-before--- > bot.onReply = async (session, reply) => { > await reply.reply(text`Thanks for your reply, ${reply.actor}!`); > }; > > bot.onMention = async (session, message) => { > if (message.replyTarget == null) { // [!code highlight] > await message.reply(text`You called me, ${message.actor}?`); > } > }; > ``` > > Or the other way around, you can avoid the `~Bot.onReply` event handler from > being called when a reply message mentioning the bot is received: > > ```typescript {6} twoslash > import { type Bot, text } from "@fedify/botkit"; > const bot = {} as unknown as Bot; > // ---cut-before--- > bot.onMention = async (session, message) => { > await message.reply(text`You called me, ${message.actor}?`); > }; > > bot.onReply = async (session, reply) => { > if (!reply.mentions.some(m => m.id?.href === session.actorId.href)) { > await reply.reply(text`Thanks for your reply, ${reply.actor}!`); > } > }; > ``` ## Quote *This API is available since BotKit 0.2.0.* The `~Bot.onQuote` event handler is called when someone quotes one of your bot's messages. It receives a `Message` object, which represents the message that quotes your bot's message, as the second argument. The following is an example of a quote event handler that responds to a message that quotes one of your bot's posts: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onQuote = async (session, quote) => { await quote.reply(text`I see you quoted my post, ${quote.actor}!`); }; ``` If you want to get the quoted message, you can use the `~Message.quoteTarget` property. See also the [*Quotes* section](./message.md#quotes) in the *Message* concept document. ## Quote request *This API is available since BotKit 0.5.0.* The `~Bot.onQuoteRequest` event handler is called when a remote actor asks permission to quote one of your bot's messages through [FEP-044f]. It receives a `QuoteRequest` object, whose `quote` property is the remote quote post and whose `target` property is the bot's quoted message. If the message's quote policy automatically accepts or rejects the request, BotKit sends the response before the event handler is called. The handler can inspect `~QuoteRequest.state` to see whether the request is still pending: ```typescript twoslash import { type Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onQuoteRequest = async (session, request) => { if (request.state === "pending") { await request.accept(); } }; ``` If you later need to revoke a quote authorization, call `~AuthorizedMessage.unauthorizeQuote()` on the target message with the quote message: ```typescript twoslash import { type Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onQuoteRequest = async (session, request) => { if (request.state === "accepted") { await request.target.unauthorizeQuote(request.quote); } }; ``` Use the `quotePolicy` option on `createBot()` or `Session.publish()` to choose which quote requests are automatic and which require manual review. For details, see the [*Quote policy* section](./message.md#quote-policy) in the *Message* concept document. [FEP-044f]: https://w3id.org/fep/044f ## Quote accepted, rejected, and revoked *This API is available since BotKit 0.5.0.* The `~Bot.onQuoteAccepted` and `~Bot.onQuoteRejected` event handlers are called when a remote author responds to a quote request that your bot sent for one of its own quote posts. When the request is accepted, BotKit verifies the returned `QuoteAuthorization` stamp, applies it to the stored message, sends an `Update` activity, and then calls `~Bot.onQuoteAccepted`: ```typescript twoslash import { type Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onQuoteAccepted = (_session, message, approver) => { console.log(`${approver.id} approved ${message.id}`); console.log(message.quoteApprovalState); // "accepted" }; ``` When the request is rejected, BotKit removes the quote target and fallback quote link from the stored message, sends an `Update` activity, and then calls `~Bot.onQuoteRejected`: ```typescript twoslash import { type Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onQuoteRejected = (_session, message, rejecter) => { console.log(`${rejecter.id} rejected ${message.id}`); console.log(message.quoteTarget); // undefined }; ``` If a previously accepted `QuoteAuthorization` stamp is later deleted by the quoted author's server, BotKit forwards the `Delete` activity to the quote post's audience, removes the quote target and fallback quote link from the stored message, sends an `Update` activity, and then calls `~Bot.onQuoteRevoked`: ```typescript twoslash import { type Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onQuoteRevoked = (_session, message, revoker) => { console.log(`${revoker.id} revoked approval for ${message.id}`); console.log(message.quoteTarget); // undefined }; ``` ## Message The `~Bot.onMessage` event handler is called when any message is received by your bot, which includes normal messages on the bot's timeline, mentions, replies, direct messages, and so on. It receives a `Message` object, which represents the received message, as the second argument. The following is an example of a message event handler that replies to a message that contains the word *BotKit*: ```typescript twoslash import { type Bot, em, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMessage = async (session, message) => { if (message.text.match(/\bbotkit\b/i)) { await message.reply(text`You mentioned ${em("BotKit")}!`); } }; ``` > \[!NOTE] > If your bot does not follow anyone, the `~Bot.onMessage` event handler is > called only when your bot receives mentions, replies, and direct messages. > > To learn more about following others, see the [*Following an actor* > section](./session.md#following-an-actor) in the *Session* concept document. > \[!CAUTION] > The `~Bot.onMessage` event handler is called for every message that your > bot receives, which includes mentions, replies, and quotes. If your bot > listens to the `~Bot.onMention` or `~Bot.onReply` or `~Bot.onQuote` events > with the `~Bot.onMessage` event handler, the `~Bot.onMention` or > `~Bot.onReply` or `~Bot.onQuote` event handlers are called first. > You should be careful not to perform unexpected actions. > > The below example shows how to avoid the `~Bot.onMessage` event handler from > being called when a mention message is received: > > ```typescript {6-8} twoslash > import { type Bot, em, text } from "@fedify/botkit"; > const bot = {} as unknown as Bot; > // ---cut-before--- > bot.onMention = async (session, message) => { > await message.reply(text`You called me, ${message.actor}?`); > }; > > bot.onMessage = async (session, message) => { > if (message.mentions.some(m => m.id?.href === session.actorId.href)) { > return; > } > if (message.text.match(/\bbotkit\b/i)) { > await message.reply(text`You mentioned ${em("BotKit")}!`); > } > }; > ``` ## Shared message The `~Bot.onSharedMessage` event handler is called when someone shares (i.e., boosts) a message on your bot. It receives a `SharedMessage` object, which represents the shared message, as the second argument. The following is an example of a shared message event handler that re-shares the shared message: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onSharedMessage = async (session, sharedMessage) => { await sharedMessage.original.share(); }; ``` > \[!TIP] > The `~Bot.onSharedMessage` event handler can be called when someone your > bot does not follow shares a message on your bot. ## Like The `~Bot.onLike` event handler is called when someone likes messages on your bot or actors your bot follows. It receives a `Like` object, which represents the like activity, as the second argument. The following is an example of a like event handler that sends a direct message when someone likes a message on your bot: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onLike = async (session, like) => { if (like.message.actor.id?.href !== session.actorId.href) return; await session.publish( text`Thanks for liking my message, ${like.actor}!`, { visibility: "direct" }, ); }; ``` ## Unlike The `~Bot.onUnlike` event handler is called when someone undoes a `Like` activity on messages on your bot or actors your bot follows. It receives a `Like` object, which represents the `Like` activity which was undone, as the second argument. The following is an example of an unlike event handler that sends a direct message when someone undoes a like activity on a message on your bot: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onUnlike = async (session, like) => { if (like.message.actor.id?.href !== session.actorId.href) return; await session.publish( text`I'm sorry to hear that you unliked my message, ${like.actor}.`, { visibility: "direct" }, ); }; ``` ## Emoji reaction *This API is available since BotKit 0.2.0.* The `~Bot.onReact` event handler is called when someone reacts with an emoji to messages on your bot or actors your bot follows. It receives a `Reaction` object, which represents the reaction activity, as the second argument. The following is an example of a reaction event handler that sends a direct message when someone reacts with an emoji to a message on your bot: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onReact = async (session, reaction) => { if (reaction.message.actor.id?.href !== session.actorId.href) return; await session.publish( text`Thanks for reacting with ${reaction.emoji} to my message, ${reaction.actor}!`, { visibility: "direct" }, ); }; ``` ## Undoing emoji reaction *This API is available since BotKit 0.2.0.* The `~Bot.onUnreact` event handler is called when someone removes an emoji reaction from messages on your bot or actors your bot follows. It receives a `Reaction` object, which represents the emoji reaction activity which was undone, as the second argument. The following is an example of an unreact event handler that sends a direct message when someone removes a heart reaction from a message on your bot: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onUnreact = async (session, reaction) => { if (reaction.message.actor.id?.href !== session.actorId.href) return; if (reaction.emoji === "❤️") { await session.publish( text`I see you took back your heart reaction, ${reaction.actor}.`, { visibility: "direct" }, ); } }; ``` ## Vote *This API is available since BotKit 0.3.0.* The `~Bot.onVote` event handler is called when someone votes on a poll created by your bot. It receives a `Vote` object, which represents the vote activity, as the second argument. The following is an example of a vote event handler that sends a direct message when someone votes on your bot's poll: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onVote = async (session, vote) => { await session.publish( text`Thanks for voting "${vote.option}" on my poll, ${vote.actor}!`, { visibility: "direct" }, ); }; ``` The `Vote` object contains the following properties: `~Vote.actor` : The actor who voted. `~Vote.option` : The option that was voted for (as a string). `~Vote.poll` : Information about the poll including whether it allows `~Poll.multiple` choices, all available `~Poll.options`, and the `~Poll.endTime`. `~Vote.message` : The poll message that was voted on. > \[!TIP] > You can check if a poll allows multiple selections by accessing the > `vote.poll.multiple` property: > > ```typescript twoslash > import { type Bot, text } from "@fedify/botkit"; > const bot = {} as unknown as Bot; > // ---cut-before--- > bot.onVote = async (session, vote) => { > if (vote.poll.multiple) { > await vote.message.reply( > text`${vote.actor} selected "${vote.option}" in the multiple choice poll!` > ); > } else { > await vote.message.reply( > text`${vote.actor} voted for "${vote.option}"!` > ); > } > }; > ``` > \[!NOTE] > The `~Bot.onVote` event handler is only called for votes on polls created by > your bot. Votes on polls created by others will not trigger this event. > \[!NOTE] > The bot author cannot vote on their own polls—such votes are automatically > ignored and will not trigger the `~Bot.onVote` event handler. > \[!NOTE] > On a poll with multiple options, each selection creates a separate `Vote` > object, even if the same user selects multiple options. This means that if > a user selects multiple options in a multiple-choice poll, the `~Bot.onVote` > event handler will be called multiple times, once for each selected option. --- --- url: /examples.md --- # Examples Here are some examples of how to use BotKit. ## Greeting bot The following example shows how to publish messages in various ways using BotKit. The bot performs the following actions: * Sends a direct message with an image attachment when someone follows the bot. * Sends a direct message when someone unfollows the bot. * Replies to it when someone replies to a message from the bot. * Replies to it when someone mentions the bot. * Publishes a greeting message every minute. * Deletes the greeting message after 30 seconds. ::: code-group <<< @/../examples/greet/greet.ts \[greet.ts] ::: ## One-time passcode authentication bot This example demonstrates how to implement an emoji-based one-time passcode authentication system using BotKit's poll functionality. The bot provides a simple two-factor authentication mechanism through the fediverse. The authentication flow works as follows: 1. *Initial setup*: The user visits the web interface and enters their fediverse handle (e.g., `@username@server.com`). 2. *Challenge generation*: The system generates a random set of emojis and sends a direct message containing a poll with all available emoji options to the user's fediverse account. 3. *Web interface display*: The correct emoji sequence is displayed on the web page. 4. *User response*: The user votes for the matching emojis in the poll they received via direct message. 5. *Verification*: The system verifies that the user selected exactly the same emojis shown on the web page. 6. *Authentication result*: If the emoji selection matches, authentication is successful. Key features: * Uses BotKit's [poll functionality](./concepts/message.md#polls) for secure voting * Implements a 15-minute expiration for both the challenge and authentication attempts * Provides a clean web interface using [Hono] framework and [Pico CSS] * Stores temporary data using [Deno KV] for session management * Supports both direct message delivery and real-time vote tracking This example showcases how to combine ActivityPub's social features with web authentication, demonstrating BotKit's capability to bridge fediverse interactions with traditional web applications. [Hono]: https://hono.dev/ [Pico CSS]: https://picocss.com/ [Deno KV]: https://deno.com/kv ::: code-group <<< @/../examples/otp/otp.tsx \[otp.tsx] ::: ## Multiple bots on one server The following example shows how to host two independent bots on a single server using `createInstance()`. The two bots have distinct handles and event handlers, but share the same infrastructure (key–value store and message queue): * *@greetbot* sends a welcome direct message to every new follower and replies with a greeting when mentioned. * *@echobot* echoes back the plain text of every mention. ::: code-group <<< @/../examples/multiple-bots/multiple-bots.ts \[multiple-bots.ts] ::: ## On-demand bots The following example shows how to create a group of bots that are resolved on demand from a dispatcher function. Each bot has the handle `@lang_@your-domain`, where `` is one of the supported BCP 47 language codes (`en`, `ko`, `ja`, `es`, `fr`). The dispatcher returns the bot profile when the code is recognized and `null` otherwise, so only a handful of handles resolve while the rest return 404. ::: code-group <<< @/../examples/dynamic-bots/on-demand-bots.ts \[on-demand-bots.ts] ::: ## Static and dynamic bots The following example combines a static bot and a group of dynamic bots on the same instance. Static bots take precedence over dynamic ones, and multiple bot groups are probed in the order they were created. * *@announce* is a static bot that rebroadcasts every mention as a public post, effectively acting as an announcement channel. * *@lang\_en*, *@lang\_ko*, and *@lang\_ja* are dynamic bots that greet followers and mentions in the respective language. ::: code-group <<< @/../examples/static-and-dynamic-bots/static-and-dynamic-bots.ts \[static-and-dynamic-bots.ts] ::: ## FediChatBot [FediChatBot] is an LLM-powered chatbot for fediverse, of course, built on top of BotKit. It consists of about 350 lines of code, and it's a good example of how to build a chatbot with BotKit. You can find the source code at: . If you want to try FediChatBot, follow [@FediChatBot@fedichatbot.deno.dev] on your fediverse instance. You can mention it or send a direct message to it. [FediChatBot]: https://github.com/fedify-dev/fedichatbot [@FediChatBot@fedichatbot.deno.dev]: https://fedichatbot.deno.dev/ --- --- url: /start.md description: Learn how to install BotKit and create an ActivityPub bot with it. --- # Getting started ## Installing BotKit BotKit is available for both Node.js and Deno. You can install BotKit depending on your environment. ### Deno You need to create a new project for your bot and install BotKit as a dependency: ```bash deno add jsr:@fedify/botkit ``` Since BotKit uses the [Temporal] API, it is recommended to use Deno 2.7 or later, where the Temporal API is stabilized and available by default. > \[!NOTE] > If you are using Deno version 2.6 or older, you must enable the unstable > Temporal API by adding the `"unstable": ["temporal"]` option to your > *deno.json* file: > > ```json [deno.json] > { > "imports": { > "@fedify/botkit": "jsr:@fedify/botkit@0.3.0" > }, > "unstable": ["temporal"] > } > ``` [Temporal]: https://tc39.es/proposal-temporal/docs/ ### Node.js You can install BotKit from npm: ::: code-group ```bash [npm] npm add @fedify/botkit ``` ```bash [pnpm] pnpm add @fedify/botkit ``` ```bash [Yarn] yarn add @fedify/botkit ``` ::: ## Creating a bot To create a bot, you need to create a new TypeScript file and define your [`Bot`](./concepts/bot.md) instance using the [`createBot()`](./concepts/bot.md#instantiation) function: ```typescript [bot.ts] {8-14} twoslash import { createBot, InProcessMessageQueue, MemoryKvStore, text, } from "@fedify/botkit"; const bot = createBot({ username: "mybot", name: "My Bot", summary: text`A bot powered by BotKit.`, kv: new MemoryKvStore(), queue: new InProcessMessageQueue(), }); ``` In the above code snippet, we created a new bot instance named `bot` with the [`username`](./concepts/bot.md#createbotoptions-username) `mybot`—the first part of the fediverse handle. The bot will be addressed as `@mybot@your-domain` in the fediverse. The [`name`](./concepts/bot.md#createbotoptions-name) and [`summary`](./concepts/bot.md#createbotoptions-summary) are the display name and the bio of the bot, respectively. Note that the `summary` is not a string, but a `Text` object that can be used to format rich text. For more information on `Text`, see the [*Text* chapter](./concepts/text.md). The [`kv`](./concepts/bot.md#createbotoptions-kv) is the underlying key–value store that BotKit uses to store data. In the above code snippet, we used the [`MemoryKvStore`] class, which stores data in memory for development purposes. The [`queue`](./concepts/bot.md#createbotoptions-queue) is the message queue that BotKit uses to deal with background tasks. In the above code snippet, we used the [`InProcessMessageQueue`] class, which processes messages in the same process for development purposes. > \[!CAUTION] > Although [`MemoryKvStore`] and [`InProcessMessageQueue`] are useful for > development, they must not be used in production. You should use a persistent > key–value store and a message queue service in production. For more drivers > available, see the Fedify's related docs: > > * [*Key–value store*] > * [*Message queue*] > \[!TIP] > A bot created by `createBot()` occupies its whole server. If you want to > host multiple bots on a single server, create them on an *instance* > instead; see the [*Instance* chapter](./concepts/instance.md). [`MemoryKvStore`]: https://fedify.dev/manual/kv#memorykvstore [`InProcessMessageQueue`]: https://fedify.dev/manual/mq#inprocessmessagequeue [*Key–value store*]: https://fedify.dev/manual/kv [*Message queue*]: https://fedify.dev/manual/mq ## Handling events BotKit supports various [events](./concepts/events.md) such as [`onFollow`](./concepts/events.md#follow) and [`onMention`](./concepts/events.md#mention). You can handle these events by setting the corresponding event handlers on the `Bot` instance. Here, we will let the bot [publish](./concepts/message.md#publishing-a-message) a direct message when someone [follows](./concepts/events.md#follow) the bot: ```typescript [bot.ts] twoslash import type { Bot, Session } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const bot = {} as unknown as Bot; const session = {} as unknown as Session; // ---cut-before--- bot.onFollow = async (session, follower) => { await session.publish(text`Thanks for following me, ${follower}!`, { visibility: "direct", }); }; ``` ## Running the bot To run the bot, you need to first connect the bot to the HTTP server. There are different ways to run the bot depending on the environment you are using. Here, we will show you how to run the bot in Deno and Node.js. ### Deno We will utilize [`deno serve`] command. In order to connect the bot to Deno's HTTP server, you need to `export` the `bot` instance as a default export in the *bot.ts* file: ```typescript [bot.ts] twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- export default bot; ``` Then, you can run the bot using the following command: ```bash deno serve -A ./bot.ts ``` Then, it will show the following message: ``` deno serve: Listening on http://0.0.0.0:8000/ ``` And your bot will be available at . [`deno serve`]: https://docs.deno.com/runtime/reference/cli/serve/ ### Node.js On Node.js, export the `bot` as a default export the same way as on Deno: ```typescript [bot.ts] twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- export default bot; ``` Then run it with the [srvx] CLI, which serves the fetch handler and executes the TypeScript entry directly, so you don't need a separate build step: ::: code-group ```bash [npm] npx srvx serve --port 8000 --entry ./bot.ts ``` ```bash [pnpm] pnpx srvx serve --port 8000 --entry ./bot.ts ``` ```bash [Yarn] yarn dlx srvx serve --port 8000 --entry ./bot.ts ``` ::: Your bot will be available at . The CLI defaults to port 3000 when you omit `--port`. [srvx]: https://srvx.h3.dev/ ## Exposing the bot to the public internet However, other fediverse servers cannot interact with your bot if it is only available on your local machine. To expose your bot to the public internet, you can use a tunneling service like [`fedify tunnel`], [ngrok], [Tailscale Funnel], etc. Since those tunneling services practically act as an L7 reverse proxy, you need to turn on the [`behindProxy`](./concepts/bot.md#createbotoptions-behindproxy) option: ```typescript [bot.ts] twoslash import { createBot, InProcessMessageQueue, MemoryKvStore, text, } from "@fedify/botkit"; // ---cut-before--- const bot = createBot({ username: "mybot", name: "My Bot", summary: text`A bot powered by BotKit.`, kv: new MemoryKvStore(), queue: new InProcessMessageQueue(), behindProxy: true, // [!code highlight] }); ``` Here, we will use [`fedify tunnel`] to expose the bot to the public internet. [Install the `fedify` command first][2], and then run the following command: ```bash fedify tunnel 8000 ``` The above command will expose your bot to the public internet, and you will get a temporary public hostname that your bot can be accessed from the fediverse:\[^1] ``` ✔ Your local server at 8000 is now publicly accessible: https://c4d3933be87bc2.lhr.life/ Press ^C to close the tunnel. ``` \[^1]: The hostname will be different in your case. [`fedify tunnel`]: https://fedify.dev/cli#fedify-tunnel-exposing-a-local-http-server-to-the-public-internet [ngrok]: https://ngrok.com/ [Tailscale Funnel]: https://tailscale.com/kb/1223/funnel [2]: https://fedify.dev/cli#installation ## Testing the bot To test the bot, you can use [ActivityPub.Academy], a development-purpose Mastodon instance. ActivityPub.Academy allows you to immediately create an ephemeral fediverse account and provides several tools to debug your ActivityPub application such as *Activity Log*. Okay, let's create a new account on ActivityPub.Academy and follow your bot account: `@mybot@c4d3933be87bc2.lhr.life`—replace `c4d3933be87bc2.lhr.life` with the domain name assigned by the [`fedify tunnel`] command. Here's how you can follow your bot account—in ActivityPub.Academy's search bar, type `@mybot@c4d3933be87bc2.lhr.life` and click the account: ![The search result of the bot account in ActivityPub.Academy](./start/academy-search.png) Then, click the *Follow* button: ![The Follow button shows up in the bot account's profile in ActivityPub.Academy](./start/academy-profile.png) Few seconds later, you will receive a direct message from your bot: ![The direct message from the bot in ActivityPub.Academy](./start/academy-message.png) [ActivityPub.Academy]: https://activitypub.academy/ --- --- url: /concepts/instance.md description: >- An Instance owns the shared infrastructure and can host multiple bots, each with its own actor identity and event handlers. Learn how to create an instance, host static and dynamic bots on it, and migrate an existing single-bot deployment. --- # Instance *This API is available since BotKit 0.5.0.* An `Instance` is a server that can host multiple bots. It owns the shared infrastructure: the key–value store, the message queue, the repository, and HTTP handling. Each bot hosted on it has its own actor identity, fediverse handle, collections, and event handlers, while all of them share one [Fedify federation] under the hood. If your server hosts a single bot, you don't need this API: `createBot()` creates a dedicated instance for the bot internally, and everything described in the [*Bot* concept document](./bot.md) keeps working as before. Reach for `createInstance()` when you want several bots, or a whole family of bots resolved from a database, to share one process and one domain. [Fedify federation]: https://fedify.dev/manual/federation ## Creating an instance You can create an `Instance` by calling the `createInstance()` function: ```typescript twoslash import { createInstance } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); ``` The `CreateInstanceOptions` take the infrastructure-related options that `createBot()` used to take: `~CreateInstanceOptions.kv`, `~CreateInstanceOptions.repository`, `~CreateInstanceOptions.queue`, `~CreateInstanceOptions.software`, `~CreateInstanceOptions.behindProxy`, and `~CreateInstanceOptions.pages`. A single repository stores the data of every bot hosted on the instance, scoped by their identifiers; see the [*Repository* concept document](./repository.md) for details. Two options are specific to multi-bot instances: `~CreateInstanceOptions.instanceActorIdentifier` overrides the reserved identifier of [the instance actor](#the-instance-actor), and `~CreateInstanceOptions.legacyObjectUris` keeps object URIs from an older single-bot deployment working (see [*Migrating a single-bot deployment*](#migrating-a-single-bot-deployment)). Like a `Bot`, an `Instance` has a `~Instance.fetch()` method to be connected to the HTTP server: ```typescript twoslash import { createInstance } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); // ---cut-before--- export default instance; // Deno ``` ## Static bots The `Instance.createBot()` method creates a bot with a fixed identifier and profile, hosted on the instance: ```typescript twoslash import { createInstance, text } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); // ---cut-before--- const greetBot = instance.createBot("greet", { username: "greetbot", name: "Greeting Bot", }); greetBot.onFollow = async (session, followRequest) => { await followRequest.accept(); await session.publish(text`Welcome, ${followRequest.follower}!`); }; ``` The first argument is the bot's internal identifier, which is used for the actor URI and *should not* be changed after the bot is federated. The second argument is a `BotProfile`, which takes the profile-related options that `createBot()` used to take: `~BotProfile.username`, `~BotProfile.name`, `~BotProfile.summary`, `~BotProfile.icon`, `~BotProfile.image`, `~BotProfile.properties`, `~BotProfile.class`, and `~BotProfile.followerPolicy`. Identifiers and usernames must be unique across the instance; `~Instance.createBot()` throws a `TypeError` on duplicates. Handler registration works exactly like on a `Bot` created by `createBot()`; see the [*Events* concept document](./events.md). Incoming activities are routed to the bots they are relevant to: a `Follow` reaches the followed bot, a `Like` reaches the owner of the liked message, a mention reaches the mentioned bot, and a message from a followed account reaches the bots that follow its author. ## Dynamic bots Passing a function instead of an identifier to `~Instance.createBot()` creates a `BotGroup`: a family of bots resolved on demand by a `BotDispatcher`. This suits scenarios like “one bot per region,” where thousands of potential bots are backed by a database rather than declared up front: ```typescript twoslash declare const db: { getRegion(code: string): Promise<{ name: string } | null>; getWeather(code: string): Promise; }; import { createInstance, text } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); // ---cut-before--- const weatherBots = instance.createBot(async (ctx, identifier) => { // Return null for identifiers this dispatcher doesn't handle: if (!identifier.startsWith("weather_")) return null; const region = await db.getRegion(identifier.slice("weather_".length)); if (region == null) return null; return { username: identifier, name: `${region.name} Weather Bot`, }; }); weatherBots.onMention = async (session, message) => { // session.bot tells which bot is being mentioned: const code = session.bot.identifier.slice("weather_".length); const weather = await db.getWeather(code); await message.reply(text`Current weather: ${weather}`); }; ``` The dispatcher is invoked whenever an identifier needs to be resolved, e.g. for serving an actor or routing an incoming activity, so it should be fast. Resolutions are memoized for the duration of a request, but not across requests; look profiles up from a database rather than computing them expensively. Event handlers registered on the group are shared by every bot it resolves, and they are read at dispatch time, so the registration order of handlers and the first resolution of a bot don't matter. Static bots take precedence over dynamic ones, and when multiple groups are registered, their dispatchers are probed in the order the groups were created: a dispatcher that returns `null` passes the identifier on to the next group, and an identifier no dispatcher recognizes does not resolve at all. ### Usernames of dynamic bots By default, BotKit assumes that a dynamic bot's username equals its identifier, which makes WebFinger lookups (`@weather_kr@your-domain`) work without extra configuration. If your usernames differ from your identifiers, provide a `~CreateBotGroupOptions.mapUsername` callback: ```typescript twoslash declare const db: { getRegionByName(name: string): Promise<{ code: string } | null>; }; import { createInstance } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); declare function dispatcher(ctx: unknown, identifier: string): null; // ---cut-before--- const weatherBots = instance.createBot( async (ctx, identifier) => { // …resolve the profile from the identifier… return dispatcher(ctx, identifier); }, { async mapUsername(ctx, username) { const region = await db.getRegionByName(username); return region == null ? null : `weather_${region.code}`; }, }, ); ``` When both are provided, it is your responsibility to keep the identifier-to-profile and username-to-identifier mappings consistent. ### Sessions for dynamic bots Since a group has no single identifier, `BotGroup.getSession()` takes the identifier of the bot to control: ```typescript twoslash declare const dispatcher: import("@fedify/botkit").BotDispatcher; import { createInstance, text } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); const weatherBots = instance.createBot(dispatcher); // ---cut-before--- const session = await weatherBots.getSession( "https://mydomain", "weather_kr", ); await session.publish(text`It's sunny in Seoul today!`); ``` It returns a `Promise` since the dispatcher has to resolve the identifier first, and it rejects with a `TypeError` when the dispatcher doesn't recognize the identifier. ## Multiple bot groups An instance can host several bot groups side by side, each serving a different kind of bot. Since a dispatcher that returns `null` passes the identifier on to the next group, giving each group its own identifier namespace (e.g. a prefix) is all it takes to keep them apart: ```typescript twoslash declare const db: { getRegion(code: string): Promise<{ name: string } | null>; getWeather(code: string): Promise; getTopic(slug: string): Promise<{ name: string } | null>; getHeadlines(slug: string): Promise; }; import { createInstance, text } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; const instance = createInstance({ kv: new MemoryKvStore(), }); // ---cut-before--- // A static bot for instance-wide announcements: const announcements = instance.createBot("announcements", { username: "announcements", name: "Announcements", }); // One bot per region, under the weather_ prefix: const weatherBots = instance.createBot(async (ctx, identifier) => { if (!identifier.startsWith("weather_")) return null; const region = await db.getRegion(identifier.slice("weather_".length)); if (region == null) return null; return { username: identifier, name: `${region.name} Weather Bot` }; }); // One bot per topic, under the news_ prefix: const newsBots = instance.createBot(async (ctx, identifier) => { if (!identifier.startsWith("news_")) return null; const topic = await db.getTopic(identifier.slice("news_".length)); if (topic == null) return null; return { username: identifier, name: `${topic.name} News Bot` }; }); weatherBots.onMention = async (session, message) => { const code = session.bot.identifier.slice("weather_".length); await message.reply(text`Current weather: ${await db.getWeather(code)}`); }; newsBots.onMention = async (session, message) => { const slug = session.bot.identifier.slice("news_".length); await message.reply(text`Today's headlines: ${await db.getHeadlines(slug)}`); }; ``` Resolving the identifier `news_tech` on this instance walks through the registrations in order: it is not a static bot, `weatherBots` returns `null` because the prefix doesn't match, and `newsBots` resolves it. Each group keeps its own event handlers, so a mention of `@news_tech@your-domain` reaches `newsBots.onMention` only. Registering any number of groups is allowed and never an error. BotKit cannot tell at registration time whether two arbitrary dispatchers overlap, so overlaps are resolved by order instead: if two groups would resolve the same identifier, the group created first wins and the later group's dispatcher is not even invoked for it. WebFinger username resolution has its own order: static bots' usernames win first, then the groups' `~CreateBotGroupOptions.mapUsername` callbacks are tried in creation order, and the username-as-identifier fallback for groups without the callback comes last. In practice, disjoint namespaces like the prefixes above are the way to keep group boundaries obvious. ## The instance actor A multi-bot instance has no single obvious actor whose key should sign shared-inbox related requests, so it exposes an *instance actor*: an internal, non-discoverable `Application` actor, similar to Mastodon's instance actor. It lives under a reserved identifier, which defaults to `DEFAULT_INSTANCE_ACTOR_IDENTIFIER` (`__botkit_instance__`). Bots cannot take the reserved identifier, whether they are static or resolved by a dispatcher. In the unlikely case that the default collides with an identifier you need, override it through the `~CreateInstanceOptions.instanceActorIdentifier` option: ```typescript twoslash import { createInstance } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; // ---cut-before--- const instance = createInstance({ kv: new MemoryKvStore(), instanceActorIdentifier: "fetcher", }); ``` Like bot identifiers, the instance actor identifier is used for the actor URI, so it *should not* be changed after the instance is federated: remote servers that have seen the instance actor would no longer be able to fetch its key. Instances created through the single-bot `createBot()` function keep the pre-0.5 behavior: the sole bot's key signs shared-inbox requests and no instance actor is exposed. ## Web pages A multi-bot instance serves a list of its static bots at the web root, and each bot's pages under a path derived from its username: * `/@{username}`: the bot's profile * `/@{username}/{messageId}`: a message permalink * `/@{username}/followers`: the bot's followers * `/@{username}/tags/{hashtag}`: the bot's messages with a hashtag * `/@{username}/feed.xml`: the bot's Atom feed Dynamic bots get the same pages once their usernames resolve. Single-bot deployments created through `createBot()` keep serving their pages at the web root as before. ## Custom emojis Custom emojis belong to the instance and are shared by every bot hosted on it. The `Instance.addCustomEmojis()` method works like [`Bot.addCustomEmojis()`](./text.md#custom-emojis), and emoji names must be unique across the instance. ## Migrating a single-bot deployment An existing deployment created with `createBot()` keeps working without any changes: its data is migrated to the bot-scoped storage layout on startup, object URIs in the old format are recognized and redirected, and its web pages stay at the root. If you later want to move a single-bot deployment onto a multi-bot instance, two things need to carry over. First, the repository data: the automatic migration runs only in the `createBot()` compatibility path, so either run the deployment once with `createBot()` on BotKit 0.5.0 before switching to `createInstance()`, or call the repository's `~Repository.migrate()` method with the bot's identifier yourself. Second, the object URIs: create the instance with the `~CreateInstanceOptions.legacyObjectUris` option, so that object URIs generated by BotKit 0.4 or earlier are still attributed to the original bot: ```typescript twoslash import { createInstance } from "@fedify/botkit"; import { MemoryKvStore } from "@fedify/fedify"; // ---cut-before--- const instance = createInstance({ kv: new MemoryKvStore(), legacyObjectUris: { identifier: "bot" }, }); ``` Note that the web page paths change in that case (from `/` to `/@{username}`), while actor URIs and follower relationships are preserved. --- --- url: /concepts/message.md description: >- The Message object represents a message that is published to the fediverse. Learn what things you can do with the Message object. --- # Message The `Message` object is a representation of a message that is published to the fediverse. You can interact with the `Message` object such as [liking it](#liking-a-message), [replying to it](#replying-to-a-message), [sharing it](#sharing-a-message), [deleting it](#deleting-a-message), and so on. ## Where to get a `Message` object There are two ways to get a `Message` object in general: ### Event handler When you receive a message from the fediverse, you can get a `Message` object from the event handler. For example, in the following code snippet, the `~Bot.onMention` event handler is called with a `Message` object: ```typescript twoslash // @noErrors: 2345 import { createBot } from "@fedify/botkit"; const bot = createBot({ // Omitted other options for brevity }); bot.onMention = async (session, message) => { // `message` is a `Message` object }; ``` ### Session When you publish a new message to the fediverse, you can get a `Message` object as a return value of the invoked method. For example, in the following code snippet, the `Session.publish()` method returns an `AuthorizedMessage` object: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- const session = bot.getSession("https://mydomain"); const message = await session.publish(text`Hello, world!`); // [!code highlight] ``` For more information about publishing a message, see the section right below. ## `Message` vs. `AuthorizedMessage` There are two types of `Message` objects: `Message` and `AuthorizedMessage`. Everything you can do with the `Message` object can be done with the `AuthorizedMessage` object as well. The only difference between them is that the `AuthorizedMessage` object has additional methods that require the authorization of the author of the message. For example, the [`delete()`](#deleting-a-message) method requires the authorization of the author of the message. Therefore, the `delete()` method is available only in the `AuthorizedMessage` object. In general, you will get the `AuthorizedMessage` object when you publish a new message to the fediverse: [`Session.publish()`](#publishing-a-message) or [`Message.reply()`](#replying-to-a-message) method. Or you can get all the published messages from the bot's outbox: [`Session.getOutbox()`](#get-published-messages). ## Publishing a message You can publish a message to the fediverse by calling the `Session.publish()` method: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`Hello, world!`); ``` As you can see, the `~Session.publish()` method does not take a string, but a `Text` object. The `Text` object can be instantiated by the `text()` string template tag. Texts can include emphases, links, mentions, and so on: ```typescript twoslash import { type Session, em, link, mention, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text` ${link("BotKit", "https://botkit.fedify.dev/")} is a framework for \ creating ${em("ActivityPub")} bots. It is powered by \ ${mention("@fedify@hollo.social")}. `); ``` For more information about the `Text` object, see the [*Text* section](./text.md). The `Session.publish()` method returns an `AuthorizedMessage` object that represents the message that was published. You can use this object to interact with the message, such as [deleting](./message.md#deleting-a-message) it or [replying](./message.md#replying-to-a-message) to it: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This message will be deleted in a minute.` ); setTimeout(async () => { await message.delete(); // [!code highlight] }, 1000 * 60); ``` For more information about the `Message` object, see the [*Message* section](./message.md). ### Visibility You can control the visibility of the message by providing `~SessionPublishOptions.visibility` option. The value of the option has to be one of the following strings: `"public"` : The message is visible to everyone, and discoverable by the public timeline. `"unlisted"` : The message is visible to everyone, but not discoverable by the public timeline. Corresponds to Mastodon's *quiet public*. `"followers"` : The message is visible only to the followers of the bot and the mentioned users. `"direct"` : The message is visible only to the mentioned users. Corresponds to Mastodon's *specific people*. Here's an example of publishing a direct message: ```typescript twoslash import { type Session, mention, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`Hello, ${mention("@fedify@hollo.social")}!`, { visibility: "direct", // [!code highlight] }); ``` ### Quote policy You can control who may quote a message by providing `~SessionPublishOptions.quotePolicy`. BotKit serializes this policy as the [FEP-044f] interaction policy on the ActivityPub object and uses it when answering incoming quote requests. The shorthand values are: `"public"` : Anyone may quote the message. This is the default. `"followers"` : Followers may quote the message automatically. `"nobody"` : Nobody may quote the message, except the bot itself. `"manual"` : Quote requests are left pending for `~Bot.onQuoteRequest`. ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`Followers can quote this.`, { quotePolicy: "followers", }); ``` You can also use the two-axis form to distinguish actors whose quotes are approved automatically from actors whose quotes await manual review: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`Followers are reviewed before quoting this.`, { quotePolicy: { manual: "followers" }, }); ``` The quote policy can be changed when editing a message: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish(text`This starts public.`); await message.update(text`This is now harder to quote.`, { quotePolicy: "nobody", }); ``` If a quote was already authorized through an incoming quote request, you can revoke its authorization stamp from the quoted message: ```typescript twoslash import { type AuthorizedMessage, type Message, type MessageClass } from "@fedify/botkit"; declare const message: AuthorizedMessage; declare const quote: Message; // ---cut-before--- await message.unauthorizeQuote(quote); ``` [FEP-044f]: https://w3id.org/fep/044f ### Attaching media You can attach media files to a message by providing `~SessionPublishOptions.attachments` option. The value of the option has to be an array of `Document` objects (which is provided by Fedify). For example: ```typescript {2-12} twoslash import { Image, type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`Here's a cute dino!`, { attachments: [ new Image({ mediaType: "image/png", url: new URL( "https://repository-images.githubusercontent.com/913141583/852a1091-14d5-46a0-b3bf-8d2f45ef6e7f", ), name: "BotKit logo", width: 1280, height: 640, }), ], }); ``` > \[!TIP] > `Document` and its subclasses `Audio`, `Image`, and `Video` are re-exported > by BotKit: > > ```typescript twoslash > import { Audio, Document, Image, Video } from "@fedify/botkit"; > ``` > \[!NOTE] > You are responsible for hosting the media files. BotKit does not provide > any media hosting service. > > Usually you would host the media files on an object storage service like > Amazon S3, Google Cloud Storage, or a self-hosted MinIO. > \[!NOTE] > Even though you can attach unlimited number of media files to a message, > in practice, Mastodon et al. limit the number of attachments to 4 or 5. > Also note that the most of the fediverse servers have a limit on > the size of the media files. Media files reaching those limits usually > are silently ignored by the servers. ### Language hint You can provide a hint to the fediverse about the language of the message by providing `~SessionPublishOptions.language` option. The value of the option has to be an [BCP 47], e.g., `"en"` for English, `"en-US"` for American English, `"zh-Hant"` for Traditional Chinese. Here's an example of publishing a message with the language hint: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`你好,世界!`, { language: "zh", // [!code highlight] }); ``` > \[!TIP] > We highly recommend to provide the language hint to the fediverse. It helps > the fediverse servers to render the message correctly, especially for the > right-to-left languages like Arabic and Hebrew or the East Asian languages > like Chinese, Japanese, and Korean. [BCP 47]: https://tools.ietf.org/html/bcp47 ### Quoting *This API is available since BotKit 0.2.0.* You can quote a message by providing `~SessionPublishOptions.quoteTarget` option. The value of the option has to be a `Message` object that you want to quote. For example: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { await session.publish( text`This message quotes the message.`, { quoteTarget: message }, // [!code highlight] ); }; ``` > \[!NOTE] > Quoting behavior can vary significantly between different ActivityPub > implementations. Some platforms like Misskey display quotes prominently, > while others like Mastodon might implement quotes differently or not support > them at all. When the quoted message supports [FEP-044f], BotKit also sends a quote request to the quoted message's author. Until that author accepts the request, the returned `AuthorizedMessage` reports a pending approval state: ```typescript twoslash import { type Message, type MessageClass, type Session, text } from "@fedify/botkit"; declare const session: Session; declare const quoted: Message; // ---cut-before--- const message = await session.publish(text`This message quotes another one.`, { quoteTarget: quoted, }); console.log(message.quoteApprovalState); // "pending" ``` If the author accepts the quote request, BotKit stores the received authorization stamp on the message and sends an `Update` activity. If the author rejects it, or the author's server later revokes the authorization stamp, BotKit removes the quote target and the fallback quote link from the stored message and sends an `Update` activity with the stripped content. ### Polls *This API is available since BotKit 0.3.0.* You can attach a poll to a message by providing the `~SessionPublishOptionsWithQuestion.poll` option along with the message class `Question`. The poll option allows users to vote on different choices. For example: ```typescript twoslash import { type Session, Question, text } from "@fedify/botkit"; import { Temporal } from "@js-temporal/polyfill"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`What's your favorite color?`, { class: Question, poll: { multiple: false, // Single choice poll options: ["Red", "Blue", "Green"], endTime: Temporal.Now.instant().add({ hours: 24 }), }, }); ``` For multiple choice polls, set `~Poll.multiple` to `true`: ```typescript twoslash import { type Session, Question, text } from "@fedify/botkit"; import { Temporal } from "@js-temporal/polyfill"; const session = {} as unknown as Session; // ---cut-before--- await session.publish(text`Which programming languages do you know?`, { class: Question, poll: { multiple: true, // Multiple choice poll options: ["JavaScript", "TypeScript", "Python", "Rust"], endTime: Temporal.Now.instant().add({ hours: 24 * 7 }), }, }); ``` The poll configuration includes: `~Poll.multiple` : Whether the poll allows multiple selections (`true` for multiple choice, `false` for single choice). `~Poll.options` : An array of strings representing the poll options. Each option must be unique and non-empty. `~Poll.endTime` : A [`Temporal.Instant`] representing when the poll closes. > \[!NOTE] > Polls are represented as ActivityPub `Question` objects. Not all ActivityPub > implementations support polls, and the behavior may vary between different > platforms. > \[!TIP] > When someone votes on your bot's poll, the `~Bot.onVote` event handler will > be called. See the [*Vote* section](./events.md#vote) in the *Events* concept > document for more information. [`Temporal.Instant`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant ## Extracting information from a message You can get various information about the message through the `Message` object: the textual content, the content in HTML, the author of the message, the timestamp when the message was created, and so on. ### Content You can get the textual content of the message through the `~Message.text` property. It contains the plain text content of the message. If the message contains any rich text, the `~Message.text` property strips them out. For example, if the content of the message is: > Hello, **world**! The `~Message.text` property will contain `"Hello, world!"`. Recommend to use the `~Message.text` property when you need pattern matching or text processing. On the other hand, there is the `~Message.html` property that contains the HTML content of the message. It includes the rich text like emphases, links, mentions, and so on, but dangerous HTML tags are sanitized to prevent XSS attacks. For example, if the content of the message is: > Hello, **world**! The `~Message.html` property will contain: `"

Hello, world!

"`. Recommend to use the `~Message.html` property when you need to render the message in a web page. > \[!TIP] > If you want to get the raw content of the message, which is not sanitized, > you can use the `~Message.raw` property. ### Author You can get the author of the message through the `~Message.actor` property. It is represented as an `Actor` object (which is provided by Fedify). The `Actor` object contains the information about the author, such as the display name, the username, the avatar, and so on: ```typescript twoslash import { type Message, type MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- const actor = message.actor; console.log(actor.id); // The URI of the author console.log(actor.name); // The display name of the author console.log(actor.preferredUsername); // The username of the author console.log(actor.url); // The URL of the profile of the author console.log(actor.iconId?.href); // The URL of the avatar of the author ``` ### Visibility You can get the visibility of the message through the `~Message.visibility` property. It is represented as a string, which is one of the following: `"public"` : The message is visible to everyone, and discoverable by the public timeline. `"unlisted"` : The message is visible to everyone, but not discoverable by the public timeline. Corresponds to Mastodon's *quiet public*. `"followers"` : The message is visible only to the followers of the bot and the mentioned users. `"direct"` : The message is visible only to the mentioned users. Corresponds to Mastodon's *specific people*. `"unknown"` : The visibility of the message is unknown. It is usually the case when the message is published by a minor fediverse server that is incompatible with Mastodon-style visibility. ### Language hint You can get the language hint of the message through the `~Message.language` property. It is represented as an [`Intl.Locale`] object. If you want just a BCP 47 language tag string, you can call the [`Intl.Locale.toString()`] method: ```typescript twoslash import { type Message, type MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- message.language?.toString() // e.g., "en", "en-US", "zh-Hant" ``` [`Intl.Locale`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale [`Intl.Locale.toString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/toString ### Traversing the conversation You can get the parent message of a reply message through the `~Message.replyTarget` property, which is either another `Message` object or `undefined` if the message is not a reply: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onReply = async (session, reply) => { if (reply.replyTarget != null) { console.log("This is a reply to the message:", reply.replyTarget); } }; ``` You can traverse the conversation by following the `~Message.replyTarget` property recursively: ```typescript twoslash import type { Bot, Message, MessageClass } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onReply = async (session, reply) => { let message: Message | undefined = reply; while (message != null) { console.log(message); message = message.replyTarget; } }; ``` ### Mentions You can get the mentioned accounts in the message through the `~Message.mentions` property. It is an array of `Actor` objects: ```typescript twoslash import { type Message, type MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- for (const mention of message.mentions) { console.log(mention.name); } ``` > \[!TIP] > Although the `Actor` type is declared by Fedify, it is re-exported by BotKit. #### Hashtags You can get the hashtags in the message through the `~Message.hashtags` property. It is an array of `Hashtag` objects: ```typescript twoslash import { type Message, type MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- for (const hashtag of message.hashtags) { console.log(hashtag.name); } ``` > \[!TIP] > Although the `Hashtag` class is declared by Fedify, it is re-exported by > BotKit. ### Attachments You can get the attachments in the message through the `~Message.attachments` property. It is an array of `Document` objects: ```typescript twoslash import { type Message, type MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- for (const attachment of message.attachments) { console.log(attachment.mediaType); console.log(attachment.url); console.log(attachment.width); console.log(attachment.height); } ``` > \[!TIP] > Although the `Document` class and its subclasses are declared by Fedify, > they are re-exported by BotKit. ### Times You can get the timestamp when the message was published through the `~Message.published` property. It is represented as a [`Temporal.Instant`] object. You can get the timestamp when the message was last updated through the `~Message.updated` property. It is also represented as a [`Temporal.Instant`] object. ### Quotes *This API is available since BotKit 0.2.0.* You can get the message that is quoted in the message through the `~Message.quoteTarget` property. It is either another `Message` object or `undefined` if the message is not a quote. For messages received from the fediverse, `~Message.quoteApproved` reports whether the quote has FEP-044f approval. It is `undefined` when the message does not quote anything, `true` for self-quotes and quotes with a valid `QuoteAuthorization` stamp, and `false` when the stamp is missing, invalid, or could not be fetched. Legacy quote links such as Misskey quote tags and `quoteUrl` without a `QuoteAuthorization` stamp are reported as unapproved. BotKit checks `~Message.quoteApproved` when it materializes the message, so a message can reflect a later stamp revocation when it is loaded again. For authorized messages created by your bot, `~AuthorizedMessage.quoteApprovalState` describes whether the quote target still awaits FEP-044f approval. It is `"pending"` for remote quote targets that have not sent an authorization stamp, `"accepted"` after a valid stamp has been received, and `"notRequired"` for self-quotes. Since the quoted message itself can be a quote, you can traverse the conversation by following the `~Message.quoteTarget` property recursively: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- let quote: Message | undefined = message.quoteTarget; while (quote != null) { console.log(quote); quote = quote.quoteTarget; } ``` ### Want more? If you want more data from the message, you can get the raw object of the message through the `~Message.raw` property. It is an instance of one of [`Article`], [`ChatMessage`], [`Note`], or [`Question`] class (which are provided by Fedify). You can get the raw data from the object. For example, if you want to get the location of the message (Pixelfed et al. provide the geo location of the message), you can get it through the `~Message.raw` property: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; import { Place } from "@fedify/vocab"; const message = {} as unknown as Message; // ---cut-before--- const location = await message.raw.getLocation(); if (location instanceof Place) { console.log(location.name); console.log(location.latitude); console.log(location.longitude); } ``` In the above example, the [`Place`] class is declared by Fedify, so you need to install it and import it: ```typescript twoslash import { Place } from "@fedify/vocab"; ``` [`Article`]: https://jsr.io/@fedify/vocab/doc/~/Article [`ChatMessage`]: https://jsr.io/@fedify/vocab/doc/~/ChatMessage [`Note`]: https://jsr.io/@fedify/vocab/doc/~/Note [`Question`]: https://jsr.io/@fedify/vocab/doc/~/Question [`Place`]: https://jsr.io/@fedify/vocab/doc/~/Place ## Getting published messages You can get the published messages by calling the `Session.getOutbox()` method. It returns an [`AsyncIterable`] object that yields the `AuthorizedMessage` objects: ```typescript twoslash import type { Session } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- for await (const message of session.getOutbox()) { console.log(message.text); } ``` The yielded messages are in the descending order of the published timestamp by default, but you can specify the order by providing the `~SessionGetOutboxOptions.order` option: ```typescript twoslash import type { Session } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- session.getOutbox({ order: "oldest" }) ``` Or you can specify the range of the messages by providing the `~SessionGetOutboxOptions.since` and `~SessionGetOutboxOptions.until` options: ```typescript twoslash import type { Session } from "@fedify/botkit"; import { Temporal } from "@js-temporal/polyfill"; const session = {} as unknown as Session; // ---cut-before--- session.getOutbox({ since: Temporal.Instant.from("2025-01-01T00:00:00Z"), until: Temporal.Instant.from("2025-01-31T23:59:59.999Z"), }) ``` [`AsyncIterable`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols ## Updating a message You can update a message's content by calling the `~AuthorizedMessage.update()` method: ```typescript twoslash import type { Session } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This message will be updated in a minute.` ); setTimeout(async () => { await message.update(text`This message has been updated.`); // [!code highlight] }, 1000 * 60); ``` > \[!NOTE] > Since the `~AuthorizedMessage.update()` method belongs to > the `AuthorizedMessage` type, you cannot call it on an unauthorized `Message` > object. > \[!CAUTION] > Some ActivityPub implementations like older versions of Mastodon and Misskey > do not support updating messages. For those implementations, once published > messages are shown as-is forever even if you update them. ## Deleting a message You can delete a message by calling the `~AuthorizedMessage.delete()` method: ```typescript twoslash import type { Session } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This message will be deleted in a minute.` ); setTimeout(async () => { await message.delete(); // [!code highlight] }, 1000 * 60); ``` > \[!NOTE] > Since the `~AuthorizedMessage.delete()` method belongs to > the `AuthorizedMessage` type, you cannot call it on an unauthorized `Message` > object. ## Liking a message You can like a message by calling the `~Message.like()` method: ```typescript twoslash import type { Session } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This message will be liked.` ); await message.like(); // [!code highlight] ``` > \[!CAUTION] > You may call the `~Message.like()` method on a message that is already liked, > but it will not raise an error. Under the hood, such a call actually sends > multiple `Like` activities to the fediverse, whose behavior is > undefined—some servers may ignore the duplicated activities, some servers > may allow them and count them as multiple likes. If you need to undo the liking, you can call the `~AuthorizedLike.unlike()` method: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- const like = await message.like(); await like.unlike(); // [!code highlight] ``` ## Reacting to a message with an emoji *This API is available since BotKit 0.2.0.* You can react to a message with an emoji by calling the `~Message.react()` method: ```typescript twoslash import { emoji, type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This message will be reacted to with an emoji.` ); await message.react(emoji`👍`); // [!code highlight] ``` > \[!NOTE] > The tagged template literal function `emoji()` takes a string and returns > an `Emoji` value, which is a brand of `string`. If it takes anything other > than a single emoji, it will throw a `TypeError` at runtime. Or you can use the `~Message.react()` method with a custom emoji. You need to define custom emojis in advance by calling the `Bot.addCustomEmojis()` method: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- const emojis = bot.addCustomEmojis({ // Use a remote image URL: yesBlob: { url: "https://cdn3.emoji.gg/emojis/68238-yesblob.png", type: "image/png", }, // Use a local image file: noBlob: { file: `${import.meta.dirname}/emojis/no_blob.png`, type: "image/webp", }, }); ``` Then you can use the custom emojis in the `~Message.react()` method: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; import type { DeferredCustomEmoji } from "@fedify/botkit/emoji"; const message = {} as unknown as Message; const emojis = {} as Readonly>>; // ---cut-before--- await message.react(emojis.yesBlob); ``` If you need to undo the reaction, you can call the `~AuthorizedReaction.unreact()` method: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; import type { DeferredCustomEmoji } from "@fedify/botkit/emoji"; const message = {} as unknown as Message; const emojis = {} as Readonly>>; // ---cut-before--- const reaction = await message.react(emojis.noBlob); await reaction.unreact(); // [!code highlight] ``` > \[!NOTE] > Some ActivityPub implementations like Mastodon do not support emoji reactions, > so even if you call the `~Message.react()` method to messages from those > implementations, they will ignore the reactions. > \[!NOTE] > Some ActivityPub implementations like Misskey do not allow an actor to leave > multiple reactions to the same message. In that case, only one reaction > will be shown, and the other reactions will be ignored. ## Replying to a message You can reply to a message by calling the `~Message.reply()` method: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This is a message that will be replied to.` ); const reply = await message.reply(text`This is a reply to the message.`); const reply2 = await reply.reply(text`This is a reply to the reply.`); ``` You can use the same set of options as the `Session.publish()` method when calling the `~Message.reply()` method: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- const reply = await message.reply( text`A reply with a language hint.`, { language: "en" }, // [!code highlight] ); ``` Like the `Session.publish()` method, the `~Message.reply()` method returns an `AuthorizedMessage` object that represents the reply message: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- const reply = await message.reply(text`This reply will be deleted in a minute.`); setTimeout(async () => { await reply.delete(); // [!code highlight] }, 1000 * 60); ``` > \[!TIP] > It does not mention the original author in the reply message by default. > However, you can manually [mention them](./text.md#mentions) in the reply > message by using the `mention()` function: > > ```typescript {2} twoslash > import { type Message, type MessageClass, mention, text } from "@fedify/botkit"; > const message = {} as unknown as Message; > // ---cut-before--- > const reply = await message.reply( > text`${mention(message.actor)} This is a reply to the message.` > ); > ``` > \[!TIP] > The visibility of the reply message is inherited from the original message > by default. However, you can specify the visibility of the reply message: > > ```typescript twoslash > import { type Message, type MessageClass, mention, text } from "@fedify/botkit"; > const message = {} as unknown as Message; > // ---cut-before--- > const reply = await message.reply( > text`This is a direct reply to the message.`, > { visibility: "direct" }, // [!code highlight] > ); > ``` ## Sharing a message You can share (i.e., boost) a message by calling the `~Message.share()` method: ```typescript twoslash import { type Session, text } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const message = await session.publish( text`This is a message that will be shared.` ); await message.share(); // [!code highlight] ``` > \[!TIP] > You can specify the visibility of the shared message: > > ```typescript twoslash > import { type Message, type MessageClass } from "@fedify/botkit"; > const message = {} as unknown as Message; > // ---cut-before--- > await message.share({ visibility: "followers" }); > ``` If you need to undo the sharing, you can call the `~AuthorizedSharedMessage.unshare()` method: ```typescript twoslash import type { Message, MessageClass } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- const sharedMessage = await message.share(); await sharedMessage.unshare(); // [!code highlight] ``` > \[!NOTE] > `Message` and `SharedMessage` are totally different objects. The `Message` > object represents the original message, and the `SharedMessage` object > represents the pointer to the original message. Type-wise there is no > inheritance between them. --- --- url: /recipes.md description: >- The recipes section contains a list of recipes that demonstrate how to implement common tasks with BotKit. --- # Recipes The recipes section contains a list of recipes that demonstrate how to implement common tasks with BotKit. ## Sending a direct message It is very simple to send a direct message to a user: set the [`visibility`](./concepts/message.md#visibility) option to `"direct"` when calling the [`Session.publish()`](./concepts/message.md#publishing-a-message) method: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { await session.publish( text`Hi, ${message.actor}!`, { visibility: "direct" }, ); }; ``` > \[!CAUTION] > You need to mention at least one actor in the message to send a direct > message. Otherwise, the message won't be read by anyone. ## Following back To let your bot follow back all of its followers, you can use the [`onFollow`](./concepts/events.md#follow) event with the [`Session.follow()`](./concepts//session.md#following-an-actor) method together: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onFollow = async (session, followRequest) => { await followRequest.accept(); await session.follow(followRequest.follower); }; ``` > \[!NOTE] > It is not guaranteed that the follow request will be accepted. > The actor may reject your bot's follow request. ## Automatically deleting old messages To automatically delete old messages after a certain period, you can use [`Session.getOutbox()`](./concepts/message.md#getting-published-messages) method, [`AuthorizedMessage.delete()`](./concepts/message.md#deleting-a-message) method, and the [`setInterval()`] function together. The following example shows how to delete all messages older than a week: ```typescript twoslash import type { Bot, Session } from "@fedify/botkit"; import { Temporal } from "@js-temporal/polyfill"; const bot = {} as unknown as Bot; // ---cut-before--- async function deleteOldPosts(session: Session): Promise { const now = Temporal.Now.instant(); const oneWeekAgo = now.subtract({ hours: 7 * 24 }); const oldPosts = session.getOutbox({ until: oneWeekAgo }); for await (const post of oldPosts) { await post.delete(); } } setInterval( deleteOldPosts, 1000 * 60 * 60, bot.getSession("https://yourdomain") ); ``` [`setInterval()`]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval ## Scheduled messages You can use the [`setInterval()`] function to send messages at regular intervals: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- setInterval(async () => { const session = bot.getSession("https://yourdomain"); await session.publish(text`Hello, world!`); }, 1000 * 60 * 60); ``` Or if you use Deno, you can use non-standard APIs like [`Deno.cron()`] to send messages at specific times: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- Deno.cron("scheduled messages", "0 0 12 * * *", async () => { const session = bot.getSession("https://yourdomain"); await session.publish(text`Hello, world!`); }); ``` [`Deno.cron()`]: https://docs.deno.com/api/deno/~/Deno.cron ## Automatically liking mentions It is simple to automatically like mentions of your bot. You can use the [`onMention`](./concepts/events.md#mention) event handler and the [`Message.like()`](./concepts/message.md#liking-a-message) method together: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { await message.like(); }; ``` ## Automatically replying to mentions It is simple to automatically reply to mentions of your bot. You can use the [`onMention`](./concepts/events.md#mention) event handler and the [`Message.reply()`](./concepts/message.md#replying-to-a-message) method together: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { await message.reply(text`You mentioned me, ${message.actor}!`); }; ``` ## Thread creation Although BotKit has no limitation on characters in a message, sometimes you may want to create a thread for storytelling or to make audience engagement easier. You can use the [`Message.reply()`](./concepts/message.md#replying-to-a-message) method to your own messages too: ```typescript twoslash import type { AuthorizedMessage, Bot, Note, Session, Text } from "@fedify/botkit"; import { text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- async function *createThread( session: Session, messages: Text<"block", TContextData>[] ): AsyncIterable> { let parent = await session.publish(messages[0]); yield parent; for (let i = 1; i < messages.length; i++) { parent = await parent.reply(messages[i]); yield parent; } } const messages = [ text`Once upon a time, there was a bot named BotKit.`, text`BotKit was created by a developer who wanted to make a bot.`, text`The developer used BotKit to create a bot that could do anything.`, text`The bot was so powerful that it could even create other bots.`, text`And so, BotKit lived happily ever after.`, ]; const session = bot.getSession("https://yourdomain", undefined); for await (const message of createThread(session, messages)) { console.debug(`Created message ${message.id}`); } ``` ## Thread traversal Suppose your bot is an LLM-based chatbot and you want to give your LLM model the previous messages in a thread as context. You can achieve this by recursively traversing the [`Message.replyTarget`](./concepts/message.md#traversing-the-conversation) property: ```typescript twoslash import type { Message, MessageClass, Session } from "@fedify/botkit"; // ---cut-before--- async function traverseThread( session: Session, message: Message ): Promise[]> { const thread: Message[] = []; let m: Message | undefined = message; while (m != null) { thread.push(m); m = m.replyTarget; } thread.reverse(); return thread; } ``` --- --- url: /concepts/repository.md description: >- A repository is a data access object that provides an abstraction over the underlying data source. This document provides an overview of repositories and how they are used in the framework. --- # Repository A repository is a data access object that provides an abstraction over the underlying data source. BotKit provides a few built-in repositories that can be used to interact with the database, but you can also create your own repositories to interact with other data sources. ## Bot-scoped storage Since BotKit 0.5.0, a single repository stores the data of every bot hosted on an [instance](./instance.md): every `Repository` method takes the identifier of the owning bot actor as its first parameter, and data belonging to different identifiers are isolated from each other. The `Repository.forIdentifier()` method returns an `ActorScopedRepository`, a view of the repository bound to one bot actor, which exposes the same operations without the `identifier` parameter. Custom `Repository` implementations written for BotKit 0.4 or earlier need to be updated to the new method signatures. Besides the added parameter, two methods joined the interface: `~Repository.findFollowedBots()`, a reverse lookup answering which bots follow a given actor (used for routing incoming messages to the right bots), and the optional `~Repository.migrate()`, which adopts data stored by BotKit 0.4 or earlier for a bot actor identifier. The built-in repositories migrate legacy data automatically when the bot is created through `createBot()`. [FEP-044f] quote support also adds quote authorization storage methods and quote authorization reference methods. The reference methods map a received `QuoteAuthorization` stamp URI back to the local quote message that depends on it, so BotKit can update that message when the remote author later changes the quote's authorization state. [FEP-044f]: https://w3id.org/fep/044f ## `KvRepository` It is the default repository provided by BotKit. If you omit the [`repository`](./bot.md#createbotoptions-repository) option of the [`createBot()`](./bot.md#instantiation) function, BotKit will use the `KvRepository` by default. The `KvRepository` is a repository that stores data in a key–value store through the [`KvStore`] interface, which is provided by the Fedify. Since the [`KvStore`] interface itself also abstracts over the underlying data source, you can easily switch between different key–value stores without changing the repository implementation. There are several [`KvStore`] implementations available in the Fedify: [`RedisKvStore`] : [`RedisKvStore`] is a key–value store implementation that uses Redis as the backend storage. It provides scalability and high performance, making it suitable for production use in distributed systems. It requires a Redis server setup and maintenance. ``` > [!NOTE] > The [`RedisKvStore`] class is available in the [@fedify/redis] package. ``` [`PostgresKvStore`] : [`PostgresKvStore`] is a key–value store implementation that uses PostgreSQL as the backend storage. It provides scalability and high performance, making it suitable for production use in distributed systems. It requires a PostgreSQL server setup and maintenance. ``` > [!NOTE] > The [`PostgresKvStore`] class is available in the [@fedify/postgres] > package. ``` [`DenoKvStore`] (Deno only) : [`DenoKvStore`] is a key–value store implementation for [Deno] runtime that uses Deno's built-in [`Deno.openKv()`] API. It provides persistent storage and good performance for Deno environments. It's suitable for production use in Deno applications. ``` > [!NOTE] > The [`DenoKvStore`] class is available in *x/deno* module of > the [@fedify/fedify] package. ``` [`MemoryKvStore`] : A simple in-memory key–value store that doesn't persist data. It's best suited for development and testing environments where data don't have to be shared across multiple nodes. No setup is required, making it easy to get started. ``` > [!TIP] > Although [`MemoryKvStore`] is provided by Fedify, BotKit also re-exports > it for convenience. ``` [`KvStore`]: https://fedify.dev/manual/kv [`RedisKvStore`]: https://fedify.dev/manual/kv#rediskvstore [@fedify/redis]: https://github.com/fedify-dev/redis [`PostgresKvStore`]: https://fedify.dev/manual/kv#postgreskvstore [@fedify/postgres]: https://github.com/fedify-dev/postgres [`DenoKvStore`]: https://fedify.dev/manual/kv#denokvstore-deno-only [Deno]: https://deno.com/ [`Deno.openKv()`]: https://docs.deno.com/api/deno/~/Deno.openKv [@fedify/fedify]: https://fedify.dev/ [`MemoryKvStore`]: https://fedify.dev/manual/kv#memorykvstore ## `SqliteRepository` *This API is available since BotKit 0.3.0.* The `SqliteRepository` is a repository that stores data in a SQLite database using the [`node:sqlite`] module. It provides a production-ready storage solution with excellent performance and reliability, while maintaining compatibility with both Deno and Node.js environments. Unlike [`KvRepository`](#kvrepository) which requires a separate key–value store setup, `SqliteRepository` can operate with either an in-memory database for development/testing or a file-based database for production use. It offers ACID compliance through transactions, write-ahead logging (WAL) mode for optimal performance, and proper indexing for efficient data retrieval. In order to use `SqliteRepository`, you need to install the *@fedify/botkit-sqlite* package: ::: code-group ```sh [Deno] deno add jsr:@fedify/botkit-sqlite ``` ```sh [npm] npm add @fedify/botkit-sqlite ``` ```sh [pnpm] pnpm add @fedify/botkit-sqlite ``` ```sh [Yarn] yarn add @fedify/botkit-sqlite ``` ::: The `SqliteRepository` constructor accepts an options object with the following properties: `path` (optional) : Path to the SQLite database file. Defaults to `":memory:"` for an in-memory database. Use a file path for persistent storage. `wal` (optional) : Whether to enable write-ahead logging (WAL) mode for better performance. Defaults to `true` for file-based databases. WAL mode is not applicable for in-memory databases. [`node:sqlite`]: https://nodejs.org/api/sqlite.html ## `RedisRepository` *This API is available since BotKit 0.5.0.* The `RedisRepository` is a repository that stores data in [Redis] using the [node-redis] client. It is suited for deployments where multiple bot processes need to share the same repository state, or where you already operate Redis for other BotKit infrastructure. Unlike [`KvRepository`](#kvrepository), `RedisRepository` stores BotKit data directly in Redis data structures such as strings, sets, and sorted sets. It supports either an internally owned Redis client created from a connection URL or an injected client whose lifecycle stays under your control. In order to use `RedisRepository`, you need to install the *@fedify/botkit-redis* package: ::: code-group ```sh [Deno] deno add jsr:@fedify/botkit-redis ``` ```sh [npm] npm add @fedify/botkit-redis ``` ```sh [pnpm] pnpm add @fedify/botkit-redis ``` ```sh [Yarn] yarn add @fedify/botkit-redis ``` ::: The `RedisRepository` constructor accepts the following properties: `url` : A Redis connection string used to create an internal client. Exactly one of `url` and `client` must be provided. `client` : An existing node-redis compatible client. When this is provided, the repository does not own the client and calling `close()` will not shut it down. `prefix` (optional) : The Redis key prefix used for BotKit data. Defaults to `"botkit"`. `clientOptions` (optional) : Additional node-redis client options. This option is only valid when `url` is used. `lockTimeoutMs` (optional) : How long a Redis lock can live without renewal, in milliseconds. Defaults to `30000`. `lockPollIntervalMs` (optional) : How long to wait before retrying a held Redis lock, in milliseconds. Defaults to `20`. `lockRenewIntervalMs` (optional) : How often to renew a held Redis lock, in milliseconds. Defaults to `10000`. [Redis]: https://redis.io/ [node-redis]: https://github.com/redis/node-redis ## `PostgresRepository` *This API is available since BotKit 0.4.0.* The `PostgresRepository` is a repository that stores data in PostgreSQL using the [Postgres.js] driver. It is suited for deployments where multiple bot processes need to share the same persistent repository state, or where you already operate PostgreSQL for other infrastructure. Unlike [`KvRepository`](#kvrepository), `PostgresRepository` stores BotKit data in ordinary PostgreSQL tables rather than a key-value abstraction. It creates tables inside a dedicated PostgreSQL schema, uses transactions for multi-step updates, and supports either an internally owned connection pool or an injected Postgres.js client. In order to use `PostgresRepository`, you need to install the *@fedify/botkit-postgres* package: ::: code-group ```sh [Deno] deno add jsr:@fedify/botkit-postgres ``` ```sh [npm] npm add @fedify/botkit-postgres ``` ```sh [pnpm] pnpm add @fedify/botkit-postgres ``` ```sh [Yarn] yarn add @fedify/botkit-postgres ``` ::: The `PostgresRepository` constructor accepts an options object with the following properties: `sql` : An existing [Postgres.js] client. When this is provided, the repository does not own the client and calling `close()` will not shut it down. `url` : A PostgreSQL connection string used to create an internal connection pool. Exactly one of `sql` and `url` must be provided. `schema` (optional) : The PostgreSQL schema used for BotKit tables. Defaults to `"botkit"`. `maxConnections` (optional) : The maximum number of connections for the internally created pool. This option is only valid when `url` is used. `prepare` (optional) : Whether to use prepared statements for repository queries. Defaults to `true`. These options are mutually exclusive: use either `sql` or `url`. The `maxConnections` option is only meaningful together with `url`. The repository initializes its tables and indexes automatically. If you want to provision them before creating the repository, use the exported `initializePostgresRepositorySchema()` helper: ```typescript import postgres from "postgres"; import { initializePostgresRepositorySchema } from "@fedify/botkit-postgres"; const sql = postgres("postgresql://localhost/botkit"); await initializePostgresRepositorySchema(sql, "botkit"); ``` If you disable prepared statements for PgBouncer-style deployments, pass `false` as the third argument so schema initialization uses the same setting. [Postgres.js]: https://github.com/porsager/postgres ## `MemoryRepository` The `MemoryRepository` is a repository that stores data in memory, which means that data is not persisted across restarts. It's best suited for development and testing environments where data don't have to be shared across multiple nodes. No setup is required, making it easy to get started. > \[!TIP] > How does it differ from using [`KvRepository`](#kvrepository) with > [`MemoryKvStore`]?—In practice, there's no difference between using > `MemoryRepository` and [`KvRepository`](#kvrepository) with > [`MemoryKvStore`]. The only differences are that `MemoryRepository` is > a more convenient way to use `MemoryKvStore` and that it's slightly more > efficient because it doesn't have to go through the [`KvStore`] interface. ## `MemoryCachedRepository` *This API is available since BotKit 0.3.0.* The `MemoryCachedRepository` is a repository decorator that adds an in-memory cache layer on top of another repository. This is useful for improving performance by reducing the number of accesses to the underlying persistent storage, but it increases memory usage. The cache is not persistent and will be lost when the process exits. It takes an existing `Repository` instance (like `KvRepository` or even another `MemoryCachedRepository`) and wraps it. Write operations are performed on both the underlying repository and the cache. Read operations first check the cache; if the data is found, it's returned directly. Otherwise, the data is fetched from the underlying repository, stored in the cache, and then returned. > \[!NOTE] > List operations like `getMessages` and `getFollowers`, and count operations > like `countMessages` and `countFollowers` are not cached due to the > complexity of handling various filtering and pagination options. These > operations always delegate directly to the underlying repository. ## Implementing a custom repository You can create your own repository by implementing the `Repository` interface. The `Repository` interface is a generic interface that defines the basic CRUD (create, read, update, delete) operations for data access. The `Repository` interface consists of the following five domains of operations in general: Key pairs : Key pairs are the singleton data that are used for the bot actor. At the surface level, the key pairs are represented as [`CryptoKeyPair`] objects, but you would typically store them as [JWK]. ``` > [!TIP] > Fedify provides `exportJwk()` and `importJwk()` functions to convert > a [`CryptoKey`] object to a JWK object and vice versa. ``` Messages : Messages are the data of the messages that are published by the bot. Remote messages received from the remote server are not included in this domain. Each message has its own UUID and represented by either a `Create` object or an `Announce` object (which both are provided by Fedify). ``` You probably want to serialize the messages into JSON before storing them, so you can use [`toJsonLd()`] method that belong to the `Create` and `Announce` objects, and use [`fromJsonLd()`] method to deserialize them. ``` Followers : Followers are the data of the actors that follow the bot. Each follower is represented by `Actor` object (provided by Fedify) and is associated with a follow ID, a URI of the `Follow` activity. ``` Similar to messages, you can serialize the `Actor` object into JSON using the [`toJsonLd()`] method, and deserialize it using the [`fromJsonLd()`] method. > [!CAUTION] > The `Repository.hasFollower()` method takes the URI of an `Actor`, not > the follow ID. ``` Sent follows : Sent follows are the data of the follow requests that the bot has sent. Each sent follow is represented by a `Follow` object (provided by Fedify) and is associated with its own UUID. ``` Similar to messages and followers, you can serialize the `Follow` object into JSON using the [`toJsonLd()`] method, and deserialize it using the [`fromJsonLd()`] method. ``` Followees : Followees are the data of the actors that the bot follows. Each followee is represented by a `Follow` object (provided by Fedify) instead of `Actor`, and associated with an actor ID (not a follow ID). ``` Similar to messages, followers, and sent follows, you can serialize the `Follow` object into JSON using the [`toJsonLd()`] method, and deserialize it using the [`fromJsonLd()`] method. ``` [`CryptoKeyPair`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair [JWK]: https://datatracker.ietf.org/doc/html/rfc7517 [`CryptoKey`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey [`toJsonLd()`]: https://fedify.dev/manual/vocab#json-ld [`fromJsonLd()`]: https://fedify.dev/manual/vocab#json-ld --- --- url: /deploy/self-hosting.md description: >- Learn how to deploy your BotKit bot on your own Linux server with Deno or Node.js, systemd, and a Caddy reverse proxy. --- # Self-hosted deployment For complete control over your bot's environment, you can self-host it on your own server. This guide deploys a BotKit bot on a Linux server with SSH access, puts Caddy in front of it for HTTPS, and keeps it running under systemd. It covers both the Deno and Node.js runtimes; follow the one your bot uses. ## Prerequisites 1. A Linux server with SSH access 2. A domain name whose DNS points to your server 3. Either [Deno] or [Node.js] installed on your server, matching your bot [Deno]: https://deno.land/ [Node.js]: https://nodejs.org/ ## Installation steps 1. Install the runtime your bot uses: ::: code-group ```bash [Deno] curl -fsSL https://deno.land/install.sh | sh ``` ```bash [Node.js] # Install the current LTS from nodejs.org, your distribution, or a version # manager such as fnm. For example, with fnm: curl -fsSL https://fnm.vercel.app/install | bash fnm install --lts ``` ::: 2. Clone your bot repository: ```bash git clone https://github.com/yourusername/your-bot.git cd your-bot ``` 3. Set up environment variables: ```bash echo 'export SERVER_NAME="your-domain.com"' >> ~/.bashrc source ~/.bashrc ``` ## Setting up HTTPS with [Caddy] [Caddy] is recommended for handling HTTPS and reverse proxy. It automatically obtains and renews SSL/TLS certificates from [Let's Encrypt] without any manual configuration: 1. [Install Caddy]: ::: code-group ```bash [Debian/Ubuntu] # Install required packages sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https # Add Caddy repository curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \ sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \ sudo tee /etc/apt/sources.list.d/caddy-stable.list # Install Caddy sudo apt update sudo apt install caddy ``` ```bash [RHEL/Fedora] # Add Caddy repository dnf install 'dnf-command(copr)' dnf copr enable @caddy/caddy # Install Caddy dnf install caddy ``` ```bash [Arch Linux] # Install from official repositories sudo pacman -S caddy ``` ::: > \[!TIP] > For other installation methods or operating systems, > see the [official Caddy installation docs][Install Caddy]. 2. Create a Caddyfile at */etc/caddy/Caddyfile*: ```caddyfile [/etc/caddy/Caddyfile] your-domain.com { # Automatic HTTPS tls { # Email for Let's Encrypt notifications email your-email@example.com } # Proxy all requests to your bot reverse_proxy localhost:8000 # Basic security headers header { # Enable HSTS Strict-Transport-Security "max-age=31536000;" # Prevent clickjacking X-Frame-Options "DENY" # XSS protection X-Content-Type-Options "nosniff" X-XSS-Protection "1; mode=block" } } ``` > \[!NOTE] > Make sure your domain's DNS A record points to your server's IP address > before starting Caddy. Caddy will automatically obtain and manage SSL > certificates from Let's Encrypt. 3. Set proper ownership and permissions: ```bash sudo chown root:root /etc/caddy/Caddyfile sudo chmod 644 /etc/caddy/Caddyfile ``` 4. Restart Caddy: ```bash sudo systemctl restart caddy ``` 5. Check Caddy's status: ```bash sudo systemctl status caddy sudo journalctl -u caddy --follow ``` > \[!IMPORTANT] > Caddy terminates TLS and forwards requests to the bot over plain HTTP, so the > bot has to trust the `X-Forwarded-*` headers to reconstruct its public HTTPS > origin. Turn on > [`behindProxy`](../concepts/bot.md#createbotoptions-behindproxy) in > `createBot()`. The systemd units in the next section export > `BEHIND_PROXY=true` for the bot to read into that option, following the > pattern in > [*Exposing the bot to the public internet*](../concepts/bot.md#exposing-the-bot-to-the-public-internet). [Caddy]: https://caddyserver.com/ [Let's Encrypt]: https://letsencrypt.org/ [Install Caddy]: https://caddyserver.com/docs/install ## Creating a [systemd] service The [systemd] is a system and service manager for Linux that starts and manages services. It is adopted by most modern Linux distributions including Debian, Ubuntu, Fedora, and Arch Linux. > \[!NOTE] > A self-hosted bot runs as one long-lived process, so BotKit starts its > message queue automatically once the first task is enqueued. You don't need > to start the queue worker yourself. To run your bot as a systemd service: 1. Create a dedicated user for your bot (recommended for security): ```bash sudo useradd -r -s /bin/false botkit ``` 2. Set up the bot's directory: ```bash sudo mkdir -p /opt/botkit sudo chown botkit:botkit /opt/botkit ``` 3. Create a service file at */etc/systemd/system/botkit-bot.service*. The unit is the same for both runtimes apart from `ExecStart`, which starts the bot the way its runtime expects (see [*Running the bot*](../start.md#running-the-bot)): ::: code-group ```ini [Deno] [Unit] Description=BotKit Bot After=network.target Wants=caddy.service [Service] Type=simple User=botkit Group=botkit Environment=SERVER_NAME=your-domain.com # Add any other environment variables your bot needs Environment=NODE_ENV=production Environment=BEHIND_PROXY=true WorkingDirectory=/opt/botkit # A bot uses `export default bot`, so start it with `deno serve`, not # `deno run`. ExecStart=/usr/local/bin/deno serve -A --port 8000 bot.ts # Restart policy Restart=always RestartSec=10 # Security settings NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true # Resource limits CPUQuota=80% MemoryMax=1G [Install] WantedBy=multi-user.target ``` ```ini [Node.js] [Unit] Description=BotKit Bot After=network.target Wants=caddy.service [Service] Type=simple User=botkit Group=botkit Environment=SERVER_NAME=your-domain.com # Add any other environment variables your bot needs Environment=NODE_ENV=production Environment=BEHIND_PROXY=true WorkingDirectory=/opt/botkit # Serve the `export default bot` entry through the srvx CLI, whose binary # ships with the bot's dependencies under node_modules. ExecStart=/opt/botkit/node_modules/.bin/srvx serve --prod --port 8000 --entry bot.ts # Restart policy Restart=always RestartSec=10 # Security settings NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true # Resource limits CPUQuota=80% MemoryMax=1G [Install] WantedBy=multi-user.target ``` ::: The unit hardens the service with `ProtectHome=true`, so it cannot read anything under */home*. Install the runtime (the `deno` or `node` binary) somewhere system-wide, such as */usr/local/bin*, so that `ExecStart` and, on Node.js, srvx's `#!/usr/bin/env node` launcher can find it. > \[!NOTE] > On Node.js, add the [srvx] package to the project's dependencies and > install them in */opt/botkit* (for example, with `npm ci`), so that > */opt/botkit/node\_modules/.bin/srvx* exists before the service starts. > The *bot.ts* file should end with `export default bot`, exactly as on > Deno. 4. Set proper permissions: ```bash sudo chown root:root /etc/systemd/system/botkit-bot.service sudo chmod 644 /etc/systemd/system/botkit-bot.service ``` 5. Reload systemd, enable and start the service: ```bash # Reload systemd to recognize the new service sudo systemctl daemon-reload # Enable service to start on boot sudo systemctl enable botkit-bot # Start the service sudo systemctl start botkit-bot ``` 6. Verify the service is running: ```bash # Check service status sudo systemctl status botkit-bot # View logs sudo journalctl -u botkit-bot -f ``` > \[!TIP] > To update your bot: > > 1. Copy new files to */opt/botkit* > 2. Set proper ownership: `sudo chown -R botkit:botkit /opt/botkit` > 3. Restart the service: `sudo systemctl restart botkit-bot` [systemd]: https://systemd.io/ [srvx]: https://srvx.h3.dev/ ## Monitoring logs Monitor your bot's logs using systemd: ```bash sudo journalctl -u botkit-bot -f ``` --- --- url: /concepts/session.md description: >- The Session object is a short-lived object that actively communicates with the fediverse. Learn how to create a session and publish messages to the fediverse. --- # Session The `Session` object is a short-lived object that actively communicates with the fediverse. It can be [created by yourself](#creating-a-session), or you can get it when an event handler is called. ## Creating a session You can create a session by calling the `Bot.getSession()` method: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- const session = bot.getSession("https://mydomain"); ``` It takes a single argument, the origin of the server to which your bot belongs. In practice, you would have an environment variable that contains the hostname of your server, and you would pass it to the `~Bot.getSession()` method: ::: code-group ```typescript [Deno] twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- const SERVER_NAME = Deno.env.get("SERVER_NAME"); if (SERVER_NAME == null) { console.error("The SERVER_NAME environment variable is not set."); Deno.exit(1); } const session = bot.getSession(`https://${SERVER_NAME}`); // [!code highlight] ``` ```typescript [Node.js] twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- const SERVER_NAME = process.env.SERVER_NAME; if (SERVER_NAME == null) { console.error("The SERVER_NAME environment variable is not set."); Deno.exit(1); } const session = bot.getSession(`https://${SERVER_NAME}`); // [!code highlight] ``` ::: > \[!NOTE] > A dynamic [bot group](./instance.md#dynamic-bots) hosts many bots, so > `BotGroup.getSession()` additionally takes the identifier of the bot to > control and returns a `Promise`: > > ```typescript twoslash > import type { BotGroup } from "@fedify/botkit"; > const weatherBots = {} as unknown as BotGroup; > // ---cut-before--- > const session = await weatherBots.getSession( > "https://mydomain", > "weather_kr", > ); > ``` ## Getting a session from an event handler When an event handler is called, you can get a session from the `Session` object that is passed as the first argument: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { // `session` is a `Session` object }; ``` To learn more about event handlers, see the [*Events* section](./events.md). ## Determining which bot the session belongs to The `Session.bot` property is a `ReadonlyBot`: a read-only view of the bot's identity and profile, including its `~ReadonlyBot.identifier`, `~ReadonlyBot.username`, and `~ReadonlyBot.name`. It is particularly useful in handlers registered on a [dynamic bot group](./instance.md#dynamic-bots), where the same handler runs for many bots: ```typescript twoslash import type { BotGroup } from "@fedify/botkit"; const weatherBots = {} as unknown as BotGroup; // ---cut-before--- weatherBots.onMention = async (session, message) => { const identifier = session.bot.identifier; // …look up the data this particular bot serves… }; ``` > \[!NOTE] > Before BotKit 0.5.0, this property was typed as `Bot`, so event handlers > could be reassigned through it. It is now a `ReadonlyBot`, which exposes > the identity and profile only. If you need the full `Bot`, hold on to > the object returned by `createBot()` instead. ## Determining the actor URI of the bot The `Session` object has an `actorId` property that contains the URI of the bot actor. You can use this URI to refer to the bot in messages: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onFollow = async (session, actor) => { await session.publish( text`Hi, ${actor}! I'm ${session.actorId}. Thanks for following me!` ); }; ``` ## Determining the fediverse handle of the bot The `Session` object has an `actorHandle` property that contains the fediverse handle of the bot. It looks like an email address except that it starts with an `@` symbol: `@myBot@myDomain`. You can use this handle to refer to the bot in messages: ```typescript twoslash import type { Bot } from "@fedify/botkit"; import { markdown } from "@fedify/botkit/text"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onFollow = async (session, actor) => { await session.publish( markdown(`I'm ${session.actorHandle}. Thanks for following me!`) ); }; ``` ## Getting the bot's `Actor` object The `Session` object has a `~Session.getActor()` method that returns the `Actor` object of the bot: ```typescript twoslash import type { Actor, Session } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- const actor: Actor = await session.getActor(); ``` ## Republishing the bot profile If you change the bot's profile metadata, such as the display name, bio, avatar, or header image, remote servers may keep showing the old cached profile until they refresh it themselves. You can explicitly notify your followers by calling the `~Session.republishProfile()` method: ```typescript twoslash import type { Session } from "@fedify/botkit"; const session = {} as unknown as Session; // ---cut-before--- await session.republishProfile(); ``` This sends an ActivityPub `Update` activity for the bot actor to the bot's followers. Call it after your application updates the bot profile and you want the change to propagate without waiting for the next post. ## Publishing a message See the [*Publishing a message* section](./message.md#publishing-a-message) in the *Message* concept document. ## Getting published messages See the [*Getting published messages* section](./message.md#getting-published-messages) in the *Message* concept document. ## Following an actor Your bot can follow an actor by calling the `Session.follow()` method. The following example shows how to get the `bot` follow back all of its followers: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onFollow = async (session, followRequest) => { await followRequest.accept(); await session.follow(followRequest.follower); }; ``` > \[!CAUTION] > The `~Session.follow()` method just sends a follow request to the actor, > but it does not guarantee that the actor will accept the follow request. > The actor may reject the follow request, and your bot will not be able to > follow the actor. > > If you want to know whether the actor has accepted or rejected the follow > request, you need to register > the [`Bot.onAcceptFollow`](./events.md#accept-follow) and > [`Bot.onRejectFollow`](./events.md#reject-follow) event handlers. > \[!TIP] > It takes several kinds of objects as an argument, such as `Actor`, `string`, > and `URL`: > > `Actor` > : The actor to follow. > > `URL` > : The URI of the actor to follow. > E.g., `new URL("https://example.com/users/alice")`. > > `string` > : The URI or the fediverse handle of the actor to follow. > E.g., `"https://example.com/users/alice"` or `"@alice@example.com"`. > \[!NOTE] > If you try to follow an actor that is already followed, the method will just > do nothing. ## Unfollowing an actor Likewise, your bot can unfollow an actor by calling the `Session.unfollow()` method. The following example shows how to make the `bot` unfollow if any of its followers unfollow it: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onUnfollow = async (session, actor) => { await session.unfollow(actor); }; ``` > \[!TIP] > Like the `~Session.follow()` method, the `~Session.unfollow()` method takes > several kinds of objects as an argument, such as `Actor`, `string`, and `URL`. > \[!NOTE] > If you try to unfollow an actor that is not followed, the method will just > do nothing. ## Checking if the bot follows an actor The `Session` object has a `~Session.follows()` method that returns a boolean value indicating whether your bot follows a given actor. The following example shows how to check if your bot follows an actor and respond accordingly: ```typescript twoslash import { type Bot, text } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- bot.onMention = async (session, message) => { const follows = await session.follows(message.actor); await session.publish( follows ? text`Hi ${message.actor}, I'm already following you!` : text`Hi ${message.actor}, I don't follow you yet.` ); }; ``` > \[!TIP] > Like other methods, `~Session.follows()` accepts several types of arguments > such as `Actor`, `string`, and `URL`. > \[!NOTE] > This method returns `false` if the given actor doesn't exist or is > inaccessible. --- --- url: /deploy/store-mq.md description: >- Learn how to configure the key–value store, message queue, and repository that back your BotKit bot, and how to choose backends for production. --- # Storage and message queue A BotKit bot relies on three pieces of backing infrastructure, each configured through an option on [`createBot()`](../concepts/bot.md#instantiation) or [`createInstance()`](../concepts/instance.md): [`kv`](../concepts/bot.md#createbotoptions-kv) : A key–value store that Fedify uses for federation internals, such as the bot's cryptographic keys and caches of remote objects. This one is required. [`queue`](../concepts/bot.md#createbotoptions-queue) : A message queue that processes incoming and outgoing activities in the background. It is optional during development but expected in production, where you don't want activity delivery to block HTTP responses. [`repository`](../concepts/bot.md#createbotoptions-repository) : A store for the bot's own data: its posts, followers, followees, sent follow requests, and poll votes. It is optional and, when omitted, defaults to a [`KvRepository`](../concepts/repository.md#kvrepository) layered on the same key–value store. This guide covers the choices for each and recommends pairings for production. For the repository API in full, see the [*Repository* concept chapter](../concepts/repository.md). ## Key–value stores Key–value stores are used to store persistent data for your bot, such as messages, followers, and followees. Usually you would want to pair a key–value store with the main database for your application. BotKit supports the following key–value store implementations: ### [Deno KV] (Deno Deploy) [Deno KV] is the simplest option when running on [Deno Deploy]. It's built into the Deno runtime with no additional infrastructure needed, provides automatic replication on Deno Deploy, and supports ACID transactions. However, it's only available in Deno environments, has limited querying capabilities, and size limits per value (64KB on Deno Deploy). ```typescript import { DenoKvStore } from "@fedify/denokv"; const kv = await Deno.openKv(); const bot = createBot({ username: "mybot", kv: new DenoKvStore(kv), // ... other configuration }); ``` Since [`DenoKvStore`] is provided by [Fedify], you need to install the *@fedify/denokv* package to use it: ```sh [Deno] deno add jsr:@fedify/denokv ``` [Deno KV]: https://deno.land/manual/runtime/kv [Deno Deploy]: https://deno.com/deploy [`DenoKvStore`]: https://fedify.dev/manual/kv#denokvstore-deno-only [Fedify]: https://fedify.dev/ ### [SQLite] [SQLite] is a good choice for local development and testing, as well as for small-scale production deployments. It's lightweight and easy to set up, provides ACID compliance and transaction support, making it excellent for development and testing environments. However, it's not suitable for high-concurrency production use and has limited scalability. ```typescript twoslash import { createBot } from "@fedify/botkit"; import { SqliteKvStore } from "@fedify/sqlite"; import { DatabaseSync } from "node:sqlite"; const sqlite = new DatabaseSync("bot-data.db"); const bot = createBot({ username: "mybot", kv: new SqliteKvStore(sqlite), }); ``` You need to install the *@fedify/sqlite* package to use the [`SqliteKvStore`]: ::: code-group ```sh [Deno] deno add jsr:@fedify/sqlite ``` ```sh [npm] npm add @fedify/sqlite ``` ```sh [pnpm] pnpm add @fedify/sqlite ``` ```sh [Yarn] yarn add @fedify/sqlite ``` ::: [SQLite]: https://www.sqlite.org/ [`SqliteKvStore`]: https://fedify.dev/manual/kv#sqlitekvstore ### [Redis] or [Valkey] [Redis] (or its open source fork [Valkey]) is recommended for production deployments needing high performance. It offers excellent performance, clustering support, and wide hosting options, making it ideal for scalable production environments. ::: code-group ```typescript [Deno] twoslash import { createBot } from "@fedify/botkit"; import { RedisKvStore } from "@fedify/redis"; import { Redis } from "ioredis"; const redis = new Redis({ host: Deno.env.get("REDIS_HOST"), port: parseInt(Deno.env.get("REDIS_PORT") ?? "6379"), password: Deno.env.get("REDIS_PASSWORD"), tls: Deno.env.get("REDIS_TLS") === "true" ? {} : undefined, }); const bot = createBot({ username: "mybot", kv: new RedisKvStore(redis), }); ``` ```typescript [Node.js] twoslash import { createBot } from "@fedify/botkit"; import { RedisKvStore } from "@fedify/redis"; import { Redis } from "ioredis"; const redis = new Redis({ host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT ?? "6379"), password: process.env.REDIS_PASSWORD, tls: process.env.REDIS_TLS === "true" ? {} : undefined, }); const bot = createBot({ username: "mybot", kv: new RedisKvStore(redis), }); ``` ::: You need to install the *@fedify/redis* package to use the [`RedisKvStore`]: ::: code-group ```sh [Deno] deno add jsr:@fedify/redis ``` ```sh [npm] npm add @fedify/redis ``` ```sh [pnpm] pnpm add @fedify/redis ``` ```sh [Yarn] yarn add @fedify/redis ``` ::: [Redis]: https://redis.io/ [Valkey]: https://valkey.io/ [`RedisKvStore`]: https://fedify.dev/manual/kv#rediskvstore ### [PostgreSQL] [PostgreSQL] is suitable for deployments needing complex queries or transactions. It provides ACID compliance, complex query support, robust backup solutions, and a mature ecosystem, making it an excellent choice when you need advanced database features. ::: code-group ```typescript [Deno] twoslash import { createBot } from "@fedify/botkit"; import { PostgresKvStore } from "@fedify/postgres"; import postgres from "postgres"; const sql = postgres(Deno.env.get("DATABASE_URL")!); const bot = createBot({ username: "mybot", kv: new PostgresKvStore(sql), }); ``` ```typescript [Node.js] twoslash import { createBot } from "@fedify/botkit"; import { PostgresKvStore } from "@fedify/postgres"; import postgres from "postgres"; const sql = postgres(process.env.DATABASE_URL!); const bot = createBot({ username: "mybot", kv: new PostgresKvStore(sql), }); ``` ::: You need to install the *@fedify/postgres* package to use the [`PostgresKvStore`]: ::: code-group ```sh [Deno] deno add jsr:@fedify/postgres ``` ```sh [npm] npm add @fedify/postgres ``` ```sh [pnpm] pnpm add @fedify/postgres ``` ```sh [Yarn] yarn add @fedify/postgres ``` ::: [PostgreSQL]: https://www.postgresql.org/ [`PostgresKvStore`]: https://fedify.dev/manual/kv#postgreskvstore ## Message queues Message queues are used to handle background tasks, such as sending messages and processing incoming activities. Usually you would want to pair a message queue with a key–value store for a compact and complete backend solution. ### [Deno KV Queue] Built on top of [Deno KV] and available in Deno runtimes. It needs no additional infrastructure and pairs naturally with a `DenoKvStore`, though it has limited throughput compared to dedicated message queue solutions. > \[!IMPORTANT] > Deno KV queues work only with a local Deno KV database, such as on a > self-hosted Deno process. The rebuilt [Deno Deploy] does not support KV > queues, so use a dedicated queue (or none) there instead; see the > [*Deno Deploy*](./deno-deploy.md) guide. ```typescript import { DenoKvMessageQueue } from "@fedify/denokv"; const kv = await Deno.openKv(); const bot = createBot({ username: "mybot", kv: new DenoKvStore(kv), queue: new DenoKvMessageQueue(kv), }); ``` Since [`DenoKvMessageQueue`] is provided by [Fedify], you need to install the *@fedify/denokv* package to use it: ```sh [Deno] deno add jsr:@fedify/denokv ``` [Deno KV Queue]: https://docs.deno.com/examples/queues/ [`DenoKvMessageQueue`]: https://fedify.dev/manual/mq#denokvmessagequeue-deno-only ### [Redis] or [Valkey] Recommended for production deployments, offering high performance, reliable message delivery, the ability to share infrastructure with your key–value store, and good monitoring tools. ::: code-group ```typescript [Deno] twoslash import { createBot } from "@fedify/botkit"; import { RedisKvStore, RedisMessageQueue } from "@fedify/redis"; import { Redis } from "ioredis"; function getRedis(): Redis { return new Redis({ host: Deno.env.get("REDIS_HOST"), port: parseInt(Deno.env.get("REDIS_PORT") ?? "6379"), password: Deno.env.get("REDIS_PASSWORD"), tls: Deno.env.get("REDIS_TLS") === "true" ? {} : undefined, }); } const bot = createBot({ username: "mybot", kv: new RedisKvStore(getRedis()), queue: new RedisMessageQueue(getRedis), }); ``` ```typescript [Node.js] twoslash import { createBot } from "@fedify/botkit"; import { RedisKvStore, RedisMessageQueue } from "@fedify/redis"; import { Redis } from "ioredis"; function getRedis(): Redis { return new Redis({ host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT ?? "6379"), password: process.env.REDIS_PASSWORD, tls: process.env.REDIS_TLS === "true" ? {} : undefined, }); } const bot = createBot({ username: "mybot", kv: new RedisKvStore(getRedis()), queue: new RedisMessageQueue(getRedis), }); ``` ::: You need to install the *@fedify/redis* package to use the [`RedisMessageQueue`]: ::: code-group ```sh [Deno] deno add jsr:@fedify/redis ``` ```sh [npm] npm add @fedify/redis ``` ```sh [pnpm] pnpm add @fedify/redis ``` ```sh [Yarn] yarn add @fedify/redis ``` ::: [`RedisMessageQueue`]: https://fedify.dev/manual/mq#redismessagequeue ### [PostgreSQL] Suitable when already using [PostgreSQL] for storage. It provides ACID compliance, can share infrastructure with your key–value store, offers good long-term persistence, and supports transactions, making it ideal when you want to consolidate your backend infrastructure. ::: code-group ```typescript [Deno] twoslash import { createBot } from "@fedify/botkit"; import { PostgresKvStore, PostgresMessageQueue } from "@fedify/postgres"; import postgres from "postgres"; const sql = postgres(Deno.env.get("DATABASE_URL")!); const bot = createBot({ username: "mybot", kv: new PostgresKvStore(sql), queue: new PostgresMessageQueue(sql), }); ``` ```typescript [Node.js] twoslash import { createBot } from "@fedify/botkit"; import { PostgresKvStore, PostgresMessageQueue } from "@fedify/postgres"; import postgres from "postgres"; const sql = postgres(process.env.DATABASE_URL!); const bot = createBot({ username: "mybot", kv: new PostgresKvStore(sql), queue: new PostgresMessageQueue(sql), }); ``` ::: You need to install the *@fedify/postgres* package to use the [`PostgresMessageQueue`]: ::: code-group ```sh [Deno] deno add jsr:@fedify/postgres ``` ```sh [npm] npm add @fedify/postgres ``` ```sh [pnpm] pnpm add @fedify/postgres ``` ```sh [Yarn] yarn add @fedify/postgres ``` ::: [`PostgresMessageQueue`]: https://fedify.dev/manual/mq#postgresmessagequeue ## Repositories The key–value store and message queue back Fedify's federation layer. BotKit keeps its own data, the bot's posts, followers, followees, sent follow requests, and poll votes, in a *repository*. When you omit the [`repository`](../concepts/bot.md#createbotoptions-repository) option, BotKit wraps your key–value store in a [`KvRepository`](../concepts/repository.md#kvrepository), so the default persists exactly as durably as the `kv` backend you chose above. You only need to set `repository` explicitly when you want a different trade-off. > \[!NOTE] > Choosing a dedicated repository does not remove the need for a key–value > store. Fedify still uses `kv` for federation internals, so it stays required > whichever repository you run. The [*Repository* concept chapter](../concepts/repository.md) documents every class and its options. For deployment, the practical question is which one to run: [`KvRepository`](../concepts/repository.md#kvrepository) (default) : Stores everything through the key–value store you already configured. When that store is durable, such as Deno KV, Redis, or PostgreSQL, this needs no extra setup and is a sound production choice. [`SqliteRepository`](../concepts/repository.md#sqliterepository) : Keeps bot data in a local SQLite file with write-ahead logging. It suits a single-machine deployment where you would rather not run a separate database server. Provided by the *@fedify/botkit-sqlite* package. [`PostgresRepository`](../concepts/repository.md#postgresrepository) : Stores bot data in PostgreSQL tables under a dedicated schema (named `botkit` by default). Reach for it when several bot processes share one persistent store, or when you already operate PostgreSQL. Provided by the *@fedify/botkit-postgres* package. [`RedisRepository`](../concepts/repository.md#redisrepository) : Stores bot data directly in Redis data structures. Like `PostgresRepository`, it fits deployments that span several processes, and it is convenient when Redis is already part of your stack. Provided by the *@fedify/botkit-redis* package. A fourth class, [`MemoryCachedRepository`](../concepts/repository.md#memorycachedrepository), wraps any of the above with an in-memory cache that trades memory for lower read latency. It changes performance, not durability. For a single-machine bot, SQLite can cover both roles without an external service. Point the key–value store and the repository at separate files so they don't contend for the same database lock: ```typescript twoslash import { createBot } from "@fedify/botkit"; import { SqliteKvStore } from "@fedify/sqlite"; import { SqliteRepository } from "@fedify/botkit-sqlite"; import { DatabaseSync } from "node:sqlite"; const bot = createBot({ username: "mybot", kv: new SqliteKvStore(new DatabaseSync("federation.db")), repository: new SqliteRepository({ path: "bot-data.db" }), }); ``` Install the package for whichever repository you choose. For the SQLite example above: ::: code-group ```sh [Deno] deno add jsr:@fedify/botkit-sqlite ``` ```sh [npm] npm add @fedify/botkit-sqlite ``` ```sh [pnpm] pnpm add @fedify/botkit-sqlite ``` ```sh [Yarn] yarn add @fedify/botkit-sqlite ``` ::: --- --- url: /concepts/text.md description: >- The Text object is a mini-language for representing rich text formatting commonly used in the fediverse. Learn how to format your text using the Text object. --- # Text The `Text` object is a mini-language for representing rich text formatting commonly used in the fediverse. For example, you can [emphasize](#emphases) some part of the text, or include a [mention](#mentions) to another fediverse account. ## Blocks and inlines The `Text` object consists of two types of elements: blocks and inlines. Type-wise they are represented by the `Text<"block">` and `Text<"inline">`.\[^1] Blocks are usually used for [paragraphs](#paragraphs), and inlines are used for formatting constructs like [emphases](#emphases) or [links](#links). The distinction between blocks and inlines is important because some formatting constructs are only allowed in blocks or inlines. For example, you cannot include a paragraph inside an emphasis construct. Since the concept of blocks and inlines corresponds to the same concept in the HTML, you can think of them as the `
` and `` elements in the HTML, respectively. The parameters that take the `Text` object, such as `Session.publish()` method or [`createBot()` function's `summary` parameter](./bot.md#createbotoptions-summary), are usually of the type `Text<"block">`. The simplest way to create a `Text<"block">` object is to use the `text()` template string tag, which we will discuss in the right next section. \[^1]: More precisely, the `Text` type has two type parameters: the first one is the type of the element: `"block"` or `"inline"`, and the second one is [`TContextData`], the [Fedify context data]. [`TContextData`]: https://fedify.dev/manual/federation#tcontextdata [Fedify context data]: https://fedify.dev/manual/context ## Template string tag First of all, BotKit provides a template string tag `text()` to format your text. Here's how you can use it: ```typescript twoslash import { text } from "@fedify/botkit"; const yourText = text`Your text goes here!`; // [!code highlight] ``` For example, if you want to publish a [message](./message.md) with the text Hello, **world**! to the fediverse, you can write: ```typescript twoslash // @noErrors: 2307 import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- import { strong, text } from "@fedify/botkit"; import { bot } from "./bot.ts"; // A hypothetical bot object const session = bot.getSession("https://mydomain"); await session.publish( text`Hello, ${strong("world")}!` // [!code highlight] ); ``` As you can see in the example above, you can compose other `Text` objects together using the template string tag. In this document, we will discuss various formatting constructs that you can use in the `Text` object. ## Interpolation You can put any JavaScript object inside the `${}` interpolation. Those objects will be expanded or converted to a string according to their types: ### `Text` object If you put another `Text` object inside the interpolation, it will be concatenated to the parent `Text` object. For example: ```typescript twoslash import { text, em } from "@fedify/botkit"; // ---cut-before--- text`Hello, ${em("world")}.` ``` The above code will create a text like this: > Hello, *world*. There are other formatting constructs that you can use in the `Text` object. See the below sections for more information. > \[!NOTE] > Although you can put a block object, it will close the current paragraph and > start a new block. For example: > > ```typescript twoslash > import { text } from "@fedify/botkit"; > // ---cut-before--- > text`Hello! ${text`This is a new paragraph.`}` > ``` > > The above code will create two paragraphs like this: > > > Hello! > > > > This is a new paragraph. > > If you put a block object at the boundary of the block, it will work as > expected. For example: > > ```typescript twoslash > import { text } from "@fedify/botkit"; > // ---cut-before--- > text`Hello!\n\n${text`This is a new paragraph.`}\n\nThis is the last paragraph.` > ``` > > The above code will create three paragraphs like this: > > > Hello! > > > > This is a new paragraph. > > > > This is the last paragraph. ### `Actor` object If you put an `Actor` object (provided by Fedify) inside the interpolation, it will be rendered as a mention. For example: ```typescript twoslash import { type Message, type MessageClass, text } from "@fedify/botkit"; const message = {} as unknown as Message; // ---cut-before--- text`Hello, ${message.actor}.` ``` The above code will create a text like this: > Hello, [@fedify@hollo.social]. [@fedify@hollo.social]: https://hollo.social/@fedify ### [`Emoji`] *This API is available since BotKit 0.2.0.* If you put an [`Emoji`] object (provided by Fedify) inside the interpolation, it will be rendered as a custom emoji. You usually get [`Emoji`] objects from the `Reaction.emoji` property. For example: ```typescript twoslash import { type Reaction, text } from "@fedify/botkit"; const reaction = {} as unknown as Reaction; // ---cut-before--- text`Here's a custom emoji: ${reaction.emoji}.` ``` [`Emoji`]: https://jsr.io/@fedify/fedify/doc/~/Emoji ### `URL` object If you put a `URL` object inside the interpolation, it will be rendered as a link. For example: ```typescript twoslash import { text } from "@fedify/botkit"; // ---cut-before--- text`Here's a link: ${new URL("https://botkit.fedify.dev/")}.` ``` The above code will create a text like this: > Here's a link: . ### Anything else If you put any other JavaScript object inside the interpolation, it will be converted to a string—it's the same as calling `String()` on the object. For example: ```typescript twoslash import { text } from "@fedify/botkit"; // ---cut-before--- text`The number is ${42}.` ``` The above code will create a text like this: > The number is 42. If an interpolated string has line breaks, they will be preserved in the text. For example: ```typescript twoslash import { text } from "@fedify/botkit"; // ---cut-before--- text`Here's a multiline text: ${"First line.\nSecond line."}` ``` The above code will create a text like this: > Here's a multiline text: > > First line.\ > Second line. > \[!NOTE] > Even if you put an HTML string inside the interpolation, it will be escaped > automatically: > > ```typescript twoslash > import { text } from "@fedify/botkit"; > // ---cut-before--- > text`The following HTML will be escaped: ${"bold"}.` > ``` > > The above code will create a text like this: > > > The following HTML will be escaped: \bold\. > > Only way to format the text is to use the formatting constructs described in > the following sections. ## Paragraphs The `text()` template string tag creates a block `Text` object. You can create a paragraph by simply writing a text. If you want to create multiple paragraphs, you can split them with two or more consecutive line breaks. For example: ```typescript twoslash import { text } from "@fedify/botkit"; // ---cut-before--- text`This is the first paragraph. This is the second paragraph.\n\nThis is the last paragraph.` ``` The above code will create three paragraphs like this: > This is the first paragraph. > > This is the second paragraph. > > This is the last paragraph. Paragraphs are separated by the `

` HTML element. ## Hard line breaks If you want to insert a hard line break, put a single line break (`\n`) between the lines. For example: ```typescript twoslash import { text } from "@fedify/botkit"; // ---cut-before--- text`This is the first line of the first paragraph. This is the second line of the first paragraph. This is the second paragraph.` ``` The above code will create two paragraphs like this: > This is the first line of the first paragraph.\ > This is the second line of the first paragraph. > > This is the second paragraph. It corresponds to the `
` HTML element. ## Emphases BotKit provides two kinds of emphasis: `strong()` emphasizes which is usually rendered as **bold**, and `em()` emphasizes which is usually rendered as *italic*. Both are inlines, so you can put them inside the interpolation. For example: ```typescript twoslash import { em, strong, text } from "@fedify/botkit"; // ---cut-before--- text`You can emphasize ${strong("this")} or ${em("this")}!` ``` The above code will create a text like this: > You can emphasize **this** or *this*! You can nest the emphasis constructs: ```typescript twoslash import { em, strong, text } from "@fedify/botkit"; // ---cut-before--- text`You can emphasize ${strong(em("this"))}!` ``` The above code will create a text like this: > You can emphasize ***this***! ## Links You can make a link to a URL by using the `link()` function. It returns an inline `Text` object that represents a link. For example: ```typescript twoslash import { link, text } from "@fedify/botkit"; // ---cut-before--- text`Here's a link: ${link("https://fedify.dev/")}.` ``` The above code will create a text like this: > Here's a link: . You can customize the label of the link if you provide two arguments: ```typescript twoslash import { link, text } from "@fedify/botkit"; // ---cut-before--- text`Here's a link: ${link("Fedify", "https://fedify.dev/")}.` ``` The above code will create a text like this: > Here's a link: [Fedify]. The label can have other formatting constructs: ```typescript twoslash import { em, link, text } from "@fedify/botkit"; // ---cut-before--- text`Here's a link: ${link(em("Fedify"), "https://fedify.dev/")}.` ``` The above code will create a text like this: > Here's a link: [*Fedify*][_Fedify_]. [Fedify]: https://fedify.dev/ [_Fedify_]: https://fedify.dev/ ## Mentions You can mention another fediverse account by using the `mention()` function. It returns an inline `Text` object that represents a mention. For example: ```typescript twoslash import { mention, text } from "@fedify/botkit"; // ---cut-before--- text`Hello, ${mention("@fedify@hollo.social")}!` ``` The above code will create a text like this: > Hello, [@fedify@hollo.social]! Or you can mention an account by its actor URI: ```typescript twoslash import { mention, text } from "@fedify/botkit"; // ---cut-before--- text`Hello, ${mention(new URL("https://hollo.social/@fedify"))}!` ``` The result is equivalent to the previous example: > Hello, [@fedify@hollo.social]! You can customize the label of the mention: ```typescript twoslash import { mention, text } from "@fedify/botkit"; // ---cut-before--- text`Hello, ${mention("Fedify", new URL("https://hollo.social/@fedify"))}!` ``` The above code will create a text like this: > Hello, [Fedify]! > \[!NOTE] > The `mention()` construct does not only format the text but also notifies > the mentioned account. The mentioned account will receive a notification > about the mention. If you want to just link to the account without > notifying, use the `link()` construct instead. ## Hashtags You can include a hashtag in the text using the `hashtag()` function. It is an inline construct. For example: ```typescript twoslash import { hashtag, text } from "@fedify/botkit"; // ---cut-before--- text`Here's a hashtag: ${hashtag("#BotKit")}.` ``` The above code will create a text like this: > Here's a hashtag: [#BotKit]. It does not matter if you put the leading `"#"` or not. The `hashtag()` function will add the leading `"#"` if it is not present. For example: ```typescript twoslash import { hashtag, text } from "@fedify/botkit"; // ---cut-before--- text`Here's a hashtag: ${hashtag("BotKit")}.` ``` The result is equivalent to the previous example: > Here's a hashtag: [#BotKit]. > \[!NOTE] > The `hashtag()` function does not only format the hashtag but also denotes > the hashtag so that ActivityPub software can recognize it as a hashtag. > The hashtag will be searchable in the fediverse (some software may search it > only from public messages though). If you want to just link to the hashtag > without denoting it, use the `link()` construct instead. [#BotKit]: https://mastodon.social/tags/botkit ## Code You can include a code in the text using the `code()` function, which is usually rendered as monospaced font. It is an inline construct. For example: ```typescript twoslash import { code, text } from "@fedify/botkit/text"; // ---cut-before--- text`Here's a code: ${code("console.log('Hello, world!')")}.` ``` The above code will create a text like this: > Here's a code: `console.log('Hello, world!')`. > \[!CAUTION] > It is not a code block, but an inline code. ## Custom emojis *This API is available since BotKit 0.2.0.* You can include a custom emoji in the text using the `customEmoji()` function. It is an inline construct. In order to use the `customEmoji()` function, you need to define custom emojis first. You can define custom emojis by using the `Bot.addCustomEmojis()` method after creating the bot: ```typescript twoslash import type { Bot } from "@fedify/botkit"; const bot = {} as unknown as Bot; // ---cut-before--- // Define custom emojis: const emojis = bot.addCustomEmojis({ // Use a local image file: botkit: { file: `${import.meta.dirname}/botkit.png`, type: "image/png" }, // Use a remote image URL: fedify: { url: "https://fedify.dev/logo.png", type: "image/png" }, }); ``` The `~Bot.addCustomEmojis()` method returns an object that contains the custom emojis. You can use the keys of the object to refer to the custom emojis. For example: ```typescript twoslash import { customEmoji, text } from "@fedify/botkit/text"; import type { DeferredCustomEmoji } from "@fedify/botkit/emoji"; const emojis = {} as Readonly>>; // ---cut-before--- text`Here's a custom emoji: ${customEmoji(emojis.botkit)} by ${customEmoji(emojis.fedify)}.` ``` ## Markdown > \[!NOTE] > The `markdown()` function does not support raw HTML syntax. Sometimes you have a Markdown text and want to render it as a `Text` object. You can use the `markdown()` function to convert the Markdown text to the `Text` object. It is a block construct. For example: ```typescript twoslash import { markdown } from "@fedify/botkit/text"; // ---cut-before--- markdown(` Here's a Markdown text. - I can have a list. - I can have a **bold** text. - I can have an _italic_ text. `) ``` The above code will create a text like this: > Here's a Markdown text. > > * I can have a list. > * I can have a **bold** text. > * I can have an *italic* text. You can also put the `markdown()` function inside the interpolation: ```typescript twoslash import { markdown, text } from "@fedify/botkit/text"; // ---cut-before--- text`The following is a Markdown text: ${markdown(` Here's a Markdown text. - I can have a list. - I can have a **bold** text. - I can have an _italic_ text. `)} ` ``` The above code will create a text like this: > The following is a Markdown text: > > Here's a Markdown text. > > * I can have a list. > * I can have a **bold** text. > * I can have an *italic* text. Besides the standard Markdown syntax, the `markdown()` function also supports mentioning and hashtag syntax for the fediverse. ### Mentions The following example shows how to mention an account: ```typescript twoslash import { markdown } from "@fedify/botkit/text"; // ---cut-before--- markdown(`Hello, @fedify@hollo.social!`) ``` The above code will create a text like this: > Hello, [@fedify@hollo.social]! > \[!NOTE] > The `markdown()` function does not only format the mention but also notifies > the mentioned account. The mentioned account will receive a notification > about the mention. If you want to just link to the account without > notifying, use the normal link syntax instead: > > ```typescript twoslash > import { markdown } from "@fedify/botkit/text"; > // ---cut-before--- > markdown(`Hello, [@fedify@hollo.social](https://hollo.social/@fedify)!`) > ``` If you want `@`-syntax to be treated as a normal text, turn off the syntax by setting the `mentions` option to `false`: ```typescript twoslash import { markdown } from "@fedify/botkit/text"; // ---cut-before--- markdown(`Hello, @fedify@hollo.social!`, { mentions: false }) ``` The above code will create a text like this: > Hello, @fedify@hollo.social! ### Hashtags The following example shows how to include a hashtag: ```typescript twoslash import { markdown } from "@fedify/botkit/text"; // ---cut-before--- markdown(`Here's a hashtag: #BotKit`) ``` The above code will create a text like this: > Here's a hashtag: [#BotKit]. > \[!NOTE] > The `markdown()` function does not only format the hashtag but also denotes > the hashtag so that ActivityPub software can recognize it as a hashtag. > The hashtag will be searchable in the fediverse (some software may search it > only from public messages though). If you want to just link to the hashtag > without denoting it, use the normal link syntax instead: > > ```typescript twoslash > import { markdown } from "@fedify/botkit/text"; > // ---cut-before--- > markdown(`Here's a hashtag: [#BotKit](https://mastodon.social/tags/botkit)`) > ``` If you want `#`-syntax to be treated as a normal text, turn off the syntax by setting the `hashtags` option to `false`: ```typescript twoslash import { markdown } from "@fedify/botkit/text"; // ---cut-before--- markdown(`Here's a hashtag: #BotKit`, { hashtags: false }) ``` The above code will create a text like this: > Here's a hashtag: #BotKit. ## Determining if the text mentions an account You can determine if the text mentions an account by using the `mentions()` function. It returns `true` if the text mentions the account, otherwise `false`: ```typescript twoslash import type { Session } from "@fedify/botkit"; /** A hypothetical function that returns an Actor object. **/ declare function getActor(handle: string): Actor; const session = {} as unknown as Session; // ---cut-before--- import type { Actor } from "@fedify/botkit"; import { markdown, mention, mentions, text } from "@fedify/botkit/text"; const actor: Actor = getActor( // A hypothetical function that returns an Actor object "@fedify@hollo.social" ); const actor2: Actor = getActor("@another@example.com"); const md = markdown("Hello, @fedify@hollo.social!"); console.log(await mentions(session, md, actor)); // true console.log(await mentions(session, md, actor2)); // false const txt = text`Hi, ${actor2}!` console.log(await mentions(session, txt, actor)); // false console.log(await mentions(session, txt, actor2)); // true const noMention = text`Hello, world!`; console.log(await mentions(session, noMention, actor)); // false console.log(await mentions(session, noMention, actor2)); // false ``` --- --- url: /intro.md --- # What is BotKit? BotKit is a TypeScript framework for building [ActivityPub] bots that belong to your application. A BotKit bot is not a script driving an account on someone else's Mastodon or Misskey server. It is an ActivityPub actor served by your app, on your domain, backed by your storage, your queue, and your code. It can still talk to Mastodon, Misskey, and the wider [fediverse], but you do not have to provision a social account for every bot identity you want to run. That model matters once a bot becomes part of a product. You may want one bot per project, one bot per region, one bot per workspace, one bot per customer, or a family of bots created from rows in your database. BotKit gives those bots fediverse identities without turning each one into a manually managed account. BotKit is built on [Fedify], which handles the lower-level federation work: actors, ActivityPub objects, WebFinger discovery, signatures, inboxes, outboxes, delivery, queues, and compatibility with other fediverse software. You write bot behavior in TypeScript. BotKit and Fedify handle the protocol machinery. Here is a small weather bot: ```typescript twoslash import { createBot, MemoryKvStore, text } from "@fedify/botkit"; const bot = createBot({ username: "weatherbot", name: "Seoul Weather Bot", summary: text`I post daily weather updates for Seoul!`, kv: new MemoryKvStore(), }); bot.onMention = async (session, message) => { await message.reply( text`Current temperature in Seoul is 18°C with clear skies!` ); }; setInterval(async () => { const session = bot.getSession("https://weather.example.com"); await session.publish( text`Good morning! Today's forecast for Seoul: 🌡️ High: 22°C 💨 Low: 15°C ☀️ Clear skies expected` ); }, 1000 * 60 * 60 * 24); ``` [ActivityPub]: https://activitypub.rocks/ [fediverse]: https://fediverse.info/ [Fedify]: https://fedify.dev/ ## From account automation to bot applications The Mastodon and Misskey APIs are good tools when you want to automate one account. Create the account, get an access token, post updates, reply to mentions. For a personal bot or a small integration, that may be the right shape. BotKit starts where that shape gets tight. If bots are part of your app, you often need to create, configure, route, store, and remove bot identities from application code. A workspace might need its own announcement bot. A monitoring service might expose one fediverse actor per system or region. A project hosting service might create a bot for every repository. In those cases, a bot is not really “an account somewhere.” It is one of your application resources. BotKit lets you model it that way. ## Standalone operation BotKit runs your bot as a standalone ActivityPub server. That gives you direct control over the parts that account automation usually leaves inside someone else's platform: * the actor URL and fediverse handle; * the database or repository that stores bot state; * the message queue used for delivery and incoming activities; * the deployment target; * the message size limits and other product rules. The bot still has to follow fediverse protocols and good federation behavior. Standalone does not mean isolated. It means the bot is served by your application instead of being hosted as an account on another server. ## One application, many bots A BotKit `Instance` can host several bots on one domain. Each bot has its own actor identity, handle, collections, profile, and event handlers. The instance owns the shared infrastructure: the key–value store, repository, message queue, and HTTP handling. You can declare static bots up front: ```typescript twoslash import { createInstance, MemoryKvStore } from "@fedify/botkit"; const instance = createInstance({ kv: new MemoryKvStore(), }); const greetBot = instance.createBot("greet", { username: "greetbot", name: "Greeting Bot", }); const echoBot = instance.createBot("echo", { username: "echobot", name: "Echo Bot", }); export default instance; ``` You can also use dynamic bot groups, where BotKit resolves a bot on demand from your database. That is the model for “one bot per region,” “one bot per project,” or “one bot per customer” without declaring every possible bot in source code. ```typescript twoslash import { createInstance, MemoryKvStore, text } from "@fedify/botkit"; declare const db: { findCity(code: string): Promise<{ name: string } | null>; }; // ---cut-before--- const instance = createInstance({ kv: new MemoryKvStore(), }); const weatherBots = instance.createBot(async (_ctx, identifier) => { if (!identifier.startsWith("weather_")) return null; const city = await db.findCity(identifier.slice("weather_".length)); if (city == null) return null; return { username: identifier, name: `${city.name} Weather Bot`, }; }); weatherBots.onMention = async (session, message) => { const code = session.bot.identifier.slice("weather_".length); await message.reply(text`Weather for ${code}: sunny`); }; ``` See [*Instance*](./concepts/instance.md) for the full model. ## Events as TypeScript handlers BotKit turns fediverse activity into TypeScript callbacks. You can handle mentions, replies, follows, unfollows, direct messages, quotes, quote requests, poll votes, and emoji reactions without writing ActivityPub routing code by hand. ```typescript twoslash import type { Bot } from "@fedify/botkit"; import { text } from "@fedify/botkit"; declare const bot: Bot; // ---cut-before--- bot.onFollow = async (session, follower) => { await session.publish( text`Thanks for following me, ${follower}!`, { visibility: "direct" }, ); }; bot.onReact = async (session, reaction) => { await reaction.message.reply( text`Thanks for reacting with ${reaction.emoji}!`, ); }; ``` Handlers receive typed objects, not raw JSON blobs. The protocol details still matter, but they do not have to dominate your bot code. See [*Events*](./concepts/events.md) for the available handlers. ## TypeScript all the way down BotKit is written for TypeScript applications. Event handlers receive typed objects, message builders expose typed options, and sessions carry the context type you choose for your bots. That does not mean TypeScript can prove that every remote server will accept a message or support every fediverse feature. Federation still happens at runtime, across different implementations. What the types do give you is a firmer boundary inside your own app: bot behavior, message composition, session context, and multi-bot routing can be checked before the code runs. ```typescript twoslash import type { Session } from "@fedify/botkit"; import { createBot, MemoryKvStore, text } from "@fedify/botkit"; interface AppContext { readonly tenantId: string; } const bot = createBot({ username: "support", name: "Support Bot", kv: new MemoryKvStore(), }); async function postWelcome(session: Session) { await session.publish(text`Welcome to ${session.actorHandle}!`); } bot.onFollow = async (session) => { await postWelcome(session); }; const session = bot.getSession( "https://support.example", { tenantId: "acme" }, ); await postWelcome(session); ``` ## Messages with fediverse features BotKit messages are built with the `text` template tag, which escapes HTML and knows how to include mentions, hashtags, links, and custom emoji. You can attach media, update or delete posts, set visibility, open polls, send emoji reactions, and publish quote posts. Quotes include both Misskey-style quote posts and consent-respecting quote policies based on [FEP-044f]. That matters because fediverse software does not all treat quotes the same way. BotKit gives you APIs for the feature while keeping quote permission part of the model. ```typescript twoslash import type { Session } from "@fedify/botkit"; import { hashtag, Image, text } from "@fedify/botkit"; declare const session: Session; declare const url: URL; // ---cut-before--- await session.publish( text`New chart is up! ${hashtag("BotKit")}`, { attachments: [new Image({ url, mediaType: "image/png" })], visibility: "public", }, ); ``` See [*Message*](./concepts/message.md) for publishing, editing, polls, reactions, and quotes. [FEP-044f]: https://w3id.org/fep/044f ## Your storage, your queue, your deployment BotKit is meant to fit inside your infrastructure. It can use Deno KV, SQLite, Redis or Valkey, PostgreSQL, and custom backends through its repository and queue abstractions. A multi-bot instance can store data for all hosted bots in one repository while keeping each bot's data scoped by identifier. That makes bot state part of the same operational world as the rest of your app: backups, migrations, admin tools, logs, and deployment rules. You are not coordinating with an external social account from the outside. You are running federated actors as part of your own system. BotKit supports Deno, Node.js, Cloudflare Workers, Deno Deploy, Docker-based deployments, and self-hosted servers. See [*Repository*](./concepts/repository.md), [*Store and message queue*](./deploy/store-mq.md), and [*Deploy*](./deploy/deno-deploy.md) for details. ## When BotKit fits Use BotKit when your bot is more than a single account automation script. It fits best when bot identities come from your app, when several bots should share one deployment, or when storage and delivery need to live under your control. For a personal bot that posts occasional updates from one Mastodon account, the Mastodon API may be enough. For a fediverse bot product, a multi-tenant service, or an app that needs actors of its own, BotKit gives you the more direct model: ActivityPub bots as application resources.