Skip to content

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.
ts
import {
  createBot,
  customEmoji,
  hashtag,
  Image,
  link,
  mention,
  text,
} from "@fedify/botkit";
import { DenoKvMessageQueue, DenoKvStore } from "@fedify/denokv";

const kv = await Deno.openKv();

const bot = createBot<void>({
  username: "greetbot",
  name: "Greet Bot",
  summary: text`Hi, there! I'm a simple fediverse bot created by ${
    mention("@hongminhee@hollo.social")
  }.`,
  icon: new URL(
    "https://repository-images.githubusercontent.com/913141583/852a1091-14d5-46a0-b3bf-8d2f45ef6e7f",
  ),
  properties: {
    "Source code": link(
      "examples/greet.ts",
      "https://github.com/fedify-dev/botkit/blob/main/examples/greet.ts",
    ),
    "Powered by": link("BotKit", "https://botkit.fedify.dev/"),
  },
  kv: new DenoKvStore(kv),
  queue: new DenoKvMessageQueue(kv),
  behindProxy: true,
  pages: { color: "green" },
});

const emojis = bot.addCustomEmojis({
  botkit: {
    type: "image/png",
    file: `${import.meta.dirname}/../docs/public/favicon-192x192.png`,
  },
});

bot.onFollow = async (session, followRequest) => {
  await session.publish(
    text`Thanks for following me, ${followRequest.follower}! ${
      customEmoji(emojis.botkit)
    }`,
    {
      visibility: "direct",
      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,
        }),
      ],
    },
  );
};

bot.onUnfollow = async (session, follower) => {
  await session.publish(
    text`Goodbye, ${follower}! ${customEmoji(emojis.botkit)}`,
    { visibility: "direct" },
  );
};

bot.onReply = async (session, message) => {
  const botUri = session.actorId.href;
  if (message.mentions.some((a) => a.id?.href === botUri)) return;
  await message.reply(
    text`Thanks for your reply, ${message.actor}! ${
      customEmoji(emojis.botkit)
    }`,
  );
};

bot.onMention = async (_session, message) => {
  await message.reply(text`Hi, ${message.actor}!`);
};

const session = bot.getSession(Deno.env.get("ORIGIN") ?? "http://localhost");
setInterval(async () => {
  const message = await session.publish(
    text`Hi, folks! It's a minutely greeting. It will be deleted in 30 seconds. ${
      customEmoji(emojis.botkit)
    } ${hashtag("greet")}`,
  );
  setTimeout(async () => {
    await message.delete();
  }, 1000 * 30);
}, 1000 * 60);

export default bot;

// cSpell: ignore greetbot

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 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.

tsx
/** @jsx react-jsx */
/** @jsxImportSource hono/jsx */
import { createBot, isActor, Question, text } from "@fedify/botkit";
import { DenoKvMessageQueue, DenoKvStore } from "@fedify/denokv";
import { Hono } from "hono";
import type { FC } from "hono/jsx";
import { getXForwardedRequest } from "x-forwarded-fetch";

const kv = await Deno.openKv();

const bot = createBot<void>({
  username: "otp",
  name: "OTP Bot",
  summary:
    text`This bot provides a simple one-time passcode authentication using emojis.`,
  icon: new URL("https://botkit.fedify.dev/favicon-192x192.png"),
  kv: new DenoKvStore(kv),
  queue: new DenoKvMessageQueue(kv),
});

bot.onVote = async (_session, vote) => {
  const recipient = await kv.get<string>(["recipients", vote.message.id.href]);
  if (recipient?.value !== vote.actor.id?.href) return;
  await kv.set(["votes", vote.message.id.href, vote.option], vote.option, {
    expireIn: 15 * 60 * 1000, // 15 minutes
  });
};

const EMOJI_CODES = [
  "🌈",
  "🌟",
  "🌸",
  "🍀",
  "🍉",
  "🍦",
  "🍿",
  "🎈",
  "🎉",
  "🎨",
  "🐢",
  "🐬",
  "👻",
  "👾",
  "💎",
  "🔥",
];

function generateRandomEmojis(): readonly string[] {
  // Generate a random 16-bit number (except for zero):
  const randomBytes = new Uint8Array(2);
  while (true) {
    crypto.getRandomValues(randomBytes);
    // Regenerate if the number is zero:
    if (randomBytes[0] !== 0 || randomBytes[1] !== 0) break;
  }
  // Turn the 16-bit number into 16 emojis, e.g.,
  // 1000_1000_1001_0000 becomes ["🌟","🍉", "🎉", "🐬"]:
  const emojis: string[] = [];
  for (let i = 0; i < 16; i++) {
    // Get the i-th bit from the random number:
    const bit = (randomBytes[i >> 3] >> (7 - (i & 0b111))) & 1;
    // If the bit is 1, add the corresponding emoji to the array:
    if (bit === 1) emojis.push(EMOJI_CODES[i]);
  }
  return emojis;
}

const Layout: FC = (props) => {
  return (
    <html>
      <head>
        <meta charset="utf-8" />
        <title>OTP bot</title>
        <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.lime.min.css"
        />
      </head>
      <body>
        <main class="container">
          {props.children}
        </main>
      </body>
    </html>
  );
};

const Form: FC = () => {
  return (
    <Layout>
      <hgroup>
        <h1>OTP Demo using BotKit</h1>
        <p>
          This demo shows how to create a simple emoji-based one-time passcode
          authentication using <a href="https://botkit.fedify.dev/">BotKit</a>.
        </p>
      </hgroup>
      <form action="/otp" method="post">
        <fieldset>
          <label>
            Your fediverse handle
            <input
              name="handle"
              type="text"
              placeholder="@username@server.com"
              required
              inputmode="email"
              pattern="^@[^@]+@[^@]+$"
            />
          </label>
        </fieldset>
        <input type="submit" value="Authenticate" />
      </form>
    </Layout>
  );
};

const EmojiCode: FC<
  { handle: string; emojis: readonly string[]; messageId: URL }
> = (
  props,
) => {
  return (
    <Layout>
      <hgroup>
        <h1>A direct message has been sent</h1>
        <p>
          A direct message has been sent to{" "}
          <strong>{props.handle}</strong>. Please choose the emojis below to
          authenticate:
        </p>
      </hgroup>
      <ul style="padding: 0; display: flex; justify-content: center; gap: 1em; margin-top: 2em; margin-bottom: 2em;">
        {props.emojis.map((emoji) => (
          <li key={emoji} style="list-style: none; font-size: 3em;">{emoji}</li>
        ))}
      </ul>
      <form action="/authenticate" method="post">
        <input
          type="hidden"
          name="messageId"
          value={props.messageId.href}
        />
        <input type="submit" value="I chose the emojis above" />
      </form>
    </Layout>
  );
};

const Result: FC<{ authenticated: boolean }> = (props) => {
  return (
    <Layout>
      <hgroup>
        <h1>
          {props.authenticated ? "Authenticated" : "Authentication failed"}
        </h1>
        {props.authenticated
          ? <p>You have successfully authenticated!</p>
          : <p>Authentication failed. Please try again.</p>}
      </hgroup>
    </Layout>
  );
};

const app = new Hono();

app.get("/", (c) => {
  return c.html(<Form />);
});

app.post("/otp", async (c) => {
  const form = await c.req.formData();
  const handle = form.get("handle")?.toString();
  if (handle == null) return c.notFound();
  const emojis = generateRandomEmojis();
  const session = bot.getSession(c.req.url);
  const recipient = await session.context.lookupObject(handle);
  if (!isActor(recipient)) return c.notFound();
  const message = await session.publish(
    text`${recipient} Please choose the only emojis you see in the web page to authenticate:`,
    {
      visibility: "direct",
      class: Question,
      poll: {
        multiple: true,
        options: EMOJI_CODES,
        endTime: Temporal.Now.instant().add({ minutes: 15 }),
      },
    },
  );
  await kv.set(["emojis", message.id.href], emojis, {
    expireIn: 15 * 60 * 1000, // 15 minutes
  });
  await kv.set(["recipients", message.id.href], recipient.id?.href, {
    expireIn: 15 * 60 * 1000, // 15 minutes
  });
  return c.html(
    <EmojiCode handle={handle} emojis={emojis} messageId={message.id} />,
  );
});

app.post("/authenticate", async (c) => {
  const form = await c.req.formData();
  const messageId = form.get("messageId")?.toString();
  if (messageId == null) return c.notFound();
  const key = await kv.get<string[]>(["emojis", messageId]);
  if (key?.value == null) return c.notFound();
  const emojis = new Set(key.value);
  const answer = new Set<string>();
  for await (const entry of kv.list({ prefix: ["votes", messageId] })) {
    if (entry.key.length < 3 || typeof entry.key[2] !== "string") continue;
    answer.add(entry.key[2]);
  }
  const authenticated = answer.size === emojis.size &&
    answer.difference(emojis).size === 0;
  return c.html(<Result authenticated={authenticated} />);
});

export default {
  async fetch(request: Request): Promise<Response> {
    request = await getXForwardedRequest(request);
    const url = new URL(request.url);
    if (
      url.pathname.startsWith("/.well-known/") ||
      url.pathname.startsWith("/ap/")
    ) {
      return await bot.fetch(request);
    }
    return await app.fetch(request);
  },
};

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.
ts
// Two static bots on one instance:
//
//  - @greetbot sends a welcome DM to every new follower and says hi when
//    mentioned.
//  - @echobot echoes back the plain text of every mention.
//
// Run:  deno serve --allow-net --allow-env --unstable-kv static-bots.ts
// Set:  ORIGIN=https://your-domain

import { createInstance, text } from "@fedify/botkit";
import { DenoKvMessageQueue, DenoKvStore } from "@fedify/denokv";

const kv = await Deno.openKv();

const instance = createInstance<void>({
  kv: new DenoKvStore(kv),
  queue: new DenoKvMessageQueue(kv),
  behindProxy: true,
});

// ── @greetbot ─────────────────────────────────────────────────────────────────

const greetBot = instance.createBot("greet", {
  username: "greetbot",
  name: "Greeting Bot",
  summary: text`I send a warm welcome to every new follower!`,
  followerPolicy: "accept",
});

greetBot.onFollow = async (session, followRequest) => {
  await session.publish(
    text`Welcome, ${followRequest.follower}! Thanks for the follow!`,
    { visibility: "direct" },
  );
};

greetBot.onMention = async (_session, message) => {
  await message.reply(text`Hello, ${message.actor}!`);
};

// ── @echobot ──────────────────────────────────────────────────────────────────

const echoBot = instance.createBot("echo", {
  username: "echobot",
  name: "Echo Bot",
  summary: text`Mention me and I'll echo back whatever you say.`,
  followerPolicy: "accept",
});

echoBot.onMention = async (_session, message) => {
  await message.reply(text`Echo: ${message.text}`);
};

export default instance;

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_<code>@your-domain, where <code> 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.

ts
// A group of per-language bots resolved on demand from an in-memory table.
//
// Each bot has the handle @lang_<code>@your-domain where <code> is one of
// the supported BCP 47 language codes.  The dispatcher returns null for any
// identifier it doesn't recognize, so only the codes in LANGUAGES resolve.
//
// Run:  deno serve --allow-net --allow-env --unstable-kv dynamic-bots.ts
// Set:  ORIGIN=https://your-domain

import { createInstance, text } from "@fedify/botkit";
import { DenoKvMessageQueue, DenoKvStore } from "@fedify/denokv";

const kv = await Deno.openKv();

const LANGUAGES: Record<string, { name: string; greeting: string }> = {
  en: { name: "English Bot", greeting: "Hello" },
  ko: { name: "한국어 봇", greeting: "안녕하세요" },
  ja: { name: "日本語ボット", greeting: "こんにちは" },
  es: { name: "Spanish Bot", greeting: "¡Hola" },
  fr: { name: "French Bot", greeting: "Bonjour" },
};

const instance = createInstance<void>({
  kv: new DenoKvStore(kv),
  queue: new DenoKvMessageQueue(kv),
  behindProxy: true,
});

const langBots = instance.createBot((_ctx, identifier) => {
  if (!identifier.startsWith("lang_")) return null;
  const code = identifier.slice("lang_".length);
  const lang = LANGUAGES[code];
  if (lang == null) return null;
  return {
    username: identifier,
    name: lang.name,
    summary: text`I greet you in ${code.toUpperCase()}!`,
    followerPolicy: "accept",
  };
});

langBots.onFollow = async (session, followRequest) => {
  const code = session.bot.identifier.slice("lang_".length);
  const lang = LANGUAGES[code]!;
  await session.publish(
    text`${lang.greeting}, ${followRequest.follower}!`,
    { visibility: "direct" },
  );
};

langBots.onMention = async (session, message) => {
  const code = session.bot.identifier.slice("lang_".length);
  const lang = LANGUAGES[code]!;
  await message.reply(text`${lang.greeting}, ${message.actor}!`);
};

export default instance;

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.
ts
// One instance that combines a static bot and a group of dynamic bots.
//
//  Static:  @announce@your-domain
//           Accepts follows and rebroadcasts every mention as a public post.
//
//  Dynamic: @lang_<code>@your-domain  (en, ko, ja)
//           Greets followers and mentions in the matching language.
//
// Run:  deno serve --allow-net --allow-env --unstable-kv multi-bots.ts
// Set:  ORIGIN=https://your-domain

import { createInstance, text } from "@fedify/botkit";
import { DenoKvMessageQueue, DenoKvStore } from "@fedify/denokv";

const kv = await Deno.openKv();

const instance = createInstance<void>({
  kv: new DenoKvStore(kv),
  queue: new DenoKvMessageQueue(kv),
  behindProxy: true,
});

// ── Static: @announce ─────────────────────────────────────────────────────────

const announceBot = instance.createBot("announce", {
  username: "announce",
  name: "Announcement Bot",
  summary:
    text`Follow me for announcements, or mention me to broadcast a message.`,
  followerPolicy: "accept",
});

announceBot.onMention = async (session, message) => {
  await session.publish(
    text`📣 ${message.actor} says: ${message.text}`,
  );
};

// ── Dynamic: @lang_<code> ─────────────────────────────────────────────────────

const LANGUAGES: Record<string, { name: string; greeting: string }> = {
  en: { name: "English Bot", greeting: "Hello" },
  ko: { name: "한국어 봇", greeting: "안녕하세요" },
  ja: { name: "日本語ボット", greeting: "こんにちは" },
};

const langBots = instance.createBot((_ctx, identifier) => {
  if (!identifier.startsWith("lang_")) return null;
  const code = identifier.slice("lang_".length);
  const lang = LANGUAGES[code];
  if (lang == null) return null;
  return {
    username: identifier,
    name: lang.name,
    summary: text`I greet you in ${code.toUpperCase()}!`,
    followerPolicy: "accept",
  };
});

langBots.onFollow = async (session, followRequest) => {
  const code = session.bot.identifier.slice("lang_".length);
  const lang = LANGUAGES[code]!;
  await session.publish(
    text`${lang.greeting}, ${followRequest.follower}!`,
    { visibility: "direct" },
  );
};

langBots.onMention = async (session, message) => {
  const code = session.bot.identifier.slice("lang_".length);
  const lang = LANGUAGES[code]!;
  await message.reply(text`${lang.greeting}, ${message.actor}!`);
};

export default instance;

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: https://github.com/fedify-dev/fedichatbot.

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.