Skip to content

Draft: feat: rewrite library in TypeScript with Zod runtime typing#1302

Draft
yagop wants to merge 22 commits into
masterfrom
feat/typescript
Draft

Draft: feat: rewrite library in TypeScript with Zod runtime typing#1302
yagop wants to merge 22 commits into
masterfrom
feat/typescript

Conversation

@yagop
Copy link
Copy Markdown
Owner

@yagop yagop commented May 9, 2026

Full rewrite of node-telegram-bot-api targeting Node 18+, replacing the Babel/CommonJS source with strict TypeScript and Zod-validated payload schemas. The public API stays the same; the implementation is modernised end-to-end.

Project setup

  • type: module + dual ESM/CJS exports via tsconfig + tsc emit
  • tsconfig.json (strict) + tsconfig.build.json for dist output
  • Bumped engines to node >=18 to take advantage of built-in fetch/FormData

Source (src/)

  • TelegramBot ported to TypeScript with all 187 Bot API methods typed
  • Polling rewritten with AbortController + native timers (no bl/pump)
  • WebHook rewritten using node:http/https with optional secret-token auth
  • HTTP transport unified on built-in fetch + FormData + Blob
  • Errors hierarchy preserves prototype chain after re-throw
  • prepareFile()/prepareFiles() handle paths, buffers and streams uniformly

Types (src/types/)

  • Zod schemas for User, Chat, Message (recursive), Update, CallbackQuery, InlineQuery, Reaction, Payments, Stickers, ForumTopic, ChatMember, ChatBoost, BusinessConnection, etc., cross-referenced against the go-telegram/bot models package
  • Inferred TypeScript types exported alongside each schema
  • .passthrough() on top-level objects keeps the library forward-compatible with new Telegram fields

Dependencies

  • Removed: @cypress/request, @cypress/request-promise, bl, pump, eventemitter3, file-type, mime, debug, array.prototype.findindex, babel-* packages, mocha, istanbul, is, is-ci, node-static
  • Added: zod (runtime types) + tsx + typescript (dev only)
  • Tiny zero-dep replacements live in src/internal/{debug,mime,file-type}.ts

Tests

  • Migrated from Mocha+Babel to Node's built-in node:test runner
  • 5 unit suites: errors, utils, schemas, format-send-data (via stubbed fetch), TelegramBot dispatch + onText + reply listeners
  • Integration suite auto-detects whether api.telegram.org is reachable; falls back to a local Bot-API-shaped mock server when offline
  • 62/62 tests passing, tsc --noEmit clean
  • All tests pass
  • I have run npm run doc

Description

References

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a draft full rewrite of node-telegram-bot-api targeting Node 18+, migrating the implementation to strict TypeScript, adding Zod-based runtime schemas for Bot API payloads, and modernizing HTTP/polling/webhook internals around native fetch, FormData, and AbortController, while aiming to keep the public API compatible.

Changes:

  • Replaces legacy CommonJS/Babel implementation with TypeScript ESM modules and a new typed core (TelegramBot, polling, webhook, HTTP transport).
  • Adds comprehensive Zod schemas + exported inferred types for Telegram payloads.
  • Migrates tests from Mocha to Node’s built-in node:test runner, adding unit + integration suites (with a local mock server fallback).

Reviewed changes

Copilot reviewed 35 out of 38 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tsconfig.json Adds strict TS config for development/typechecking.
tsconfig.build.json Defines build emit configuration for dist/.
package.json Switches to ESM package, adds TS build/test scripts, updates deps/engines/exports.
index.js Removes legacy Node-version-based entrypoint logic.
src/index.ts Adds new public TS entrypoint with exports + default export.
src/telegram.ts New TypeScript TelegramBot implementation with typed methods and event dispatch.
src/http.ts New fetch-based HTTP client, body building, and error mapping.
src/polling.ts New polling implementation using AbortController and timers.
src/webhook.ts New webhook server using node:http/https, health endpoint, optional secret token.
src/utils.ts Adds file preparation helpers, stable stringify, stream-to-buffer helpers, deprecate helper.
src/utils.js Removes legacy deprecate helper JS file.
src/errors.ts New typed error hierarchy with code and preserved prototype chain.
src/errors.js Removes legacy JS error implementations.
src/internal/debug.ts Adds a tiny debug-compatible shim.
src/internal/mime.ts Adds minimal built-in MIME lookup (replacing external dep).
src/internal/file-type.ts Adds minimal magic-byte file-type detection (replacing external dep).
src/types/schemas.ts Adds Zod schemas + inferred types for Telegram payloads and message type constants.
src/types/options.ts Adds TS option/request payload types for common API methods.
src/types/index.ts Re-exports schemas and option types from a single types entrypoint.
src/telegramWebHook.js Removes legacy JS webhook implementation.
src/telegramPolling.js Removes legacy JS polling implementation.
test/unit/errors.test.ts Adds unit tests for the new error hierarchy.
test/unit/utils.test.ts Adds unit tests for new utils (stringify/prepareFile/prepareFiles/streamToBuffer).
test/unit/format-send-data.test.ts Adds unit tests for request body formatting via HttpClient + file pipeline.
test/unit/schemas.test.ts Adds unit tests validating key Zod schemas and passthrough behavior.
test/unit/telegram.test.ts Adds unit tests for TelegramBot request formatting and update dispatch behavior.
test/integration/mock-server.ts Adds a local Telegram-compatible mock API server for integration tests.
test/integration/telegram.test.ts Adds integration tests that probe live API availability and otherwise use the mock server.
test/utils.js Removes legacy Mocha test utilities (mock/static servers, request helpers).
test/test.format-send-data.js Removes legacy Mocha suite for _formatSendData.
test/telegram.js Removes legacy large Mocha integration suite.
test/mocha.opts Removes Mocha configuration file.
.travis.yml Removes Travis CI configuration.
.eslintrc Removes legacy ESLint configuration (Airbnb + Babel parser).
.eslintignore Removes legacy ESLint ignore file.
.babelrc Removes Babel configuration.
.gitignore Updates ignore list (adds dist, .claude).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json Outdated
Comment thread src/telegram.ts Outdated
Comment thread src/telegram.ts
Comment on lines +1530 to +1540
if (!content.type) throw new FatalError("content.type is required");
const qs: Record<string, unknown> = {
...options,
business_connection_id: businessConnectionId,
active_period: activePeriod,
};
const { formData, fileIds } = await prepareFiles(content.type, [content as never], {}, this.options.filepath);
const inputContent: Record<string, unknown> = { ...content };
inputContent[content.type] = fileIds[0] ?? `attach://${content.type}_0`;
qs.content = stringify(inputContent);
return this._request("postStory", { qs, formData });
Comment thread src/telegram.ts
Comment on lines +1563 to +1574
if (!content.type) throw new FatalError("content.type is required");
const qs: Record<string, unknown> = {
...options,
business_connection_id: businessConnectionId,
story_id: storyId,
};
const { formData, fileIds } = await prepareFiles(content.type, [content as never], {}, this.options.filepath);
const inputContent: Record<string, unknown> = { ...content };
inputContent[content.type] = fileIds[0] ?? `attach://${content.type}_0`;
qs.content = stringify(inputContent);
return this._request("editStory", { qs, formData });
}
Comment thread src/webhook.ts
Comment on lines +66 to +70
this.host = options.host ?? "0.0.0.0";
this.port = options.port ?? 8443;
this.healthEndpoint = options.healthEndpoint ?? "/healthz";
this._healthRegex = new RegExp(this.healthEndpoint);
this._secretToken = options.secretToken;
Comment thread src/webhook.ts Outdated
Comment on lines +147 to +152
if (!url.includes(this.bot.token)) {
debug("WebHook request unauthorized");
res.statusCode = 401;
res.end();
return;
}
Comment thread src/webhook.ts Outdated
req.on("data", (chunk: Buffer) => {
total += chunk.length;
if (total > MAX_PAYLOAD_BYTES) {
reject(new FatalError("Webhook payload exceeds 50MB safety cap"));
Comment thread test/integration/telegram.test.ts Outdated
Comment thread test/integration/telegram.test.ts Outdated
Comment on lines +42 to +46
const response = await fetch(`https://api.telegram.org/bot${TOKEN}/getMe`, {
signal: ctrl.signal,
});
clearTimeout(timer);
return response.ok;
@yagop yagop force-pushed the feat/typescript branch from 016ed8b to cd2b5a9 Compare May 9, 2026 23:30
yagop and others added 4 commits May 10, 2026 06:05
Full rewrite of node-telegram-bot-api targeting Node 18+, replacing the
Babel/CommonJS source with strict TypeScript and Zod-validated payload
schemas. The public API stays the same; the implementation is modernised
end-to-end.

Project setup
- type: module + dual ESM/CJS exports via tsconfig + tsc emit
- tsconfig.json (strict) + tsconfig.build.json for dist output
- Bumped engines to node >=18 to take advantage of built-in fetch/FormData

Source (src/)
- TelegramBot ported to TypeScript with all 187 Bot API methods typed
- Polling rewritten with AbortController + native timers (no bl/pump)
- WebHook rewritten using node:http/https with optional secret-token auth
- HTTP transport unified on built-in fetch + FormData + Blob
- Errors hierarchy preserves prototype chain after re-throw
- prepareFile()/prepareFiles() handle paths, buffers and streams uniformly

Types (src/types/)
- Zod schemas for User, Chat, Message (recursive), Update, CallbackQuery,
  InlineQuery, Reaction, Payments, Stickers, ForumTopic, ChatMember,
  ChatBoost, BusinessConnection, etc., cross-referenced against the
  go-telegram/bot models package
- Inferred TypeScript types exported alongside each schema
- .passthrough() on top-level objects keeps the library forward-compatible
  with new Telegram fields

Dependencies
- Removed: @cypress/request, @cypress/request-promise, bl, pump,
  eventemitter3, file-type, mime, debug, array.prototype.findindex,
  babel-* packages, mocha, istanbul, is, is-ci, node-static
- Added: zod (runtime types) + tsx + typescript (dev only)
- Tiny zero-dep replacements live in src/internal/{debug,mime,file-type}.ts

Tests
- Migrated from Mocha+Babel to Node's built-in node:test runner
- 5 unit suites: errors, utils, schemas, format-send-data (via stubbed
  fetch), TelegramBot dispatch + onText + reply listeners
- Integration suite auto-detects whether api.telegram.org is reachable;
  falls back to a local Bot-API-shaped mock server when offline.
  TEST_TELEGRAM_TOKEN env var is required (no hardcoded fallback)
- 62/62 tests passing, tsc --noEmit clean

CI (.github/workflows/ci.yml)
- typecheck job runs tsc --noEmit
- unit-node matrix runs npm test on Node 18, 20 and 22
- unit-bun runs the same suite under latest Bun (uses node:test compat)
- build job verifies dist/{index,telegram}.{js,d.ts} after tsc emit
- integration job is gated on workflow_dispatch input + repo secret
- concurrency cancels superseded in-flight runs
danielperez9430 and others added 6 commits May 10, 2026 15:35
- Track package-lock.json (remove from .gitignore) so setup-node@v4
  npm cache step no longer fails the CI job
- Add @types/node dev dependency — was missing, causing tsc to exit 2
- Remove CJS require export — build only emits ESM .js via NodeNext
- startPolling(): default restart to false (idempotent by default)
- postStory/editStory: extract file from content[content.type] as media
  so prepareFiles() actually picks up the upload
- webhook: replace RegExp health-check with exact pathname comparison
  to prevent regex metachar injection via healthEndpoint config
- webhook: check pathname only (not full URL) for token auth to prevent
  bypass via query-string token
- webhook: only wrap non-BaseError errors in FatalError to avoid
  EFATAL: EFATAL double-prefix messages
- probeLive(): move clearTimeout into finally so the timer always clears

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The project had no appveyor.yml so AppVeyor fell back to its default
which uses Node 8 — incompatible with every dependency (tsx, typescript,
esbuild all require >=18). Explicit config installs Node 18/20/22 x64
and runs typecheck + unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Windows cmd.exe doesn't expand globs, and Node's --test only got
native glob support in v22 — so the existing
  node --test --import tsx test/unit/*.test.ts
worked on Linux (bash expansion) and on Node 22 Windows, but failed
on Node 18/20 Windows with 'Could not find test/unit/*.test.ts'.

Replace with a tiny wrapper that resolves the file list via
fs.readdirSync and forwards to node --test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added:
- deleteMessageReaction
- deleteAllMessageReactions
@danielperez9430
Copy link
Copy Markdown
Collaborator

Already review and fix types from method "getMe" to "sendPhoto" (list from oficial docs Telegram Bot API).

I will continue checking manual the rest of the methods for missing types and have it all in place.

yagop and others added 7 commits May 10, 2026 19:18
…HttpClient

- Drop the FORCE_MOCK / mock-server harness; integration suite now hits the
  real Bot API and exercises ~60 methods (sends, file variants, media group,
  forwarding/copying, editing, deletes, reactions, chat info, invite link
  round-trip, idempotent self-management, stickers, listeners, error mapping)
- Honor Telegram's 429 retry_after inside HttpClient.request so callers no
  longer have to wrap calls in their own retry loop; gated by a new
  request.maxRetriesOn429 option (default 2, set to 0 to opt out)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…actions

Both methods are exercised against a real bot-sent message, with a soft-skip
on ETELEGRAM so the suite stays green when the chat lacks the required admin
rights or the endpoint isn't available for the bot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Strings are compiled with `new RegExp(pattern)` when the listener is added,
so processUpdate() no longer needs to defensively re-wrap each entry on
every text message it dispatches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Rename scripts to test:{node,bun}:{unit,integration}
- New test:bun:integration with --timeout 120000 to accommodate the
  in-library 429 retry path (Bun's default 5s per-test timeout was too tight)
- Add softSkip(t, reason) helper that no-ops on Bun, where node:test's
  t.skip() throws NotImplementedError; keeps the same 'skipped' fidelity
  on Node and lets Bun pass the test silently

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scripts were split into test:node:{unit,integration} and
test:bun:{unit,integration}, so the previous npm test / npm run
test:integration invocations no longer exist. Update GitHub
Actions and AppVeyor to call the new names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@danielperez9430
Copy link
Copy Markdown
Collaborator

Reviewed up to the "sendLocation" method, added support for the sendLivePhoto method + the files to run the test.

yagop and others added 5 commits May 11, 2026 03:37
Replace the obsolete `npm run doc` checkbox with the current
test:node:{unit,integration} + typecheck checklist, and add a
short guide covering the four runner commands (Node + Bun ×
unit + integration), the required env vars
(NODE_TELEGRAM_TOKEN, TEST_GROUP_ID, TEST_USER_ID) and the
optional sticker/emoji overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Continue the type-correctness pass against the official Telegram
Bot API docs:

- Replace generic Record<string, unknown> form/option arguments with
  explicit inline shapes for getMe / getFile / deleteWebHook /
  getWebHookInfo, the chat-member admin methods (ban/unban/restrict/
  promote/setChatMemberTag, banChatSenderChat/unbanChatSenderChat),
  invite-link methods (export/create/edit/revoke), join-request
  approval/decline, getUserProfilePhotos/Audios, setMessageReaction,
  setUserEmojiStatus, sendMessageDraft, etc.
- Refine sendLivePhoto to accept per-file FileMeta so callers can set
  distinct content-type/filename for the live photo and the still
- Add SentGuestMessage and BotAccessSettings Zod schemas + inferred
  TypeScript types
- Add getUserPersonalChatMessages helper
- Extend test/unit/telegram.test.ts coverage for the new shapes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Continue the type-correctness pass against the official Telegram Bot
API docs, covering the remaining methods left in src/telegram.ts:

- Payments / Stars / Games: createInvoiceLink, answerShippingQuery,
  answerPreCheckoutQuery, getMyStarBalance, getStarTransactions,
  refundStarPayment, editUserStarSubscription, sendGame, setGameScore,
  getGameHighScores
- Delete messages: deleteMessage, deleteMessages, deleteMessageReaction,
  deleteAllMessageReactions
- Gifts / verification / business accounts / stories: sendGift,
  giftPremiumSubscription, verifyUser / verifyChat / remove*,
  readBusinessMessage, deleteBusinessMessages, setBusinessAccount*,
  getBusinessAccountGifts, getUserGifts, getChatGifts,
  convertGiftToStars / upgradeGift / transferGift, postStory,
  repostStory, editStory, deleteStory

Also fixes the deleteAllMessageReactions integration test, which was
passing an unsupported `message_id` parameter (the API removes
reactions chat-wide by user/actor, not per-message).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rams

Serialize suggested_post_parameters, link_preview_options, and story areas in the request pipeline. Extend _fixEntitiesField to cover description_entities, question_entities, title_entities, and text_entities.

Apply all fix helpers consistently to both form and query-string request bodies.
…emas

sendPoll now accepts InputPollOption[] instead of string[], supporting per-option text formatting and media. Fill in missing fields on SendVenue/SendContact/SendLocation/SendPoll options (business_connection_id, suggested_post_parameters, correct_option_ids, etc).

Add Zod schemas for all InputMedia discriminated unions used by sendMediaGroup, editMessageMedia, and sendPoll.
@danielperez9430
Copy link
Copy Markdown
Collaborator

Reviewed up to the "sendPoll" , more changes in progress

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants