@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
@@ -0,0 +1,80 @@
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, invocations) {
7
+ const fakeClient = {
8
+ invoke: async (req) => {
9
+ invocations.push(req);
10
+ return undefined;
11
+ },
12
+ };
13
+ const service = new TelegramService(1, "hash");
14
+ const internals = service;
15
+ internals.client = fakeClient;
16
+ internals.connected = true;
17
+ internals.resolveChat = async () => entity;
18
+ return service;
19
+ }
20
+ describe("TelegramService.togglePrehistoryHidden", () => {
21
+ it("invokes channels.TogglePreHistoryHidden with enabled=true for supergroup", async () => {
22
+ const megagroup = new Api.Channel({
23
+ id: bigInt(12345),
24
+ title: "supergroup",
25
+ photo: new Api.ChatPhotoEmpty(),
26
+ date: 0,
27
+ accessHash: bigInt(1),
28
+ megagroup: true,
29
+ });
30
+ const invocations = [];
31
+ const service = makeService(megagroup, invocations);
32
+ await service.togglePrehistoryHidden("12345", true);
33
+ const call = invocations.find((r) => r instanceof Api.channels.TogglePreHistoryHidden);
34
+ assert.ok(call, "TogglePreHistoryHidden was invoked");
35
+ assert.strictEqual(call.enabled, true);
36
+ });
37
+ it("invokes channels.TogglePreHistoryHidden with enabled=false", async () => {
38
+ const megagroup = new Api.Channel({
39
+ id: bigInt(22222),
40
+ title: "supergroup",
41
+ photo: new Api.ChatPhotoEmpty(),
42
+ date: 0,
43
+ accessHash: bigInt(1),
44
+ megagroup: true,
45
+ });
46
+ const invocations = [];
47
+ const service = makeService(megagroup, invocations);
48
+ await service.togglePrehistoryHidden("22222", false);
49
+ const call = invocations.find((r) => r instanceof Api.channels.TogglePreHistoryHidden);
50
+ assert.ok(call);
51
+ assert.strictEqual(call.enabled, false);
52
+ });
53
+ it("rejects broadcast channels (not megagroup)", async () => {
54
+ const broadcast = new Api.Channel({
55
+ id: bigInt(33333),
56
+ title: "broadcast",
57
+ photo: new Api.ChatPhotoEmpty(),
58
+ date: 0,
59
+ accessHash: bigInt(1),
60
+ broadcast: true,
61
+ });
62
+ const invocations = [];
63
+ const service = makeService(broadcast, invocations);
64
+ await assert.rejects(service.togglePrehistoryHidden("33333", true), /supergroups, not broadcast channels/);
65
+ assert.strictEqual(invocations.find((r) => r instanceof Api.channels.TogglePreHistoryHidden), undefined, "no API call on invalid target");
66
+ });
67
+ it("rejects non-channel entities (basic group)", async () => {
68
+ const chat = new Api.Chat({
69
+ id: bigInt(44444),
70
+ title: "basic group",
71
+ photo: new Api.ChatPhotoEmpty(),
72
+ participantsCount: 5,
73
+ date: 0,
74
+ version: 0,
75
+ });
76
+ const invocations = [];
77
+ const service = makeService(chat, invocations);
78
+ await assert.rejects(service.togglePrehistoryHidden("44444", true), /supergroups/);
79
+ });
80
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,221 @@
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 { peerToCompact, summarizeChannelDifference, summarizeUpdatesDifference, TelegramService, } from "../telegram-client.js";
6
+ function makeService(invocations, responder, chatEntity) {
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
+ internals.resolveChat = async () => chatEntity;
18
+ return service;
19
+ }
20
+ function makeState(pts, qts, date, seq = 0, unreadCount = 0) {
21
+ return new Api.updates.State({ pts, qts, date, seq, unreadCount });
22
+ }
23
+ function makeChannel(id, megagroup = true) {
24
+ return new Api.Channel({
25
+ id: bigInt(id),
26
+ title: "c",
27
+ photo: new Api.ChatPhotoEmpty(),
28
+ date: 0,
29
+ accessHash: bigInt(1),
30
+ megagroup,
31
+ });
32
+ }
33
+ function makeMessage(id, peerId, text = "hi") {
34
+ return new Api.Message({ id, peerId, date: 10, message: text });
35
+ }
36
+ describe("peerToCompact", () => {
37
+ it("maps PeerUser/PeerChat/PeerChannel to compact kind+id", () => {
38
+ assert.deepStrictEqual(peerToCompact(new Api.PeerUser({ userId: bigInt(5) })), { kind: "user", id: "5" });
39
+ assert.deepStrictEqual(peerToCompact(new Api.PeerChat({ chatId: bigInt(6) })), { kind: "chat", id: "6" });
40
+ assert.deepStrictEqual(peerToCompact(new Api.PeerChannel({ channelId: bigInt(7) })), {
41
+ kind: "channel",
42
+ id: "7",
43
+ });
44
+ assert.strictEqual(peerToCompact(undefined), undefined);
45
+ });
46
+ });
47
+ describe("TelegramService.getUpdatesState", () => {
48
+ it("invokes updates.GetState and returns compact state", async () => {
49
+ const invocations = [];
50
+ const service = makeService(invocations, () => makeState(100, 200, 300, 4, 5));
51
+ const out = await service.getUpdatesState();
52
+ assert.ok(invocations.find((r) => r instanceof Api.updates.GetState));
53
+ assert.deepStrictEqual(out, { pts: 100, qts: 200, date: 300, seq: 4, unreadCount: 5 });
54
+ });
55
+ });
56
+ describe("summarizeUpdatesDifference", () => {
57
+ it("handles DifferenceEmpty as final with no messages", () => {
58
+ const diff = new Api.updates.DifferenceEmpty({ date: 400, seq: 1 });
59
+ const out = summarizeUpdatesDifference(diff, { pts: 10, qts: 20, date: 30 });
60
+ assert.strictEqual(out.isFinal, true);
61
+ assert.strictEqual(out.state.pts, 10);
62
+ assert.strictEqual(out.state.qts, 20);
63
+ assert.strictEqual(out.state.date, 400);
64
+ assert.deepStrictEqual(out.newMessages, []);
65
+ assert.deepStrictEqual(out.deletedMessageIds, []);
66
+ assert.strictEqual(out.fallback, undefined);
67
+ });
68
+ it("handles Difference (final) with newMessages, deleted updates, and final state", () => {
69
+ const msg = makeMessage(1, new Api.PeerUser({ userId: bigInt(7) }), "yo");
70
+ const del = new Api.UpdateDeleteMessages({ messages: [5, 6], pts: 11, ptsCount: 2 });
71
+ const delCh = new Api.UpdateDeleteChannelMessages({
72
+ channelId: bigInt(999),
73
+ messages: [20, 21],
74
+ pts: 22,
75
+ ptsCount: 2,
76
+ });
77
+ const diff = new Api.updates.Difference({
78
+ newMessages: [msg],
79
+ newEncryptedMessages: [],
80
+ otherUpdates: [del, delCh],
81
+ chats: [],
82
+ users: [],
83
+ state: makeState(50, 60, 70, 8, 1),
84
+ });
85
+ const out = summarizeUpdatesDifference(diff, { pts: 1, qts: 2, date: 3 });
86
+ assert.strictEqual(out.isFinal, true);
87
+ assert.deepStrictEqual(out.state, { pts: 50, qts: 60, date: 70, seq: 8, unreadCount: 1 });
88
+ assert.strictEqual(out.newMessages.length, 1);
89
+ assert.strictEqual(out.newMessages[0].text, "yo");
90
+ assert.deepStrictEqual(out.newMessages[0].peer, { kind: "user", id: "7" });
91
+ assert.strictEqual(out.newMessages[0].isService, false);
92
+ assert.strictEqual(out.deletedMessageIds.length, 2);
93
+ assert.deepStrictEqual(out.deletedMessageIds[0], { messageIds: [5, 6] });
94
+ assert.deepStrictEqual(out.deletedMessageIds[1], {
95
+ peer: { kind: "channel", id: "999" },
96
+ messageIds: [20, 21],
97
+ });
98
+ assert.deepStrictEqual(out.otherUpdates.map((u) => u.type), ["UpdateDeleteMessages", "UpdateDeleteChannelMessages"]);
99
+ });
100
+ it("handles DifferenceSlice as non-final with intermediateState", () => {
101
+ const diff = new Api.updates.DifferenceSlice({
102
+ newMessages: [],
103
+ newEncryptedMessages: [],
104
+ otherUpdates: [],
105
+ chats: [],
106
+ users: [],
107
+ intermediateState: makeState(77, 88, 99, 0, 0),
108
+ });
109
+ const out = summarizeUpdatesDifference(diff, { pts: 1, qts: 2, date: 3 });
110
+ assert.strictEqual(out.isFinal, false);
111
+ assert.strictEqual(out.state.pts, 77);
112
+ });
113
+ it("handles DifferenceTooLong with fallback hint", () => {
114
+ const diff = new Api.updates.DifferenceTooLong({ pts: 500 });
115
+ const out = summarizeUpdatesDifference(diff, { pts: 1, qts: 2, date: 3 });
116
+ assert.strictEqual(out.isFinal, true);
117
+ assert.strictEqual(out.state.pts, 500);
118
+ assert.ok(out.fallback);
119
+ assert.strictEqual(out.fallback?.kind, "tooLong");
120
+ assert.match(out.fallback?.suggestedAction ?? "", /telegram-read-messages|resync/);
121
+ });
122
+ });
123
+ describe("TelegramService.getUpdates", () => {
124
+ it("invokes updates.GetDifference with cursor and caps ptsLimit to 1000", async () => {
125
+ const invocations = [];
126
+ const service = makeService(invocations, () => new Api.updates.DifferenceEmpty({ date: 99, seq: 0 }));
127
+ await service.getUpdates({ pts: 10, qts: 20, date: 30, ptsLimit: 5000 });
128
+ const call = invocations.find((r) => r instanceof Api.updates.GetDifference);
129
+ assert.ok(call);
130
+ assert.strictEqual(call.pts, 10);
131
+ assert.strictEqual(call.qts, 20);
132
+ assert.strictEqual(call.date, 30);
133
+ assert.strictEqual(call.ptsLimit, 1000);
134
+ });
135
+ it("applies defaults when ptsLimit/ptsTotalLimit omitted", async () => {
136
+ const invocations = [];
137
+ const service = makeService(invocations, () => new Api.updates.DifferenceEmpty({ date: 1, seq: 0 }));
138
+ await service.getUpdates({ pts: 1, qts: 1, date: 1 });
139
+ const call = invocations.find((r) => r instanceof Api.updates.GetDifference);
140
+ assert.ok(call);
141
+ assert.strictEqual(call.ptsLimit, 100);
142
+ assert.strictEqual(call.ptsTotalLimit, 1000);
143
+ });
144
+ });
145
+ describe("summarizeChannelDifference", () => {
146
+ it("summarizes ChannelDifferenceEmpty", () => {
147
+ const diff = new Api.updates.ChannelDifferenceEmpty({ final: true, pts: 42, timeout: 30 });
148
+ const out = summarizeChannelDifference(diff, "100", 0);
149
+ assert.strictEqual(out.pts, 42);
150
+ assert.strictEqual(out.isFinal, true);
151
+ assert.strictEqual(out.timeout, 30);
152
+ assert.deepStrictEqual(out.newMessages, []);
153
+ });
154
+ it("summarizes ChannelDifference with newMessages/otherUpdates", () => {
155
+ const msg = makeMessage(10, new Api.PeerChannel({ channelId: bigInt(100) }), "chan-msg");
156
+ const diff = new Api.updates.ChannelDifference({
157
+ final: false,
158
+ pts: 55,
159
+ newMessages: [msg],
160
+ otherUpdates: [],
161
+ chats: [],
162
+ users: [],
163
+ });
164
+ const out = summarizeChannelDifference(diff, "100", 0);
165
+ assert.strictEqual(out.pts, 55);
166
+ assert.strictEqual(out.isFinal, false);
167
+ assert.strictEqual(out.newMessages.length, 1);
168
+ assert.strictEqual(out.newMessages[0].text, "chan-msg");
169
+ });
170
+ it("summarizes ChannelDifferenceTooLong with fallback and dialog snapshot messages", () => {
171
+ const msg = makeMessage(1, new Api.PeerChannel({ channelId: bigInt(100) }), "snap");
172
+ const diff = new Api.updates.ChannelDifferenceTooLong({
173
+ final: true,
174
+ dialog: new Api.Dialog({
175
+ peer: new Api.PeerChannel({ channelId: bigInt(100) }),
176
+ topMessage: 1,
177
+ readInboxMaxId: 0,
178
+ readOutboxMaxId: 0,
179
+ unreadCount: 0,
180
+ unreadMentionsCount: 0,
181
+ unreadReactionsCount: 0,
182
+ notifySettings: new Api.PeerNotifySettings({}),
183
+ }),
184
+ messages: [msg],
185
+ chats: [],
186
+ users: [],
187
+ });
188
+ const out = summarizeChannelDifference(diff, "100", 42);
189
+ assert.strictEqual(out.pts, 42);
190
+ assert.strictEqual(out.newMessages.length, 1);
191
+ assert.ok(out.fallback);
192
+ assert.strictEqual(out.fallback?.kind, "tooLong");
193
+ });
194
+ });
195
+ describe("TelegramService.getChannelUpdates", () => {
196
+ it("invokes updates.GetChannelDifference with resolved channel and caps limit", async () => {
197
+ const invocations = [];
198
+ const chan = makeChannel(100);
199
+ const service = makeService(invocations, () => new Api.updates.ChannelDifferenceEmpty({ final: true, pts: 7 }), chan);
200
+ await service.getChannelUpdates("100", { pts: 5, limit: 9999999, force: true });
201
+ const call = invocations.find((r) => r instanceof Api.updates.GetChannelDifference);
202
+ assert.ok(call);
203
+ assert.strictEqual(call.pts, 5);
204
+ assert.strictEqual(call.limit, 1_000);
205
+ assert.strictEqual(call.force, true);
206
+ assert.ok(call.filter instanceof Api.ChannelMessagesFilterEmpty);
207
+ });
208
+ it("rejects when chat entity is not a channel", async () => {
209
+ const invocations = [];
210
+ const notChan = new Api.Chat({
211
+ id: bigInt(1),
212
+ title: "c",
213
+ photo: new Api.ChatPhotoEmpty(),
214
+ participantsCount: 1,
215
+ date: 0,
216
+ version: 1,
217
+ });
218
+ const service = makeService(invocations, () => new Api.updates.ChannelDifferenceEmpty({ final: true, pts: 0 }), notChan);
219
+ await assert.rejects(service.getChannelUpdates("1", { pts: 1 }), /channels\/supergroups/i);
220
+ });
221
+ });
@@ -19,8 +19,14 @@ export declare class RateLimiter {
19
19
  private initialRetryDelay;
20
20
  private maxRetryDelay;
21
21
  constructor(options?: RateLimiterOptions);
22
- /** Execute a function with rate limiting and automatic retry */
23
- execute<T>(fn: () => Promise<T>, context?: string): Promise<T>;
22
+ /**
23
+ * Execute a function with rate limiting and automatic retry.
24
+ * @param throwOnFloodWait If true, throw immediately on FLOOD_WAIT instead of sleeping (use for
25
+ * endpoints with very long rate-limit windows like stats APIs).
26
+ */
27
+ execute<T>(fn: () => Promise<T>, context?: string, options?: {
28
+ throwOnFloodWait?: boolean;
29
+ }): Promise<T>;
24
30
  private executeWithRetry;
25
31
  private waitForSlot;
26
32
  }
@@ -15,27 +15,34 @@ export class RateLimiter {
15
15
  this.initialRetryDelay = options.initialRetryDelay ?? 1000;
16
16
  this.maxRetryDelay = options.maxRetryDelay ?? 60000;
17
17
  }
18
- /** Execute a function with rate limiting and automatic retry */
19
- async execute(fn, context = "API call") {
20
- return this.executeWithRetry(fn, context, 0);
18
+ /**
19
+ * Execute a function with rate limiting and automatic retry.
20
+ * @param throwOnFloodWait If true, throw immediately on FLOOD_WAIT instead of sleeping (use for
21
+ * endpoints with very long rate-limit windows like stats APIs).
22
+ */
23
+ async execute(fn, context = "API call", options) {
24
+ return this.executeWithRetry(fn, context, 0, options);
21
25
  }
22
- async executeWithRetry(fn, context, attempt) {
26
+ async executeWithRetry(fn, context, attempt, options) {
23
27
  await this.waitForSlot();
24
28
  try {
25
29
  return await fn();
26
30
  }
27
31
  catch (error) {
28
32
  const errorMessage = error.errorMessage || error.message || String(error);
29
- // FLOOD_WAIT — wait the exact time Telegram requires
33
+ // FLOOD_WAIT — wait the exact time Telegram requires (or throw immediately if requested)
30
34
  const floodMatch = errorMessage.match(/FLOOD_WAIT[_]?(\d+)/i);
31
35
  if (floodMatch) {
32
36
  const waitSeconds = Number.parseInt(floodMatch[1], 10);
37
+ if (options?.throwOnFloodWait) {
38
+ throw new Error(`Rate limit: Telegram requires a ${waitSeconds}s wait for ${context}. Try again in ${Math.ceil(waitSeconds / 60)} minute(s).`);
39
+ }
33
40
  if (attempt >= this.maxRetries) {
34
41
  throw new Error(`Rate limit exceeded after ${this.maxRetries} retries. Telegram requires ${waitSeconds}s wait. Try again later.`);
35
42
  }
36
43
  console.error(`[rate-limiter] FLOOD_WAIT for ${context}. Waiting ${waitSeconds}s (attempt ${attempt + 1}/${this.maxRetries})`);
37
44
  await sleep(waitSeconds * 1000);
38
- return this.executeWithRetry(fn, context, attempt + 1);
45
+ return this.executeWithRetry(fn, context, attempt + 1, options);
39
46
  }
40
47
  // Network/timeout errors — exponential backoff
41
48
  if (isNetworkError(errorMessage)) {
@@ -45,7 +52,7 @@ export class RateLimiter {
45
52
  const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
46
53
  console.error(`[rate-limiter] Network error for ${context}. Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
47
54
  await sleep(delay);
48
- return this.executeWithRetry(fn, context, attempt + 1);
55
+ return this.executeWithRetry(fn, context, attempt + 1, options);
49
56
  }
50
57
  // Temporary server errors (5xx) — exponential backoff
51
58
  if (isTemporaryError(errorMessage)) {
@@ -54,7 +61,7 @@ export class RateLimiter {
54
61
  }
55
62
  const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
56
63
  await sleep(delay);
57
- return this.executeWithRetry(fn, context, attempt + 1);
64
+ return this.executeWithRetry(fn, context, attempt + 1, options);
58
65
  }
59
66
  // Non-retryable — throw immediately
60
67
  throw error;