easytg
v0.4.0
Published
Page-based Telegram bot UIs on top of grammY: one page definition, rendered the same whether it is sent, replied or edited.
Maintainers
Readme
easytg
Build Telegram bot interfaces like pages, not message handlers.
Menus, forms and navigation on top of grammY. Define a screen once and easytg sends, replies or edits it in place.
Getting started · Documentation · Examples · Try it in your browser
Why easytg?
With the plain Bot API, one screen means a command handler that sends it, a
callback handler that edits it, answering the callback query, packing state
into 64-byte callback_data, and escaping every user-provided string. easytg
takes care of that plumbing.
With grammY alone, one screen takes three handlers:
bot.command('start', (ctx) =>
ctx.reply(`Hi <b>${escape(ctx.from.first_name)}</b>`, {
parse_mode: 'HTML',
reply_markup: new InlineKeyboard().text('Order #42', 'order:42'),
}));
bot.callbackQuery(/^order:(.+)$/, async (ctx) => {
const id = ctx.match[1]; // unchecked string
await ctx.editMessageText(`Order <b>${escape(id)}</b>`, {
parse_mode: 'HTML',
reply_markup: new InlineKeyboard().text('Back', 'home'),
});
await ctx.answerCallbackQuery();
});
bot.callbackQuery('home', async (ctx) => {
// …the start screen again, as an edit this time
});With easytg, you describe the screens and easytg sends, edits and answers:
const home = page('home').render(({ ctx, nav }) => ({
text: md`Hi **${ctx.from?.first_name}**`,
keyboard: [[nav.button('Order #42', order, { id: '42' })]],
}));
const order = page<{ id: string }>('order').render(({ params, nav }) => ({
text: md`Order **${params.id}**`,
keyboard: [[nav.button('Back', home)]],
}));
const app = new EasyTG().register(home, order);
bot.use(app);
bot.command('start', (ctx) => app.open(ctx, home));Features
- 🧭 Pages & navigation. Describe screens, not API calls. Send, reply, edit, media swaps and long-text splitting are handled for you.
nav.back()remembers where the user came from, and{ mode: 'send' }keeps a message (like a video) instead of replacing it. - ⌨️ Both keyboards. Inline buttons on pages, plus a main menu on the reply keyboard (
replyMenu) with its own close button. - 🖼 Media. Photos, videos, GIFs, documents, audio, albums and copies of existing messages, with optional
protectContent. - 🔒 Type-safe links.
nav.button('Open', order, { id })fails to compile if a param is missing or misspelled, even when pages link to each other in cycles. - 📝 Dialogues. Multi-step forms with answers typed from the steps, validation (or zod/valibot schemas), choices, several options at once, number pickers, an inline calendar, file uploads, phone number, location and Mini App input, conditional steps, Back/Cancel and timeouts.
- 📱 Mini Apps. Buttons that open them,
initDatachecks, acting for the user from your server, answering into the chat and sharing. - 🔎 Text input on pages.
page.onTextturns a page into a search box or any other typed input. - 🛡️ Secure by default. Escaping
md/htmltemplates, signed or stored tamper-proof button params, params validation with.params(parse)or any Standard Schema, owner-only group menus (orallowedUsers), admin-only pages, and anti-spam with events. - 🌍 Languages. Your messages with
t('cart.items', { count }): placeholders, plural rules, fallbacks and escaping;setLocalefor language pickers, remembered for notifications. - 💾 Sessions and storage. State per user in a chat, per user across chats, and per chat; saved once per update, typed keys, TTLs and migrations. Built-in
MemoryStorage,SqliteStorageandRedisStorage, or any database behind a 3-method interface. - ⏰ Scheduled tasks and queues. Persistent
task()s with retries and recurring runs,deleteLater/sendLater/broadcastLater, self-refreshing pages, and concurrency-limited queues for slow jobs. - 💳 Payments.
invoicepages for Telegram Stars, checkout checks and a success handler. - 🚀 Production-ready. Per-user ordering of concurrent updates, loading indicators for slow pages, a throttle that keeps sends within Telegram's limits, and
clustermode sharing rate limits, locks, queues and throttling across processes (Redis/SQLite). - 🔗 Deep links, inline mode, notifications, broadcasts.
t.me/bot?start=…links, pages as inline results,sendToandapp.editfrom cron jobs and webhooks, pacedbroadcastto thousands of users,autoRetryfor rate limits. - 📊 Events for analytics and monitoring:
update(with timings),pageView,sent, dialogue events,spam,error,payment,queueWait,taskError,broadcastBatch,webAppData. - 🤖 Multi-bot ready. One app instance can serve several bots sharing one storage; sessions, buttons and rate limits stay per bot.
- 🧪 Testable.
easytg/testingruns your bot against a fake Telegram API, with no token and no network. - 💬 Telegram simulator.
easytg/simulatoris an in-memory Telegram with users, groups, buttons, inline mode and payments, plus a chat window for the browser. The docs site uses it for live playgrounds. - 👀 Preview without a token.
bunx easytg preview bot.tsruns your bot file in a simulated Telegram in the browser, restarts it on save, and turns what you click into abun testfile. - ⌘ Commands and relays.
app.commandbinds/commandsto pages and dialogues and syncs Telegram's command menu;app.relaylets two users talk through the bot, replies and edits included. - 🗺 Flow map.
app.flowchart()draws your bot as a Mermaid diagram of pages, dialogues, commands and the buttons between them. - 👁 Only for you, in groups.
mode: 'ephemeral'opens a page or a dialogue from a shared group menu for the presser only (Bot API 10.2 ephemeral messages); ephemeral commands; disabled buttons withnav.disabled. - ✍️ Streaming.
app.streamshows an AI answer as it is written (Telegram drafts in private chats, with a stop button), then sends it as a formatted message. - 📰 Rich messages.
richinstead oftext: headings, tables, task lists, collapsible details, formulas, pictures and buttons in one message (Bot API 10.3), withmdescaping user input for it. - ☁️ Serverless and edge. Webhooks on Cloudflare Workers, Deno Deploy or Vercel, with Redis over HTTP and cron-driven tasks.
- 🪶 Zero dependencies. grammY is the only peer dependency. Bun first; works on Node ≥ 20, Deno and in browsers.
Install
bun add easytg grammynpm install easytg grammyQuick start
import { Bot } from 'grammy';
import { EasyTG, md, page } from 'easytg';
const home = page('home').render(({ ctx, nav }) => ({
text: md`Hello **${ctx.from?.first_name}**!`,
keyboard: [[nav.button('👋 About', about)], [nav.close()]],
}));
const about = page('about').render(({ nav }) => ({
text: 'Built with **easytg**.',
keyboard: [[nav.button('⬅️ Back', home)]],
}));
const bot = new Bot(process.env.BOT_TOKEN!);
const app = new EasyTG().register(home, about);
bot.use(app); // register before your other handlers
bot.command('start', (ctx) => app.open(ctx, home));
bot.start();Next, Getting started walks through building a small shop bot with pages, a form and sessions in about ten minutes.
Core concepts
| | |
|---|---|
| page(id).render(fn) | A screen. render returns { text, photo, keyboard }, a redirect, or a dialogue to start. Pages can also validate params (.params), show a loading indicator (.loading) and take typed text (.onText). |
| nav | Builds buttons and navigation: nav.button, nav.self, nav.back, nav.home, nav.close, nav.url, nav.webApp, nav.pay, nav.deepLink, and the render results nav.redirect / nav.startDialogue. |
| replyMenu([...]) | A main menu on the reply keyboard; its buttons open pages or start dialogues. |
| dialogue(id).steps([...]) | A multi-step form. Answers arrive in onFinish, typed from the steps. |
| session | State of the user in this chat; app.userSession(ctx) follows the user into every chat, app.chatSession(ctx) is shared by a chat. All saved automatically. |
| Middlewares | Run before every page and dialogue, for login checks, admin-only screens and logging. |
| app.open(ctx, page) | Shows a page: edits the message when a button was pressed, replies otherwise. |
| app.sendTo / app.edit / app.broadcast | Send or update pages without an incoming update: notifications, status cards, newsletters. |
| task(id).run(fn) | A job that runs later (app.schedule), saved in storage so it survives restarts. |
// A dialogue in a nutshell
const signup = dialogue('signup')
.steps([
{
id: 'name', type: 'text', text: 'Your name?',
validate: (v) => v.length >= 2 || 'Too short',
},
{
id: 'plan', type: 'choice', text: 'Plan?',
options: [{ text: 'Free', value: 'free' }, { text: 'Pro', value: 'pro' }],
},
])
// answers: { name: string; plan: 'free' | 'pro' }, read off the steps
.onFinish(({ answers }) => ({
text: md`Welcome, ${answers.name}! Plan: ${answers.plan}`,
}));Examples
| Example | Shows |
|---|---|
| getting-started.ts | The shop bot from the getting-started guide |
| shop.ts | Reply-keyboard menu, catalog with pagination and Back, album gallery, checkout with size, phone number and location |
| media.ts | Photo, video, GIF, document (generated), audio and album pages |
| search.ts | A searchable catalog with page.onText, in two languages with t() |
| course.ts | Members-only videos copied from a storage channel with protectContent, access requests approved by an admin, app.edit, videos that delete themselves (deleteAfterMs) |
| reminders.ts | Reminders that survive restarts: scheduled and recurring tasks in SQLite |
| queue.ts | Slow AI-style jobs behind a queue: concurrency, one job per user, place in line |
| payments.ts | A Telegram Stars shop: invoices, stock check at checkout, delivery, refunds |
| inline.ts | Product pages shared with @yourbot query, params checked by a schema, cached photo file ids |
| mini-app.ts | A bot and its Mini App in one file: a shop with a checked server API, a color picker step, feedback, sharing |
| sessions.ts | State per chat, per user across chats, and per group, side by side |
| loading.ts | Slow pages with placeholders, "typing…" and toasts that only show when needed |
| throttle.ts | Keeping sends within Telegram's limits, with rules per chat |
| group.ts | A group bot: admin-only settings, a self-refreshing scoreboard, a quiz with a timeout |
| production.ts | Redis or SQLite, several processes with cluster, webhooks |
| broadcast.ts | Newsletter: subscriptions, admin-only compose dialogue, a background broadcast with live progress, broadcastLater, events |
| match.ts | A swipe-style "meet people" bot: profile cards with ❤️ 💌 👎 💤 on the reply keyboard, sign-up dialogue, likes, matches and anonymous chats (relay), app.command |
| booking.ts | Table reservations: an inline calendar (date), a guest counter (number) and extras (multiChoice) |
| support.ts | A support desk: tickets, an agent and a customer talking through the bot (app.relay), a link filter, ratings |
| team.ts | A group board where settings and votes open for the presser only (ephemeral pages, dialogue and commands), nav.disabled |
| ai-chat.ts | An AI chat whose answers stream in as a rich draft with ⏹ Stop (app.stream), with a fake model |
| rich.ts | A handbook of rich messages: a pricing table, collapsible FAQ with a button inside the text, release notes from blocks |
| flowchart.ts | A bot that sends its own map (app.flowchart()) |
| cloudflare-worker.ts | A Cloudflare Worker: webhook, Redis over HTTP, a Cron Trigger for scheduled tasks |
| captcha.ts | Button and typed captchas, server-side answers, per-key TTL |
| notify.ts | SQLite storage, tamper-proof buttons, sendLater notifications that survive restarts |
| locales/id.ts | Translating the built-in texts |
| testing.test.ts | Testing a bot with easytg/testing: navigation, dialogues, errors, broadcasts |
BOT_TOKEN=123:abc bun run examples/captcha.tsDocumentation
- Docs site: these docs with live playgrounds, and every example running in your browser.
- Getting started: a hands-on tutorial.
- Documentation: one page per feature.
- UI: Pages · Media · Main menu · Commands · Flow map · Dialogues · Text input · Text formatting · Rich messages · Languages · Inline mode · Mini Apps · Groups · Streaming
- Data: Sessions · Storage · Button params · Deep links
- Beyond one update: Notifications & broadcast · Relays · Scheduled tasks · Queues · Payments
- Production: Anti-spam · Scaling · Serverless and edge · Security · Events · Error helpers · All options · Testing · Preview · Simulator
- API reference: every export and
appmethod. - Migrating: breaking changes and how to update.
- Changelog
Requirements
- grammY ≥ 1.30
- Bun, Node.js ≥ 20, Deno, or a browser
- TypeScript is optional but recommended: it type-checks navigation.
Contributing
Issues and pull requests are welcome.
bun install
bun test # unit and integration tests (fake Telegram API)
bun run typecheck
bun run build
bun run docs:dev # the docs site with its playgrounds, at localhost:5173Releases are automated. See RELEASING.md.
License
MIT © viandwi24
