@overpod/mcp-telegram 1.25.0 → 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 (62) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +35 -5
  3. package/dist/__tests__/approve-join-request.test.d.ts +1 -0
  4. package/dist/__tests__/approve-join-request.test.js +107 -0
  5. package/dist/__tests__/boosts.test.d.ts +1 -0
  6. package/dist/__tests__/boosts.test.js +310 -0
  7. package/dist/__tests__/broadcast-stats.test.d.ts +1 -0
  8. package/dist/__tests__/broadcast-stats.test.js +172 -0
  9. package/dist/__tests__/business-chat-links.test.d.ts +1 -0
  10. package/dist/__tests__/business-chat-links.test.js +102 -0
  11. package/dist/__tests__/get-message-buttons.test.d.ts +1 -0
  12. package/dist/__tests__/get-message-buttons.test.js +122 -0
  13. package/dist/__tests__/group-calls.test.d.ts +1 -0
  14. package/dist/__tests__/group-calls.test.js +503 -0
  15. package/dist/__tests__/inline-query-send.test.d.ts +1 -0
  16. package/dist/__tests__/inline-query-send.test.js +94 -0
  17. package/dist/__tests__/inline-query.test.d.ts +1 -0
  18. package/dist/__tests__/inline-query.test.js +115 -0
  19. package/dist/__tests__/megagroup-stats.test.d.ts +1 -0
  20. package/dist/__tests__/megagroup-stats.test.js +166 -0
  21. package/dist/__tests__/press-button.test.d.ts +1 -0
  22. package/dist/__tests__/press-button.test.js +123 -0
  23. package/dist/__tests__/quick-replies.test.d.ts +1 -0
  24. package/dist/__tests__/quick-replies.test.js +245 -0
  25. package/dist/__tests__/set-chat-reactions.test.d.ts +1 -0
  26. package/dist/__tests__/set-chat-reactions.test.js +129 -0
  27. package/dist/__tests__/stars-status.test.d.ts +1 -0
  28. package/dist/__tests__/stars-status.test.js +205 -0
  29. package/dist/__tests__/stars-transactions.test.d.ts +1 -0
  30. package/dist/__tests__/stars-transactions.test.js +82 -0
  31. package/dist/__tests__/stories.test.d.ts +1 -0
  32. package/dist/__tests__/stories.test.js +361 -0
  33. package/dist/__tests__/toggle-anti-spam.test.d.ts +1 -0
  34. package/dist/__tests__/toggle-anti-spam.test.js +80 -0
  35. package/dist/__tests__/toggle-channel-signatures.test.d.ts +1 -0
  36. package/dist/__tests__/toggle-channel-signatures.test.js +80 -0
  37. package/dist/__tests__/toggle-forum-mode.test.d.ts +1 -0
  38. package/dist/__tests__/toggle-forum-mode.test.js +80 -0
  39. package/dist/__tests__/toggle-prehistory-hidden.test.d.ts +1 -0
  40. package/dist/__tests__/toggle-prehistory-hidden.test.js +80 -0
  41. package/dist/__tests__/updates.test.d.ts +1 -0
  42. package/dist/__tests__/updates.test.js +221 -0
  43. package/dist/rate-limiter.d.ts +8 -2
  44. package/dist/rate-limiter.js +15 -8
  45. package/dist/telegram-client.d.ts +580 -0
  46. package/dist/telegram-client.js +1322 -0
  47. package/dist/tools/account.js +16 -0
  48. package/dist/tools/boosts.d.ts +3 -0
  49. package/dist/tools/boosts.js +65 -0
  50. package/dist/tools/chats.js +150 -0
  51. package/dist/tools/group-calls.d.ts +4 -0
  52. package/dist/tools/group-calls.js +77 -0
  53. package/dist/tools/index.js +10 -0
  54. package/dist/tools/messages.js +192 -0
  55. package/dist/tools/quick-replies.d.ts +4 -0
  56. package/dist/tools/quick-replies.js +58 -0
  57. package/dist/tools/reactions.js +43 -0
  58. package/dist/tools/stars.d.ts +4 -0
  59. package/dist/tools/stars.js +71 -0
  60. package/dist/tools/stories.d.ts +3 -0
  61. package/dist/tools/stories.js +107 -0
  62. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ 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
+ ## [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
+
8
43
  ## [1.25.0] - 2026-04-20
9
44
 
10
45
  ### Added
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
- - **Comprehensive tool coverage** -- the most full-featured 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
@@ -329,9 +335,28 @@ All tools are auto-discoverable via MCP — your AI client will see the full lis
329
335
  | **Account** | `telegram-get-sessions`, `telegram-terminate-session`, `telegram-set-privacy`, `telegram-set-auto-delete` |
330
336
  | **Pinning** | `telegram-pin-message`, `telegram-unpin-message` |
331
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`) |
332
345
 
333
346
  > **Tip**: Ask your AI assistant *"What Telegram tools are available?"* to get the full list with parameters and descriptions.
334
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
+
335
360
  ## Development
336
361
 
337
362
  ```bash
@@ -353,14 +378,19 @@ src/
353
378
  qr-login-cli.ts -- CLI utility for QR code login
354
379
  tools/ -- Modular tool definitions
355
380
  auth.ts -- Connection & login
356
- messages.ts -- Send, read, search, edit, delete, forward
357
- 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
358
383
  contacts.ts -- Contacts, profiles, moderation
359
384
  media.ts -- Files, photos, downloads
360
- reactions.ts -- Reactions
385
+ reactions.ts -- Reactions, set-chat-reactions
361
386
  extras.ts -- Pin, schedule, polls, topics
362
387
  stickers.ts -- Sticker sets, send, search, browse
363
- 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)
364
394
  shared.ts -- Shared utilities
365
395
  ```
366
396
 
@@ -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 {};