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.

pipeline
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:

providers
// 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.message

2. 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
// 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:

schema diff
# 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.
one regen, and 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:

sugar/message.ts
// 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);
  }
}
handler.ts
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.

getters.ts
// 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.

shortcuts.ts
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 / puregramyaebal
shortcutshand-writtengenerated from schema
coveragewhat a maintainer wrappedeverything fillable
new API methodwait for a PRpnpm generate
version lagcontexts trail the APIcontexts == schema version
ergonomicshand-tunedautogen + thin sugar layer