@overpod/mcp-telegram 1.24.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +45 -13
  3. package/dist/__tests__/admin-log.test.d.ts +1 -0
  4. package/dist/__tests__/admin-log.test.js +41 -0
  5. package/dist/__tests__/approve-join-request.test.d.ts +1 -0
  6. package/dist/__tests__/approve-join-request.test.js +107 -0
  7. package/dist/__tests__/boosts.test.d.ts +1 -0
  8. package/dist/__tests__/boosts.test.js +310 -0
  9. package/dist/__tests__/broadcast-stats.test.d.ts +1 -0
  10. package/dist/__tests__/broadcast-stats.test.js +172 -0
  11. package/dist/__tests__/business-chat-links.test.d.ts +1 -0
  12. package/dist/__tests__/business-chat-links.test.js +102 -0
  13. package/dist/__tests__/get-message-buttons.test.d.ts +1 -0
  14. package/dist/__tests__/get-message-buttons.test.js +122 -0
  15. package/dist/__tests__/group-calls.test.d.ts +1 -0
  16. package/dist/__tests__/group-calls.test.js +503 -0
  17. package/dist/__tests__/inline-query-send.test.d.ts +1 -0
  18. package/dist/__tests__/inline-query-send.test.js +94 -0
  19. package/dist/__tests__/inline-query.test.d.ts +1 -0
  20. package/dist/__tests__/inline-query.test.js +115 -0
  21. package/dist/__tests__/megagroup-stats.test.d.ts +1 -0
  22. package/dist/__tests__/megagroup-stats.test.js +166 -0
  23. package/dist/__tests__/press-button.test.d.ts +1 -0
  24. package/dist/__tests__/press-button.test.js +123 -0
  25. package/dist/__tests__/quick-replies.test.d.ts +1 -0
  26. package/dist/__tests__/quick-replies.test.js +245 -0
  27. package/dist/__tests__/reactions.test.d.ts +1 -0
  28. package/dist/__tests__/reactions.test.js +23 -0
  29. package/dist/__tests__/set-chat-permissions-merge.test.d.ts +1 -0
  30. package/dist/__tests__/set-chat-permissions-merge.test.js +107 -0
  31. package/dist/__tests__/set-chat-reactions.test.d.ts +1 -0
  32. package/dist/__tests__/set-chat-reactions.test.js +129 -0
  33. package/dist/__tests__/stars-status.test.d.ts +1 -0
  34. package/dist/__tests__/stars-status.test.js +205 -0
  35. package/dist/__tests__/stars-transactions.test.d.ts +1 -0
  36. package/dist/__tests__/stars-transactions.test.js +82 -0
  37. package/dist/__tests__/stories.test.d.ts +1 -0
  38. package/dist/__tests__/stories.test.js +361 -0
  39. package/dist/__tests__/toggle-anti-spam.test.d.ts +1 -0
  40. package/dist/__tests__/toggle-anti-spam.test.js +80 -0
  41. package/dist/__tests__/toggle-channel-signatures.test.d.ts +1 -0
  42. package/dist/__tests__/toggle-channel-signatures.test.js +80 -0
  43. package/dist/__tests__/toggle-forum-mode.test.d.ts +1 -0
  44. package/dist/__tests__/toggle-forum-mode.test.js +80 -0
  45. package/dist/__tests__/toggle-prehistory-hidden.test.d.ts +1 -0
  46. package/dist/__tests__/toggle-prehistory-hidden.test.js +80 -0
  47. package/dist/__tests__/updates.test.d.ts +1 -0
  48. package/dist/__tests__/updates.test.js +221 -0
  49. package/dist/rate-limiter.d.ts +8 -2
  50. package/dist/rate-limiter.js +15 -8
  51. package/dist/telegram-client.d.ts +711 -2
  52. package/dist/telegram-client.js +2167 -99
  53. package/dist/tools/account.js +108 -0
  54. package/dist/tools/boosts.d.ts +3 -0
  55. package/dist/tools/boosts.js +65 -0
  56. package/dist/tools/chats.js +388 -1
  57. package/dist/tools/group-calls.d.ts +4 -0
  58. package/dist/tools/group-calls.js +77 -0
  59. package/dist/tools/index.js +10 -0
  60. package/dist/tools/media.js +120 -1
  61. package/dist/tools/messages.js +379 -0
  62. package/dist/tools/quick-replies.d.ts +4 -0
  63. package/dist/tools/quick-replies.js +58 -0
  64. package/dist/tools/reactions.js +102 -1
  65. package/dist/tools/stars.d.ts +4 -0
  66. package/dist/tools/stars.js +71 -0
  67. package/dist/tools/stories.d.ts +3 -0
  68. package/dist/tools/stories.js +107 -0
  69. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,7 +5,73 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [1.26.0] - 2026-04-20
9
+
10
+ ### Added
11
+ - **Phase 2 — Admin Toggles, Customization, Stats (8 tools)**
12
+ - `telegram-toggle-channel-signatures` — toggle post signatures on a channel
13
+ - `telegram-toggle-anti-spam` — toggle native anti-spam in a supergroup (`ban_users` admin)
14
+ - `telegram-toggle-forum-mode` — enable/disable forum mode on a supergroup (disable requires `confirm: true` — destructive, removes all topics)
15
+ - `telegram-approve-join-request` — approve or reject a single chat join request
16
+ - `telegram-toggle-prehistory-hidden` — show/hide pre-history for new supergroup members
17
+ - `telegram-set-chat-reactions` — set allowed reactions on a chat (`all` / `some` / `none`)
18
+ - `telegram-get-broadcast-stats` — channel stats overview (Premium admin may be required; pass `includeGraphs: true` for raw series)
19
+ - `telegram-get-megagroup-stats` — supergroup stats overview (rate-limited by Telegram to ~1 req/30 min per channel)
20
+ - **Phase 3 — Inline Bots, Buttons, Real-Time Updates (7 tools)**
21
+ - `telegram-inline-query` — query an inline bot in a chat context (queryId TTL ≈ 1 min)
22
+ - `telegram-inline-query-send` — send an inline bot result by queryId + result id
23
+ - `telegram-press-button` — press a callback button on a message by row/col or raw data
24
+ - `telegram-get-message-buttons` — list a message's reply-markup buttons with indices and types
25
+ - `telegram-get-state` — initialize a polling cursor (`pts`, `qts`, `date`, `seq`)
26
+ - `telegram-get-updates` — fetch global updates since a known cursor via `updates.GetDifference`; returns `{newMessages, deletedMessageIds, otherUpdates, state, isFinal}` and surfaces `DifferenceTooLong` as a history-fallback hint
27
+ - `telegram-get-channel-updates` — per-channel polling via `updates.GetChannelDifference`
28
+ - Cursors are client-owned (stateless server) — the agent stores `{pts, qts, date}` between calls
29
+ - **Phase 4 ship — Stories, Boosts, Business (8 tools)**
30
+ - `telegram-get-all-stories` — list stories across peers with pagination state
31
+ - `telegram-get-peer-stories` — list stories posted by one peer (compact, media refs only)
32
+ - `telegram-get-stories-by-id` — fetch specific story items by id
33
+ - `telegram-get-story-views` — list views on your own stories (Premium for full stats)
34
+ - `telegram-get-my-boosts` — list boost slots assigned by your account
35
+ - `telegram-get-boosts-status` — boost status for a channel/supergroup
36
+ - `telegram-get-boosts-list` — list boosters for a channel (admin)
37
+ - `telegram-get-business-chat-links` — list your Telegram Business chat links
38
+ - **Phase 4 opt-in (env-gated, 6 tools)** — registered only when the corresponding flag is set:
39
+ - `MCP_TELEGRAM_ENABLE_GROUP_CALLS=1` → `telegram-get-group-call`, `telegram-get-group-call-participants`
40
+ - `MCP_TELEGRAM_ENABLE_STARS=1` → `telegram-get-stars-status`, `telegram-get-stars-transactions`
41
+ - `MCP_TELEGRAM_ENABLE_QUICK_REPLIES=1` → `telegram-get-quick-replies`, `telegram-get-quick-reply-messages`
42
+
43
+ ## [1.25.0] - 2026-04-20
44
+
45
+ ### Added
46
+ - **Scheduled messages** — `telegram-get-scheduled`, `telegram-delete-scheduled`
47
+ - **Threads & replies** — `telegram-get-replies` for channel post comments
48
+ - **Message links** — `telegram-get-message-link` returns public t.me URL for a message
49
+ - **Mentions & unread reactions** — `telegram-get-unread-mentions`, `telegram-get-unread-reactions`
50
+ - **Translate** — `telegram-translate-message` (requires Telegram Premium)
51
+ - **Typing indicator** — `telegram-send-typing` with configurable action
52
+ - **Dialog management** — `telegram-archive-chat`, `telegram-pin-chat`, `telegram-mark-dialog-unread`
53
+ - **Drafts** — `telegram-save-draft`, `telegram-get-drafts`, `telegram-clear-drafts`
54
+ - **Saved Messages dialogs** — `telegram-get-saved-dialogs` for the new per-peer Saved Messages folders
55
+ - **Admin log** — `telegram-get-admin-log` for channel/supergroup moderation history
56
+ - **Reactions catalog** — `telegram-set-default-reaction`, `telegram-get-top-reactions`, `telegram-get-recent-reactions`
57
+ - **Chat permissions** — `telegram-set-chat-permissions` for default banned rights
58
+ - **Slow mode** — `telegram-set-slow-mode` for supergroups
59
+ - **Forum topics CRUD** — `telegram-create-topic`, `telegram-edit-topic`, `telegram-delete-topic`
60
+ - **Web page preview** — `telegram-get-web-preview` to inspect link previews before sending
61
+
62
+ ### Fixed
63
+ - `telegram-set-chat-permissions` now merges with the chat's current `defaultBannedRights` — omitted flags keep their current state instead of being silently cleared
64
+ - `telegram-clear-drafts` requires `chatId` (single-chat) or `confirmAllChats: true` to wipe drafts account-wide, preventing accidental loss of all drafts in one call
65
+ - `telegram-get-unread-mentions` and `telegram-get-unread-reactions` are now annotated as `WRITE` — they mark the listed items as read on the server
66
+ - `telegram-translate-message` is now annotated as `WRITE` (consumes Premium translate quota); `toLang` is validated against an ISO-639 / locale pattern and `messageIds` is capped at 1–100 positive integers
67
+ - `telegram-delete-scheduled` caps `messageIds` at 1–100 positive integers
68
+ - `telegram-set-default-reaction` validates `emoji` length (1–8 characters)
69
+ - `telegram-get-web-preview` rejects non-`http(s)` URLs, preventing use as an SSRF proxy
70
+ - `telegram-send-typing` throttles non-`cancel` actions to once per 10 seconds per chat
71
+ - `telegram-get-saved-dialogs` no longer returns a hard-coded `unreadCount: 0`
72
+ - `telegram-create-topic` now reads the new topic ID from `UpdateNewChannelMessage` (authoritative) and fails loudly if neither source is available
73
+ - `telegram-save-draft` drops `replyTo` when the draft text is empty, avoiding `MESSAGE_EMPTY` errors when clearing drafts
74
+ - Removed unused `chatMap` build in `getAdminLog`
9
75
 
10
76
  ## [1.24.1] - 2026-04-20
11
77
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
18
18
 
19
19
  ## Features
20
20
 
21
- - **59 tools** -- the most comprehensive Telegram MCP server available
21
+ - **Comprehensive tool coverage** -- the most full-featured Telegram MCP server available (80+ tools)
22
22
  - **MTProto protocol** -- direct Telegram API access, not the limited Bot API
23
23
  - **Userbot** -- operates as your personal account, not a bot
24
24
  - **Full-featured** -- messaging, reactions, polls, scheduled messages, stickers, media, contacts, and more
@@ -26,6 +26,12 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
26
26
  - **Stickers** -- search sticker sets, browse installed/recent stickers, send stickers to any chat
27
27
  - **Account management** -- update profile, manage privacy settings, sessions, auto-delete timers
28
28
  - **Global search** -- search messages across all chats at once
29
+ - **Real-time polling** -- fetch updates via stateless cursors; agent owns `{pts, qts, date}` state
30
+ - **Inline bots & buttons** -- query inline bots, send results, press callback buttons
31
+ - **Stories** -- read stories from peers, get story view stats
32
+ - **Admin controls** -- toggle channel signatures, anti-spam, forum mode, prehistory; approve join requests
33
+ - **Stats** -- channel and supergroup analytics (GetBroadcastStats / GetMegagroupStats)
34
+ - **Boosts & Business** -- boost status, boosters list, Telegram Business chat links
29
35
  - **QR code login** -- authenticate by scanning a QR code in the Telegram app
30
36
  - **Session persistence** -- login once, stay connected across restarts
31
37
  - **Human-readable output** -- sender names are resolved, not just numeric IDs
@@ -304,21 +310,23 @@ const telegramMcp = new MCPClient({
304
310
  });
305
311
  ```
306
312
 
307
- ## Tools (59)
313
+ ## Tools
308
314
 
309
315
  All tools are auto-discoverable via MCP — your AI client will see the full list with parameters and descriptions when connected.
310
316
 
311
317
  | Category | Tools |
312
318
  |----------|-------|
313
319
  | **Auth** | `telegram-status`, `telegram-login` |
314
- | **Messaging** | `telegram-send-message`, `telegram-edit-message`, `telegram-delete-message`, `telegram-forward-message`, `telegram-send-scheduled` |
315
- | **Reading** | `telegram-list-chats`, `telegram-read-messages`, `telegram-search-messages`, `telegram-search-global`, `telegram-search-chats`, `telegram-get-unread`, `telegram-mark-as-read` |
316
- | **Forum Topics** | `telegram-list-topics`, `telegram-read-topic-messages` |
320
+ | **Messaging** | `telegram-send-message`, `telegram-edit-message`, `telegram-delete-message`, `telegram-forward-message`, `telegram-send-scheduled`, `telegram-send-typing`, `telegram-translate-message`, `telegram-get-message-link` |
321
+ | **Scheduled** | `telegram-get-scheduled`, `telegram-delete-scheduled` |
322
+ | **Reading** | `telegram-list-chats`, `telegram-read-messages`, `telegram-search-messages`, `telegram-search-global`, `telegram-search-chats`, `telegram-get-unread`, `telegram-mark-as-read`, `telegram-get-replies`, `telegram-get-unread-mentions`, `telegram-get-unread-reactions`, `telegram-get-saved-dialogs` |
323
+ | **Drafts** | `telegram-save-draft`, `telegram-get-drafts`, `telegram-clear-drafts` |
324
+ | **Forum Topics** | `telegram-list-topics`, `telegram-read-topic-messages`, `telegram-create-topic`, `telegram-edit-topic`, `telegram-delete-topic` |
317
325
  | **Polls** | `telegram-create-poll` |
318
- | **Reactions** | `telegram-send-reaction`, `telegram-get-reactions` |
326
+ | **Reactions** | `telegram-send-reaction`, `telegram-get-reactions`, `telegram-set-default-reaction`, `telegram-get-top-reactions`, `telegram-get-recent-reactions` |
319
327
  | **Stickers** | `telegram-send-sticker`, `telegram-get-installed-stickers`, `telegram-get-recent-stickers`, `telegram-get-sticker-set`, `telegram-search-sticker-sets` |
320
- | **Media** | `telegram-send-file`, `telegram-download-media`, `telegram-get-profile-photo` |
321
- | **Groups** | `telegram-create-group`, `telegram-edit-group`, `telegram-invite-to-group`, `telegram-join-chat`, `telegram-leave-group`, `telegram-kick-user`, `telegram-ban-user`, `telegram-unban-user`, `telegram-set-admin`, `telegram-remove-admin`, `telegram-get-my-role` |
328
+ | **Media** | `telegram-send-file`, `telegram-download-media`, `telegram-get-profile-photo`, `telegram-get-web-preview` |
329
+ | **Groups** | `telegram-create-group`, `telegram-edit-group`, `telegram-invite-to-group`, `telegram-join-chat`, `telegram-leave-group`, `telegram-kick-user`, `telegram-ban-user`, `telegram-unban-user`, `telegram-set-admin`, `telegram-remove-admin`, `telegram-get-my-role`, `telegram-set-chat-permissions`, `telegram-set-slow-mode`, `telegram-get-admin-log` |
322
330
  | **Chat Info** | `telegram-get-chat-info`, `telegram-get-chat-members`, `telegram-get-chat-folders` |
323
331
  | **Invite Links** | `telegram-create-invite-link`, `telegram-get-invite-links`, `telegram-revoke-invite-link` |
324
332
  | **Contacts** | `telegram-get-contacts`, `telegram-add-contact`, `telegram-get-contact-requests` |
@@ -326,10 +334,29 @@ All tools are auto-discoverable via MCP — your AI client will see the full lis
326
334
  | **Profiles** | `telegram-get-profile`, `telegram-update-profile` |
327
335
  | **Account** | `telegram-get-sessions`, `telegram-terminate-session`, `telegram-set-privacy`, `telegram-set-auto-delete` |
328
336
  | **Pinning** | `telegram-pin-message`, `telegram-unpin-message` |
329
- | **Chat Settings** | `telegram-mute-chat` |
337
+ | **Chat Settings** | `telegram-mute-chat`, `telegram-archive-chat`, `telegram-pin-chat`, `telegram-mark-dialog-unread` |
338
+ | **Admin Toggles** | `telegram-toggle-channel-signatures`, `telegram-toggle-anti-spam`, `telegram-toggle-forum-mode`, `telegram-toggle-prehistory-hidden`, `telegram-set-chat-reactions`, `telegram-approve-join-request` |
339
+ | **Stats** | `telegram-get-broadcast-stats`, `telegram-get-megagroup-stats` |
340
+ | **Inline Bots & Buttons** | `telegram-inline-query`, `telegram-inline-query-send`, `telegram-press-button`, `telegram-get-message-buttons` |
341
+ | **Real-Time Polling** | `telegram-get-state`, `telegram-get-updates`, `telegram-get-channel-updates` |
342
+ | **Stories** | `telegram-get-all-stories`, `telegram-get-peer-stories`, `telegram-get-stories-by-id`, `telegram-get-story-views` |
343
+ | **Boosts & Business** | `telegram-get-my-boosts`, `telegram-get-boosts-status`, `telegram-get-boosts-list`, `telegram-get-business-chat-links` |
344
+ | **Opt-in (env-gated)** | `telegram-get-group-call`, `telegram-get-group-call-participants` (requires `MCP_TELEGRAM_ENABLE_GROUP_CALLS=1`), `telegram-get-stars-status`, `telegram-get-stars-transactions` (requires `MCP_TELEGRAM_ENABLE_STARS=1`), `telegram-get-quick-replies`, `telegram-get-quick-reply-messages` (requires `MCP_TELEGRAM_ENABLE_QUICK_REPLIES=1`) |
330
345
 
331
346
  > **Tip**: Ask your AI assistant *"What Telegram tools are available?"* to get the full list with parameters and descriptions.
332
347
 
348
+ ## Optional Features
349
+
350
+ Some tools are disabled by default and must be opted in via environment variables:
351
+
352
+ | Variable | Value | Tools enabled |
353
+ |----------|-------|---------------|
354
+ | `MCP_TELEGRAM_ENABLE_GROUP_CALLS` | `1` | `telegram-get-group-call`, `telegram-get-group-call-participants` |
355
+ | `MCP_TELEGRAM_ENABLE_STARS` | `1` | `telegram-get-stars-status`, `telegram-get-stars-transactions` |
356
+ | `MCP_TELEGRAM_ENABLE_QUICK_REPLIES` | `1` | `telegram-get-quick-replies`, `telegram-get-quick-reply-messages` |
357
+
358
+ Add these to your `.env` file or MCP client config to enable them.
359
+
333
360
  ## Development
334
361
 
335
362
  ```bash
@@ -351,14 +378,19 @@ src/
351
378
  qr-login-cli.ts -- CLI utility for QR code login
352
379
  tools/ -- Modular tool definitions
353
380
  auth.ts -- Connection & login
354
- messages.ts -- Send, read, search, edit, delete, forward
355
- chats.ts -- Chat listing, group management, admin
381
+ messages.ts -- Send, read, search, edit, delete, forward; inline bots; real-time polling
382
+ chats.ts -- Chat listing, group management, admin toggles, stats
356
383
  contacts.ts -- Contacts, profiles, moderation
357
384
  media.ts -- Files, photos, downloads
358
- reactions.ts -- Reactions
385
+ reactions.ts -- Reactions, set-chat-reactions
359
386
  extras.ts -- Pin, schedule, polls, topics
360
387
  stickers.ts -- Sticker sets, send, search, browse
361
- account.ts -- Sessions, privacy, auto-delete, profile, chat mute/folders, invite links
388
+ account.ts -- Sessions, privacy, auto-delete, profile, chat mute/folders, invite links, business chat links
389
+ boosts.ts -- Boost status, my boosts, boosters list
390
+ stories.ts -- Stories: list all, peer, by-id, view stats
391
+ group-calls.ts -- Group call info and participants (opt-in: MCP_TELEGRAM_ENABLE_GROUP_CALLS)
392
+ stars.ts -- Stars wallet status and transactions (opt-in: MCP_TELEGRAM_ENABLE_STARS)
393
+ quick-replies.ts -- Quick replies and messages (opt-in: MCP_TELEGRAM_ENABLE_QUICK_REPLIES)
362
394
  shared.ts -- Shared utilities
363
395
  ```
364
396
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { Api } from "telegram/tl/index.js";
4
+ import { describeAdminLogAction, describeAdminLogDetails } from "../telegram-client.js";
5
+ describe("describeAdminLogAction", () => {
6
+ it("converts ChangeTitle to snake_case", () => {
7
+ const action = new Api.ChannelAdminLogEventActionChangeTitle({ prevValue: "a", newValue: "b" });
8
+ assert.strictEqual(describeAdminLogAction(action), "change_title");
9
+ });
10
+ it("converts ParticipantJoin to snake_case", () => {
11
+ const action = new Api.ChannelAdminLogEventActionParticipantJoin();
12
+ assert.strictEqual(describeAdminLogAction(action), "participant_join");
13
+ });
14
+ it("handles ToggleSlowMode", () => {
15
+ const action = new Api.ChannelAdminLogEventActionToggleSlowMode({ prevValue: 0, newValue: 30 });
16
+ assert.strictEqual(describeAdminLogAction(action), "toggle_slow_mode");
17
+ });
18
+ it("handles ChangeHistoryTTL without splitting acronym", () => {
19
+ const action = new Api.ChannelAdminLogEventActionChangeHistoryTTL({ prevValue: 0, newValue: 86400 });
20
+ assert.strictEqual(describeAdminLogAction(action), "change_history_ttl");
21
+ });
22
+ });
23
+ describe("describeAdminLogDetails", () => {
24
+ const describeUser = (id) => `user_${id.toString()}`;
25
+ it("formats title change", () => {
26
+ const action = new Api.ChannelAdminLogEventActionChangeTitle({ prevValue: "old", newValue: "new" });
27
+ assert.strictEqual(describeAdminLogDetails(action, describeUser), '"old" → "new"');
28
+ });
29
+ it("formats username change", () => {
30
+ const action = new Api.ChannelAdminLogEventActionChangeUsername({ prevValue: "old", newValue: "new" });
31
+ assert.strictEqual(describeAdminLogDetails(action, describeUser), "@old → @new");
32
+ });
33
+ it("formats slow mode change", () => {
34
+ const action = new Api.ChannelAdminLogEventActionToggleSlowMode({ prevValue: 0, newValue: 30 });
35
+ assert.strictEqual(describeAdminLogDetails(action, describeUser), "0s → 30s");
36
+ });
37
+ it("returns empty string for unknown actions", () => {
38
+ const action = new Api.ChannelAdminLogEventActionParticipantJoin();
39
+ assert.strictEqual(describeAdminLogDetails(action, describeUser), "");
40
+ });
41
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import bigInt from "big-integer";
4
+ import { Api } from "telegram/tl/index.js";
5
+ import { TelegramService } from "../telegram-client.js";
6
+ function makeService(entity, user, invocations) {
7
+ const fakeClient = {
8
+ invoke: async (req) => {
9
+ invocations.push(req);
10
+ return undefined;
11
+ },
12
+ getEntity: async (_id) => user,
13
+ };
14
+ const service = new TelegramService(1, "hash");
15
+ const internals = service;
16
+ internals.client = fakeClient;
17
+ internals.connected = true;
18
+ internals.resolveChat = async () => entity;
19
+ return service;
20
+ }
21
+ describe("TelegramService.approveChatJoinRequest", () => {
22
+ const makeUser = (id) => new Api.User({
23
+ id: bigInt(id),
24
+ accessHash: bigInt(42),
25
+ firstName: "Test",
26
+ });
27
+ it("invokes HideChatJoinRequest with approved=true for channel peer", async () => {
28
+ const megagroup = new Api.Channel({
29
+ id: bigInt(10000),
30
+ title: "sg",
31
+ photo: new Api.ChatPhotoEmpty(),
32
+ date: 0,
33
+ accessHash: bigInt(1),
34
+ megagroup: true,
35
+ });
36
+ const invocations = [];
37
+ const service = makeService(megagroup, makeUser(555), invocations);
38
+ await service.approveChatJoinRequest("10000", "555", true);
39
+ const call = invocations.find((r) => r instanceof Api.messages.HideChatJoinRequest);
40
+ assert.ok(call, "HideChatJoinRequest was invoked");
41
+ assert.strictEqual(call.approved, true);
42
+ assert.ok(call.userId instanceof Api.InputUser);
43
+ assert.strictEqual(call.userId.userId.toString(), "555");
44
+ });
45
+ it("invokes HideChatJoinRequest with approved=false (denied)", async () => {
46
+ const megagroup = new Api.Channel({
47
+ id: bigInt(20000),
48
+ title: "sg",
49
+ photo: new Api.ChatPhotoEmpty(),
50
+ date: 0,
51
+ accessHash: bigInt(1),
52
+ megagroup: true,
53
+ });
54
+ const invocations = [];
55
+ const service = makeService(megagroup, makeUser(777), invocations);
56
+ await service.approveChatJoinRequest("20000", "777", false);
57
+ const call = invocations.find((r) => r instanceof Api.messages.HideChatJoinRequest);
58
+ assert.ok(call);
59
+ assert.strictEqual(call.approved, false);
60
+ });
61
+ it("rejects basic Chat peer (basic groups do not support join requests)", async () => {
62
+ const chat = new Api.Chat({
63
+ id: bigInt(33333),
64
+ title: "basic group",
65
+ photo: new Api.ChatPhotoEmpty(),
66
+ participantsCount: 5,
67
+ date: 0,
68
+ version: 0,
69
+ });
70
+ const invocations = [];
71
+ const service = makeService(chat, makeUser(888), invocations);
72
+ await assert.rejects(service.approveChatJoinRequest("33333", "888", true), /supergroups and channels/i);
73
+ assert.strictEqual(invocations.find((r) => r instanceof Api.messages.HideChatJoinRequest), undefined, "no API call for basic group");
74
+ });
75
+ it("rejects when user resolves to non-User entity", async () => {
76
+ const megagroup = new Api.Channel({
77
+ id: bigInt(44444),
78
+ title: "sg",
79
+ photo: new Api.ChatPhotoEmpty(),
80
+ date: 0,
81
+ accessHash: bigInt(1),
82
+ megagroup: true,
83
+ });
84
+ const notAUser = new Api.Chat({
85
+ id: bigInt(99),
86
+ title: "not a user",
87
+ photo: new Api.ChatPhotoEmpty(),
88
+ participantsCount: 1,
89
+ date: 0,
90
+ version: 0,
91
+ });
92
+ const invocations = [];
93
+ const service = makeService(megagroup, notAUser, invocations);
94
+ await assert.rejects(service.approveChatJoinRequest("44444", "99", true), /not a user/i);
95
+ assert.strictEqual(invocations.find((r) => r instanceof Api.messages.HideChatJoinRequest), undefined, "no API call when target is not a user");
96
+ });
97
+ it("rejects when chat entity is a private user (not a group/channel)", async () => {
98
+ const userPeer = new Api.User({
99
+ id: bigInt(1),
100
+ accessHash: bigInt(1),
101
+ firstName: "Peer",
102
+ });
103
+ const invocations = [];
104
+ const service = makeService(userPeer, makeUser(123), invocations);
105
+ await assert.rejects(service.approveChatJoinRequest("1", "123", true), /supergroups and channels/i);
106
+ });
107
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,310 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import bigInt from "big-integer";
4
+ import { Api } from "telegram/tl/index.js";
5
+ import { summarizeBoost, summarizeBoostsList, summarizeBoostsStatus, summarizeMyBoost, summarizeMyBoosts, summarizePrepaidGiveaway, TelegramService, } from "../telegram-client.js";
6
+ function makeService(invocations, responder) {
7
+ const fakeClient = {
8
+ invoke: async (req) => {
9
+ invocations.push(req);
10
+ return responder(req);
11
+ },
12
+ };
13
+ const service = new TelegramService(1, "hash");
14
+ const internals = service;
15
+ internals.client = fakeClient;
16
+ internals.connected = true;
17
+ return service;
18
+ }
19
+ describe("summarizeMyBoost", () => {
20
+ it("maps slot, peer, dates and cooldown", () => {
21
+ const boost = new Api.MyBoost({
22
+ slot: 2,
23
+ peer: new Api.PeerChannel({ channelId: bigInt(900) }),
24
+ date: 1710000000,
25
+ expires: 1720000000,
26
+ cooldownUntilDate: 1715000000,
27
+ });
28
+ const out = summarizeMyBoost(boost);
29
+ assert.strictEqual(out.slot, 2);
30
+ assert.deepStrictEqual(out.peer, { kind: "channel", id: "900" });
31
+ assert.strictEqual(out.date, 1710000000);
32
+ assert.strictEqual(out.expires, 1720000000);
33
+ assert.strictEqual(out.cooldownUntilDate, 1715000000);
34
+ });
35
+ it("leaves peer undefined when boost is unassigned", () => {
36
+ const boost = new Api.MyBoost({
37
+ slot: 1,
38
+ date: 100,
39
+ expires: 200,
40
+ });
41
+ const out = summarizeMyBoost(boost);
42
+ assert.strictEqual(out.slot, 1);
43
+ assert.strictEqual(out.peer, undefined);
44
+ assert.strictEqual(out.cooldownUntilDate, undefined);
45
+ });
46
+ });
47
+ describe("summarizeMyBoosts", () => {
48
+ it("computes count from myBoosts length and maps each entry", () => {
49
+ const resp = new Api.premium.MyBoosts({
50
+ myBoosts: [
51
+ new Api.MyBoost({
52
+ slot: 1,
53
+ peer: new Api.PeerChannel({ channelId: bigInt(10) }),
54
+ date: 1,
55
+ expires: 2,
56
+ }),
57
+ new Api.MyBoost({ slot: 2, date: 3, expires: 4 }),
58
+ ],
59
+ chats: [],
60
+ users: [],
61
+ });
62
+ const out = summarizeMyBoosts(resp);
63
+ assert.strictEqual(out.count, 2);
64
+ assert.strictEqual(out.myBoosts.length, 2);
65
+ assert.deepStrictEqual(out.myBoosts[0].peer, { kind: "channel", id: "10" });
66
+ assert.strictEqual(out.myBoosts[1].peer, undefined);
67
+ });
68
+ it("handles empty boost list", () => {
69
+ const resp = new Api.premium.MyBoosts({ myBoosts: [], chats: [], users: [] });
70
+ const out = summarizeMyBoosts(resp);
71
+ assert.strictEqual(out.count, 0);
72
+ assert.deepStrictEqual(out.myBoosts, []);
73
+ });
74
+ });
75
+ describe("summarizePrepaidGiveaway", () => {
76
+ it("maps premium PrepaidGiveaway (months + quantity)", () => {
77
+ const g = new Api.PrepaidGiveaway({
78
+ id: bigInt(123),
79
+ months: 3,
80
+ quantity: 10,
81
+ date: 1700000000,
82
+ });
83
+ const out = summarizePrepaidGiveaway(g);
84
+ assert.deepStrictEqual(out, {
85
+ kind: "premium",
86
+ id: "123",
87
+ months: 3,
88
+ quantity: 10,
89
+ date: 1700000000,
90
+ });
91
+ });
92
+ it("maps PrepaidStarsGiveaway with stars + boosts", () => {
93
+ const g = new Api.PrepaidStarsGiveaway({
94
+ id: bigInt(456),
95
+ stars: bigInt(5000),
96
+ quantity: 20,
97
+ boosts: 4,
98
+ date: 1710000000,
99
+ });
100
+ const out = summarizePrepaidGiveaway(g);
101
+ assert.deepStrictEqual(out, {
102
+ kind: "stars",
103
+ id: "456",
104
+ stars: "5000",
105
+ quantity: 20,
106
+ boosts: 4,
107
+ date: 1710000000,
108
+ });
109
+ });
110
+ });
111
+ describe("summarizeBoostsStatus", () => {
112
+ it("maps core counters and boost url", () => {
113
+ const resp = new Api.premium.BoostsStatus({
114
+ level: 3,
115
+ currentLevelBoosts: 10,
116
+ boosts: 15,
117
+ giftBoosts: 2,
118
+ nextLevelBoosts: 25,
119
+ boostUrl: "https://t.me/boost/test",
120
+ myBoost: true,
121
+ myBoostSlots: [1, 2],
122
+ });
123
+ const out = summarizeBoostsStatus(resp);
124
+ assert.strictEqual(out.level, 3);
125
+ assert.strictEqual(out.boosts, 15);
126
+ assert.strictEqual(out.currentLevelBoosts, 10);
127
+ assert.strictEqual(out.nextLevelBoosts, 25);
128
+ assert.strictEqual(out.giftBoosts, 2);
129
+ assert.strictEqual(out.boostUrl, "https://t.me/boost/test");
130
+ assert.strictEqual(out.myBoost, true);
131
+ assert.deepStrictEqual(out.myBoostSlots, [1, 2]);
132
+ assert.strictEqual(out.premiumAudience, undefined);
133
+ assert.strictEqual(out.prepaidGiveaways, undefined);
134
+ });
135
+ it("includes premiumAudience and prepaidGiveaways when present", () => {
136
+ const resp = new Api.premium.BoostsStatus({
137
+ level: 1,
138
+ currentLevelBoosts: 0,
139
+ boosts: 5,
140
+ boostUrl: "https://t.me/boost/x",
141
+ premiumAudience: new Api.StatsPercentValue({ part: 2, total: 100 }),
142
+ prepaidGiveaways: [new Api.PrepaidGiveaway({ id: bigInt(1), months: 6, quantity: 5, date: 111 })],
143
+ });
144
+ const out = summarizeBoostsStatus(resp);
145
+ assert.deepStrictEqual(out.premiumAudience, { part: 2, total: 100 });
146
+ assert.ok(out.prepaidGiveaways);
147
+ assert.strictEqual(out.prepaidGiveaways?.length, 1);
148
+ assert.strictEqual(out.prepaidGiveaways?.[0].kind, "premium");
149
+ assert.strictEqual(out.prepaidGiveaways?.[0].id, "1");
150
+ });
151
+ it("omits empty prepaidGiveaways list", () => {
152
+ const resp = new Api.premium.BoostsStatus({
153
+ level: 0,
154
+ currentLevelBoosts: 0,
155
+ boosts: 0,
156
+ boostUrl: "https://t.me/boost/y",
157
+ prepaidGiveaways: [],
158
+ });
159
+ const out = summarizeBoostsStatus(resp);
160
+ assert.strictEqual(out.prepaidGiveaways, undefined);
161
+ });
162
+ });
163
+ describe("TelegramService.getBoostsStatus", () => {
164
+ it("invokes premium.GetBoostsStatus with resolved peer and returns summary", async () => {
165
+ const invocations = [];
166
+ const service = makeService(invocations, () => new Api.premium.BoostsStatus({
167
+ level: 2,
168
+ currentLevelBoosts: 5,
169
+ boosts: 7,
170
+ boostUrl: "https://t.me/boost/foo",
171
+ }));
172
+ const internals = service;
173
+ internals.resolvePeer = async (_id) => new Api.InputPeerChannel({ channelId: bigInt(500), accessHash: bigInt(0) });
174
+ const out = await service.getBoostsStatus("@foo");
175
+ const call = invocations.find((r) => r instanceof Api.premium.GetBoostsStatus);
176
+ assert.ok(call);
177
+ assert.strictEqual(out.level, 2);
178
+ assert.strictEqual(out.boosts, 7);
179
+ assert.strictEqual(out.boostUrl, "https://t.me/boost/foo");
180
+ });
181
+ });
182
+ describe("summarizeBoost", () => {
183
+ it("maps core boost fields and converts bigInt ids to strings", () => {
184
+ const boost = new Api.Boost({
185
+ id: "boost-1",
186
+ userId: bigInt(123),
187
+ date: 1700000000,
188
+ expires: 1702000000,
189
+ gift: true,
190
+ giveaway: false,
191
+ unclaimed: false,
192
+ giveawayMsgId: 55,
193
+ usedGiftSlug: "slug-abc",
194
+ multiplier: 2,
195
+ stars: bigInt(500),
196
+ });
197
+ const out = summarizeBoost(boost);
198
+ assert.strictEqual(out.id, "boost-1");
199
+ assert.strictEqual(out.userId, "123");
200
+ assert.strictEqual(out.date, 1700000000);
201
+ assert.strictEqual(out.expires, 1702000000);
202
+ assert.strictEqual(out.gift, true);
203
+ assert.strictEqual(out.giveaway, false);
204
+ assert.strictEqual(out.unclaimed, false);
205
+ assert.strictEqual(out.giveawayMsgId, 55);
206
+ assert.strictEqual(out.usedGiftSlug, "slug-abc");
207
+ assert.strictEqual(out.multiplier, 2);
208
+ assert.strictEqual(out.stars, "500");
209
+ });
210
+ it("leaves optional fields undefined when missing", () => {
211
+ const boost = new Api.Boost({
212
+ id: "boost-2",
213
+ date: 10,
214
+ expires: 20,
215
+ });
216
+ const out = summarizeBoost(boost);
217
+ assert.strictEqual(out.id, "boost-2");
218
+ assert.strictEqual(out.userId, undefined);
219
+ assert.strictEqual(out.stars, undefined);
220
+ assert.strictEqual(out.multiplier, undefined);
221
+ });
222
+ });
223
+ describe("summarizeBoostsList", () => {
224
+ it("maps count, boosts and nextOffset", () => {
225
+ const resp = new Api.premium.BoostsList({
226
+ count: 2,
227
+ boosts: [
228
+ new Api.Boost({ id: "a", userId: bigInt(1), date: 1, expires: 2 }),
229
+ new Api.Boost({ id: "b", giveaway: true, date: 3, expires: 4 }),
230
+ ],
231
+ nextOffset: "cursor-xyz",
232
+ users: [],
233
+ });
234
+ const out = summarizeBoostsList(resp);
235
+ assert.strictEqual(out.count, 2);
236
+ assert.strictEqual(out.boosts.length, 2);
237
+ assert.strictEqual(out.boosts[0].id, "a");
238
+ assert.strictEqual(out.boosts[0].userId, "1");
239
+ assert.strictEqual(out.boosts[1].giveaway, true);
240
+ assert.strictEqual(out.nextOffset, "cursor-xyz");
241
+ });
242
+ it("handles empty boosts and missing nextOffset", () => {
243
+ const resp = new Api.premium.BoostsList({
244
+ count: 0,
245
+ boosts: [],
246
+ users: [],
247
+ });
248
+ const out = summarizeBoostsList(resp);
249
+ assert.strictEqual(out.count, 0);
250
+ assert.deepStrictEqual(out.boosts, []);
251
+ assert.strictEqual(out.nextOffset, undefined);
252
+ });
253
+ });
254
+ describe("TelegramService.getBoostsList", () => {
255
+ it("invokes premium.GetBoostsList with defaults (empty offset, limit 50)", async () => {
256
+ const invocations = [];
257
+ const service = makeService(invocations, () => new Api.premium.BoostsList({
258
+ count: 1,
259
+ boosts: [new Api.Boost({ id: "boost-1", userId: bigInt(7), date: 10, expires: 20 })],
260
+ nextOffset: "next",
261
+ users: [],
262
+ }));
263
+ const internals = service;
264
+ internals.resolvePeer = async (_id) => new Api.InputPeerChannel({ channelId: bigInt(500), accessHash: bigInt(0) });
265
+ const out = await service.getBoostsList("@foo");
266
+ const call = invocations.find((r) => r instanceof Api.premium.GetBoostsList);
267
+ assert.ok(call);
268
+ assert.strictEqual(call.offset, "");
269
+ assert.strictEqual(call.limit, 50);
270
+ assert.strictEqual(call.gifts, undefined);
271
+ assert.strictEqual(out.count, 1);
272
+ assert.strictEqual(out.boosts[0].id, "boost-1");
273
+ assert.strictEqual(out.nextOffset, "next");
274
+ });
275
+ it("passes gifts/offset/limit through to GetBoostsList", async () => {
276
+ const invocations = [];
277
+ const service = makeService(invocations, () => new Api.premium.BoostsList({ count: 0, boosts: [], users: [] }));
278
+ const internals = service;
279
+ internals.resolvePeer = async (_id) => new Api.InputPeerChannel({ channelId: bigInt(1), accessHash: bigInt(0) });
280
+ await service.getBoostsList("@bar", { gifts: true, offset: "cur", limit: 10 });
281
+ const call = invocations.find((r) => r instanceof Api.premium.GetBoostsList);
282
+ assert.ok(call);
283
+ assert.strictEqual(call.gifts, true);
284
+ assert.strictEqual(call.offset, "cur");
285
+ assert.strictEqual(call.limit, 10);
286
+ });
287
+ });
288
+ describe("TelegramService.getMyBoosts", () => {
289
+ it("invokes premium.GetMyBoosts and returns summary", async () => {
290
+ const invocations = [];
291
+ const service = makeService(invocations, () => new Api.premium.MyBoosts({
292
+ myBoosts: [
293
+ new Api.MyBoost({
294
+ slot: 1,
295
+ peer: new Api.PeerChannel({ channelId: bigInt(42) }),
296
+ date: 100,
297
+ expires: 200,
298
+ }),
299
+ ],
300
+ chats: [],
301
+ users: [],
302
+ }));
303
+ const out = await service.getMyBoosts();
304
+ const call = invocations.find((r) => r instanceof Api.premium.GetMyBoosts);
305
+ assert.ok(call);
306
+ assert.strictEqual(out.count, 1);
307
+ assert.strictEqual(out.myBoosts[0].slot, 1);
308
+ assert.deepStrictEqual(out.myBoosts[0].peer, { kind: "channel", id: "42" });
309
+ });
310
+ });
@@ -0,0 +1 @@
1
+ export {};