/telegraf

Modern Telegram Bot Framework for Node.js

Primary LanguageTypeScriptMIT LicenseMIT

Bot API Version install size Russian chat English chat

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

Example

const { Telegraf } = require('telegraf')

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('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('telegraf')

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 examples folder.

Resources

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:AbCdfGhIJKlmNoQQRsTUVwxyZ.

Installation

$ npm install telegraf

or

$ yarn add telegraf

or

$ pnpm add telegraf

Bot

A Telegraf bot is an object containing an array of middlewares which are composed and executed in a stack-like manner upon request. Is similar to many other middleware systems that you may have encountered such as Express, Koa, Ruby's Rack, Connect.

Middleware

Middleware is an essential part of any modern framework. It allows you to modify requests and responses as they pass between the Telegram and your bot.

You can imagine middleware as a chain of logic connection your bot to the Telegram request.

Middleware normally takes the two parameters: ctx and next.

ctx is the context for one Telegram update. It contains mainly two things:

  • the update object, containing for example the incoming message and the respective chat, and
  • a number of useful methods for reacting to the update, such as replying to the message or answering a callback query.

See the context section below for a detailed overview.

next is a function that is invoked to execute the downstream middleware. It returns a Promise with a function then for running code after completion.

Here is a simple example for how to use middleware to track the response time, using async and await to deal with the Promise.

const bot = new Telegraf(process.env.BOT_TOKEN)

bot.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log('Response time: %sms', ms)
})

bot.on('text', (ctx) => ctx.reply('Hello World'))
bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

Note how the function next is used to invoke the subsequent layers of the middleware stack, performing the actual processing of the update (in this case, replying with “Hello World”).

What you can do with middleware

Middleware is an extremely flexible concept that can be used for a myriad of things, including these:

  • storing data per chat, per user, you name it
  • allowing access to old messages (by storing them)
  • making internationalization available
  • rate limiting
  • tracking response times (see above)
  • much more

All important kinds of middleware have already been implemented, and the community keeps on adding more. Just install a package via npm, add it to your bot and you're ready to go.

Here is a list of

Known middleware

Error handling

By default Telegraf will print all errors to stderr and rethrow error.

To perform custom error-handling logic, use following snippet:

const bot = new Telegraf(process.env.BOT_TOKEN)
bot.catch((err, ctx) => {
  console.log(`Ooops, encountered an error for ${ctx.updateType}`, err)
})
bot.start((ctx) => {
  throw new Error('Example error')
})
bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

Context

A Telegraf Context encapsulates telegram update. One Context is created for each incoming Update.

Extending context

The recommended way to extend bot context:

const bot = new Telegraf(process.env.BOT_TOKEN)

bot.context.db = {
  getScores: () => { return 42 }
}

bot.on('text', (ctx) => {
  const scores = ctx.db.getScores(ctx.message.from.username)
  return ctx.reply(`${ctx.message.from.username}: ${scores}`)
})

bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

If you're using TypeScript, have a look at the section below about usage with TypeScript. (You need to extend the type of the context.)

Shortcuts
const bot = new Telegraf(process.env.BOT_TOKEN)

bot.command('quit', (ctx) => {
  // Explicit usage
  ctx.telegram.leaveChat(ctx.message.chat.id)

  // Using context shortcut
  ctx.leaveChat()
})

bot.on('text', (ctx) => {
  // Explicit usage
  ctx.telegram.sendMessage(ctx.message.chat.id, `Hello ${ctx.state.role}`)

  // Using context shortcut
  ctx.reply(`Hello ${ctx.state.role}`)
})

bot.on('callback_query', (ctx) => {
  // Explicit usage
  ctx.telegram.answerCbQuery(ctx.callbackQuery.id)

  // Using context shortcut
  ctx.answerCbQuery()
})

bot.on('inline_query', (ctx) => {
  const result = []
  // Explicit usage
  ctx.telegram.answerInlineQuery(ctx.inlineQuery.id, result)

  // Using context shortcut
  ctx.answerInlineQuery(result)
})

bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

State

The recommended namespace to share information between middlewares.

const bot = new Telegraf(process.env.BOT_TOKEN)

// Naive authorization middleware
bot.use((ctx, next) => {
  ctx.state.role = getUserRole(ctx.message)
  return next()
})

bot.on('text', (ctx) => {
  return ctx.reply(`Hello ${ctx.state.role}`)
})

bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

Session

Sessions are used to store data per user or per chat (or per whatever if you want, this is the session key).

Think of a session as an object that can hold any kind of information you provide. This could be the ID of the last message of the bot, or simply a counter about how many photos a user already sent to the bot.

You can use session middleware to add sessions support to your bot. This will do the heavy lifting for you. Using session middleware will result in a sequence like this:

  1. A new update comes in.
  2. The session middleware loads the current session data for the respective chat/user/whatever.
  3. The session middleware makes that session data available on the context object ctx.
  4. Your middleware stack is run, all of your code can do its work.
  5. The session middleware takes back control and checks how you altered the session data on the ctx object.
  6. The session middleware write the session back to your storage, i.e. a file, a database, an in-memory storage, or even a cloud storage solution.

Here is a simple example of how the built-in session middleware of Telegraf can be used to count photos.

const { session } = require('telegraf')

const bot = new Telegraf(process.env.BOT_TOKEN)
bot.use(session())
bot.on('photo', (ctx) => {
  ctx.session ??= { counter: 0 }
  ctx.session.counter++
  return ctx.reply(`Photo counter: ${ctx.session.counter}`)
})

bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

The default session key is ${ctx.from.id}:${ctx.chat.id}. If either ctx.from or ctx.chat is undefined, default session key and thus ctx.session are also undefined. You can customize the session key resolver function by passing in the options argument:

const { session } = require('telegraf')

const bot = new Telegraf(process.env.BOT_TOKEN)
bot.use(session({
  makeKey: (ctx) => ctx.from?.id // only store data per user, but across chats
}))
bot.on('photo', (ctx) => {
  ctx.session ??= { counter: 0 }
  ctx.session.counter++
  return ctx.reply(`Photo counter: ${ctx.session.counter}`)
})

bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

Tip: To use same session in private chat with bot and in inline mode, use following session key resolver:

{
  makeKey: (ctx) => {
    if (ctx.from && ctx.chat) {
      return `${ctx.from.id}:${ctx.chat.id}`
    } else if (ctx.from && ctx.inlineQuery) {
      return `${ctx.from.id}:${ctx.from.id}`
    }
    return undefined
  }
}

However, in the above example, the session middleware just stores the counters in-memory. This means that all counters will be lost when the process is terminated. If you want to store data across restarts, or share it among workers, you need to use persistent sessions.

There are already a lot of packages that make this a breeze. You can simply add npm install one and to your bot to support exactly the type of storage you want.

Alternatively, telegraf also allows you to easily integrate your own persistence without any other package. The session function can take a storage in the options object. A storage must have three methods: one for loading, one for storing, and one for deleting a session. This works as follows:

const { session } = require('telegraf')

// may also return `Promise`s (or use `async` functions)!
const storage = {
  getItem(key) { /* load a session for `key` ... */ },
  setItem(key, value) { /* save a session for `key` ... */ },
  deleteItem(key) { /* delete a session for `key` ... */ }
}

const bot = new Telegraf(process.env.BOT_TOKEN)
bot.use(session({ storage }))
bot.on('photo', (ctx) => {
  ctx.session.counter = ctx.session.counter || 0
  ctx.session.counter++
  return ctx.reply(`Photo counter: ${ctx.session.counter}`)
})

bot.launch()

// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'))
process.once('SIGTERM', () => bot.stop('SIGTERM'))

Update types

// Handle message update
bot.on('message', (ctx) => {
  return ctx.reply('Hello')
})

// Handle sticker or photo update
bot.on(['sticker', 'photo'], (ctx) => {
  console.log(ctx.message)
  return ctx.reply('Cool!')
})

Official Docs

Webhooks

require('dotenv')

const bot = new Telegraf(process.env.BOT_TOKEN)

// TLS options
const tlsOptions = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  ca: [
    // This is necessary only if the client uses a self-signed certificate.
    fs.readFileSync('client-cert.pem')
  ]
}

// Set telegram webhook
// The second argument is necessary only if the client uses a self-signed 
// certificate. Including it for a verified certificate may cause things to break.
bot.telegram.setWebhook('https://server.tld:8443/secret-path', {
  source: 'server-cert.pem'
})

// Start https webhook
bot.startWebhook('/secret-path', tlsOptions, 8443)

// Http webhook, for nginx/heroku users.
bot.startWebhook('/secret-path', null, 5000)

Use webhookCallback() if you want to attach Telegraf to an existing http server.

require('http')
  .createServer(bot.webhookCallback('/secret-path'))
  .listen(3000)

require('https')
  .createServer(tlsOptions, bot.webhookCallback('/secret-path'))
  .listen(8443)

Working with files

Supported file sources:

  • Existing file_id
  • File path
  • Url
  • Buffer
  • ReadStream

Also, you can provide an optional name of a file as filename when you send the file.

bot.on('message', (ctx) => {
  // resend existing file by file_id
  ctx.replyWithSticker('123123jkbhj6b')

  // send file
  ctx.replyWithVideo({ source: '/path/to/video.mp4' })

  // send stream
  ctx.replyWithVideo({
    source: fs.createReadStream('/path/to/video.mp4')
  })

  // send buffer
  ctx.replyWithVoice({
    source: Buffer.alloc()
  })

  // send url via Telegram server
  ctx.replyWithPhoto('https://picsum.photos/200/300/')

  // pipe url content
  ctx.replyWithPhoto({
    url: 'https://picsum.photos/200/300/?random',
    filename: 'kitten.jpg'
  })
})

Telegraf Modules

Telegraf Modules is higher level abstraction for writing modular Telegram bots.

A module is simply a .js file that exports Telegraf middleware:

module.exports = (ctx) => ctx.reply('Hello from Telegraf Module!')
const Composer = require('telegraf/composer')

module.exports = Composer.mount(
  'sticker', 
  (ctx) => ctx.reply('Wow, sticker')
)

To run modules, you can use telegraf module runner, it allows you to start Telegraf module easily from the command line.

npm install telegraf -g

Telegraf CLI usage

telegraf [opts] <bot-file>
  -t  Bot token [$BOT_TOKEN]
  -d  Webhook domain
  -H  Webhook host [0.0.0.0]
  -p  Webhook port [$PORT or 3000]
  -l  Enable logs
  -h  Show this help message
Telegraf Module example

Create module with name bot.js and following content:

const Composer = require('telegraf/composer')
const PhotoURL = 'https://picsum.photos/200/300/?random'

const bot = new Composer()
bot.start((ctx) => ctx.reply('Hello there!'))
bot.help((ctx) => ctx.reply('Help message'))
bot.command('photo', (ctx) => ctx.replyWithPhoto({ url: PhotoURL }))

module.exports = bot

then run it:

telegraf -t "bot token" bot.js

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.

Custom Context Type and Middleware

Recap from the above section about Middleware that ctx is the context object that holds information about the incoming update, as well as a number of convenience functions such as ctx.reply.

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 "telegraf";

// 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()
})
// ...
Session Middleware

If you are using session middleware, you need to define your session property on your custom context object. This could look like this:

import { Context, Telegraf } from 'telegraf'
import session from 'telegraf/session'

interface SessionData {
  lastMessageId?: number
  photoCount?: number
  // ... more session data go here
}

// Define your own context type
interface MyContext extends Context {
  session?: SessionData
  // ... more props go here
}

// Create your bot and tell it about your context type
const bot = new Telegraf<MyContext>('SECRET TOKEN')

// Make session data available
bot.use(session())
// Register middleware and launch your bot as usual
bot.use((ctx, next) => {
  // Yay, `session` is now available here as `SessionData`!
  if (ctx.message !== undefined)
    ctx.session.lastMessageId = ctx.message.message_id
  return next()
})
bot.on('photo', (ctx, next) => {
  ctx.session.photoCount = 1 + (ctx.session.photoCount ?? 0)
  return next()
})
// ...

API reference

Coming soon!

Scenes namespace

telegraf#705 (comment)