Skip to content

feat: rewrite library in strict TypeScript with generated API types#1302

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

feat: rewrite library in strict TypeScript with generated API types#1302
yagop wants to merge 49 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 from Babel/CommonJS to strict TypeScript. Hand-written source in src/ compiles to dist/ (the published artifact). The public API surface is preserved; the implementation is modernised end-to-end.

Warning

Breaking changes. This is a major release (1.0.0):

  • ESM-only. The package is "type": "module" with an import-only exports map. require('node-telegram-bot-api') no longer works — consumers must use ESM (import).
  • Node 18+ required (engines.node >= 18), to rely on built-in fetch/FormData/Blob.
  • Heavy runtime dependencies removed (see below).

Project setup

  • "type": "module", NodeNext resolution — every relative import carries a .js extension.
  • tsconfig.json (strict) for typecheck over src/ + test/; tsconfig.build.json emits dist/ from src/ only.
  • engines.node bumped to >= 18.
  • There is no separate lint stepnpm run typecheck under strict is the static-analysis gate.

Source (src/)

  • TelegramBot ported to TypeScript with one method per Bot API method.
  • Polling rewritten with AbortController + native timers (no bl/pump); polling and webhook remain mutually exclusive.
  • WebHook rewritten on node:http/https with optional secretToken auth, a health endpoint, and a 50 MB payload cap.
  • HTTP transport (src/http.ts) unified on built-in fetch + FormData + Blob; it builds the URL, picks x-www-form-urlencoded vs multipart/form-data, unwraps Telegram's { ok, result, description, error_code } envelope, and retries on 429 honoring retry_after.
  • Error hierarchy (FatalError/ParseError/TelegramError) preserves the prototype chain after re-throw.
  • prepareFile()/prepareFiles() handle filesystem paths, Buffers and streams uniformly, falling back to treating a plain string as a Telegram file_id or public URL.
  • src/internal/ holds tiny zero-dep replacements: debug, magic-byte file-type sniffing, and mime lookup.

Types (src/types/)

  • src/types/schemas.ts is generated by scripts/api-parser.ts (npm run generate:types), which scrapes https://core.telegram.org/bots/api and emits plain TypeScript type aliases — one per documented object, abstract "one of" objects as unions, and per method a <Method>Params + <Method>Result.
  • No Zod, no runtime validation. The file is kept docs-faithful: the generator is strict (any unmappable type or unresolved reply type is a hard error rather than unknown) and types carry no index signatures.
  • Each telegram.ts method types its trailing options argument inline as Omit<<Method>Params, ...positional args>, so the positional-vs-options split lives next to the method.
  • Re-run the generator when Telegram adds methods/fields rather than editing the file by hand.

Dependencies

  • Removed: @cypress/request, @cypress/request-promise, bl, pump, eventemitter3, file-type, mime, debug, array.prototype.findindex, the babel-* toolchain, mocha, istanbul, is, is-ci, node-static.
  • Added (dev only): typescript, tsx, @types/node. No runtime dependencies.

Tests

  • Migrated from Mocha + Babel to Node's built-in node:test + node:assert/strict (runs identically on Node and Bun).
  • Unit suites stub globalThis.fetch to assert the wire format (URL, body params, structured-field serialization, multipart uploads) — no network, no token: errors, utils, schemas, http, format-send-data, and telegram (dispatch + onText + reply listeners).
  • Integration suite (test/integration/telegram.test.ts) hits the real api.telegram.org, throttles ~1.1s between calls, and deliberately skips irreversible mutations. Requires NODE_TELEGRAM_TOKEN, TEST_GROUP_ID, TEST_USER_ID.

Checklist

  • npm run typecheck is clean
  • npm run test:node:unit passes
  • Migration notes for ESM-only + Node 18+ documented for the changelog

References

  • Closes the TypeScript-rewrite effort tracked on feat/typescript.

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

danielperez9430 commented May 25, 2026

GitHub is deprecating Node.js 20 on Actions runners.

The following timeline applies:

  • June 2, 2026 — Node.js 24 becomes the default; actions still on Node 20 will be force-migrated
  • September 16, 2026 — Node.js 20 removed entirely from runners

This triggered a warning: actions/checkout@v4 and actions/setup-node@v4 run on Node 20 and will stop working.

Changes made in the last commit:

  • actions/checkout@v4 → @v6 (Node.js 24 runtime)
  • actions/setup-node@v4 → @v6 (Node.js 24 runtime)
  • Single-node jobs: node-version: 22 → 24 (current LTS)
  • Unit test matrix: ["18", "20", "22"] → ["20", "22", "24"] (drop EOL 18, add LTS 24)

More info: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

yagop and others added 3 commits June 1, 2026 11:18
…conditionally

deleteMessageReaction/deleteAllMessageReactions were soft-skipping because
they never identified whose reaction to remove. The bot's own reaction is a
user reaction, so pass the bot's user_id (resolved once via getMe in the
before hook). Drop the try/softSkip so these hard-fail on regression.

getCustomEmojiStickers no longer skips when TEST_CUSTOM_EMOJI_ID is unset;
the env var now falls back to a default id, matching the TEST_STICKER_SET_NAME
pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganize test/integration/telegram.test.ts into one describe() block per
Bot API method, and add live-API coverage for the high-value untested methods:
chat management (title/description/permissions/pinning), invite links, message
editing parameters, forwarding/copying parameters, member moderation (tolerating
the expected ETELEGRAM refusals so no real member is harmed), and misc queries.

Mutations capture-and-restore chat state; capability-gated and irreversible
methods (setChatPhoto, subscription links, answerCallbackQuery) soft-skip.

Also add a glob-based integration runner so the npm script picks up every
test/integration/*.test.ts, and a type-checker-based coverage audit
(scripts/coverage-audit.mjs → docs/integration-coverage.md) that reports which
methods and option parameters are exercised: now 74/175 methods and 66/419
option params.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the softSkip and isTelegramError helpers along with every try/catch
and .catch in the suite. Tolerant tests that the live chat supports become
straight-line happy-path assertions; methods Telegram structurally refuses
(owner moderation, Stars subscription links, invalid callback ids) become
assert.rejects(..., TelegramError). setChatPhoto/deleteChatPhoto now set a
photo and clean it up so both make real assertions and leave the chat as
found. Full live run: 91 pass, 0 skipped, 0 fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yagop yagop force-pushed the feat/typescript branch 2 times, most recently from cbdcbfa to 36b152b Compare June 1, 2026 12:46
yagop and others added 16 commits June 1, 2026 21:13
… audit

Add integration tests driving previously-uncovered Bot API methods:
- Sticker-set lifecycle: createNewStickerSet -> exercise (add/replace/
  position/emoji/keywords/title/thumbnail/upload, plus mask- and
  custom-emoji-typed sets) -> deleteStickerSet, each with a real assertion.
- Forum-topic methods via unconditional ETELEGRAM error-path tests (and
  optional env-gated happy paths), chat-join-request error paths, and the
  remaining send/edit option-param coverage.

Regenerate docs/integration-coverage.md (methods 74->104/175) and annotate
every remaining gap with a testability classification (DESTRUCTIVE /
SPECIAL-SETUP / ENV-LIMITED / TESTABLE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…verage audit

Add real, asserting integration tests for the gaps in
docs/integration-coverage.md that are exercisable with the standard
TEST_GROUP_ID / TEST_USER_ID fixtures: getAvailableGifts,
getMyStarBalance, getStarTransactions, setChatMenuButton and
setMyDefaultAdministratorRights (with save/restore), plus previously
untested option params on sendMessage, sendLocation, sendVenue, sendDice,
sendContact, sendPoll, getUpdates, promoteChatMember and
answerCallbackQuery.

Gaps tagged DESTRUCTIVE / SPECIAL-SETUP / ENV-LIMITED are intentionally
left uncovered — they need context a bare token + group cannot
manufacture (business connections, inline/callback queries, forum or
channel chats, the stars/gifts economy, payment providers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-written Zod schemas (src/types/schemas.ts) and option
types (src/types/options.ts) with a single docs-faithful, generated
src/types/schemas.ts.

- scripts/api-parser.ts scrapes https://core.telegram.org/bots/api with
  Bun's HTMLRewriter and emits plain TypeScript `type` aliases: every
  documented object, "one of" unions, and per method a `<Method>Params`
  (full request) and `<Method>Result` (reply type parsed from prose).
  Strict: unmappable types / unresolved replies are hard errors, never
  `unknown`; no index signatures. Run via `npm run generate:types`.
- telegram.ts consumes the generated types: each method's options arg is
  `Omit<<Method>Params, <positional args>>` inline, and the 15 former
  `Promise<unknown>` returns now use real reply types.
- Tests drop runtime `.parse` validation in favour of compile-time typing;
  this surfaced two real test bugs (allow_revoting -> allows_revoting,
  correct_option_id -> correct_option_ids).
- Remove the zod dependency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace hand-rolled inline option types and inline return types on every
TelegramBot API method with the generated docs-faithful types:

- Options/positional args now derive from `<Method>Params` (bare or
  `Omit<…Params, …positional>`); `Record<string, unknown>`/`unknown`
  positional args (result, button, checklist, content, sticker, gift
  settings, admin rights) now use their real generated member types.
- Return types now use `Promise<<Method>Result>` instead of inline
  `Promise<boolean>` / `Promise<Message>` etc.
- Normalize the setWebhook endpoint string (was "setWebHook") to match
  its siblings and map to SetWebhookResult.

Multipart upload helpers (sendMediaGroup, sendPaidMedia, editMessageMedia,
createNewStickerSet, addStickerToSet, setStickerSetThumbnail) keep their
curated shapes — the generated InputMedia/InputSticker (`media: string`)
can't carry a FileInput. Local helpers (getFileLink, downloadFile,
getFileStream, polling/webhook lifecycle) are unchanged.

The integration test's ad-hoc `Record<string, boolean>` AdminRights alias
now uses the generated ChatAdministratorRights.

typecheck clean; node + bun unit suites 62/62; build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ams`

Add `satisfies <Method>Params` to the inline object literal passed to
this._form(...) for 99 methods, so the constructed request is checked
against the generated request type at compile time. Expand every such
literal with more than one member to multiline for readability.

satisfies is omitted where it cannot hold:
- payloads that stringify a structured field (the field becomes a string),
- 7 methods whose signature accepts the broader ChatId (number | string)
  while the docs type chat_id/from_chat_id as number (sendMessageDraft,
  approve/declineSuggestedPost, repostStory, readBusinessMessage, …).

Pulls the remaining positional-only methods' `*Params` into the imports.

typecheck clean; node + bun unit suites 62/62; build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…arams

Move the remaining manual stringify() of structured fields into a new
_fixJsonFields normalizer in _request (button, menu_button, checklist,
custom_emoji_ids, emoji_list, keywords, mask_position, results, sticker),
so the method bodies pass typed values through and can assert their
payload with `satisfies <Method>Params`:

  savePreparedKeyboardButton, setChatMenuButton, editMessageChecklist,
  getCustomEmojiStickers, setStickerEmojiList, setStickerKeywords,
  setStickerMaskPosition, answerInlineQuery, deleteBusinessMessages.

- replaceStickerInSet: `form` is now Omit<ReplaceStickerInSetParams, …>
  with the docs-required `sticker` (the param is now required).
- approveSuggestedPost, declineSuggestedPost, readBusinessMessage,
  repostStory: narrow chat_id/from_chat_id from ChatId to number to
  match the API, enabling satisfies.

Also type the sole argument of the no-positional methods (getWebHookInfo,
getMe, logOut, close, getForumTopicIconStickers, removeMyProfilePhoto,
getMyStarBalance, getAvailableGifts) as their generated *Params instead
of `{}`.

typecheck clean; node + bun unit suites 62/62; build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On 429 the client still retries (up to maxRetriesOn429) but now sleeps
min(retry_after + 1, maxRetryAfterSeconds) seconds, so a long flood-wait
(e.g. retry_after 1847) no longer blocks the process for ~31 minutes. New
request.maxRetryAfterSeconds option (default 60; Infinity restores the
old always-wait behavior).

Refactor: request() is the retry loop, _attempt() performs one HTTP
round-trip (timeout + abort + parse). Introduce ERROR_CODE_TOO_MANY_REQUESTS
and the default/buffer constants.

Adds test/unit/http.test.ts covering retry, the sleep cap, Infinity, and
maxRetriesOn429: 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sendPoll: allow_adding_options requires a non-anonymous poll, so set
  is_anonymous:false (Telegram returned ANONYMOUS_OPEN_INVALID).
- getChatMenuButton: chat_id targets a private chat only, so use USER_ID
  instead of the supergroup (was rejected with "invalid chat_id specified").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ci.yml unit-test matrix: add "26".
- telegram.ts: remove 40 type imports left unused after the args/return
  rewiring (data types now reached via *Params/*Result).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The only Bot API method documented in schemas.ts that lacked an
implementation. Adds the method (errors serialized via _fixJsonFields,
asserted with `satisfies SetPassportDataErrorsParams`); all 176 documented
methods are now implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove maxRetryAfterSeconds — on 429 the client again sleeps the full
retry_after + 1 (up to maxRetriesOn429). Keeps the request()/_attempt()
split. Test covers retry, give-up-after-maxRetries, and no-retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Targets a dedicated test bot; saves the current name, sets a new one,
asserts getMyName reflects it, then restores. (setMyProfilePhoto is left
uncovered: the endpoint hangs server-side for a standard bot token —
requires managed-bot/business capability.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflects setMyName/setMyDefaultAdministratorRights coverage and the new
setPassportDataErrors method (110/176 methods). Fix the stale options.ts
source-of-truth reference (now schemas.ts) in both the doc and the
generator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on guide

Review the rewrite against v0.67.0 and clean up the docs/examples to match.

- feat(telegram): typed `on()` event overloads via a new `TelegramBotEvents`
  map (exported alongside `EventMetadata`); handlers now infer their payload
  (e.g. `bot.on('callback_query', q => …)`) instead of `unknown`
- docs: add doc/migration.md (0.6x → 1.0 breaking changes: ESM-only, bundled
  types, removed option aliases, error `.response` shape, NTBA_FIX_350, …)
- docs: fix CHANGELOG (drop inaccurate Zod claims; the generated types carry
  no runtime validation) and correct the removed-items list
- docs: README now shows ESM/TS usage; drop the misleading `@types/...` note;
  fix usage.md (NTBA_FIX_350 removed, error `.response` shape)
- examples: rewrite in TypeScript (polling, webhook/{express,https,heroku},
  game) with idiomatic `import TelegramBot from 'node-telegram-bot-api'`; add
  examples/README.md, tsconfig.json and an express type shim; remove the
  defunct Zeit-Now and OpenShift-2 webhook examples
- docs(setMyProfilePhoto): warn that a static photo must be JPEG

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add a happy-path test uploading a static .jpeg, with removeMyProfilePhoto
  cleanup (no API to read back the bot's current photo, so not a round-trip)
- add a fixture test/data/chat_photo.jpeg
- add a negative test: a static .png is rejected — Telegram's
  photos.uploadProfilePhoto backend only accepts JPEG and nothing in the stack
  converts it, so the call returns 504 (ETELEGRAM) or stalls; a short-timeout
  bot bounds the wait and we assert it never resolves to true
- move the coverage audit doc/index: docs/ -> doc/ (consolidate the two dirs);
  point scripts/coverage-audit.mjs at the new path and regenerate
- PR template: document the TEST_CUSTOM_EMOJI_ID default

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add integration tests for the no-fixture gaps surfaced in
doc/integration-coverage.md (saved as doc/missing-tests-plan.md):

- new happy-path blocks: createInvoiceLink (XTR/Stars link),
  unbanChatMember (only_if_banned no-op), getChatGifts (read-only)
- param top-ups on existing blocks: setMyName language_code,
  sendPoll country_codes + hide_results_until_closes, copyMessage
  caption_entities/reply_parameters/video_start_timestamp,
  stopMessageLiveLocation reply_markup

Gaps needing a Premium/paid/special fixture (business connection,
Stars subscription, BotFather game, managed bot, channel suggested
post, forum supergroup) are left to the plan doc rather than added
as reject-only tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yagop yagop changed the title Draft: feat: rewrite library in TypeScript with Zod runtime typing feat: rewrite library in strict TypeScript with generated API types Jun 3, 2026
TEST_GROUP_ID is now a real forum supergroup with the bot holding
can_manage_topics, so the forum methods no longer reject. Convert the
11 reject-path forum blocks to happy-path tests and add a new
deleteForumTopic block.

Per-topic blocks create a throwaway topic in before() and delete it in
after() (tolerating ETELEGRAM). General-topic blocks restore General to
its default via a shared restoreGeneral helper: hiding General
auto-closes it and unhiding does not reopen it, so the restore order is
unhide -> reopen -> rename "General".

Verified live against the forum group: 13 forum tests pass, typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread package.json
"file-type": "^3.9.0",
"mime": "^1.6.0",
"pump": "^2.0.0"
"node": ">=18"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

https://nodejs.org/en/about/previous-releases

Support "Maintenance" LTS onwards i.e. >= 22 (which will be supported for at-least another year) before switching over to >= 24 in 2027.

yagop and others added 2 commits June 4, 2026 10:08
Add integration coverage for previously-untested option params, raising
option-param coverage from 172/419 (41%) to 209/419 (50%):

- allow_paid_broadcast: sendMessage, sendDice, sendLocation, sendVenue,
  sendContact, sendMediaGroup, sendPoll
- forwardMessage: video_start_timestamp
- promoteChatMember: all can_* flags + is_anonymous (via the owner-rejection
  assertion, exercising every flag on the wire)
- getChatGifts: all exclude_*/sort_by_price/offset/limit
- createInvoiceLink: photo_url/size/width/height, subscription_period
- setChatMenuButton: chat_id (targets a private chat, reads back, resets)
- copyMessages: add a real assertion to the previously empty result loop

message_thread_id was intentionally not added: TEST_GROUP_ID is not a
forum-enabled chat (createForumTopic returns "the chat is not a forum"), so
those cases are not exercisable. createInvoiceLink need_*/send_*_to_provider
are inapplicable to XTR (Stars) invoices (STARS_INVOICE_INVALID).

Regenerate doc/integration-coverage.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add .claude/skills/run-tests/SKILL.md documenting how to run the unit and
integration suites: bun (preferred, auto-loads .env) vs node, the required
NODE_TELEGRAM_TOKEN/TEST_GROUP_ID/TEST_USER_ID env vars, scoping a run to one
method after a method change, methods that need special chat conditions (e.g.
a forum-enabled TEST_GROUP_ID), and when a full-suite run is required (core
pipeline changes).

Move the test how-to detail out of CLAUDE.md into the skill, leaving a pointer.
Un-ignore .claude/skills/ in .gitignore so the skill is shared (the rest of
.claude stays ignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

4 participants