2.3.0 adds a durable S3 session store, makes bot.command() ignore commands addressed to other bots, makes ctx.reply() answer where the message came from (business connections, forum topics), and stops a graceful shutdown from redelivering the update that was being handled. This release rolls up 2.3.0-rc0 and 2.3.0-rc1; the notes below describe the final 2.3.0 behavior.
Session storage
- New
S3SessionStorage(exported from the corenode-telegram-bot-api): a durable, cross-instance session backend for any S3-compatible object store (AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces). It speaks the S3 REST API directly overfetchwith AWS Signature V4 signing, so it is runtime-agnostic (Node 19+, Bun, Deno, Workers, edge).fetchandcryptoare injectable, so Node 18 can passnode:crypto'swebcrypto. There is no per-object TTL - use a bucket lifecycle rule on the prefix.
import { createSession, S3SessionStorage } from "node-telegram-bot-api";
bot.use(createSession({
store: new S3SessionStorage({
bucket: "my-bot-sessions",
region: "us-east-1",
credentials: { accessKeyId: "...", secretAccessKey: "..." },
prefix: "sessions/", // optional; `endpoint` targets R2 / MinIO / Spaces
}),
initial: () => ({ count: 0 }),
}));Dispatch
bot.command(name, ...)now ignores commands addressed to another bot. In a group,/start@some_botruns the handler only when@some_botis this bot's username (compared case-insensitively);/start@other_botfalls through to downstream middleware, and a bare/startstill always matches. The bot's identity is exposed asbot.botInfoand, in handlers,ctx.me.ctx.reply()now answers where the incoming message came from instead of the bare chat (#1386, #1387):business_connection_idforbusiness_message,edited_business_messageanddeleted_business_messages- previously the bot replied as itself (403 if the customer never started it).message_thread_idfor forum-topic messages (only whenis_topic_messageis set) - previously the reply landed in General. A plain reply/comment thread is still not followed.direct_messages_topic_idfor channel direct-messages chats, where it is required.- For a callback query, these come from the message the button is attached to. Anything passed in
otherstill wins; passundefinedfor a key to opt out.
ctx.reply()now throws forguest_messageupdates, pointing toapi.answerGuestQuerywith the message'sguest_query_id: a guest bot is not a member of the chat, sosendMessagethere cannot reach it.
Polling
- Stopping the bot (
bot.stop(), or SIGINT/SIGTERM viarun()) while a handler runs no longer leaves that update unconfirmed (#1385, #1388). The loop used to exit before the nextgetUpdatescarried the new offset, so a deploy's SIGTERM during a slow handler made Telegram redeliver the update on restart and non-idempotent handlers ran twice. On stop,longPollnow sends one final best-effortgetUpdates({ offset, limit: 1, timeout: 0 }), capped at 5s. Updates that were never handled (the rest of a batch) are still redelivered, and an update whosebot.catch()throws (fail-loud) is never confirmed.
Uploads
InputFileuploads no longer throwReferenceError: crypto is not definedon Node 18: the multipart boundary no longer needs the global WebCrypto, so uploads work across the full supported Node 18.2+ range (#1378).
Upgrading from 2.2.0
bot.init()(run bystartPolling(),run()and the firsthandleUpdate()) now callsgetMe()once to learn the bot's identity. Tests that inject a fakefetchneed to answergetMe.- Handlers that relied on
ctx.reply()landing in the bare chat for business or forum-topic messages now reply in the business connection / topic; pass{ business_connection_id: undefined }or{ message_thread_id: undefined }to keep the old target. ctx.reply()in aguest_messagehandler now throws; useapi.answerGuestQuery.
Full changelog: v2.2.0...v2.3.0