yaebal meta
the batteries-included entry point — the core engine, the auto-generated
per-update contexts, and the most-used plugins behind a single import. media.path() just works on node, bun and deno, and the same bot runs behind long
polling or a webhook on the edge. for a minimal build, use @yaebal/core directly.
install
pnpm add yaebalquick start
import { Bot, html } from "yaebal";
const bot = new Bot(process.env.BOT_TOKEN!)
.command("start", (ctx) => ctx.send("hi 🐴"))
.on("message:text", (ctx) => ctx.reply(html`you said: <b>${ctx.text}</b>`));
await bot.start(); // long polling — or hand updates to a webhook.on("message:text") narrows the context — inside, ctx.text is a string, not string | undefined. every chain method (command / on / derive / install / …) flows the context type
forward, so plugin-added properties are typed with no casting.
rich, typed contexts
createBot() grafts the auto-generated shortcut methods onto every update — ctx.react, ctx.editText, ctx.pin, … — typed to the matching
update, not just present at runtime.
import { createBot } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.react("🔥")); // MessageContext
bot.on("callback_query:data", (ctx) => ctx.answer("ok")); // CallbackQueryContextwhat's in the box
one import { … } from "yaebal" gives you, ready to use:
| from | exports | what |
|---|---|---|
| core | Bot, Composer, Context, media | the engine, filter queries, transports |
| contexts | createBot, per-update context classes | typed ctx.react / ctx.editText / … |
| keyboard | InlineKeyboard, Keyboard | fluent keyboard builders |
| callback-data | callbackData | typed callback_data pack / unpack |
| fmt | html, md | tagged templates with auto-escaping |
| filters | filters, and, or, not | composable, type-narrowing filters |
| session | session | per-chat state, pluggable storage |
| i18n | i18n | per-chat locale, ctx.t |
| web | serve, webhook, setWebhook | webhooks on edge/web runtimes |
a quick tour
keyboards + typed callback data:
import { InlineKeyboard, callbackData } from "yaebal";
const vote = callbackData("vote", { id: Number });
bot.command("poll", (ctx) =>
ctx.send("pick one", {
reply_markup: new InlineKeyboard()
.text("👍", vote.pack({ id: 1 }))
.text("👎", vote.pack({ id: 2 }))
.build(),
}),
);
bot.on("callback_query:data", (ctx) => {
const data = vote.unpack(ctx.callbackQuery.data);
if (data) ctx.answer(`voted ${data.id}`);
});per-chat sessions and i18n:
import { session, i18n } from "yaebal";
bot.install(session({ initial: () => ({ count: 0 }) }));
bot.command("count", (ctx) => ctx.reply(`#${++ctx.session.count}`));
bot.install(i18n({
defaultLocale: "en",
locales: { en: { hi: "hello" }, ru: { hi: "привет" } },
}));
bot.command("start", (ctx) => ctx.reply(ctx.t("hi")));media — no platform package to pick:
import { media } from "yaebal";
bot.command("pic", (ctx) => ctx.sendPhoto(media.path("./cat.jpg"))); // node/bun/deno
// on edge, send media.url(...) / media.buffer(...) insteadrun on the edge over webhooks:
import { Bot, webhook } from "yaebal";
export default {
fetch(request: Request, env: { BOT_TOKEN: string; SECRET: string }) {
const bot = new Bot(env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("running on the edge ⚡"));
return webhook(bot, { secretToken: env.SECRET })(request);
},
};Bot exposes a contextFactory hook; yaebal injects one (richContext) that
builds the base Context and grafts the matching generated context's
shortcut methods + payload fields onto it. core stays decoupled from @yaebal/contexts — the meta-package does the wiring.