Modern Telegram Bot Framework for Node.js
link_preview_options.url
caused client to try sending it as mediaCommandContextExtn
type to Composer::{start|help|settings}
(adds ctx.command, payload, args)Fixed Context::from
: now uses callbackQuery.from
instead of msg.from
Phew, what a feature-packed release! 🎉
*tsk tsk* There's a big announcement at the end of this release!
To listen on reaction addition and removal, use Composer.reaction
:
bot.reaction("👍", (ctx) => {
// user added a 👍 reaction
});
// prefix with - to listen to reaction removal
bot.reaction("-👍", (ctx) => {
// user removed a 👍 reaction
});
This also just works with custom emoji IDs.
bot.reaction("5368742036629364794", (ctx) => {
// user added a reaction with the given custom emoji ID
});
bot.reaction("-5368742036629364794", (ctx) => {
// user removed a reaction with the given custom emoji ID
});
You can probe and inspect the reaction list with the ctx.reactions
smart object:
bot.on("message_reaction", async (ctx) => {
// remember that ctx.reactions is a smart object, but not an array
// message has a 👍 reaction
ctx.reactions.has("👍");
// message has a reaction with the given custom emoji ID
ctx.reactions.has("5368742036629364794");
// number of reactions from this user on the message
ctx.reaction.count;
// indexed access is allowed:
const first = ctx.reactions[0];
// the 👍 emoji was added in this update
if (ctx.reactions.added.has("👍")) {
// user added a 👍 reaction
await User.updateOne({ id: ctx.from.id }, { $inc: { likes: 1 } });
}
// the 👍 emoji was removed in this update
if (ctx.reactions.removed.has("👍")) {
// user removed a 👍 reaction
await User.updateOne({ id: ctx.from.id }, { $inc: { likes: -1 } });
};
// to copy any of these smart objects into an array, call the `toArray` method
const reactions = ctx.reactions.toArray();
const added = ctx.reactions.added.toArray();
const removed = ctx.reactions.removed.toArray();
});
To react to a message, use ctx.react
:
bot.on("text", async (ctx) => {
await ctx.react("👍");
});
You can also react to a message with a custom emoji ID:
bot.on("text", async (ctx) => {
await ctx.react("5368742036629364794");
});
The bot.telegram.setMessageReaction
method is also available.
Context::text
and Context::entities()
helpersAdded the ctx.entities()
method to fetch entities in any message.
bot.on("text", (ctx) => {
// fetch all entities
const entities = ctx.entities();
// fetch all command entities
const commandEntities = ctx.entities("bot_command");
// fetch all mentions and text mentions
const mentions = ctx.entities("mention", "text_mention");
});
Not only does this method fetch entities from any message, but also works with captions, game text, and poll explanations. In short, if an update has any text and entities, this method will find them.
ctx.text
is the companion to ctx.entities()
. It fetches the text from the update, and works with any message type. This includes message text, media captions, game text, and poll explanations.
bot.on(message("text"), (ctx) => {
// fetch the text from the update
const text = ctx.text;
});
bot.on(message("photo"), (ctx) => {
// fetch the caption from the photo
const caption = ctx.text;
});
🎁 Bonus! Every entity in the ctx.entities()
array will have a fragment of the text they represent!
bot.on("text", (ctx) => {
const entities = ctx.entities("bold");
for (const entity of entities) {
// the text that is bold
const boldText = entity.fragment;
}
});
Context::msg
and Context::msgId
shorthandsContext::msg
shorthand to get any message in the update.
bot.use((ctx) => {
// finds one of:
// ctx.message ?? ctx.editedMessage ?? ctx.callbackQuery?.message ?? ctx.channelPost ?? ctx.editedChannelPost
const msg = ctx.msg;
});
ctx.msg
is decorated with the isAccessible
and has
methods. The has
method works similarly to the message()
filter in bot.on
. It checks if the message has a certain property.
if (ctx.msg.isAccessible()) {
// msg is accessible, not deleted or otherwise unavailable
// this is a type-guard based on the runtime check for msg.date === 0
}
if (ctx.msg.has("text")) {
// ctx.msg.text exists
}
Context::msgId
shorthand to get any available message ID in the update. This also includes message_id
present in updates that do not contain a message, such as message_reaction
, and message_reaction_count
.
bot.use((ctx) => {
// finds one of:
// ctx.msg.message_id ?? ctx.messageReaction.message_id ?? ctx.messageReactionCount.message_id
const msgId = ctx.msgId;
});
bot.launch
takes an onLaunch callbackbot.launch
now takes an optional callback that is called when the bot is launched.
bot.launch(() => console.log("Bot is starting!"));
If you pass LaunchOptions, the callback goes after the options.
bot.launch({ dropPendingUpdates: true }, () => console.log("Bot is starting!"));
This is useful for running some code when the bot is launched, such as logging to the console, or sending a message to a channel. Remember that errors thrown in this callback will not be caught by the bot's error handler. You must handle them yourself.
It's worth noting that the callback is called once the first
getMe
call is successful. This means network is working, and bot token is valid. Due to how networks work, there isn't a way to define when the bot is "fully" launched. The bot may still crash after the callback is called, for example if another instance of the bot is running elsewhere and pollinggetUpdates
fails. For these reasons, onLaunch callback exists under@experimental
, and may receive improvements based on feedback.
quote
format helper for Blockquotes.pre
within another entity.ctx.match
for Composer.command
(#1938).ctx.chat
now includes chat
from this.messageReaction ?? this.messageReactionCount ?? this.removedChatBoost
.ctx.from
now includes the field user
from ctx.messageReaction ?? ctx.pollAnswer ?? ctx.chatBoost?.boost.source
, in addition to fetching from
in other updates.useNewReplies
uses ctx.msgId
instead of ctx.message.message_id
to reply to messages, which works for more update types than before.types
, scenes
, filters
, format
, future
, utils
, markup
, session
. For example, via import { WizardScene } from "telegraf/scenes"
. This was previously available in v3, and was removed in v4.Markup.button.userRequest
will take extra
instead of user_is_premium
as the third parameter.Markup.button.botRequest
will take extra
before hide
as the third parameter.reply_to_message_id
and allow_sending_without_reply
replaced by reply_parameters
.disable_web_page_preview
and link_preview_options
replaced by link_preview_options
.This will be the last major update for Telegraf v4.
If you are currently using Telegraf v4, you can continue using it as you have been. Telegraf v4 will be supported until February 2025, with the following commitments:
In the coming weeks, we plan to release Telegraf v5. v5 will bring a revamped API, new functionalities, numerous convenient helpers, an improved approach to handling updates, and enhanced documentation.
One of the key highlights of Telegraf v5 is its platform-agnostic nature, allowing you to run it on any JavaScript runtime environment, including Deno, Bun, edge runtimes (such as Cloudflare Workers and Vercel), browsers, and more.
If you have closely followed the development of v4 in the past year and stayed updated with deprecation notices, the transition to v5 will be relatively seamless for you. For those still using v4, we will provide a comprehensive migration guide to assist you in upgrading to v5. Stay tuned for more information on the release of v5!
You can sponsor the maintainer (@MKRhere) via GitHub Sponsors, Patreon, or Ko-fi.
thumbnail
is now respected in all APIs that accept itEXPERIMENTAL_SESSION_CHECKS
introduced in the last minor release had been reporting false positives. This has been fixed; it will now work as intended.
🔧 Fixed sendPhoto
and friends irrecoverably erroring if passed an invalid path, such as a directory.
⚠️ Set the env var EXPERIMENTAL_SESSION_CHECKS=1
to catch session bugs in your code.
When this is enabled, Telegraf will throw errors if you access/assign to session after Telegraf thinks the middleware chain has exhausted. This can happen if you're missing awaits in async code, and session changes might not get persisted! Previously, these bugs were silently ignored until someone noticed their session was not saved. It's always safe to enable this feature. This behaviour may be default in v5.
This is a rather minor release.
anyOf
and allOf
filter combinatorsv4.11.0 introduced support for type-guard filters in Composer::on
, which allowed you to filter updates based on their content.
This release adds two new combinators to the filter API: anyOf
and allOf
. This will play very nicely with custom filters. For example:
import { anyOf, allOf } from "telegraf/filters";
// must match all filters
bot.on(allOf(message(), isGroup), ctx => {
// ...
});
hookPath
The confusingly named hookPath
in bot.launch
webhook options is now deprecated. It will be removed in the next major release. You can start using path
instead, today.
Meanwhile, we're working on new modules to add to the Telegraf ecosystem. Look forward to them, and join discussions in the official Telegraf chat!
Markup.keyboard([]).persistent()
methodThis update brings us the ability to have multiple session keys. This is achieved simply by passing property
in session options:
bot.use(session()); // creates ctx.session backed by an in-memory store
bot.use(session({
property: "chatSession",
getSessionKey: ctx => ctx.chat && String(ctx.chat.id),
store: Redis({ url: "redis://127.0.0.1:6379" });
})); // creates ctx.chatSession backed by a Redis store
Thanks to @Evertt for making the case for this feature.
It's an often requested feature to be able to parse command arguments.
As of this release, ctx.command
, ctx.payload
, and ctx.args
are available for this usecase. It's only available in bot.command
handlers.
ctx.command
is the matched command (even if you used RegExp), and it does not include the botname if it was included in the user's command. ctx.payload
is the unparsed text part excluding the command. ctx.args
is a parsed list of arguments passed to it. Have a look at the example:
// User sends /warn --delete "Offtopic chat"
bot.command("warn", async ctx => {
ctx.args; // [ "--delete", "Offtopic chat" ]
ctx.command; // "warn"
ctx.payload; // "--delete \"Offtopic chat\""
});
⚠️ ctx.args
is still considered unstable, and the parser is subject to fine-tuning and improvements based on user feedback.
The more generic ctx.payload
for all commands causes ctx.startPayload
in bot.start
to be redundant, and hence the latter is now deprecated.
bot.start(ctx => {
- console.log(ctx.startPayload);
+ console.log(ctx.payload);
});
You can also play with this feature by importing the parser directly:
import { argsParser } from "telegraf/utils";
// do not include the /command part!
argsParser('--delete "Offtopic chat"'); // [ "--delete", "Offtopic chat" ]
We have now forked Typegram to maintain types more in line with Telegraf.
Most of you will be unaffected, because Telegraf just switched its internal import to @telegraf/types
. If you have a direct dependency on typegram
for any reason, you might want to consider switching that over. typegram
will continue to be maintained as well.
Remember that all of these types are available through Telegraf without installing any additional library:
import type { Update } from "telegraf/types";
This new package is @telegraf/types
, available on Deno/x and npm with our ongoing effort to make Telegraf more platform independent.
We're a little delayed this time, but we've got them all ready for you now:
setMyDescription
, getMyDescription
, setMyShortDescription
, getMyShortDescription
, setCustomEmojiStickerSetThumbnail
, setStickerSetTitle
, deleteStickerSet
, setStickerEmojiList
, setStickerKeywords
, setStickerMaskPosition
setStickerSetThumb
-> setStickerSetThumbnail
setMyName
, getMyName
unpinAllGeneralForumTopicMessages
More exciting updates coming soon!