npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@effect-ak/tg-bot

v1.10.0

Published

Telegram bot framework with long polling, webhooks, and fluent builder API

Readme

@effect-ak/tg-bot

NPM Version NPM Downloads

Telegram bot framework for TypeScript: fluent builder, typed handlers for every update type, long polling or webhooks (Cloudflare Workers, Bun, Deno, Node), inline-keyboard screens declared as data. Built on @effect-ak/tg-bot-client — native fetch, zero runtime dependencies.

Installation

npm install @effect-ak/tg-bot

Quick Start

import { createBot } from "@effect-ak/tg-bot"

const bot = createBot()
  .command("/start", ({ ctx }) => ctx.reply("Welcome!"))
  .onText(({ payload, ctx }) => ctx.reply(`You said: ${payload.text}`))
  .onCallback("confirm", ({ ctx }) => ctx.editMessageText("Confirmed ✅"))

await bot.run({ bot_token: "YOUR_BOT_TOKEN" }) // long polling

How It Works

  1. createBot() returns a builder. .command, .onText, .onCallback register one handler each; .onMessage, .onCallbackQuery, .onInlineQuery, .on(type) register a list of guarded handlers for one update type. Handlers are tried in registration order; the first whose match passes runs.
  2. Each handler receives { payload, ctx }payload is the typed Message / CallbackQuery / … (not the Update envelope) — and returns a BotResponse (or an array of them). It never calls the API itself: the runner executes the response (send_*, edit_message_text, answer_callback_query, any method) and reports the outcome to onHandleResult.
  3. .run({ bot_token }) starts long polling; .webhook({ bot_token, secret_token }) returns a (Request) => Promise<Response> handler for serverless.

API Surface

createBot()
  .command("/start", handler)                              // shortcuts: one handler each
  .onText(handler)
  .onCallback("data" | /regex/, handler)
  .onMessage(({ command, text, photo, document, sticker, fallback }) => [...])
  .onCallbackQuery(({ data, fallback }) => [...])         // data(string | RegExp, handler)
  .onInlineQuery(({ query, fallback }) => [...])
  .on("my_chat_member", { match?, handle })                // any other update type
  .use(plugin)                                             // e.g. defineScreens(...)
  .run({ bot_token, poll?, logger?, onHandleResult? })     // → BotInstance { stop(), reload() }
  .webhook({ bot_token, secret_token, logger?, onHandleResult? })
  // → handler(request), handler.handleUpdate(update), handler.setWebhook({ url, ... })

// handler = ({ payload, ctx }) => BotResponse | BotResponse[]   (sync or async)
// payload: Message | CallbackQuery | InlineQuery | ... — typed per update type

// ctx — available in every handler
ctx.reply(text, options?)                 // send_message to the update's chat
ctx.replyWithPhoto(photo, options?)       // send_photo
ctx.replyWithDocument(document, options?) // send_document
ctx.editMessageText(text, options?)       // edit the message this update refers to
ctx.editMessageReplyMarkup(options?)
ctx.deleteMessage()
ctx.answerCallbackQuery({ text?, show_alert? }?)
ctx.call(method, params)                  // any Bot API method
ctx.command                               // "/start" | undefined
ctx.ignore                                // do nothing

// Several calls in one handler: return an array (executed in order)
onCallback("save", ({ ctx }) => [ctx.answerCallbackQuery({ text: "Saved" }), ctx.editMessageText("✅")])

// BotResponse — build responses without ctx (rarely needed)
BotResponse.call("send_chat_action", params)    // any method with full params
BotResponse.make({ type: "message", text })     // any send_* method, chat_id filled in
BotResponse.all(a, b) / a.and(b)                // same as returning an array
BotResponse.ignore

Rules the runner applies for you:

  • A callback_query handler that responds without answer_callback_query gets one sent automatically — buttons never hang.
  • command("/start") also matches /START and /start@your_bot (groups).
  • ctx.reply* from a callback query goes to the chat of the tapped message.
  • If a handler throws, the error is logged, an apology is sent, and other updates keep flowing (poll.on_error: "continue" by default).

Inline Keyboards as Data

Describe screens once; the SDK renders them, edits the message in place, adds Back buttons and answers callback queries:

import { createBot, defineScreens } from "@effect-ak/tg-bot"

const screens = defineScreens(
  {
    root: {
      text: "🏠 Main menu",
      buttons: [
        [
          { label: "Hours", next: "hours" },
          { label: "Site", url: "https://example.com" }
        ]
      ]
    },
    hours: {
      text: "Mon–Fri 9–18",
      parent: "root",
      buttons: [{ label: "Book", action: ({ ctx }) => ctx.answerCallbackQuery({ text: "Soon!" }) }]
    }
  },
  { back: "‹ Back", footer: [{ label: "Talk to a human", url: WHATSAPP_URL }] }
)

createBot().use(screens).webhook({ bot_token, secret_token })

next is checked against the screen ids at compile time. text/buttons may be functions of the update; pass a store (get/set per chat) to make Back an undo of the last step instead of the static parent. Custom entry points: command("/menu", screens.open("root")).

Webhook

const handler = createBot()
  .onMessage(({ command }) => [command("/start", ({ ctx }) => ctx.reply("Hello!"))])
  .webhook({
    bot_token: "YOUR_BOT_TOKEN",
    // Telegram echoes this back in X-Telegram-Bot-Api-Secret-Token;
    // requests without it are rejected with 403. Always set it.
    secret_token: "YOUR_WEBHOOK_SECRET"
  })

// Cloudflare Workers / Bun / Deno: a plain Request → Response function
export default { fetch: handler }

// Register the webhook once — the same secret_token is passed to set_webhook
await handler.setWebhook({ url: "https://your-app.example.com/webhook" })

Platforms that give you a parsed body instead of a Request (Express, Lambda): check the header yourself and call handler.handleUpdate(update).

Advanced: Low-Level API

Without the builder, use runBot / createWebhook directly with on_<update_type> handlers:

import { runBot } from "@effect-ak/tg-bot"

runBot({
  bot_token: "YOUR_BOT_TOKEN",
  mode: "single", // or "batch" with on_batch(updates)
  on_message: [
    {
      match: ({ payload }) => !!payload.text,
      handle: ({ payload, ctx }) => ctx.reply(payload.text!)
    }
  ]
})

Documentation

License

MIT