cheat sheet
the most common yaebal patterns on one page.
setup
pnpm add yaebalimport { createBot } from "yaebal";
export const bot = createBot(process.env.BOT_TOKEN!);
await bot.start();handlers
import { createBot } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
bot.command("start", (ctx) => ctx.reply("hi"));
bot.hears(/buy (.+)/, (ctx) => ctx.reply("matched"));
bot.callbackQuery(/^page:(\d+)$/, (ctx) => ctx.answerCallbackQuery());
bot.on("message:text", (ctx) => ctx.text);
bot.on(":photo", (ctx) => ctx.reply("nice photo"));
bot.on("edited_message", (ctx) => ctx.reply("noticed the edit"));| method | use |
|---|---|
command | /start, /help, command args |
hears | text/caption string or regexp matches |
callbackQuery | inline button callbacks |
on("message:text") | filter query with typed narrowing |
on(":photo") | any update carrying a photo — messages, edits, channel posts |
on("edited_message") | an edited message, not the original |
context enrichment
import { createBot } from "yaebal";
const loadUser = async (id: number) => ({ id, name: "somebody" });
const bot = createBot(process.env.BOT_TOKEN!)
.derive(async (ctx) => ({ user: await loadUser(ctx.from!.id) }))
.decorate({ version: "1.0.0" })
.on("message:text", (ctx) => {
ctx.user;
ctx.version;
ctx.text;
});keyboards and callback data
import { createBot, InlineKeyboard, callbackData } from "yaebal";
const vote = callbackData("vote", { id: Number });
const bot = createBot(process.env.BOT_TOKEN!);
bot.command("poll", (ctx) =>
ctx.reply("pick", {
reply_markup: new InlineKeyboard()
.text("yes", vote.pack({ id: 1 }))
.text("no", vote.pack({ id: 2 }))
.build(),
}),
);.build() is optional — InlineKeyboard/Keyboard implement toJSON(), so passing the builder straight to reply_markup works too.
this page always calls it explicitly for clarity.
formatting
import { html, format, bold, link, createBot } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
const user = { name: "Ann" };
bot.command("hi", (ctx) => {
ctx.send(html`<b>hello</b> ${user.name}`);
ctx.send(format`${bold("safe entities")} ${link("docs", "https://yaebal.mom")}`);
});edit, delete, answer
import { createBot } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
bot.callbackQuery(/^refresh$/, async (ctx) => {
await ctx.answerCallbackQuery(); // stop the client's loading spinner first
await ctx.editText({ text: "refreshed @ " + new Date().toISOString() });
});
bot.command("cleanup", (ctx) => ctx.delete());typing a helper outside the chain
import type { MessageContext } from "yaebal";
// types flow only inside the chain — a helper extracted into its own function
// names its context type explicitly. handler contexts are structural supertypes
// of the per-update class, so ctx passes in with no casts.
async function eatMessage(ctx: MessageContext) {
try {
await ctx.delete();
} catch {
/* no rights or older than 48h — fine */
}
}
// needs plugin-added fields? intersect them:
function greet(ctx: MessageContext & { session: { count: number } }) {
return ctx.reply(`hi #${++ctx.session.count}`);
}never type a helper as ctx: unknown and poke it with a structural cast
((ctx as { delete?: () => ... }).delete?.()) — the optional call
silently no-ops on the wrong update kind, hiding exactly the bugs the named type catches at
compile time.
raw api calls
import { createBot } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
// every method is fully typed, generated straight from the Bot API schema —
// reach for this when there's no ctx shortcut yet, or you need a raw result.
await bot.api.call("sendChatAction", { chat_id: 123, action: "typing" });error handling
import { createBot, TelegramError } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
bot.onError((error, ctx) => {
if (error instanceof TelegramError && error.parameters?.retry_after) {
console.warn("rate limited, retry after", error.parameters.retry_after);
return;
}
console.error("update", ctx.update.update_id, "failed:", error);
});see core concepts and hooks & errors.
media
import { createBot, media } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
bot.command("pic", async (ctx) => {
await ctx.sendPhoto(media.url("https://example.com/cat.jpg"));
await ctx.sendDocument(media.path("./report.pdf"));
await ctx.sendPhoto("AgACAgIAAx...");
});sessions and i18n
import { createBot, session, i18n } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!)
.install(session({ initial: () => ({ count: 0 }) }))
.install(i18n({ defaultLocale: "en", locales: { en: { hi: "hello" } } }))
.command("count", (ctx) => ctx.reply(String(++ctx.session.count)));scenes and prompts
import { createBot } from "yaebal";
import { defineScene, scenes } from "@yaebal/scenes";
import { prompt } from "@yaebal/prompt";
const echo = defineScene({
steps: [
async (ctx) => {
if (ctx.scene.firstTime) return ctx.send("say something");
await ctx.send("you said: " + (ctx.text ?? ""));
return ctx.scene.leave();
},
],
});
const bot = createBot(process.env.BOT_TOKEN!)
.install(scenes({ echo }))
.install(prompt())
.command("echo", (ctx) => ctx.scene.enter("echo"))
.command("name", (ctx) => ctx.prompt("what's your name?", (ctx) => ctx.reply("hi, " + (ctx.text ?? ""))));webhooks
import { createBot, webhook } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
export default { fetch: webhook(bot, { secretToken: process.env.WEBHOOK_SECRET }) };testing
import { createTestEnv } from "@yaebal/test";
import { expect, test } from "vitest";
import { bot } from "./bot.js";
test("start", async () => {
const env = createTestEnv(bot);
const user = env.createUser({ firstName: "Linia" });
await user.sendCommand("start");
expect(env.lastApiCall("sendMessage")?.params?.text).toBeDefined();
});which package do I need?
| need | package |
|---|---|
| retry Telegram calls (retry_after, 5xx) | @yaebal/again |
| throttle outgoing sends under Telegram's limits | @yaebal/throttle |
| per-chat state | @yaebal/session |
| multi-step wizards | @yaebal/scenes |
| ask-one-question flows | @yaebal/prompt |
| translated replies | @yaebal/i18n |
| mass messaging with retry and progress | @yaebal/broadcast |
| concurrent long polling | @yaebal/runner |
| in-process bot tests | @yaebal/test |