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/fedify/x/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/fedify/x/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);
  },
};

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.