telenest
v1.5.0
Published
Fully-typed NestJS module for Telegram: the Bot API (via Telegraf) and the MTProto user-account client (via GramJS). Send/receive as a bot, or sign in with your own account and control it from your app.
Maintainers
Readme
telenest
A fully-typed NestJS module for Telegram that wraps two different Telegram APIs behind one cohesive, strictly-typed package. Use the Bot API (powered by Telegraf) to run a normal @BotFather bot, and/or use the MTProto user-account client (powered by GramJS) to sign in as your own Telegram account and drive it from your app. Both sides share one error hierarchy, ship with pluggable session persistence, and are designed around testable seams so you can unit-test everything without ever touching the network.
Features
Bot API side (Telegraf — a normal bot)
TelegramBotModule.forRoot/forRootAsync— synchronous orConfigService-driven async configuration.TelegramBotService— an injectable, strongly-typed facade over Telegraf whose method signatures are derived from Telegraf's ownTelegramtype (viaParameters/ReturnType), so they never drift from the installed version.- Messaging:
sendMessage,sendPhoto,sendDocument,sendVideo,sendAudio,sendMediaGroup,sendLocation,sendChatAction,forwardMessage,copyMessage. - Editing & deletion:
editMessageText,editMessageReplyMarkup,deleteMessage. - Callbacks:
answerCbQuery. - Chat & member management:
getMe,getChat,getChatMembersCount,banChatMember,pinChatMessage. - Commands:
setMyCommands,getMyCommands. - Files:
getFile,getFileLink. - Webhooks:
setWebhook,deleteWebhook,getWebhookInfo,webhookCallback. - Handler delegates:
start,help,command,hears,action,on,use,catch.
- Messaging:
- Automatic launch/stop wired into the Nest lifecycle (long-polling by default, webhook mode supported); set
launch: falseto take manual control. - Escape hatches:
TelegramBotService.instance(rawTelegraf) andTelegramBotService.telegram(raw TelegrafTelegramclient), plus theTELEGRAM_BOTinjection token — nothing is hidden behind the facade. - Fluent keyboard builders:
InlineKeyboardBuilder,ReplyKeyboardBuilder, plus theremoveKeyboardandforceReplyhelpers. - Multiple named bots in one app: register each with
forRoot({ name })/forRootAsync({ name }), inject its facade with@InjectBot(name), and scope decorator handlers with@TelegramUpdate({ bot: name })— each bot fully isolated, nonestjs-telegraf. Omittingnamekeeps the single-bot API unchanged. See MULTIPLE-BOTS.md.
User-account side (GramJS / MTProto — sign in as yourself)
TelegramClientModule.forRoot/forRootAsync— configured with theapiId/apiHashfrom my.telegram.org.TelegramAuthService— drives the login state machine:sendCode→signIn→ (checkPasswordfor 2FA), pluslogOut,isAuthorized, andexportSession.TelegramUserService— act as your own account:getMe,getDialogs,getMessages,sendMessage/sendToSelf, plus media (sendFile,downloadMedia,downloadProfilePhoto,getMediaInfo, anddownloadMediaRange/streamMediafor progressive-video HTTP Range streaming), chat/channel management (joinChannel,leaveChannel,getParticipants,searchMessages,getFullChat), and message operations (editMessage,deleteMessages,forwardMessages,markAsRead,pinMessage).- Pluggable session persistence via the
SessionStoreinterface, withInMemorySessionStoreandFileSessionStore(writes0o600, owner-only) included — bring your own (Redis, a secrets manager, etc.) by implementing three methods (load/save/clear). - A clean test seam: services depend only on the
IGramClientinterface, and the GramJS package is touched in exactly one adapter file. Inject a fake viaclientFactoryand you never hit the network. - Library-owned DTOs (
GramUser,GramDialog,GramMessage,GramSendMessageParams, …) so consumers never import GramJS just to model a user or a message. - Multiple named user accounts in one app: register each with
forRoot({ name })/forRootAsync({ name }), inject its services with@InjectTelegramUser(name)/@InjectTelegramAuth(name), and scope inbound handlers with@OnUserMessage(filter, { client: name })— each account fully isolated, with its own session. Omittingnamekeeps the single-account API unchanged. See MULTIPLE-ACCOUNTS.md.
Shared
- One typed error hierarchy: a
TelegramErrorbase withTelegramConfigError,TelegramBotApiError,TelegramClientError,TelegramAuthError(carrying a closedTelegramAuthErrorCodeset), andTelegramSessionError, plus theisTelegramErrortype guard. TelegramModule.forRoot({ bot?, client?, isGlobal? })— an umbrella module that composes both sides from a single synchronous options object.- Strict TypeScript throughout (no
any, noenum—as constunions only), and every export documented with JSDoc.
Bot extras
- Decorator handler system —
@TelegramUpdateclasses with@Command/@Hears/@Action/@On/@Useand@Ctx/@Sender/… param decorators; optional auto-registration of the Telegram command menu from@Commanddescriptions. - Nest-style enhancers —
@UseTelegramGuards/Interceptors/Filters, with built-in allowlist and (memory-bounded) rate-limit guards. - Built-in webhook controller — secret-token-verified
POSTroute (generateWebhookSecret()helper; a secret is required by default). - Mini App init-data validation —
validateWebAppInitData()(HMAC-SHA256, constant-time, 24h freshness by default). - Helpers —
callback-datacodec (64-byte limit),splitMessageText(4096-char chunking, used bysendLongMessage), andwithRetry(429retry_afterback-off).
Production
- Observability — health indicators, update metrics, and an OpenTelemetry tracer bridge.
- Five session stores — in-memory, file (
0o600, atomic), key-value, Redis, and an AES-256-GCM encrypted wrapper. telenest/testing— ready-made mocks (createMockGramClient,createMockBotContext) and DTO builders, pulling in no SDK and no test runner.
Install
npm i telenestThe NestJS runtime peers are always required:
npm i @nestjs/common @nestjs/core reflect-metadata rxjsThen install only the Telegram client(s) you use — telegraf and telegram
are optional peer dependencies:
# Bot API only
npm i telegraf
# User account (MTProto) only
npm i telegram
# Both
npm i telegraf telegramImport only the side you need (subpath exports)
The package exposes four subpath entry points (plus the root) so a bot-only app never pulls in GramJS, and a user-account-only app never pulls in Telegraf:
| Import | Pulls in | Use when |
| ------------------------- | ------------------------ | --------------------------------- |
| telenest/bot | telegraf only | You only run a bot |
| telenest/client | telegram (GramJS) only | You only control a user account |
| telenest/common | neither | Just the shared error/types layer |
| telenest/testing | neither (test-only) | Mocking the library in your tests |
| telenest (root) | both | You use both sides |
import { TelegramBotModule } from "telenest/bot"; // no GramJS loaded
import { TelegramClientModule } from "telenest/client"; // no Telegraf loadedImporting from the root (
telenest) re-exports everything and therefore loads both SDKs — use the subpaths when you only need one side.
Two APIs at a glance
| | Bot API (Telegraf) | User account (MTProto / GramJS) |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Identity | A bot created via @BotFather | Your own real Telegram account |
| Auth | A single bot token | apiId + apiHash from my.telegram.org, then a phone/code/2FA login that yields a string session |
| What it can do | Reply to users, run commands, push notifications, manage groups/channels where the bot is an admin, inline keyboards, webhooks | Read your dialog list, fetch message history, send messages as you (including to "Saved Messages"), reach any chat you're a member of |
| Module | TelegramBotModule | TelegramClientModule |
| Main service | TelegramBotService | TelegramUserService (+ TelegramAuthService) |
| When to use | Building a bot users talk to; sending automated notifications from a server | Automating your own account; reading/aggregating your own chats; user-only actions a bot cannot perform |
📚 Documentation
→ Complete Documentation Index
Quick Links
- Getting Started — Installation & first bot/client setup
- API Reference — Complete API documentation
- Examples & Recipes — Practical copy-paste examples
- Advanced Usage — Production patterns & best practices
By Topic
- Bot API: BOT-API.md | Update Decorators | Guards/Filters/Interceptors | Multiple Bots | Mini Apps
- MTProto Client: User Client Guide | Authentication | Multiple Accounts
- General: Testing | Architecture
flowchart LR
App[Your NestJS App]
App -->|TelegramBotService| BotMod[TelegramBotModule]
App -->|TelegramUserService / TelegramAuthService| ClientMod[TelegramClientModule]
BotMod -->|Telegraf| BotAPI[(Telegram Bot API)]
ClientMod -->|GramJS / IGramClient| MTProto[(Telegram MTProto)]
ClientMod -. session string .-> Store[SessionStore]A bot cannot do everything a user can, and vice-versa. Many apps run both: a bot for user-facing interactions and a user-account client for reading/automating your own chats.
Quick Start — Bot API
Configure the bot module (async, pulling the token from ConfigService) and inject TelegramBotService anywhere.
import { Module, Injectable } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TelegramBotModule, TelegramBotService } from "telenest";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TelegramBotModule.forRootAsync({
isGlobal: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
token: config.getOrThrow<string>("BOT_TOKEN"),
// launch: false, // disable auto-launch to mount a webhook yourself
}),
}),
],
})
export class AppModule {}@Injectable()
export class NotificationsService {
public constructor(private readonly bot: TelegramBotService) {}
/** Sends a notification to a chat as the bot. */
public async notify(chatId: number | string, text: string): Promise<void> {
await this.bot.sendMessage(chatId, text);
}
}Register handlers and attach an inline keyboard with the builders:
import { InlineKeyboardBuilder } from "telenest";
this.bot.start(async (ctx) => {
const keyboard = new InlineKeyboardBuilder()
.url("Docs", "https://core.telegram.org/bots/api")
.callback("Ping", "ping")
.build();
await ctx.reply("Welcome!", { reply_markup: keyboard });
});
this.bot.action("ping", async (ctx) => {
await ctx.answerCbQuery("pong");
});By default the bot launches in long-polling mode on application bootstrap and stops on shutdown. Pass a launchOptions.webhook block (or set launch: false and mount bot.webhookCallback(...) yourself) to run in webhook mode.
Quick Start — User account (MTProto)
1. Get a session string
The user-account side authenticates your account, so it needs a one-time interactive login. The bundled CLI (examples/login-cli.ts) walks you through it and prints a portable session string.
# .env: TG_API_ID and TG_API_HASH from https://my.telegram.org
npm run loginPhone (+countrycode…): +15551234567
Login code from Telegram: 12345
Two-factor (2FA) password: ******** # only if 2FA is enabled
Signed in successfully.
=== SESSION STRING (save as TG_SESSION) ===
1BVtsOMM... # copy this into .env as TG_SESSIONThe session string grants full access to your account — treat it like a password and never commit it. See the Security note below.
2. Wire the module and use your account
Configure TelegramClientModule with your apiId / apiHash, resume from TG_SESSION, and persist future sessions with a SessionStore.
import { Module, Injectable } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TelegramClientModule, TelegramUserService, FileSessionStore } from "telenest";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TelegramClientModule.forRootAsync({
isGlobal: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
apiId: Number(config.getOrThrow<string>("TG_API_ID")),
apiHash: config.getOrThrow<string>("TG_API_HASH"),
session: config.get<string>("TG_SESSION"), // resume from the CLI output
sessionStore: new FileSessionStore("./.telegram.session"),
}),
}),
],
})
export class AppModule {}@Injectable()
export class SelfNotesService {
public constructor(private readonly user: TelegramUserService) {}
/** Sends a note to your own "Saved Messages" chat — as YOU, not a bot. */
public async remember(text: string): Promise<void> {
await this.user.sendToSelf(text);
}
}Need both sides at once? Use the umbrella module:
import { TelegramModule } from "telenest";
TelegramModule.forRoot({
isGlobal: true,
bot: { token: process.env.BOT_TOKEN! },
client: {
apiId: Number(process.env.TG_API_ID),
apiHash: process.env.TG_API_HASH!,
},
});Documentation
| Guide | What it covers |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| docs/TELEGRAM-MODULE.md | The umbrella TelegramModule, module composition, and global registration |
| docs/BOT-API.md | TelegramBotModule, TelegramBotService, keyboards, and the launch/webhook lifecycle |
| docs/BOT-UPDATE-DECORATORS.md | @TelegramUpdate handler classes — @Command/@Hears/@Action/@On + @Ctx/@Sender param decorators |
| docs/BOT-GUARDS-FILTERS-INTERCEPTORS.md | @UseTelegramGuards/@UseTelegramInterceptors/@UseTelegramFilters, built-in allowlist & rate-limit guards, default exception filter |
| docs/MULTIPLE-BOTS.md | Multiple named bots in one app — forRoot({ name }), @InjectBot(name), @TelegramUpdate({ bot }) scoping |
| docs/MINI-APP-INIT-DATA.md | validateWebAppInitData() — verify & parse Telegram Mini App initData server-side |
| docs/USER-CLIENT-MTPROTO.md | TelegramClientModule, TelegramUserService, dialogs/messages, and the DTOs |
| docs/MULTIPLE-ACCOUNTS.md | Multiple named user accounts in one app — forRoot({ name }), @InjectTelegramUser(name), @OnUserMessage(f, { client }) |
| docs/AUTHENTICATION.md | The sendCode → signIn → checkPassword flow and SessionStore persistence |
| docs/TESTING.md | Unit-testing both sides — ready-made telenest/testing mocks plus the IGramClient / clientFactory seam |
Testing
The library ships with 740+ tests and is built to keep network I/O out of your suite:
- Ready-made utilities in
telenest/testing:createMockGramClient()(a fully-typedjest.Mocked<IGramClient>),provideMockGramClient()(binds it to theTELEGRAM_GRAM_CLIENTtoken),createMockBotContext()(a spyable TelegrafContext), and DTO builders (aGramUser/aGramMessage/aGramDialog). The subpath pulls in no SDK and no test runner. See docs/TESTING.md. - The MTProto services depend only on the
IGramClientinterface — thetelegram(GramJS) package is imported in exactly one adapter file. Supply a fakeIGramClientand constructTelegramUserService/TelegramAuthServicedirectly (the bundled login CLI does exactly this when run outside of Nest DI). - Inside Nest, pass a
clientFactorytoTelegramClientModule(or override theTELEGRAM_GRAM_CLIENTtoken) to swap in a fake client without touching the network. - On the Bot side,
InMemorySessionStoregives the client side a zero-I/O session backend, and every facade method funnels through a single error-normalizing path that wraps failures inTelegramBotApiError.
Security
- A session string is a credential equivalent to your password. It encodes the auth keys that let anyone reconnect as your account without the phone code or 2FA. Never commit it, never log it, never paste it into a chat or an issue.
- Keep session files (
FileSessionStorewrites them0o600, owner read/write only) out of version control and off shared volumes. Add.telegram.sessionandTG_SESSIONto your.gitignore/ secret store. - If a session leaks, revoke it immediately: call
TelegramAuthService.logOut()(or terminate the session from Telegram's Settings → Devices), then run the login flow again to mint a fresh one. apiId/apiHashand the bottokenare likewise secrets — load them from the environment or a secrets manager, not from source.
