@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,115 @@
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(chatEntity, botEntity, invocations, response) {
7
+ const fakeClient = {
8
+ invoke: async (req) => {
9
+ invocations.push(req);
10
+ return response;
11
+ },
12
+ getEntity: async (_id) => botEntity,
13
+ };
14
+ const service = new TelegramService(1, "hash");
15
+ const internals = service;
16
+ internals.client = fakeClient;
17
+ internals.connected = true;
18
+ internals.resolveChat = async () => chatEntity;
19
+ return service;
20
+ }
21
+ function makeBotUser(id, isBot) {
22
+ return new Api.User({
23
+ id: bigInt(id),
24
+ accessHash: bigInt(42),
25
+ firstName: "InlineBot",
26
+ bot: isBot,
27
+ });
28
+ }
29
+ function makeChannel(id) {
30
+ return new Api.Channel({
31
+ id: bigInt(id),
32
+ title: "test",
33
+ photo: new Api.ChatPhotoEmpty(),
34
+ date: 0,
35
+ accessHash: bigInt(1),
36
+ megagroup: true,
37
+ });
38
+ }
39
+ function makeBotResults() {
40
+ return new Api.messages.BotResults({
41
+ queryId: bigInt("9876543210"),
42
+ nextOffset: "20",
43
+ cacheTime: 300,
44
+ gallery: false,
45
+ users: [],
46
+ results: [
47
+ new Api.BotInlineResult({
48
+ id: "r1",
49
+ type: "article",
50
+ title: "First",
51
+ description: "desc1",
52
+ url: "https://example.com/1",
53
+ sendMessage: new Api.BotInlineMessageText({ message: "pick me" }),
54
+ }),
55
+ new Api.BotInlineMediaResult({
56
+ id: "r2",
57
+ type: "gif",
58
+ title: "Media",
59
+ description: "a gif",
60
+ sendMessage: new Api.BotInlineMessageText({ message: "gif" }),
61
+ }),
62
+ ],
63
+ });
64
+ }
65
+ describe("TelegramService.getInlineBotResults", () => {
66
+ it("invokes GetInlineBotResults with resolved bot InputUser and returns compact results", async () => {
67
+ const invocations = [];
68
+ const service = makeService(makeChannel(12345), makeBotUser(777, true), invocations, makeBotResults());
69
+ const out = await service.getInlineBotResults("@gif", "12345", "cat", undefined);
70
+ const call = invocations.find((r) => r instanceof Api.messages.GetInlineBotResults);
71
+ assert.ok(call, "GetInlineBotResults was invoked");
72
+ assert.ok(call.bot instanceof Api.InputUser);
73
+ assert.strictEqual(call.bot.userId.toString(), "777");
74
+ assert.strictEqual(call.query, "cat");
75
+ assert.strictEqual(call.offset, "");
76
+ assert.strictEqual(out.queryId, "9876543210");
77
+ assert.strictEqual(out.nextOffset, "20");
78
+ assert.strictEqual(out.cacheTime, 300);
79
+ assert.strictEqual(out.gallery, false);
80
+ assert.strictEqual(out.results.length, 2);
81
+ assert.deepStrictEqual(out.results[0], {
82
+ id: "r1",
83
+ type: "article",
84
+ title: "First",
85
+ description: "desc1",
86
+ url: "https://example.com/1",
87
+ });
88
+ assert.deepStrictEqual(out.results[1], {
89
+ id: "r2",
90
+ type: "gif",
91
+ title: "Media",
92
+ description: "a gif",
93
+ });
94
+ });
95
+ it("passes through offset when provided", async () => {
96
+ const invocations = [];
97
+ const service = makeService(makeChannel(10), makeBotUser(1, true), invocations, makeBotResults());
98
+ await service.getInlineBotResults("@b", "10", "q", "20");
99
+ const call = invocations.find((r) => r instanceof Api.messages.GetInlineBotResults);
100
+ assert.ok(call);
101
+ assert.strictEqual(call.offset, "20");
102
+ });
103
+ it("rejects when bot is not a bot account", async () => {
104
+ const invocations = [];
105
+ const service = makeService(makeChannel(10), makeBotUser(1, false), invocations, makeBotResults());
106
+ await assert.rejects(service.getInlineBotResults("@notbot", "10", "q"), /not a bot/i);
107
+ assert.strictEqual(invocations.find((r) => r instanceof Api.messages.GetInlineBotResults), undefined, "no API call when target is not a bot");
108
+ });
109
+ it("rejects when bot entity is not a User", async () => {
110
+ const invocations = [];
111
+ const notUser = makeChannel(99);
112
+ const service = makeService(makeChannel(10), notUser, invocations, makeBotResults());
113
+ await assert.rejects(service.getInlineBotResults("@chan", "10", "q"), /not a user/i);
114
+ });
115
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,166 @@
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 { summarizeMegagroupStats, TelegramService } from "../telegram-client.js";
6
+ function makeService(entity, invokeImpl, invocations) {
7
+ const fakeClient = {
8
+ invoke: async (req) => {
9
+ invocations.push(req);
10
+ return invokeImpl(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 () => entity;
18
+ return service;
19
+ }
20
+ function fakeStats() {
21
+ return {
22
+ period: { minDate: 1000, maxDate: 2000 },
23
+ members: { current: 500, previous: 450 },
24
+ messages: { current: 1200, previous: 1100 },
25
+ viewers: { current: 300, previous: 280 },
26
+ posters: { current: 80, previous: 75 },
27
+ growthGraph: new Api.StatsGraphAsync({ token: "growth-token" }),
28
+ membersGraph: new Api.StatsGraphAsync({ token: "members-token" }),
29
+ newMembersBySourceGraph: new Api.StatsGraphAsync({ token: "new-members-token" }),
30
+ languagesGraph: new Api.StatsGraphError({ error: "unavailable" }),
31
+ messagesGraph: new Api.StatsGraph({
32
+ json: new Api.DataJSON({ data: '{"x":[1,2,3]}' }),
33
+ zoomToken: "zoom",
34
+ }),
35
+ actionsGraph: new Api.StatsGraphAsync({ token: "actions-token" }),
36
+ topHoursGraph: new Api.StatsGraphAsync({ token: "top-hours-token" }),
37
+ weekdaysGraph: new Api.StatsGraphAsync({ token: "weekdays-token" }),
38
+ topPosters: [
39
+ new Api.StatsGroupTopPoster({ userId: bigInt(111), messages: 42, avgChars: 120 }),
40
+ new Api.StatsGroupTopPoster({ userId: bigInt(222), messages: 21, avgChars: 80 }),
41
+ ],
42
+ topAdmins: [new Api.StatsGroupTopAdmin({ userId: bigInt(333), deleted: 5, kicked: 2, banned: 1 })],
43
+ topInviters: [new Api.StatsGroupTopInviter({ userId: bigInt(444), invitations: 10 })],
44
+ users: [],
45
+ };
46
+ }
47
+ describe("summarizeMegagroupStats", () => {
48
+ it("produces compact summary without graphs by default", () => {
49
+ const summary = summarizeMegagroupStats(fakeStats(), false);
50
+ assert.strictEqual(summary.graphs, undefined);
51
+ assert.deepStrictEqual(summary.period, { minDate: 1000, maxDate: 2000 });
52
+ assert.deepStrictEqual(summary.members, { current: 500, previous: 450 });
53
+ assert.deepStrictEqual(summary.messages, { current: 1200, previous: 1100 });
54
+ assert.deepStrictEqual(summary.viewers, { current: 300, previous: 280 });
55
+ assert.deepStrictEqual(summary.posters, { current: 80, previous: 75 });
56
+ assert.strictEqual(summary.topPosters.length, 2);
57
+ assert.deepStrictEqual(summary.topPosters[0], {
58
+ userId: "111",
59
+ messages: 42,
60
+ avgChars: 120,
61
+ });
62
+ assert.deepStrictEqual(summary.topAdmins[0], {
63
+ userId: "333",
64
+ deleted: 5,
65
+ kicked: 2,
66
+ banned: 1,
67
+ });
68
+ assert.deepStrictEqual(summary.topInviters[0], { userId: "444", invitations: 10 });
69
+ });
70
+ it("includes and decodes graphs when requested", () => {
71
+ const summary = summarizeMegagroupStats(fakeStats(), true);
72
+ assert.ok(summary.graphs);
73
+ const graphs = summary.graphs ?? {};
74
+ assert.deepStrictEqual(graphs.growth, { type: "async", token: "growth-token" });
75
+ assert.deepStrictEqual(graphs.languages, { type: "error", error: "unavailable" });
76
+ assert.deepStrictEqual(graphs.messages, { type: "data", data: { x: [1, 2, 3] }, zoomToken: "zoom" });
77
+ assert.deepStrictEqual(graphs.weekdays, { type: "async", token: "weekdays-token" });
78
+ });
79
+ it("handles empty top lists gracefully", () => {
80
+ const stats = fakeStats();
81
+ stats.topPosters = [];
82
+ stats.topAdmins = [];
83
+ stats.topInviters = [];
84
+ const summary = summarizeMegagroupStats(stats, false);
85
+ assert.deepStrictEqual(summary.topPosters, []);
86
+ assert.deepStrictEqual(summary.topAdmins, []);
87
+ assert.deepStrictEqual(summary.topInviters, []);
88
+ });
89
+ });
90
+ describe("TelegramService.getMegagroupStats", () => {
91
+ function megagroup() {
92
+ return new Api.Channel({
93
+ id: bigInt(33333),
94
+ title: "supergroup",
95
+ photo: new Api.ChatPhotoEmpty(),
96
+ date: 0,
97
+ accessHash: bigInt(1),
98
+ megagroup: true,
99
+ });
100
+ }
101
+ it("invokes stats.GetMegagroupStats and returns compact summary", async () => {
102
+ const invocations = [];
103
+ const service = makeService(megagroup(), async () => fakeStats(), invocations);
104
+ const summary = await service.getMegagroupStats("33333");
105
+ const call = invocations.find((r) => r instanceof Api.stats.GetMegagroupStats);
106
+ assert.ok(call, "GetMegagroupStats was invoked");
107
+ assert.strictEqual(summary.members.current, 500);
108
+ assert.strictEqual(summary.graphs, undefined);
109
+ });
110
+ it("passes dark=true when requested", async () => {
111
+ const invocations = [];
112
+ const service = makeService(megagroup(), async () => fakeStats(), invocations);
113
+ await service.getMegagroupStats("33333", { dark: true });
114
+ const call = invocations.find((r) => r instanceof Api.stats.GetMegagroupStats);
115
+ assert.ok(call);
116
+ assert.strictEqual(call.dark, true);
117
+ });
118
+ it("includes graphs when includeGraphs=true", async () => {
119
+ const invocations = [];
120
+ const service = makeService(megagroup(), async () => fakeStats(), invocations);
121
+ const summary = await service.getMegagroupStats("33333", { includeGraphs: true });
122
+ assert.ok(summary.graphs);
123
+ assert.strictEqual(summary.graphs?.growth.type, "async");
124
+ });
125
+ it("rejects broadcast channels with hint to use broadcast stats", async () => {
126
+ const broadcast = new Api.Channel({
127
+ id: bigInt(12345),
128
+ title: "broadcast",
129
+ photo: new Api.ChatPhotoEmpty(),
130
+ date: 0,
131
+ accessHash: bigInt(1),
132
+ broadcast: true,
133
+ });
134
+ const invocations = [];
135
+ const service = makeService(broadcast, async () => fakeStats(), invocations);
136
+ await assert.rejects(service.getMegagroupStats("12345"), /telegram-get-broadcast-stats/);
137
+ assert.strictEqual(invocations.find((r) => r instanceof Api.stats.GetMegagroupStats), undefined);
138
+ });
139
+ it("rejects non-channel entities", async () => {
140
+ const chat = new Api.Chat({
141
+ id: bigInt(44444),
142
+ title: "basic group",
143
+ photo: new Api.ChatPhotoEmpty(),
144
+ participantsCount: 5,
145
+ date: 0,
146
+ version: 0,
147
+ });
148
+ const invocations = [];
149
+ const service = makeService(chat, async () => fakeStats(), invocations);
150
+ await assert.rejects(service.getMegagroupStats("44444"), /supergroups/);
151
+ });
152
+ it("surfaces admin-required errors with a clearer message", async () => {
153
+ const invocations = [];
154
+ const service = makeService(megagroup(), async () => {
155
+ throw new Error("CHAT_ADMIN_REQUIRED");
156
+ }, invocations);
157
+ await assert.rejects(service.getMegagroupStats("33333"), /admin rights/i);
158
+ });
159
+ it("surfaces stats-unavailable errors with hint", async () => {
160
+ const invocations = [];
161
+ const service = makeService(megagroup(), async () => {
162
+ throw new Error("STATS_UNAVAILABLE");
163
+ }, invocations);
164
+ await assert.rejects(service.getMegagroupStats("33333"), /no stats/i);
165
+ });
166
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
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(chatEntity, invocations, messages, responder) {
7
+ const fakeClient = {
8
+ invoke: async (req) => {
9
+ invocations.push(req);
10
+ return responder(req);
11
+ },
12
+ getMessages: async (_entity, _opts) => messages,
13
+ };
14
+ const service = new TelegramService(1, "hash");
15
+ const internals = service;
16
+ internals.client = fakeClient;
17
+ internals.connected = true;
18
+ internals.resolveChat = async () => chatEntity;
19
+ return service;
20
+ }
21
+ function makeChannel(id) {
22
+ return new Api.Channel({
23
+ id: bigInt(id),
24
+ title: "t",
25
+ photo: new Api.ChatPhotoEmpty(),
26
+ date: 0,
27
+ accessHash: bigInt(1),
28
+ megagroup: true,
29
+ });
30
+ }
31
+ function makeMessage(id, markup) {
32
+ return new Api.Message({
33
+ id,
34
+ peerId: new Api.PeerChannel({ channelId: bigInt(100) }),
35
+ date: 0,
36
+ message: "hi",
37
+ replyMarkup: markup,
38
+ });
39
+ }
40
+ function callbackButton(text, data, requiresPassword = false) {
41
+ return new Api.KeyboardButtonCallback({ text, data, requiresPassword });
42
+ }
43
+ function urlButton(text, url) {
44
+ return new Api.KeyboardButtonUrl({ text, url });
45
+ }
46
+ function inlineMarkup(buttons) {
47
+ return new Api.ReplyInlineMarkup({
48
+ rows: buttons.map((row) => new Api.KeyboardButtonRow({ buttons: row })),
49
+ });
50
+ }
51
+ function callbackAnswer() {
52
+ return new Api.messages.BotCallbackAnswer({
53
+ alert: true,
54
+ message: "Clicked",
55
+ cacheTime: 60,
56
+ });
57
+ }
58
+ describe("TelegramService.pressButton", () => {
59
+ it("presses a callback button by (row, column) and returns the answer", async () => {
60
+ const invocations = [];
61
+ const data = Buffer.from("vote_yes");
62
+ const msg = makeMessage(42, inlineMarkup([[callbackButton("Yes", data)]]));
63
+ const service = makeService(makeChannel(100), invocations, [msg], () => callbackAnswer());
64
+ const out = await service.pressButton("100", 42, { buttonIndex: { row: 0, column: 0 } });
65
+ const call = invocations.find((r) => r instanceof Api.messages.GetBotCallbackAnswer);
66
+ assert.ok(call, "GetBotCallbackAnswer was invoked");
67
+ assert.strictEqual(call.msgId, 42);
68
+ assert.ok(call.data, "data is set");
69
+ assert.strictEqual(Buffer.from(call.data).toString(), "vote_yes");
70
+ assert.strictEqual(out.alert, true);
71
+ assert.strictEqual(out.message, "Clicked");
72
+ assert.strictEqual(out.cacheTime, 60);
73
+ });
74
+ it("accepts raw data as base64 without fetching the message", async () => {
75
+ const invocations = [];
76
+ const service = makeService(makeChannel(100), invocations, [], () => callbackAnswer());
77
+ const payload = Buffer.from([0x01, 0x02, 0xff]);
78
+ await service.pressButton("100", 7, { data: payload.toString("base64") });
79
+ const call = invocations.find((r) => r instanceof Api.messages.GetBotCallbackAnswer);
80
+ assert.ok(call);
81
+ assert.deepStrictEqual(Buffer.from(call.data), payload);
82
+ });
83
+ it("rejects when button type is not Callback", async () => {
84
+ const invocations = [];
85
+ const msg = makeMessage(1, inlineMarkup([[urlButton("Open", "https://x.test")]]));
86
+ const service = makeService(makeChannel(100), invocations, [msg], () => callbackAnswer());
87
+ await assert.rejects(service.pressButton("100", 1, { buttonIndex: { row: 0, column: 0 } }), /not callable/i);
88
+ assert.strictEqual(invocations.find((r) => r instanceof Api.messages.GetBotCallbackAnswer), undefined, "no API call when button is not callable");
89
+ });
90
+ it("rejects when message has no reply markup", async () => {
91
+ const invocations = [];
92
+ const msg = makeMessage(1, undefined);
93
+ const service = makeService(makeChannel(100), invocations, [msg], () => callbackAnswer());
94
+ await assert.rejects(service.pressButton("100", 1, { buttonIndex: { row: 0, column: 0 } }), /no reply markup/i);
95
+ });
96
+ it("rejects when reply markup is ReplyKeyboardMarkup (not inline)", async () => {
97
+ const invocations = [];
98
+ const markup = new Api.ReplyKeyboardMarkup({
99
+ rows: [new Api.KeyboardButtonRow({ buttons: [new Api.KeyboardButton({ text: "x" })] })],
100
+ });
101
+ const msg = makeMessage(1, markup);
102
+ const service = makeService(makeChannel(100), invocations, [msg], () => callbackAnswer());
103
+ await assert.rejects(service.pressButton("100", 1, { buttonIndex: { row: 0, column: 0 } }), /ReplyInlineMarkup/);
104
+ });
105
+ it("rejects when row/column is out of bounds", async () => {
106
+ const invocations = [];
107
+ const msg = makeMessage(1, inlineMarkup([[callbackButton("A", Buffer.from("a"))]]));
108
+ const service = makeService(makeChannel(100), invocations, [msg], () => callbackAnswer());
109
+ await assert.rejects(service.pressButton("100", 1, { buttonIndex: { row: 5, column: 0 } }), /out of bounds/i);
110
+ await assert.rejects(service.pressButton("100", 1, { buttonIndex: { row: 0, column: 9 } }), /out of bounds/i);
111
+ });
112
+ it("rejects when button requires 2FA password", async () => {
113
+ const invocations = [];
114
+ const msg = makeMessage(1, inlineMarkup([[callbackButton("Admin", Buffer.from("x"), true)]]));
115
+ const service = makeService(makeChannel(100), invocations, [msg], () => callbackAnswer());
116
+ await assert.rejects(service.pressButton("100", 1, { buttonIndex: { row: 0, column: 0 } }), /2FA password/i);
117
+ });
118
+ it("rejects when neither buttonIndex nor data is provided", async () => {
119
+ const invocations = [];
120
+ const service = makeService(makeChannel(100), invocations, [], () => callbackAnswer());
121
+ await assert.rejects(service.pressButton("100", 1, {}), /buttonIndex or data/i);
122
+ });
123
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,245 @@
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 { summarizeQuickReplies, summarizeQuickReply, summarizeQuickReplyMessage, summarizeQuickReplyMessages, TelegramService, } from "../telegram-client.js";
6
+ import { isQuickRepliesEnabled } from "../tools/quick-replies.js";
7
+ function makeService(invocations, responder) {
8
+ const fakeClient = {
9
+ invoke: async (req) => {
10
+ invocations.push(req);
11
+ return responder(req);
12
+ },
13
+ };
14
+ const service = new TelegramService(1, "hash");
15
+ const internals = service;
16
+ internals.client = fakeClient;
17
+ internals.connected = true;
18
+ return service;
19
+ }
20
+ describe("summarizeQuickReply", () => {
21
+ it("maps shortcutId, shortcut, topMessage, count", () => {
22
+ const out = summarizeQuickReply(new Api.QuickReply({ shortcutId: 5, shortcut: "hello", topMessage: 100, count: 3 }));
23
+ assert.deepStrictEqual(out, { shortcutId: 5, shortcut: "hello", topMessage: 100, count: 3 });
24
+ });
25
+ });
26
+ describe("summarizeQuickReplies", () => {
27
+ it("returns notModified flag for QuickRepliesNotModified", () => {
28
+ const out = summarizeQuickReplies(new Api.messages.QuickRepliesNotModified());
29
+ assert.deepStrictEqual(out, { notModified: true });
30
+ });
31
+ it("maps quickReplies array, ignoring messages/chats/users", () => {
32
+ const resp = new Api.messages.QuickReplies({
33
+ quickReplies: [
34
+ new Api.QuickReply({ shortcutId: 1, shortcut: "hi", topMessage: 10, count: 1 }),
35
+ new Api.QuickReply({ shortcutId: 2, shortcut: "bye", topMessage: 20, count: 2 }),
36
+ ],
37
+ messages: [],
38
+ chats: [],
39
+ users: [],
40
+ });
41
+ const out = summarizeQuickReplies(resp);
42
+ assert.strictEqual(out.notModified, undefined);
43
+ assert.strictEqual(out.quickReplies?.length, 2);
44
+ assert.deepStrictEqual(out.quickReplies?.[0], { shortcutId: 1, shortcut: "hi", topMessage: 10, count: 1 });
45
+ assert.deepStrictEqual(out.quickReplies?.[1], { shortcutId: 2, shortcut: "bye", topMessage: 20, count: 2 });
46
+ });
47
+ });
48
+ describe("TelegramService.getQuickReplies", () => {
49
+ it("invokes messages.GetQuickReplies with hash=0 by default", async () => {
50
+ const invocations = [];
51
+ const service = makeService(invocations, () => new Api.messages.QuickReplies({
52
+ quickReplies: [new Api.QuickReply({ shortcutId: 7, shortcut: "gm", topMessage: 4, count: 1 })],
53
+ messages: [],
54
+ chats: [],
55
+ users: [],
56
+ }));
57
+ const out = await service.getQuickReplies();
58
+ const call = invocations.find((r) => r instanceof Api.messages.GetQuickReplies);
59
+ assert.ok(call, "GetQuickReplies must be invoked");
60
+ assert.ok(call.hash.equals(bigInt(0)), "default hash should be 0");
61
+ assert.strictEqual(out.quickReplies?.length, 1);
62
+ assert.strictEqual(out.quickReplies?.[0].shortcut, "gm");
63
+ });
64
+ it("passes the provided hash through (bigInt parsed from decimal string)", async () => {
65
+ const invocations = [];
66
+ const service = makeService(invocations, () => new Api.messages.QuickRepliesNotModified());
67
+ const out = await service.getQuickReplies("123456789012345");
68
+ const call = invocations.find((r) => r instanceof Api.messages.GetQuickReplies);
69
+ assert.ok(call);
70
+ assert.ok(call.hash.equals(bigInt("123456789012345")));
71
+ assert.strictEqual(out.notModified, true);
72
+ });
73
+ it("throws when not connected", async () => {
74
+ const service = new TelegramService(1, "hash");
75
+ await assert.rejects(() => service.getQuickReplies(), /Not connected/i);
76
+ });
77
+ });
78
+ describe("summarizeQuickReplyMessage", () => {
79
+ it("returns null for MessageEmpty", () => {
80
+ const out = summarizeQuickReplyMessage(new Api.MessageEmpty({ id: 1 }));
81
+ assert.strictEqual(out, null);
82
+ });
83
+ it("maps regular message with reply header", () => {
84
+ const msg = new Api.Message({
85
+ id: 42,
86
+ peerId: new Api.PeerUser({ userId: bigInt(123) }),
87
+ date: 1_700_000_000,
88
+ message: "hello quick reply",
89
+ fromId: new Api.PeerUser({ userId: bigInt(55) }),
90
+ replyTo: new Api.MessageReplyHeader({ replyToMsgId: 40 }),
91
+ });
92
+ const out = summarizeQuickReplyMessage(msg);
93
+ assert.deepStrictEqual(out, {
94
+ id: 42,
95
+ date: 1_700_000_000,
96
+ text: "hello quick reply",
97
+ isService: false,
98
+ fromId: { kind: "user", id: "55" },
99
+ replyToMsgId: 40,
100
+ });
101
+ });
102
+ it("marks service messages with isService=true and bracketed action className", () => {
103
+ const action = new Api.MessageActionChatCreate({ title: "t", users: [] });
104
+ const msg = new Api.MessageService({
105
+ id: 7,
106
+ peerId: new Api.PeerChat({ chatId: bigInt(9) }),
107
+ date: 1_700_000_001,
108
+ action,
109
+ });
110
+ const out = summarizeQuickReplyMessage(msg);
111
+ assert.strictEqual(out?.isService, true);
112
+ assert.strictEqual(out?.id, 7);
113
+ assert.ok(out?.text.startsWith("[") && out?.text.endsWith("]"));
114
+ });
115
+ });
116
+ describe("summarizeQuickReplyMessages", () => {
117
+ it("returns notModified flag with count for MessagesNotModified", () => {
118
+ const out = summarizeQuickReplyMessages(new Api.messages.MessagesNotModified({ count: 3 }));
119
+ assert.deepStrictEqual(out, { notModified: true, count: 3 });
120
+ });
121
+ it("maps messages.Messages and drops empties", () => {
122
+ const resp = new Api.messages.Messages({
123
+ messages: [
124
+ new Api.Message({
125
+ id: 1,
126
+ peerId: new Api.PeerUser({ userId: bigInt(11) }),
127
+ date: 100,
128
+ message: "one",
129
+ }),
130
+ new Api.MessageEmpty({ id: 2 }),
131
+ new Api.Message({
132
+ id: 3,
133
+ peerId: new Api.PeerUser({ userId: bigInt(11) }),
134
+ date: 101,
135
+ message: "three",
136
+ }),
137
+ ],
138
+ chats: [],
139
+ users: [],
140
+ });
141
+ const out = summarizeQuickReplyMessages(resp);
142
+ assert.strictEqual(out.notModified, undefined);
143
+ assert.strictEqual(out.count, 2);
144
+ assert.strictEqual(out.messages?.length, 2);
145
+ assert.strictEqual(out.messages?.[0].id, 1);
146
+ assert.strictEqual(out.messages?.[1].id, 3);
147
+ });
148
+ it("maps messages.MessagesSlice using slice.count (not array length)", () => {
149
+ const resp = new Api.messages.MessagesSlice({
150
+ count: 42,
151
+ messages: [
152
+ new Api.Message({
153
+ id: 5,
154
+ peerId: new Api.PeerUser({ userId: bigInt(11) }),
155
+ date: 100,
156
+ message: "five",
157
+ }),
158
+ ],
159
+ chats: [],
160
+ users: [],
161
+ });
162
+ const out = summarizeQuickReplyMessages(resp);
163
+ assert.strictEqual(out.count, 42);
164
+ assert.strictEqual(out.messages?.length, 1);
165
+ });
166
+ });
167
+ describe("TelegramService.getQuickReplyMessages", () => {
168
+ it("invokes messages.GetQuickReplyMessages with shortcutId and default hash=0", async () => {
169
+ const invocations = [];
170
+ const service = makeService(invocations, () => new Api.messages.Messages({
171
+ messages: [
172
+ new Api.Message({
173
+ id: 10,
174
+ peerId: new Api.PeerUser({ userId: bigInt(1) }),
175
+ date: 1,
176
+ message: "x",
177
+ }),
178
+ ],
179
+ chats: [],
180
+ users: [],
181
+ }));
182
+ const out = await service.getQuickReplyMessages(7);
183
+ const call = invocations.find((r) => r instanceof Api.messages.GetQuickReplyMessages);
184
+ assert.ok(call, "GetQuickReplyMessages must be invoked");
185
+ assert.strictEqual(call.shortcutId, 7);
186
+ assert.strictEqual(call.id, undefined);
187
+ assert.ok(call.hash.equals(bigInt(0)));
188
+ assert.strictEqual(out.messages?.length, 1);
189
+ });
190
+ it("passes ids and hash through", async () => {
191
+ const invocations = [];
192
+ const service = makeService(invocations, () => new Api.messages.MessagesNotModified({ count: 5 }));
193
+ const out = await service.getQuickReplyMessages(9, { ids: [1, 2, 3], hash: "987654321" });
194
+ const call = invocations.find((r) => r instanceof Api.messages.GetQuickReplyMessages);
195
+ assert.ok(call);
196
+ assert.strictEqual(call.shortcutId, 9);
197
+ assert.deepStrictEqual(call.id, [1, 2, 3]);
198
+ assert.ok(call.hash.equals(bigInt("987654321")));
199
+ assert.strictEqual(out.notModified, true);
200
+ assert.strictEqual(out.count, 5);
201
+ });
202
+ it("throws when not connected", async () => {
203
+ const service = new TelegramService(1, "hash");
204
+ await assert.rejects(() => service.getQuickReplyMessages(1), /Not connected/i);
205
+ });
206
+ });
207
+ describe("isQuickRepliesEnabled gate", () => {
208
+ it("is off by default", () => {
209
+ const prev = process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES;
210
+ delete process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES;
211
+ try {
212
+ assert.strictEqual(isQuickRepliesEnabled(), false);
213
+ }
214
+ finally {
215
+ if (prev !== undefined)
216
+ process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES = prev;
217
+ }
218
+ });
219
+ it("turns on when env is '1'", () => {
220
+ const prev = process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES;
221
+ process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES = "1";
222
+ try {
223
+ assert.strictEqual(isQuickRepliesEnabled(), true);
224
+ }
225
+ finally {
226
+ if (prev === undefined)
227
+ delete process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES;
228
+ else
229
+ process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES = prev;
230
+ }
231
+ });
232
+ it("stays off for any non-'1' value", () => {
233
+ const prev = process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES;
234
+ process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES = "true";
235
+ try {
236
+ assert.strictEqual(isQuickRepliesEnabled(), false);
237
+ }
238
+ finally {
239
+ if (prev === undefined)
240
+ delete process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES;
241
+ else
242
+ process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES = prev;
243
+ }
244
+ });
245
+ });
@@ -0,0 +1 @@
1
+ export {};