contexts killer feature
gramio-style per-update context classes — except the shortcut methods aren't hand-written. they're generated from the Bot API schema, so they're always complete and never lag a version.
how it's built
contexts are a pure function of the schema. there's no per-method, per-context hand-coding — the generator derives everything.
Telegram Bot API (HTML)
│ ark0f parses → machine-readable JSON
▼
packages/types/schema.json ← single source of truth
│
├──────────► @yaebal/types/generate.mjs → telegram.ts (types)
│
└──────────► @yaebal/contexts/generate.mjs
├─ Update.props → 23 context types
├─ payload fields → providers (which ids it carries)
└─ each API method → matched shortcut
▼
src/generated/*.ts (one file per context)detection, in two steps
1. providers — from a payload's fields, the generator works out which ids that context can supply:
// payload field → id this context can fill
chat → chat_id = this.chat.id
message_id → message_id = this.message_id
from → user_id = this.from.id
CallbackQuery → callback_query_id = this.id
chat_id / message_id from this.message2. matching — for each of the 135 Bot API methods, it collects the id-arguments
(chat_id, message_id, user_id, query ids). if the
context's providers cover the required ones, it emits a shortcut with those keys Omit-ted from the params:
// generated/message.ts — derived, never hand-written
react(params: Omit<SetMessageReactionParams, "chat_id" | "message_id">) {
return this.api.call<boolean>("setMessageReaction", {
chat_id: this.chat.id,
message_id: this.message_id,
...params,
});
}adding a feature is free
because the contexts derive from the schema, a new Bot API method shows up on every context that has the right ids — automatically. take reactions, added in Bot API 7.0:
# Bot API 7.0 (Dec 2023) added setMessageReaction.
# nothing in the generator changed — only the schema did:
+ { "name": "setMessageReaction",
+ "arguments": [ {chat_id, required}, {message_id, required},
+ {reaction?}, {is_big?} ] }
# pnpm --filter @yaebal/contexts generate
# → ctx.react() now exists on every Message-based context.
# gramio would need a maintainer to hand-write it.ctx.react() lands on MessageContext, ChannelPostContext, BusinessMessageContext — every Message-based
context — with the right Omit signature. zero hand-written code.the sugar layer
autogen gives breadth; a thin hand-written layer gives ergonomics. a shared MessageSugar mixin adds positional-string overloads on top of the generated base —
the best of both:
// src/sugar/message.ts — thin hand-written layer (a "mixin")
export class MessageContext extends MessageContextBase {
override send(text: string, params?: SendExtra): Promise<Message>;
override send(params: Omit<SendMessageParams, "chat_id">): Promise<Message>;
override send(a, b?) {
return super.send(typeof a === "string" ? { text: a, ...b } : a);
}
}bot.on("message:text", (ctx) => {
ctx.send("hi"); // positional sugar
ctx.reply("yo", { parse_mode: "HTML" });
ctx.react("🔥"); // auto-generated, no chat_id/message_id
ctx.editText("edited");
});convenience getters
the generator also emits camel-case getters (the gramio / puregram idea) on every context that carries the field — senderId, chatId, firstName, isPM, isGroup, messageId — so you
never reach into the raw payload for the common things.
// camel-case getters generated on EVERY context that has the field
bot.on("message:text", (ctx) => {
ctx.senderId; // number | undefined
ctx.chatId; // number
ctx.firstName; // string | undefined
ctx.isPM; // boolean (also isGroup, messageId)
});per-context shortcuts
one MessageSugar mixin gives every message-based context (message, channel_post, the edited_* and business_* ones) the positional send / reply / react / editText; query contexts
get a positional answer; join requests get approve / decline.
ctx.react("🔥"); // emoji
ctx.react("🔥", "<custom_emoji_id>"); // custom emoji (+ fallback)
ctx.react([{ emoji: "👍" }, { custom_emoji_id: "1" }]); // many
ctx.react(); // clear all
callbackCtx.answer("saved"); // CallbackQueryContext
inlineCtx.answer([...results]); // InlineQueryContext
joinCtx.approve(); joinCtx.decline(); // ChatJoinRequestContext
shippingCtx.answer(true); // ShippingQueryContext| gramio / puregram | yaebal | |
|---|---|---|
| shortcuts | hand-written | generated from schema |
| coverage | what a maintainer wrapped | everything fillable |
| new API method | wait for a PR | pnpm generate |
| version lag | contexts trail the API | contexts == schema version |
| ergonomics | hand-tuned | autogen + thin sugar layer |