@overpod/mcp-telegram 1.26.0 → 1.27.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 (80) hide show
  1. package/dist/client.d.ts +12 -0
  2. package/dist/client.js +136 -0
  3. package/dist/index.js +15 -24
  4. package/dist/ipc-protocol.d.ts +19 -0
  5. package/dist/ipc-protocol.js +22 -0
  6. package/dist/lock.d.ts +12 -0
  7. package/dist/lock.js +83 -0
  8. package/dist/master.d.ts +1 -0
  9. package/dist/master.js +105 -0
  10. package/dist/rate-limiter.d.ts +1 -1
  11. package/dist/rate-limiter.js +8 -8
  12. package/dist/telegram-client.d.ts +6 -470
  13. package/dist/telegram-client.js +17 -874
  14. package/dist/telegram-helpers.d.ts +470 -0
  15. package/dist/telegram-helpers.js +870 -0
  16. package/dist/tools/account.js +7 -7
  17. package/dist/tools/boosts.js +4 -4
  18. package/dist/tools/chats.js +7 -7
  19. package/dist/tools/contacts.js +3 -3
  20. package/dist/tools/extras.js +3 -3
  21. package/dist/tools/group-calls.js +3 -3
  22. package/dist/tools/messages.js +17 -17
  23. package/dist/tools/quick-replies.js +3 -3
  24. package/dist/tools/reactions.js +2 -2
  25. package/dist/tools/shared.d.ts +3 -3
  26. package/dist/tools/shared.js +8 -7
  27. package/dist/tools/stars.js +3 -3
  28. package/dist/tools/stickers.js +5 -5
  29. package/dist/tools/stories.js +5 -5
  30. package/package.json +1 -1
  31. package/dist/__tests__/admin-log.test.d.ts +0 -1
  32. package/dist/__tests__/admin-log.test.js +0 -41
  33. package/dist/__tests__/approve-join-request.test.d.ts +0 -1
  34. package/dist/__tests__/approve-join-request.test.js +0 -107
  35. package/dist/__tests__/boosts.test.d.ts +0 -1
  36. package/dist/__tests__/boosts.test.js +0 -310
  37. package/dist/__tests__/broadcast-stats.test.d.ts +0 -1
  38. package/dist/__tests__/broadcast-stats.test.js +0 -172
  39. package/dist/__tests__/business-chat-links.test.d.ts +0 -1
  40. package/dist/__tests__/business-chat-links.test.js +0 -102
  41. package/dist/__tests__/get-message-buttons.test.d.ts +0 -1
  42. package/dist/__tests__/get-message-buttons.test.js +0 -122
  43. package/dist/__tests__/group-calls.test.d.ts +0 -1
  44. package/dist/__tests__/group-calls.test.js +0 -503
  45. package/dist/__tests__/inline-query-send.test.d.ts +0 -1
  46. package/dist/__tests__/inline-query-send.test.js +0 -94
  47. package/dist/__tests__/inline-query.test.d.ts +0 -1
  48. package/dist/__tests__/inline-query.test.js +0 -115
  49. package/dist/__tests__/megagroup-stats.test.d.ts +0 -1
  50. package/dist/__tests__/megagroup-stats.test.js +0 -166
  51. package/dist/__tests__/press-button.test.d.ts +0 -1
  52. package/dist/__tests__/press-button.test.js +0 -123
  53. package/dist/__tests__/quick-replies.test.d.ts +0 -1
  54. package/dist/__tests__/quick-replies.test.js +0 -245
  55. package/dist/__tests__/rate-limiter.test.d.ts +0 -1
  56. package/dist/__tests__/rate-limiter.test.js +0 -81
  57. package/dist/__tests__/reactions.test.d.ts +0 -1
  58. package/dist/__tests__/reactions.test.js +0 -23
  59. package/dist/__tests__/set-chat-permissions-merge.test.d.ts +0 -1
  60. package/dist/__tests__/set-chat-permissions-merge.test.js +0 -107
  61. package/dist/__tests__/set-chat-reactions.test.d.ts +0 -1
  62. package/dist/__tests__/set-chat-reactions.test.js +0 -129
  63. package/dist/__tests__/stars-status.test.d.ts +0 -1
  64. package/dist/__tests__/stars-status.test.js +0 -205
  65. package/dist/__tests__/stars-transactions.test.d.ts +0 -1
  66. package/dist/__tests__/stars-transactions.test.js +0 -82
  67. package/dist/__tests__/stories.test.d.ts +0 -1
  68. package/dist/__tests__/stories.test.js +0 -361
  69. package/dist/__tests__/toggle-anti-spam.test.d.ts +0 -1
  70. package/dist/__tests__/toggle-anti-spam.test.js +0 -80
  71. package/dist/__tests__/toggle-channel-signatures.test.d.ts +0 -1
  72. package/dist/__tests__/toggle-channel-signatures.test.js +0 -80
  73. package/dist/__tests__/toggle-forum-mode.test.d.ts +0 -1
  74. package/dist/__tests__/toggle-forum-mode.test.js +0 -80
  75. package/dist/__tests__/toggle-prehistory-hidden.test.d.ts +0 -1
  76. package/dist/__tests__/toggle-prehistory-hidden.test.js +0 -80
  77. package/dist/__tests__/tools/shared.test.d.ts +0 -1
  78. package/dist/__tests__/tools/shared.test.js +0 -110
  79. package/dist/__tests__/updates.test.d.ts +0 -1
  80. package/dist/__tests__/updates.test.js +0 -221
@@ -10,6 +10,8 @@ import { CustomFile } from "telegram/client/uploads.js";
10
10
  import { StringSession } from "telegram/sessions/index.js";
11
11
  import { Api } from "telegram/tl/index.js";
12
12
  import { RateLimiter } from "./rate-limiter.js";
13
+ import { describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, mergeBannedRights, reactionToEmoji, summarizeAllStories, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeGroupCall, summarizeGroupCallParticipants, summarizeMegagroupStats, summarizeMyBoosts, summarizePeerStories, summarizeQuickReplies, summarizeQuickReplyMessages, summarizeStarsStatus, summarizeStoriesById, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
14
+ export { describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, mergeBannedRights, peerToCompact, reactionToEmoji, summarizeAllStories, summarizeBoost, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLink, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeGroupCall, summarizeGroupCallInfo, summarizeGroupCallParticipant, summarizeGroupCallParticipants, summarizeMegagroupStats, summarizeMyBoost, summarizeMyBoosts, summarizePeerStories, summarizePrepaidGiveaway, summarizeQuickReplies, summarizeQuickReply, summarizeQuickReplyMessage, summarizeQuickReplyMessages, summarizeStarsAmount, summarizeStarsStatus, summarizeStarsSubscription, summarizeStarsTransaction, summarizeStarsTransactionPeer, summarizeStoriesById, summarizeStoryItem, summarizeStoryView, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
13
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
16
  const LEGACY_SESSION_FILE = join(__dirname, "..", ".telegram-session");
15
17
  const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
@@ -44,875 +46,6 @@ function ensureSessionDir(filePath) {
44
46
  mkdirSync(dir, { recursive: true, mode: 0o700 });
45
47
  }
46
48
  }
47
- export function describeAdminLogAction(action) {
48
- const prefix = "ChannelAdminLogEventAction";
49
- const raw = action.className.startsWith(prefix) ? action.className.slice(prefix.length) : action.className;
50
- return raw
51
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
52
- .replace(/([a-z])([A-Z])/g, "$1_$2")
53
- .toLowerCase();
54
- }
55
- export function describeAdminLogDetails(action, describeUser) {
56
- if (action instanceof Api.ChannelAdminLogEventActionChangeTitle) {
57
- return `"${action.prevValue}" → "${action.newValue}"`;
58
- }
59
- if (action instanceof Api.ChannelAdminLogEventActionChangeAbout) {
60
- return `description changed`;
61
- }
62
- if (action instanceof Api.ChannelAdminLogEventActionChangeUsername) {
63
- return `@${action.prevValue || "-"} → @${action.newValue || "-"}`;
64
- }
65
- if (action instanceof Api.ChannelAdminLogEventActionUpdatePinned) {
66
- return `message #${action.message instanceof Api.Message ? action.message.id : "?"}`;
67
- }
68
- if (action instanceof Api.ChannelAdminLogEventActionEditMessage) {
69
- return `message #${action.newMessage instanceof Api.Message ? action.newMessage.id : "?"}`;
70
- }
71
- if (action instanceof Api.ChannelAdminLogEventActionDeleteMessage) {
72
- return `message #${action.message instanceof Api.Message ? action.message.id : "?"}`;
73
- }
74
- if (action instanceof Api.ChannelAdminLogEventActionParticipantInvite) {
75
- const p = action.participant;
76
- return `invited user ${"userId" in p ? describeUser(p.userId) : "?"}`;
77
- }
78
- if (action instanceof Api.ChannelAdminLogEventActionParticipantToggleBan) {
79
- const p = action.newParticipant;
80
- if (p instanceof Api.ChannelParticipantBanned) {
81
- const uid = p.peer instanceof Api.PeerUser ? p.peer.userId : undefined;
82
- return `banned user ${uid ? describeUser(uid) : "?"}`;
83
- }
84
- return `unbanned user ${"userId" in p ? describeUser(p.userId) : "?"}`;
85
- }
86
- if (action instanceof Api.ChannelAdminLogEventActionParticipantToggleAdmin) {
87
- const p = action.newParticipant;
88
- return `admin rights changed for ${"userId" in p ? describeUser(p.userId) : "?"}`;
89
- }
90
- if (action instanceof Api.ChannelAdminLogEventActionToggleSlowMode) {
91
- return `${action.prevValue}s → ${action.newValue}s`;
92
- }
93
- if (action instanceof Api.ChannelAdminLogEventActionToggleInvites) {
94
- return `invites ${action.newValue ? "enabled" : "disabled"}`;
95
- }
96
- if (action instanceof Api.ChannelAdminLogEventActionToggleSignatures) {
97
- return `signatures ${action.newValue ? "enabled" : "disabled"}`;
98
- }
99
- if (action instanceof Api.ChannelAdminLogEventActionTogglePreHistoryHidden) {
100
- return `pre-history hidden: ${action.newValue}`;
101
- }
102
- if (action instanceof Api.ChannelAdminLogEventActionChangeHistoryTTL) {
103
- return `${action.prevValue}s → ${action.newValue}s`;
104
- }
105
- if (action instanceof Api.ChannelAdminLogEventActionChangeStickerSet) {
106
- return `sticker set changed`;
107
- }
108
- if (action instanceof Api.ChannelAdminLogEventActionChangeLinkedChat) {
109
- return `${action.prevValue.toString()} → ${action.newValue.toString()}`;
110
- }
111
- if (action instanceof Api.ChannelAdminLogEventActionStopPoll) {
112
- return `poll in message #${action.message instanceof Api.Message ? action.message.id : "?"}`;
113
- }
114
- if (action instanceof Api.ChannelAdminLogEventActionSendMessage) {
115
- return `message #${action.message instanceof Api.Message ? action.message.id : "?"}`;
116
- }
117
- if (action instanceof Api.ChannelAdminLogEventActionCreateTopic) {
118
- return `topic "${action.topic instanceof Api.ForumTopic ? action.topic.title : "?"}"`;
119
- }
120
- if (action instanceof Api.ChannelAdminLogEventActionDeleteTopic) {
121
- return `topic "${action.topic instanceof Api.ForumTopic ? action.topic.title : "?"}"`;
122
- }
123
- if (action instanceof Api.ChannelAdminLogEventActionEditTopic) {
124
- return `topic "${action.newTopic instanceof Api.ForumTopic ? action.newTopic.title : "?"}"`;
125
- }
126
- return "";
127
- }
128
- export function reactionToEmoji(reaction) {
129
- if (reaction instanceof Api.ReactionEmoji)
130
- return reaction.emoticon;
131
- if (reaction instanceof Api.ReactionCustomEmoji)
132
- return `custom:${reaction.documentId.toString()}`;
133
- if (reaction instanceof Api.ReactionPaid)
134
- return "⭐";
135
- return null;
136
- }
137
- function absValue(v) {
138
- return { current: v?.current ?? 0, previous: v?.previous ?? 0 };
139
- }
140
- function compactGraph(g) {
141
- if (g instanceof Api.StatsGraphAsync)
142
- return { type: "async", token: g.token };
143
- if (g instanceof Api.StatsGraphError)
144
- return { type: "error", error: g.error };
145
- if (g instanceof Api.StatsGraph) {
146
- let parsed = g.json?.data;
147
- if (typeof parsed === "string") {
148
- try {
149
- parsed = JSON.parse(parsed);
150
- }
151
- catch {
152
- // leave raw string
153
- }
154
- }
155
- return { type: "data", data: parsed, zoomToken: g.zoomToken };
156
- }
157
- const any = g;
158
- if (typeof any.token === "string")
159
- return { type: "async", token: any.token };
160
- if (typeof any.error === "string")
161
- return { type: "error", error: any.error };
162
- return { type: "data", data: any.json?.data, zoomToken: any.zoomToken };
163
- }
164
- export function summarizeMegagroupStats(stats, includeGraphs) {
165
- const summary = {
166
- period: {
167
- minDate: stats.period?.minDate ?? 0,
168
- maxDate: stats.period?.maxDate ?? 0,
169
- },
170
- members: absValue(stats.members),
171
- messages: absValue(stats.messages),
172
- viewers: absValue(stats.viewers),
173
- posters: absValue(stats.posters),
174
- topPosters: (stats.topPosters ?? []).map((p) => ({
175
- userId: p.userId?.toString() ?? "",
176
- messages: p.messages,
177
- avgChars: p.avgChars,
178
- })),
179
- topAdmins: (stats.topAdmins ?? []).map((a) => ({
180
- userId: a.userId?.toString() ?? "",
181
- deleted: a.deleted,
182
- kicked: a.kicked,
183
- banned: a.banned,
184
- })),
185
- topInviters: (stats.topInviters ?? []).map((i) => ({
186
- userId: i.userId?.toString() ?? "",
187
- invitations: i.invitations,
188
- })),
189
- };
190
- if (includeGraphs) {
191
- summary.graphs = {
192
- growth: compactGraph(stats.growthGraph),
193
- members: compactGraph(stats.membersGraph),
194
- newMembersBySource: compactGraph(stats.newMembersBySourceGraph),
195
- languages: compactGraph(stats.languagesGraph),
196
- messages: compactGraph(stats.messagesGraph),
197
- actions: compactGraph(stats.actionsGraph),
198
- topHours: compactGraph(stats.topHoursGraph),
199
- weekdays: compactGraph(stats.weekdaysGraph),
200
- };
201
- }
202
- return summary;
203
- }
204
- export function summarizeBroadcastStats(stats, includeGraphs) {
205
- const enabled = stats.enabledNotifications;
206
- const part = enabled?.part ?? 0;
207
- const total = enabled?.total ?? 0;
208
- const percent = total > 0 ? (part / total) * 100 : 0;
209
- const summary = {
210
- period: {
211
- minDate: stats.period?.minDate ?? 0,
212
- maxDate: stats.period?.maxDate ?? 0,
213
- },
214
- followers: absValue(stats.followers),
215
- viewsPerPost: absValue(stats.viewsPerPost),
216
- sharesPerPost: absValue(stats.sharesPerPost),
217
- reactionsPerPost: absValue(stats.reactionsPerPost),
218
- viewsPerStory: absValue(stats.viewsPerStory),
219
- sharesPerStory: absValue(stats.sharesPerStory),
220
- reactionsPerStory: absValue(stats.reactionsPerStory),
221
- enabledNotifications: { part, total, percent },
222
- recentPostsInteractions: (stats.recentPostsInteractions ?? []).map((p) => {
223
- if (p instanceof Api.PostInteractionCountersStory) {
224
- return {
225
- kind: "story",
226
- storyId: p.storyId,
227
- views: p.views,
228
- forwards: p.forwards,
229
- reactions: p.reactions,
230
- };
231
- }
232
- const m = p;
233
- return {
234
- kind: "message",
235
- msgId: m.msgId,
236
- views: m.views,
237
- forwards: m.forwards,
238
- reactions: m.reactions,
239
- };
240
- }),
241
- };
242
- if (includeGraphs) {
243
- summary.graphs = {
244
- growth: compactGraph(stats.growthGraph),
245
- followers: compactGraph(stats.followersGraph),
246
- mute: compactGraph(stats.muteGraph),
247
- topHours: compactGraph(stats.topHoursGraph),
248
- interactions: compactGraph(stats.interactionsGraph),
249
- ivInteractions: compactGraph(stats.ivInteractionsGraph),
250
- viewsBySource: compactGraph(stats.viewsBySourceGraph),
251
- newFollowersBySource: compactGraph(stats.newFollowersBySourceGraph),
252
- languages: compactGraph(stats.languagesGraph),
253
- reactionsByEmotion: compactGraph(stats.reactionsByEmotionGraph),
254
- storyInteractions: compactGraph(stats.storyInteractionsGraph),
255
- storyReactionsByEmotion: compactGraph(stats.storyReactionsByEmotionGraph),
256
- };
257
- }
258
- return summary;
259
- }
260
- const BANNED_RIGHT_FLAGS = [
261
- "sendMessages",
262
- "sendMedia",
263
- "sendStickers",
264
- "sendGifs",
265
- "sendPolls",
266
- "sendInline",
267
- "embedLinks",
268
- "changeInfo",
269
- "inviteUsers",
270
- "pinMessages",
271
- ];
272
- // Newer granular flags not exposed in ChatPermissions input but must be preserved from currentRights
273
- const EXTRA_BANNED_RIGHT_FLAGS = [
274
- "sendGames",
275
- "manageTopics",
276
- "sendPhotos",
277
- "sendVideos",
278
- "sendRoundvideos",
279
- "sendAudios",
280
- "sendVoices",
281
- "sendDocs",
282
- "sendPlain",
283
- ];
284
- export function mergeBannedRights(current, permissions) {
285
- const result = {};
286
- for (const flag of BANNED_RIGHT_FLAGS) {
287
- const userValue = permissions[flag];
288
- if (userValue !== undefined) {
289
- result[flag] = !userValue;
290
- }
291
- else {
292
- result[flag] = Boolean(current?.[flag]);
293
- }
294
- }
295
- // Preserve newer granular flags from existing rights so they are not silently cleared
296
- for (const flag of EXTRA_BANNED_RIGHT_FLAGS) {
297
- result[flag] = Boolean(current?.[flag]);
298
- }
299
- return result;
300
- }
301
- export function describeKeyboardButton(button, row, col) {
302
- const base = {
303
- row,
304
- col,
305
- type: button.className,
306
- label: "text" in button && typeof button.text === "string" ? button.text : "",
307
- };
308
- if (button instanceof Api.KeyboardButtonCallback) {
309
- base.data = Buffer.from(button.data).toString("base64");
310
- if (button.requiresPassword)
311
- base.requiresPassword = true;
312
- return base;
313
- }
314
- if (button instanceof Api.KeyboardButtonUrl) {
315
- base.url = button.url;
316
- return base;
317
- }
318
- if (button instanceof Api.KeyboardButtonUrlAuth) {
319
- base.url = button.url;
320
- base.buttonId = button.buttonId;
321
- return base;
322
- }
323
- if (button instanceof Api.KeyboardButtonSwitchInline) {
324
- base.switchQuery = button.query;
325
- base.samePeer = Boolean(button.samePeer);
326
- return base;
327
- }
328
- if (button instanceof Api.KeyboardButtonWebView || button instanceof Api.KeyboardButtonSimpleWebView) {
329
- base.url = button.url;
330
- return base;
331
- }
332
- if (button instanceof Api.KeyboardButtonUserProfile) {
333
- base.userId = button.userId?.toString();
334
- return base;
335
- }
336
- if (button instanceof Api.KeyboardButtonRequestPoll) {
337
- if (button.quiz)
338
- base.quiz = true;
339
- return base;
340
- }
341
- if (button instanceof Api.KeyboardButtonRequestPeer) {
342
- base.buttonId = button.buttonId;
343
- return base;
344
- }
345
- if (button instanceof Api.KeyboardButtonCopy) {
346
- base.copyText = button.copyText;
347
- return base;
348
- }
349
- return base;
350
- }
351
- export function peerToCompact(peer) {
352
- if (!peer)
353
- return undefined;
354
- if (peer instanceof Api.PeerUser)
355
- return { kind: "user", id: peer.userId.toString() };
356
- if (peer instanceof Api.PeerChat)
357
- return { kind: "chat", id: peer.chatId.toString() };
358
- if (peer instanceof Api.PeerChannel)
359
- return { kind: "channel", id: peer.channelId.toString() };
360
- return undefined;
361
- }
362
- function summarizeMessageForUpdates(msg) {
363
- if (msg instanceof Api.MessageEmpty)
364
- return null;
365
- const peer = peerToCompact(msg.peerId);
366
- if (!peer)
367
- return null;
368
- const fromId = peerToCompact(msg.fromId);
369
- const date = msg.date ?? 0;
370
- if (msg instanceof Api.Message) {
371
- return { id: msg.id, peer, fromId, date, text: msg.message ?? "", isService: false };
372
- }
373
- if (msg instanceof Api.MessageService) {
374
- return {
375
- id: msg.id,
376
- peer,
377
- fromId,
378
- date,
379
- text: `[${msg.action?.className ?? "service"}]`,
380
- isService: true,
381
- };
382
- }
383
- return null;
384
- }
385
- function collectDeletedMessageIds(updates) {
386
- const out = [];
387
- for (const u of updates) {
388
- if (u instanceof Api.UpdateDeleteMessages) {
389
- out.push({ messageIds: u.messages });
390
- }
391
- else if (u instanceof Api.UpdateDeleteChannelMessages) {
392
- out.push({
393
- peer: { kind: "channel", id: u.channelId.toString() },
394
- messageIds: u.messages,
395
- });
396
- }
397
- }
398
- return out;
399
- }
400
- export function summarizeUpdatesDifference(diff, cursor) {
401
- if (diff instanceof Api.updates.DifferenceEmpty) {
402
- return {
403
- state: { pts: cursor.pts, qts: cursor.qts, date: diff.date, seq: diff.seq },
404
- isFinal: true,
405
- newMessages: [],
406
- deletedMessageIds: [],
407
- otherUpdates: [],
408
- };
409
- }
410
- if (diff instanceof Api.updates.DifferenceTooLong) {
411
- return {
412
- state: { pts: diff.pts, qts: cursor.qts, date: cursor.date, seq: 0 },
413
- isFinal: true,
414
- newMessages: [],
415
- deletedMessageIds: [],
416
- otherUpdates: [],
417
- fallback: {
418
- kind: "tooLong",
419
- suggestedAction: "gap too large — call telegram-read-messages per chat or telegram-get-state to resync",
420
- },
421
- };
422
- }
423
- const isFinal = diff instanceof Api.updates.Difference;
424
- const state = isFinal
425
- ? diff.state
426
- : diff.intermediateState;
427
- const newMessages = (diff.newMessages ?? [])
428
- .map(summarizeMessageForUpdates)
429
- .filter((m) => m !== null);
430
- const otherUpdates = diff.otherUpdates ?? [];
431
- return {
432
- state: {
433
- pts: state.pts,
434
- qts: state.qts,
435
- date: state.date,
436
- seq: state.seq,
437
- unreadCount: state.unreadCount,
438
- },
439
- isFinal,
440
- newMessages,
441
- deletedMessageIds: collectDeletedMessageIds(otherUpdates),
442
- otherUpdates: otherUpdates.map((u) => ({ type: u.className })),
443
- };
444
- }
445
- export function summarizeChannelDifference(diff, channelId, fallbackPts) {
446
- if (diff instanceof Api.updates.ChannelDifferenceEmpty) {
447
- return {
448
- channelId,
449
- pts: diff.pts,
450
- isFinal: Boolean(diff.final),
451
- timeout: diff.timeout,
452
- newMessages: [],
453
- otherUpdates: [],
454
- };
455
- }
456
- if (diff instanceof Api.updates.ChannelDifferenceTooLong) {
457
- const freshPts = diff.dialog instanceof Api.Dialog ? (diff.dialog.pts ?? fallbackPts) : fallbackPts;
458
- return {
459
- channelId,
460
- pts: freshPts,
461
- isFinal: Boolean(diff.final),
462
- timeout: diff.timeout,
463
- newMessages: (diff.messages ?? [])
464
- .map(summarizeMessageForUpdates)
465
- .filter((m) => m !== null),
466
- otherUpdates: [],
467
- fallback: {
468
- kind: "tooLong",
469
- suggestedAction: "channel gap too large — dialog snapshot returned; call telegram-read-messages for full history",
470
- },
471
- };
472
- }
473
- if (diff instanceof Api.updates.ChannelDifference) {
474
- return {
475
- channelId,
476
- pts: diff.pts,
477
- isFinal: Boolean(diff.final),
478
- timeout: diff.timeout,
479
- newMessages: (diff.newMessages ?? [])
480
- .map(summarizeMessageForUpdates)
481
- .filter((m) => m !== null),
482
- otherUpdates: (diff.otherUpdates ?? []).map((u) => ({ type: u.className })),
483
- };
484
- }
485
- return {
486
- channelId,
487
- pts: fallbackPts,
488
- isFinal: false,
489
- newMessages: [],
490
- otherUpdates: [],
491
- };
492
- }
493
- export function summarizeMyBoost(boost) {
494
- const b = boost;
495
- return {
496
- slot: b.slot,
497
- peer: peerToCompact(b.peer),
498
- date: b.date,
499
- expires: b.expires,
500
- cooldownUntilDate: b.cooldownUntilDate,
501
- };
502
- }
503
- export function summarizeMyBoosts(result) {
504
- const boosts = result.myBoosts ?? [];
505
- return {
506
- count: boosts.length,
507
- myBoosts: boosts.map(summarizeMyBoost),
508
- };
509
- }
510
- export function summarizePrepaidGiveaway(g) {
511
- if (g instanceof Api.PrepaidStarsGiveaway) {
512
- return {
513
- kind: "stars",
514
- id: g.id.toString(),
515
- quantity: g.quantity,
516
- date: g.date,
517
- stars: g.stars.toString(),
518
- boosts: g.boosts,
519
- };
520
- }
521
- const p = g;
522
- return {
523
- kind: "premium",
524
- id: p.id.toString(),
525
- quantity: p.quantity,
526
- date: p.date,
527
- months: p.months,
528
- };
529
- }
530
- export function summarizeBoostsStatus(result) {
531
- const r = result;
532
- const out = {
533
- level: r.level,
534
- boosts: r.boosts,
535
- currentLevelBoosts: r.currentLevelBoosts,
536
- nextLevelBoosts: r.nextLevelBoosts,
537
- giftBoosts: r.giftBoosts,
538
- boostUrl: r.boostUrl,
539
- myBoost: r.myBoost,
540
- myBoostSlots: r.myBoostSlots,
541
- };
542
- if (r.premiumAudience) {
543
- out.premiumAudience = { part: r.premiumAudience.part, total: r.premiumAudience.total };
544
- }
545
- if (r.prepaidGiveaways && r.prepaidGiveaways.length > 0) {
546
- out.prepaidGiveaways = r.prepaidGiveaways.map(summarizePrepaidGiveaway);
547
- }
548
- return out;
549
- }
550
- export function summarizeBoost(boost) {
551
- const b = boost;
552
- return {
553
- id: b.id,
554
- userId: b.userId?.toString(),
555
- date: b.date,
556
- expires: b.expires,
557
- gift: b.gift,
558
- giveaway: b.giveaway,
559
- unclaimed: b.unclaimed,
560
- giveawayMsgId: b.giveawayMsgId,
561
- usedGiftSlug: b.usedGiftSlug,
562
- multiplier: b.multiplier,
563
- stars: b.stars?.toString(),
564
- };
565
- }
566
- export function summarizeBoostsList(result) {
567
- const r = result;
568
- return {
569
- count: r.count,
570
- boosts: (r.boosts ?? []).map(summarizeBoost),
571
- nextOffset: r.nextOffset,
572
- };
573
- }
574
- export function summarizeBusinessChatLink(link) {
575
- const l = link;
576
- return {
577
- link: l.link,
578
- message: l.message,
579
- title: l.title,
580
- views: l.views,
581
- entityCount: l.entities?.length ?? 0,
582
- };
583
- }
584
- export function summarizeBusinessChatLinks(result) {
585
- const r = result;
586
- const links = r.links ?? [];
587
- return {
588
- count: links.length,
589
- links: links.map(summarizeBusinessChatLink),
590
- };
591
- }
592
- export function summarizeGroupCallInfo(call) {
593
- if (call instanceof Api.GroupCallDiscarded) {
594
- return {
595
- kind: "discarded",
596
- id: call.id.toString(),
597
- accessHash: call.accessHash.toString(),
598
- duration: call.duration,
599
- };
600
- }
601
- const c = call;
602
- return {
603
- kind: "active",
604
- id: c.id.toString(),
605
- accessHash: c.accessHash.toString(),
606
- participantsCount: c.participantsCount,
607
- title: c.title,
608
- scheduleDate: c.scheduleDate,
609
- recordStartDate: c.recordStartDate,
610
- streamDcId: c.streamDcId,
611
- unmutedVideoCount: c.unmutedVideoCount,
612
- unmutedVideoLimit: c.unmutedVideoLimit,
613
- version: c.version,
614
- joinMuted: c.joinMuted,
615
- canChangeJoinMuted: c.canChangeJoinMuted,
616
- joinDateAsc: c.joinDateAsc,
617
- scheduleStartSubscribed: c.scheduleStartSubscribed,
618
- canStartVideo: c.canStartVideo,
619
- recordVideoActive: c.recordVideoActive,
620
- rtmpStream: c.rtmpStream,
621
- listenersHidden: c.listenersHidden,
622
- };
623
- }
624
- export function summarizeGroupCallParticipant(p) {
625
- const gp = p;
626
- return {
627
- peer: peerToCompact(gp.peer),
628
- date: gp.date,
629
- activeDate: gp.activeDate,
630
- source: gp.source,
631
- volume: gp.volume,
632
- muted: gp.muted,
633
- left: gp.left,
634
- canSelfUnmute: gp.canSelfUnmute,
635
- justJoined: gp.justJoined,
636
- self: gp.self,
637
- mutedByYou: gp.mutedByYou,
638
- volumeByAdmin: gp.volumeByAdmin,
639
- videoJoined: gp.videoJoined,
640
- about: gp.about,
641
- raiseHandRating: gp.raiseHandRating?.toString(),
642
- hasVideo: gp.video ? true : undefined,
643
- hasPresentation: gp.presentation ? true : undefined,
644
- };
645
- }
646
- export function summarizeGroupCall(result) {
647
- const r = result;
648
- return {
649
- call: summarizeGroupCallInfo(r.call),
650
- participants: (r.participants ?? []).map(summarizeGroupCallParticipant),
651
- participantsNextOffset: r.participantsNextOffset || undefined,
652
- };
653
- }
654
- export function summarizeGroupCallParticipants(result) {
655
- const r = result;
656
- return {
657
- count: r.count,
658
- participants: (r.participants ?? []).map(summarizeGroupCallParticipant),
659
- nextOffset: r.nextOffset || undefined,
660
- version: r.version,
661
- };
662
- }
663
- export function summarizeStarsAmount(amount) {
664
- const a = amount;
665
- return { amount: a.amount.toString(), nanos: a.nanos };
666
- }
667
- export function summarizeStarsTransactionPeer(peer) {
668
- if (peer instanceof Api.StarsTransactionPeerAppStore)
669
- return { kind: "appStore" };
670
- if (peer instanceof Api.StarsTransactionPeerPlayMarket)
671
- return { kind: "playMarket" };
672
- if (peer instanceof Api.StarsTransactionPeerPremiumBot)
673
- return { kind: "premiumBot" };
674
- if (peer instanceof Api.StarsTransactionPeerFragment)
675
- return { kind: "fragment" };
676
- if (peer instanceof Api.StarsTransactionPeerAds)
677
- return { kind: "ads" };
678
- if (peer instanceof Api.StarsTransactionPeerAPI)
679
- return { kind: "api" };
680
- if (peer instanceof Api.StarsTransactionPeer)
681
- return { kind: "peer", peer: peerToCompact(peer.peer) };
682
- return { kind: "unsupported" };
683
- }
684
- export function summarizeStarsTransaction(tx) {
685
- const t = tx;
686
- return {
687
- id: t.id,
688
- stars: summarizeStarsAmount(t.stars),
689
- date: t.date,
690
- peer: summarizeStarsTransactionPeer(t.peer),
691
- refund: t.refund,
692
- pending: t.pending,
693
- failed: t.failed,
694
- gift: t.gift,
695
- reaction: t.reaction,
696
- title: t.title,
697
- description: t.description,
698
- msgId: t.msgId,
699
- subscriptionPeriod: t.subscriptionPeriod,
700
- giveawayPostId: t.giveawayPostId,
701
- transactionDate: t.transactionDate,
702
- transactionUrl: t.transactionUrl,
703
- };
704
- }
705
- export function summarizeStarsSubscription(sub) {
706
- const s = sub;
707
- const pricing = s.pricing;
708
- return {
709
- id: s.id,
710
- peer: peerToCompact(s.peer),
711
- untilDate: s.untilDate,
712
- pricing: { period: pricing.period, amount: pricing.amount.toString() },
713
- canceled: s.canceled,
714
- canRefulfill: s.canRefulfill,
715
- missingBalance: s.missingBalance,
716
- botCanceled: s.botCanceled,
717
- chatInviteHash: s.chatInviteHash,
718
- title: s.title,
719
- invoiceSlug: s.invoiceSlug,
720
- };
721
- }
722
- export function summarizeQuickReply(reply) {
723
- const r = reply;
724
- return {
725
- shortcutId: r.shortcutId,
726
- shortcut: r.shortcut,
727
- topMessage: r.topMessage,
728
- count: r.count,
729
- };
730
- }
731
- export function summarizeQuickReplies(result) {
732
- if (result instanceof Api.messages.QuickRepliesNotModified) {
733
- return { notModified: true };
734
- }
735
- const r = result;
736
- return { quickReplies: r.quickReplies.map(summarizeQuickReply) };
737
- }
738
- export function summarizeQuickReplyMessage(msg) {
739
- if (msg instanceof Api.MessageEmpty)
740
- return null;
741
- const base = msg;
742
- const fromId = peerToCompact(base.fromId);
743
- const replyHeader = base.replyTo;
744
- const replyToMsgId = replyHeader instanceof Api.MessageReplyHeader ? replyHeader.replyToMsgId : undefined;
745
- if (msg instanceof Api.Message) {
746
- return {
747
- id: msg.id,
748
- date: msg.date,
749
- text: msg.message ?? "",
750
- isService: false,
751
- fromId,
752
- replyToMsgId,
753
- };
754
- }
755
- if (msg instanceof Api.MessageService) {
756
- return {
757
- id: msg.id,
758
- date: msg.date,
759
- text: `[${msg.action?.className ?? "service"}]`,
760
- isService: true,
761
- fromId,
762
- };
763
- }
764
- return null;
765
- }
766
- export function summarizeQuickReplyMessages(result) {
767
- if (result instanceof Api.messages.MessagesNotModified) {
768
- return { notModified: true, count: result.count };
769
- }
770
- const rawMessages = result
771
- .messages;
772
- const messages = rawMessages.map(summarizeQuickReplyMessage).filter((m) => m !== null);
773
- const count = result instanceof Api.messages.Messages
774
- ? messages.length
775
- : result.count;
776
- return { count, messages };
777
- }
778
- export function summarizeStarsStatus(result) {
779
- const r = result;
780
- const out = {
781
- balance: summarizeStarsAmount(r.balance),
782
- subscriptionsNextOffset: r.subscriptionsNextOffset || undefined,
783
- subscriptionsMissingBalance: r.subscriptionsMissingBalance?.toString(),
784
- nextOffset: r.nextOffset || undefined,
785
- };
786
- if (r.subscriptions && r.subscriptions.length > 0) {
787
- out.subscriptions = r.subscriptions.map(summarizeStarsSubscription);
788
- }
789
- if (r.history && r.history.length > 0) {
790
- out.history = r.history.map(summarizeStarsTransaction);
791
- }
792
- return out;
793
- }
794
- export function summarizeStoryItem(item) {
795
- if (item instanceof Api.StoryItemDeleted) {
796
- return { id: item.id, kind: "deleted" };
797
- }
798
- if (item instanceof Api.StoryItemSkipped) {
799
- return {
800
- id: item.id,
801
- kind: "skipped",
802
- date: item.date,
803
- expireDate: item.expireDate,
804
- closeFriends: item.closeFriends,
805
- };
806
- }
807
- const story = item;
808
- return {
809
- id: story.id,
810
- kind: "active",
811
- date: story.date,
812
- expireDate: story.expireDate,
813
- caption: story.caption,
814
- mediaType: story.media?.className,
815
- pinned: story.pinned,
816
- public: story.public,
817
- closeFriends: story.closeFriends,
818
- edited: story.edited,
819
- noforwards: story.noforwards,
820
- fromId: peerToCompact(story.fromId),
821
- viewsCount: story.views?.viewsCount,
822
- reactionsCount: story.views?.reactionsCount,
823
- };
824
- }
825
- export function summarizePeerStories(ps) {
826
- const peer = peerToCompact(ps.peer);
827
- if (!peer)
828
- return null;
829
- return {
830
- peer,
831
- maxReadId: ps.maxReadId,
832
- stories: (ps.stories ?? []).map(summarizeStoryItem),
833
- };
834
- }
835
- export function summarizeStoriesById(result) {
836
- return {
837
- count: result.count,
838
- stories: (result.stories ?? []).map(summarizeStoryItem),
839
- pinnedToTop: result.pinnedToTop,
840
- };
841
- }
842
- export function summarizeStoryView(view) {
843
- if (view instanceof Api.StoryViewPublicForward) {
844
- const msg = view.message;
845
- const messageId = msg instanceof Api.MessageEmpty ? undefined : msg?.id;
846
- const peer = msg instanceof Api.MessageEmpty
847
- ? undefined
848
- : peerToCompact(msg?.peerId);
849
- return {
850
- kind: "publicForward",
851
- messageId,
852
- peer,
853
- blocked: view.blocked,
854
- blockedMyStoriesFrom: view.blockedMyStoriesFrom,
855
- };
856
- }
857
- if (view instanceof Api.StoryViewPublicRepost) {
858
- const story = view.story;
859
- return {
860
- kind: "publicRepost",
861
- peer: peerToCompact(view.peerId),
862
- storyId: story?.id,
863
- blocked: view.blocked,
864
- blockedMyStoriesFrom: view.blockedMyStoriesFrom,
865
- };
866
- }
867
- const v = view;
868
- return {
869
- kind: "user",
870
- userId: v.userId.toString(),
871
- date: v.date,
872
- reaction: v.reaction ? reactionToEmoji(v.reaction) : undefined,
873
- blocked: v.blocked,
874
- blockedMyStoriesFrom: v.blockedMyStoriesFrom,
875
- };
876
- }
877
- export function summarizeStoryViewsList(result) {
878
- const list = result;
879
- return {
880
- count: list.count,
881
- viewsCount: list.viewsCount,
882
- forwardsCount: list.forwardsCount,
883
- reactionsCount: list.reactionsCount,
884
- views: (list.views ?? []).map(summarizeStoryView),
885
- nextOffset: list.nextOffset,
886
- };
887
- }
888
- export function summarizeAllStories(result) {
889
- const stealthMode = result.stealthMode
890
- ? {
891
- activeUntilDate: result.stealthMode.activeUntilDate,
892
- cooldownUntilDate: result.stealthMode.cooldownUntilDate,
893
- }
894
- : undefined;
895
- if (result instanceof Api.stories.AllStoriesNotModified) {
896
- return {
897
- modified: false,
898
- state: result.state,
899
- peerStories: [],
900
- stealthMode,
901
- };
902
- }
903
- const all = result;
904
- const peerStories = (all.peerStories ?? [])
905
- .map(summarizePeerStories)
906
- .filter((p) => p !== null);
907
- return {
908
- modified: true,
909
- state: all.state,
910
- hasMore: all.hasMore,
911
- count: all.count,
912
- peerStories,
913
- stealthMode,
914
- };
915
- }
916
49
  export class TelegramService {
917
50
  client = null;
918
51
  apiId;
@@ -922,6 +55,7 @@ export class TelegramService {
922
55
  sessionPath;
923
56
  rateLimiter = new RateLimiter();
924
57
  lastTypingAt = new Map();
58
+ entityCache = new Map();
925
59
  lastError = "";
926
60
  get sessionDir() {
927
61
  return dirname(this.sessionPath);
@@ -1038,6 +172,7 @@ export class TelegramService {
1038
172
  this.connected = false;
1039
173
  this.sessionString = "";
1040
174
  this.client = null;
175
+ this.entityCache.clear();
1041
176
  if (existsSync(this.sessionPath)) {
1042
177
  await unlink(this.sessionPath);
1043
178
  }
@@ -1055,6 +190,7 @@ export class TelegramService {
1055
190
  await this.client.destroy();
1056
191
  this.connected = false;
1057
192
  this.client = null;
193
+ this.entityCache.clear();
1058
194
  }
1059
195
  }
1060
196
  /**
@@ -1654,13 +790,17 @@ export class TelegramService {
1654
790
  * Resolve a chat by ID, username, or display name.
1655
791
  * Falls back to searching user's dialogs if getEntity() fails.
1656
792
  */
1657
- // biome-ignore lint: GramJS has no proper entity union type
1658
793
  async resolveChat(chatId) {
1659
794
  if (!this.client)
1660
795
  throw new Error(NOT_CONNECTED_ERROR);
796
+ const cached = this.entityCache.get(chatId);
797
+ if (cached)
798
+ return cached;
1661
799
  // First try direct resolve (numeric ID, username, phone)
1662
800
  try {
1663
- return await this.client.getEntity(chatId);
801
+ const entity = await this.client.getEntity(chatId);
802
+ this.entityCache.set(chatId, entity);
803
+ return entity;
1664
804
  }
1665
805
  catch {
1666
806
  // Fall through to dialog search
@@ -1670,12 +810,16 @@ export class TelegramService {
1670
810
  const query = chatId.toLowerCase();
1671
811
  // Exact match first
1672
812
  const exact = dialogs.find((d) => d.title?.toLowerCase() === query);
1673
- if (exact?.entity)
813
+ if (exact?.entity) {
814
+ this.entityCache.set(chatId, exact.entity);
1674
815
  return exact.entity;
816
+ }
1675
817
  // Partial match
1676
818
  const partial = dialogs.filter((d) => d.title?.toLowerCase().includes(query));
1677
- if (partial.length === 1 && partial[0].entity)
819
+ if (partial.length === 1 && partial[0].entity) {
820
+ this.entityCache.set(chatId, partial[0].entity);
1678
821
  return partial[0].entity;
822
+ }
1679
823
  if (partial.length > 1) {
1680
824
  const matches = partial.map((d) => ` ${d.title} (${d.entity?.id?.toString() ?? "?"})`).join("\n");
1681
825
  throw new Error(`Multiple chats match "${chatId}". Use the numeric ID instead:\n${matches}`);
@@ -1686,7 +830,6 @@ export class TelegramService {
1686
830
  * Resolve chatId to a peer string that GramJS methods accept.
1687
831
  * Handles display names by searching dialogs.
1688
832
  */
1689
- // biome-ignore lint: GramJS has no proper entity union type
1690
833
  async resolvePeer(chatId) {
1691
834
  // Normalize '@me' — GramJS only intercepts the plain 'me' string as InputPeerSelf
1692
835
  if (chatId === "@me")