teledzik
v1.0.9
Published
Modern Telegram Bot Framework
Maintainers
Readme
Developed & maintained by @lorddzik
teledzik (
teledzik) is an up-to-date fork of telegraf — the popular Telegram Bot framework for Node.js. This package tracks the latest Telegram Bot API releases and ships features ahead of the upstream package. It is a drop-in replacement: just swapnpm install telegraf→npm install teledzikand change your imports from'telegraf'to'teledzik'.
🆕 Rich Messages — Bot API 1.0.0
Added in Bot API 1.0.0 (June 11, 2026). Rich Messages let bots send highly structured content using HTML or Markdown — headings, lists, tables, media collages, code blocks, math expressions, AI thinking blocks, and more — with streaming support for AI-generated replies.
Table of Contents
- Quick Start
- Sending Methods
- InputRichMessage Format
- RichHTMLBuilder
- RichMarkdownBuilder
- Streaming with sendRichMessageDraft
- reply_markup with Rich Messages
- Inline / Web App Queries
- Exclusive Teledzik Enhancements
- TypeScript
Quick Start
import { Telegraf, RichMessage } from 'teledzik'
const { RichHTMLBuilder: HTML } = RichMessage
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.command('hello', async (ctx) => {
const msg = new HTML()
.heading(1, 'Hello World!')
.paragraph(HTML.bold('Rich Messages') + ' are now supported in Bot API 1.0.0.')
.divider()
.ul('Headings', 'Lists', 'Tables', 'Media', 'Math', 'and more')
.build()
await ctx.sendRichMessage(msg)
})
bot.launch()
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))Sending & Editing Methods
| Method | Description |
|---|---|
| ctx.sendRichMessage(msg, extra?) | Send a rich message to the current chat |
| ctx.replyWithRichMessageContent(msg, extra?) | Send a rich message quoting the current message |
| ctx.editRichMessage(msg, extra?) | In-place edit of message with native Rich UI preservation (v1.0.8+) |
| ctx.sendRichMessageDraft(draftId, msg, extra?) | Stream a partial draft (private chats only) |
| ctx.telegram.sendRichMessage(chatId, msg, extra?) | Explicit send with chat ID |
| ctx.telegram.editRichMessage(chatId, messageId, msg, extra?) | Explicit edit with chat ID and message ID |
| ctx.telegram.sendRichMessageDraft(chatId, draftId, msg, extra?) | Explicit streaming with chat ID |
extra options for sendRichMessage:
await ctx.sendRichMessage(msg, {
message_thread_id: 123, // for forum topics
direct_messages_topic_id: 456, // for DM topics
disable_notification: true,
protect_content: true,
allow_paid_broadcast: false,
message_effect_id: 'effect_id',
reply_parameters: { message_id: ctx.message.message_id },
reply_markup: { inline_keyboard: [[{ text: 'OK', callback_data: 'ok' }]] },
})InputRichMessage Format
InputRichMessage uses either html or markdown — not both:
// HTML format
const msg: InputRichMessage = {
html: '<h1>Title</h1><p>Body text</p>',
is_rtl: false,
skip_entity_detection: false,
}
// Markdown format
const msg: InputRichMessage = {
markdown: '# Title\n\nBody text',
}Use the builders — RichHTMLBuilder or RichMarkdownBuilder — to construct these conveniently.
RichHTMLBuilder
Import and instantiate:
import { RichMessage } from 'teledzik'
const { RichHTMLBuilder: HTML } = RichMessage
const msg = new HTML()
.heading(1, 'Title')
.paragraph('Content')
.build() // returns InputRichMessage { html: '...' }Text Formatting (HTML)
Static inline helpers — return strings to embed inside block methods:
HTML.bold('bold text') // <b>bold text</b>
HTML.italic('italic text') // <i>italic text</i>
HTML.underline('underlined') // <u>underlined</u>
HTML.strikethrough('crossed out') // <s>crossed out</s>
HTML.spoiler('hidden until tapped') // <tg-spoiler>hidden</tg-spoiler>
HTML.code('inline code') // <code>inline code</code>
HTML.marked('highlighted') // <mark>highlighted</mark>
HTML.sub('subscript') // <sub>subscript</sub>
HTML.sup('superscript') // <sup>superscript</sup>
HTML.url('https://t.me', 'Telegram') // <a href="...">Telegram</a>
HTML.email('[email protected]', 'Email us') // <a href="mailto:...">Email us</a>
HTML.phone('+6281234567', 'Call us') // <a href="tel:...">Call us</a>
HTML.mention(123456789, 'Alice') // <a href="tg://user?id=...">Alice</a>
HTML.customEmoji('5368324170671202286', '👍') // <tg-emoji emoji-id="...">👍</tg-emoji>
HTML.time(1647531900, 'wDT', '22:45 tomorrow') // <tg-time unix="..." format="...">...</tg-time>
HTML.inlineMath('E = mc^2') // $E = mc^2$
// Nesting
HTML.bold(HTML.italic('bold italic'))
HTML.underline(HTML.spoiler('underlined spoiler'))Headings & Paragraphs
new HTML()
.heading(1, 'Main Title')
.heading(2, 'Subtitle')
.heading(3, HTML.bold('Bold heading'))
.heading(4, 'H4')
.heading(5, 'H5')
.heading(6, 'H6')
.paragraph('Normal paragraph text.')
.paragraph(HTML.bold('Bold') + ' and ' + HTML.italic('italic') + ' combined.')
.footer('Footer text — smaller and muted')
.divider() // <hr/>
.build()Lists (HTML)
new HTML()
// Unordered list
.ul('First item', 'Second item', HTML.bold('Bold item'))
// Ordered list
.ol('Step one', 'Step two', 'Step three')
// Task list (checkboxes)
.taskList(
{ text: 'Completed task', checked: true },
{ text: 'Pending task', checked: false },
{ text: HTML.bold('Important task'), checked: false },
)
.build()Code Blocks
new HTML()
.pre('npm install teledzik', 'bash')
.pre('SELECT * FROM users WHERE active = 1;', 'sql')
.pre(
`const bot = new Telegraf(token)
bot.launch()`,
'javascript'
)
.pre('plain preformatted block without language')
.build()Supported language identifiers: javascript, typescript, python, bash, sql, json, html, css, etc.
Blockquote & Pull Quote
new HTML()
// Standard block quotation
.blockQuote(HTML.italic('"To be or not to be."'))
// Pull quotation with attribution (cite)
.pullQuote(
HTML.italic('"Design is not just what it looks like."'),
'Steve Jobs'
)
// Nested formatting inside quote
.blockQuote(
HTML.bold('Teledzik') + ' supports ' + HTML.marked('highlighted') +
' and ' + HTML.spoiler('spoiler') + ' text inside quotes.'
)
.build()Math (HTML)
new HTML()
// Inline math (embed inside paragraph)
.paragraph(
'The formula is: ' + HTML.inlineMath('a^2 + b^2 = c^2')
)
// Block math expression
.mathBlock('\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}')
.mathBlock('F(x) = \\int_{-\\infty}^{x} f(t)\\,dt')
.mathBlock('E = mc^2')
.build()Media — Photo, Video, Audio
new HTML()
// Photo
.photo('https://example.com/photo.jpg')
.photo('https://example.com/photo.jpg', 'Caption text')
.photo('https://example.com/photo.jpg', 'Spoiler photo', /* spoiler */ true)
// Video
.video('https://example.com/video.mp4')
.video('https://example.com/video.mp4', 'Caption text')
.video('https://example.com/video.mp4', 'Spoiler video', /* spoiler */ true)
// Audio / Voice note (.ogg for voice)
.audio('https://example.com/audio.mp3')
.audio('https://example.com/audio.mp3', 'Audio caption')
// With figcaption (HTML figure)
.raw('<figure><img src="https://example.com/photo.jpg"/><figcaption>Caption <b>bold</b></figcaption></figure>')
.build()You can also use a Telegram file_id in place of a URL once the file is uploaded.
Collage & Slideshow
const img = (src) => `<img src="${src}"/>`
const vid = (src) => `<video src="${src}"></video>`
new HTML()
// Collage — displays as a grid
.collage(
img('https://example.com/photo1.jpg'),
img('https://example.com/photo2.jpg'),
vid('https://example.com/clip.mp4'),
)
// Slideshow — swipeable carousel
.slideshow(
img('https://example.com/photo1.jpg'),
img('https://example.com/photo2.jpg'),
img('https://example.com/photo3.jpg'),
)
.build()Map
new HTML()
.map(-6.2088, 106.8456) // Jakarta
.map(48.8584, 2.2945, 16) // Paris, zoom 16
.map(51.5074, -0.1278, 14, 'Our office in London') // with caption
.build()Table (HTML)
new HTML()
.table(
[
['Name', 'Version', 'Downloads'], // header row (hasHeader: true)
['teledzik', '4.16.5', '—' ],
['telegraf','4.16.3', '~120k/wk' ],
],
{ bordered: true, striped: true, hasHeader: true }
)
.build()For advanced table markup (colspan, rowspan, alignment), use .raw():
new HTML()
.raw(
'<table bordered striped>' +
'<tr><th>Name</th><th colspan="2">Details</th></tr>' +
'<tr><td>Alice</td><td align="center">98</td><td align="right">Pass</td></tr>' +
'</table>'
)
.build()Details (Expandable)
new HTML()
// Collapsed by default
.details('Click to expand', '<p>Hidden content here.</p>')
// Open by default
.details(
HTML.bold('Changelog v4.16.5'),
'<ul><li>Fix InputRichMessage format</li><li>Add RichHTMLBuilder</li></ul>',
/* open */ true
)
.build()Footnote References (HTML)
new HTML()
.paragraph(
'Teledzik ' + HTML.ref('note-1', '[1]') + ' is based on Telegraf ' + HTML.ref('note-2', '[2]') + '.'
)
.divider()
.referenceDefinition('note-1', HTML.url('https://npmjs.com/package/teledzik', 'teledzik on npm'))
.referenceDefinition('note-2', HTML.url('https://github.com/telegraf/telegraf', 'telegraf on GitHub'))
.build()Anchors & In-document Links
new HTML()
.raw(HTML.anchor('section-intro')) // invisible anchor target
.heading(2, 'Introduction')
.paragraph('Jump to: ' + HTML.anchorLink('section-api', 'API Reference'))
.raw(HTML.anchor('section-api'))
.heading(2, 'API Reference')
.build()Special Elements
new HTML()
// AI thinking block (visible in sendRichMessageDraft)
.thinking(HTML.italic('Analyzing your request...'))
// Raw HTML for anything not covered by builder methods
.raw('<tg-map lat="41.9" long="12.5" zoom="14"/>')
.raw('<aside>Pull quote<cite>The Author</cite></aside>')
.build()RichMarkdownBuilder
Same API surface as RichHTMLBuilder but produces Markdown output:
import { RichMessage } from 'teledzik'
const { RichMarkdownBuilder: MD } = RichMessage
const msg = new MD()
.heading(1, 'Rich Markdown')
.paragraph(
MD.bold('bold') + ' ' +
MD.italic('italic') + ' ' +
MD.strikethrough('strike') + ' ' +
MD.marked('==highlighted==') + ' ' +
MD.spoiler('||spoiler||') + ' ' +
MD.code('`code`')
)
.divider()
.ul('Item 1', 'Item 2', MD.bold('Bold item'))
.ol('Step 1', 'Step 2', 'Step 3')
.taskList(
{ text: 'Done', checked: true },
{ text: 'Pending', checked: false },
)
.divider()
.pre('console.log("hello")', 'javascript')
.divider()
.table(
['Name', 'Score'],
[['Alice', '98'], ['Bob', '87']],
['left', 'center'],
)
.divider()
.mathBlock('E = mc^2')
.divider()
.blockQuote(MD.italic('"Quote text"'), '— Author')
.divider()
.photo('https://example.com/photo.jpg', 'Photo caption')
.collage('https://example.com/1.jpg', 'https://example.com/2.jpg')
.slideshow('https://example.com/1.jpg', 'https://example.com/2.jpg')
.divider()
.details('Expand me', '### Hidden heading\n\n- item 1\n- item 2')
.divider()
.paragraph('See footnote' + MD.sup('[1]'))
.footnote('1', MD.url('https://t.me/lorddzik', '@lorddzik'))
.build()All Markdown inline helpers:
MD.bold('text') // **text**
MD.italic('text') // *text*
MD.underline('text') // <u>text</u>
MD.strikethrough('text') // ~~text~~
MD.spoiler('text') // ||text||
MD.code('text') // `text`
MD.marked('text') // ==text==
MD.sub('text') // <sub>text</sub>
MD.sup('text') // <sup>text</sup>
MD.url('https://...', 'label') // [label](url)
MD.email('[email protected]', 'label') // [label](mailto:[email protected])
MD.phone('+123', 'label') // [label](tel:+123)
MD.mention(123456789, 'Alice') // [Alice](tg://user?id=123456789)
MD.customEmoji('id', '👍') // 
MD.time(1647531900, 'wDT') // 
MD.inlineMath('a^2') // $a^2$Streaming with sendRichMessageDraft
sendRichMessageDraft streams a partial rich message in private chats. The draft is ephemeral (30-second preview). You must finalize with sendRichMessage to persist it.
chat_id— private chat only (integer)draft_id— non-zero integer; updates with the samedraft_idare animated
bot.command('ai', async (ctx) => {
const DRAFT_ID = 1 // any non-zero integer
const steps = [
'Reading your request...',
'Searching knowledge base...',
'Composing answer...',
]
// Stream thinking blocks
for (const step of steps) {
await ctx.sendRichMessageDraft(
DRAFT_ID,
new HTML().thinking(HTML.italic(step)).build()
)
await new Promise((r) => setTimeout(r, 900))
}
// Finalize — must call sendRichMessage after streaming
await ctx.sendRichMessage(
new HTML()
.heading(2, '🤖 AI Response')
.paragraph('Here is the final answer from the AI.')
.divider()
.footer(HTML.url('https://t.me/lorddzik', '@lorddzik'))
.build()
)
})You can also call sendRichMessageDraft explicitly:
// Explicit
await ctx.telegram.sendRichMessageDraft(
ctx.chat.id, // private chat integer ID
42, // draft_id
new HTML().thinking('Processing...').build(),
{ message_thread_id: 123 }
)reply_markup with Rich Messages
All sendRichMessage calls accept a reply_markup option with inline keyboards:
import { Markup } from 'teledzik'
const msg = new HTML()
.heading(2, 'Choose an option')
.paragraph('Tap a button below:')
.build()
await ctx.sendRichMessage(msg, {
reply_markup: Markup.inlineKeyboard([
[
Markup.button.callback('✅ Yes', 'answer:yes'),
Markup.button.callback('❌ No', 'answer:no'),
],
[Markup.button.url('🌐 Visit', 'https://t.me/lorddzik')],
]).reply_markup,
})
bot.action('answer:yes', async (ctx) => {
await ctx.answerCbQuery('You chose Yes!')
})Or use reply_markup directly:
await ctx.sendRichMessage(msg, {
reply_markup: {
inline_keyboard: [
[{ text: 'Button 1', callback_data: 'btn1' }],
[{ text: 'Open URL', url: 'https://telegram.org' }],
],
},
})Native editRichMessage (Preserving 100% Rich UI)
Edit a rich message in-place while preserving 100% Native Rich UI layout (tables, headings, cards, collapsibles):
import { Telegraf, RichHTMLBuilder, Markup } from 'teledzik'
bot.action('btn:update', async (ctx) => {
// 1. Using RichHTMLBuilder instance directly
const rich = new RichHTMLBuilder()
.heading(1, '⚡ Server Stats Updated')
.table([
['Node', 'RAM', 'Status'],
['VPS-01', '3.8 GB', 'ONLINE 🟢'],
['VPS-02', '7.2 GB', 'ONLINE 🟢']
], { bordered: true, striped: true, hasHeader: true })
// Edit in-place with native Rich UI preservation
await ctx.editRichMessage(rich, {
reply_markup: Markup.inlineKeyboard([
[Markup.button.primary('🔄 Refresh', 'btn:update')]
]).reply_markup
})
})Inline / Web App Queries
Use InputRichMessageContent as input_message_content in inline query results:
import { RichMessage } from 'teledzik'
const { RichHTMLBuilder: HTML } = RichMessage
bot.on('inline_query', async (ctx) => {
const richContent: RichMessage.InputRichMessageContent = {
rich_message: new HTML()
.heading(1, 'Result from Inline Query')
.paragraph('Sent via ' + HTML.url('https://t.me/lorddzik', '@lorddzik') + '.')
.build(),
}
await ctx.answerInlineQuery([
{
type: 'article',
id: '1',
title: 'Rich Message Result',
input_message_content: richContent,
},
])
})Exclusive Teledzik Enhancements
Native editRichMessage & Auto-Sanitizer (v1.0.8+)
Standard editMessageText (parse_mode: 'HTML') fails or degrades layout when editing complex structured blocks. In teledzik v1.0.8+, ctx.editRichMessage passes the payload natively via Telegram's rich_message parameter:
- Preserves Full Rich UI: In-place edits keep native
<table>,<h1>-<h6>, collapsible cards (<details>/<blockquote expandable>), and styled elements without flattening them into plain text. - Smart Key-Value Table Handling: 2-column tables correctly retain all rows and headers without accidental truncation.
- Resilient Media & No-Op Handling: Automatically falls back to
editMessageCaptionif editing a photo or video message, and silently resolvesmessage is not modifiedinstead of throwing unhandled exceptions.
// 1. Edit with RichHTMLBuilder
const rich = new RichHTMLBuilder()
.heading(2, 'Order Confirmed')
.table([
['Product', 'Status'],
['VPS-01', 'ACTIVE 🟢']
])
await ctx.editRichMessage(rich, {
reply_markup: Markup.inlineKeyboard([
[Markup.button.callback('Back to Menu', 'menu')]
]).reply_markup
})
// 2. Edit with raw HTML / Rich Object
await ctx.editRichMessage({ html: '<h1>Title</h1><p>Body</p>' })Smart Rate-Limit Queue
Prevent Telegram 429 Too Many Requests errors automatically with sliding-window queueing and transparent auto-retries:
await ctx.sendQueued(async () => {
return ctx.reply('Buffered message with rate-limit protection')
})Live AI Streaming Adapter
Stream LLM responses (Gemini, OpenAI, Claude, Ollama) directly to Telegram with automatic throttling (max update per 800ms) to avoid rate-limit bans:
bot.on('text', async (ctx) => {
const stream = ctx.streamAI()
// Simulated LLM token stream
for await (const chunk of llmResponseStream) {
await stream.push(chunk)
}
await stream.end()
})Declarative Form Builder
Build step-by-step interactive wizards without verbose boilerplate:
import { FormScene } from 'teledzik'
const profileForm = new FormScene('user-profile')
.field('name', { prompt: 'Please enter your name:' })
.field('age', { prompt: 'Please enter your age:', type: 'number' })
.onComplete((ctx, data) => {
ctx.reply(`Profile saved! Name: ${data.name}, Age: ${data.age}`)
})Paginated Keyboard & List Builder
Create paginated inline menus (« Prev, Page X/Y, Next ») automatically:
import { PaginatedBuilder } from 'teledzik'
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6']
const result = PaginatedBuilder.create(items, {
page: 1,
pageSize: 2,
formatItem: (item, idx) => `${idx + 1}. ${item}`,
})
await ctx.reply(result.text, { reply_markup: result.keyboard.reply_markup, parse_mode: 'HTML' })In-Chat Interactive Form Card (InputBox)
Render interactive boxed form cards in chat messages that update dynamically in real-time as users fill in field inputs:
import { InputBox } from 'teledzik'
const formCard = new InputBox('user-form')
.setTitle('USER PROFILE CARD')
.addField('name', 'Full Name', { placeholder: 'Empty' })
.addField('email', 'Email Address', { placeholder: 'Empty' })
// Render card text & keyboard markup:
const cardText = formCard.renderText({ name: 'Alice' })
const keyboard = formCard.renderKeyboard({ name: 'Alice' })
await ctx.reply(cardText, { reply_markup: keyboard.reply_markup, parse_mode: 'HTML' })TypeScript
All types are exported under the RichMessage namespace:
import { RichMessage } from 'teledzik'
import type { ExtraSendRichMessage, ExtraSendRichMessageDraft } from 'teledzik/types'
// Builder types
const builder: RichMessage.RichHTMLBuilder = new RichMessage.RichHTMLBuilder()
const mdBuilder: RichMessage.RichMarkdownBuilder = new RichMessage.RichMarkdownBuilder()
// Message types
const input: RichMessage.InputRichMessage = { html: '<p>hello</p>' }
const content: RichMessage.InputRichMessageContent = { rich_message: input }
const received: RichMessage.RichMessage = ctx.message.rich_message
// Extra types
const extra: ExtraSendRichMessage = {
disable_notification: true,
reply_markup: { inline_keyboard: [] },
}
// Custom context with rich message
import { Context, Telegraf } from 'teledzik'
interface MyCtx extends Context {
session?: { lastDraftId: number }
}
const bot = new Telegraf<MyCtx>(process.env.BOT_TOKEN)For 3.x users
Introduction
Bots are special Telegram accounts designed to handle messages automatically. Users can interact with bots by sending them command messages in private or group chats. These accounts serve as an interface for code running somewhere on your server.
Telegraf is a library that makes it simple for you to develop your own Telegram bots using JavaScript or TypeScript.
Features
- Full Telegram Bot API 1.0.0 support with Rich Messages
- Excellent TypeScript typings
- Lightweight
- AWS λ / Firebase / Glitch / Fly.io / Whatever ready
http/https/fastify/Connect.js/express.jscompatible webhooks- Extensible
Example
const { Telegraf } = require('teledzik')
const { message } = require('teledzik/filters')
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.start((ctx) => ctx.reply('Welcome'))
bot.help((ctx) => ctx.reply('Send me a sticker'))
bot.on(message('sticker'), (ctx) => ctx.reply('👍'))
bot.hears('hi', (ctx) => ctx.reply('Hey there'))
bot.launch()
// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))const { Telegraf } = require('teledzik')
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.command('oldschool', (ctx) => ctx.reply('Hello'))
bot.command('hipster', Telegraf.reply('λ'))
bot.launch()
// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))For additional bot examples see the new docs repo.
Resources
- Getting started
- API reference
- Telegram groups (sorted by number of members):
- GitHub Discussions
- Dependent repositories
Getting started
Telegram token
To use the Telegram Bot API, you first have to get a bot account by chatting with BotFather.
BotFather will give you a token, something like 123456789:AbCdefGhIJKlmNoPQRsTUVwxyZ.
Installation
$ npm install teledzikor
$ yarn add teledzikor
$ pnpm add teledzikTelegraf class
Telegraf instance represents your bot. It's responsible for obtaining updates and passing them to your handlers.
Start by listening to commands and launching your bot.
Context class
ctx you can see in every example is a Context instance.
Telegraf creates one for each incoming update and passes it to your middleware.
It contains the update, botInfo, and telegram for making arbitrary Bot API requests,
as well as shorthand methods and getters.
This is probably the class you'll be using the most.
Shorthand methods
import { Telegraf } from 'teledzik'
import { message } from 'teledzik/filters'
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.command('quit', async (ctx) => {
// Explicit usage
await ctx.telegram.leaveChat(ctx.message.chat.id)
// Using context shortcut
await ctx.leaveChat()
})
bot.on(message('text'), async (ctx) => {
// Explicit usage
await ctx.telegram.sendMessage(ctx.message.chat.id, `Hello ${ctx.state.role}`)
// Using context shortcut
await ctx.reply(`Hello ${ctx.state.role}`)
})
bot.on('callback_query', async (ctx) => {
// Explicit usage
await ctx.telegram.answerCbQuery(ctx.callbackQuery.id)
// Using context shortcut
await ctx.answerCbQuery()
})
bot.on('inline_query', async (ctx) => {
const result = []
// Explicit usage
await ctx.telegram.answerInlineQuery(ctx.inlineQuery.id, result)
// Using context shortcut
await ctx.answerInlineQuery(result)
})
bot.launch()
// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))Production
Webhooks
import { Telegraf } from "teledzik";
import { message } from 'teledzik/filters';
const bot = new Telegraf(token);
bot.on(message("text"), ctx => ctx.reply("Hello"));
// Start webhook via launch method (preferred)
bot.launch({
webhook: {
// Public domain for webhook; e.g.: example.com
domain: webhookDomain,
// Port to listen on; e.g.: 8080
port: port,
// Optional path to listen for.
// `bot.secretPathComponent()` will be used by default
path: webhookPath,
// Optional secret to be sent back in a header for security.
// e.g.: `crypto.randomBytes(64).toString("hex")`
secretToken: randomAlphaNumericString,
},
});Use createWebhook() if you want to attach Telegraf to an existing http server.
import { createServer } from "http";
createServer(await bot.createWebhook({ domain: "example.com" })).listen(3000);import { createServer } from "https";
createServer(tlsOptions, await bot.createWebhook({ domain: "example.com" })).listen(8443);- AWS Lambda example integration
- Google Cloud Functions example integration
expressexample integrationfastifyexample integrationkoaexample integration- NestJS framework integration module
- Cloudflare Workers integration module
- Use
bot.handleUpdateto write new integrations
Error handling
If middleware throws an error or times out, Telegraf calls bot.handleError. If it rethrows, update source closes, and then the error is printed to console and process terminates. If it does not rethrow, the error is swallowed.
Default bot.handleError always rethrows. You can overwrite it using bot.catch if you need to.
⚠️ Swallowing unknown errors might leave the process in invalid state!
ℹ️ In production, systemd or pm2 can restart your bot if it exits for any reason.
Advanced topics
Working with files
Supported file sources:
Existing file_idFile pathUrlBufferReadStream
Also, you can provide an optional name of a file as filename when you send the file.
bot.on('message', async (ctx) => {
// resend existing file by file_id
await ctx.replyWithSticker('123123jkbhj6b')
// send file
await ctx.replyWithVideo(Input.fromLocalFile('/path/to/video.mp4'))
// send stream
await ctx.replyWithVideo(
Input.fromReadableStream(fs.createReadStream('/path/to/video.mp4'))
)
// send buffer
await ctx.replyWithVoice(Input.fromBuffer(Buffer.alloc()))
// send url via Telegram server
await ctx.replyWithPhoto(Input.fromURL('https://picsum.photos/200/300/'))
// pipe url content
await ctx.replyWithPhoto(
Input.fromURLStream('https://picsum.photos/200/300/?random', 'kitten.jpg')
)
})Middleware
In addition to ctx: Context, each middleware receives next: () => Promise<void>.
As in Koa and some other middleware-based libraries,
await next() will call next middleware and wait for it to finish:
import { Telegraf } from 'teledzik';
import { message } from 'teledzik/filters';
const bot = new Telegraf(process.env.BOT_TOKEN);
bot.use(async (ctx, next) => {
console.time(`Processing update ${ctx.update.update_id}`);
await next() // runs next middleware
// runs after next middleware finishes
console.timeEnd(`Processing update ${ctx.update.update_id}`);
})
bot.on(message('text'), (ctx) => ctx.reply('Hello World'));
bot.launch();
// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));With this simple ability, you can:
- extract information from updates and then
await next()to avoid disrupting other middleware, - like
ComposerandRouter,await next()for updates you don't wish to handle, - like
sessionandScenes, extend the context by mutatingctxbeforeawait next(), - intercept API calls,
- reuse other people's code,
- do whatever you come up with!
Usage with TypeScript
Telegraf is written in TypeScript and therefore ships with declaration files for the entire library.
Moreover, it includes types for the complete Telegram API via the typegram package.
While most types of Telegraf's API surface are self-explanatory, there's some notable things to keep in mind.
Extending Context
The exact shape of ctx can vary based on the installed middleware.
Some custom middleware might register properties on the context object that Telegraf is not aware of.
Consequently, you can change the type of ctx to fit your needs in order for you to have proper TypeScript types for your data.
This is done through Generics:
import { Context, Telegraf } from 'teledzik'
// Define your own context type
interface MyContext extends Context {
myProp?: string
myOtherProp?: number
}
// Create your bot and tell it about your context type
const bot = new Telegraf<MyContext>('SECRET TOKEN')
// Register middleware and launch your bot as usual
bot.use((ctx, next) => {
// Yay, `myProp` is now available here as `string | undefined`!
ctx.myProp = ctx.chat?.first_name?.toUpperCase()
return next()
})
// ...Native Colored Buttons (Telegram Bot API 9.4+)
Teledzik provides first-class support for native Telegram button background colors (primary Blue, success Green, danger Red) on inline keyboards:
import { Markup } from 'teledzik'
const keyboard = Markup.inlineKeyboard([
// 🔵 Blue Background (Primary action)
[Markup.button.primary('ℹ️ Bantuan', 'help')],
// 🟢 Green Background (Success / positive action)
[Markup.button.success('📊 Status Build', 'status')],
// 🔴 Red Background (Danger / destructive action)
[Markup.button.danger('📌 Perintah / Hapus', 'commands')],
// 🔵 Native Styled URL Buttons
[
Markup.button.primaryUrl('👀 Owner', 'https://t.me/LordDzik'),
Markup.button.primaryUrl('ℹ️ Information', 'https://t.me/LordDzik')
],
// 🟢 Green & 🔴 Red URL Buttons
[
Markup.button.successUrl('✅ Website', 'https://example.com'),
Markup.button.dangerUrl('🚨 Report', 'https://example.com/report')
]
])
await ctx.reply('Menu:', keyboard)Author & Maintainer
This fork is maintained by @lorddzik — t.me/lorddzik
Upstream project: telegraf/telegraf by The Telegraf Contributors.
