@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
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { chmod, readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
@@ -44,6 +44,875 @@ function ensureSessionDir(filePath) {
44
44
  mkdirSync(dir, { recursive: true, mode: 0o700 });
45
45
  }
46
46
  }
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
+ }
47
916
  export class TelegramService {
48
917
  client = null;
49
918
  apiId;
@@ -52,6 +921,7 @@ export class TelegramService {
52
921
  connected = false;
53
922
  sessionPath;
54
923
  rateLimiter = new RateLimiter();
924
+ lastTypingAt = new Map();
55
925
  lastError = "";
56
926
  get sessionDir() {
57
927
  return dirname(this.sessionPath);
@@ -311,24 +1181,12 @@ export class TelegramService {
311
1181
  return this.rateLimiter.execute(async () => {
312
1182
  const resolved = await this.resolvePeer(chatId);
313
1183
  if (topicId) {
314
- const peer = await this.client?.getInputEntity(resolved);
315
- const result = await this.client?.invoke(new Api.messages.SendMessage({
316
- peer,
1184
+ return await this.client?.sendMessage(resolved, {
317
1185
  message: text,
318
- randomId: bigInt(Math.floor(Math.random() * 1e15)),
319
- replyTo: new Api.InputReplyToMessage({
320
- replyToMsgId: replyTo ?? topicId,
321
- topMsgId: topicId,
322
- }),
323
- }));
324
- if (result instanceof Api.UpdateShortSentMessage)
325
- return result;
326
- if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
327
- const msgUpdate = result.updates.find((u) => u instanceof Api.UpdateNewMessage);
328
- if (msgUpdate?.message instanceof Api.Message)
329
- return msgUpdate.message;
330
- }
331
- return undefined;
1186
+ topMsgId: topicId,
1187
+ ...(replyTo ? { replyTo } : {}),
1188
+ ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
1189
+ });
332
1190
  }
333
1191
  return await this.client?.sendMessage(resolved, {
334
1192
  message: text,
@@ -386,7 +1244,14 @@ export class TelegramService {
386
1244
  return "image/png";
387
1245
  if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
388
1246
  return "image/gif";
389
- if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
1247
+ if (buffer[0] === 0x52 &&
1248
+ buffer[1] === 0x49 &&
1249
+ buffer[2] === 0x46 &&
1250
+ buffer[3] === 0x46 &&
1251
+ buffer[8] === 0x57 &&
1252
+ buffer[9] === 0x45 &&
1253
+ buffer[10] === 0x42 &&
1254
+ buffer[11] === 0x50)
390
1255
  return "image/webp";
391
1256
  // Fall back to document mimeType
392
1257
  const m = media;
@@ -526,28 +1391,6 @@ export class TelegramService {
526
1391
  throw new Error(NOT_CONNECTED_ERROR);
527
1392
  await this.client.markAsRead(chatId);
528
1393
  }
529
- static TYPING_ACTIONS = {
530
- typing: () => new Api.SendMessageTypingAction(),
531
- cancel: () => new Api.SendMessageCancelAction(),
532
- record_video: () => new Api.SendMessageRecordVideoAction(),
533
- upload_video: () => new Api.SendMessageUploadVideoAction({ progress: 0 }),
534
- record_audio: () => new Api.SendMessageRecordAudioAction(),
535
- upload_audio: () => new Api.SendMessageUploadAudioAction({ progress: 0 }),
536
- upload_photo: () => new Api.SendMessageUploadPhotoAction({ progress: 0 }),
537
- upload_document: () => new Api.SendMessageUploadDocumentAction({ progress: 0 }),
538
- choose_sticker: () => new Api.SendMessageChooseStickerAction(),
539
- game_play: () => new Api.SendMessageGamePlayAction(),
540
- };
541
- async setTyping(chatId, action = "typing") {
542
- if (!this.client || !this.connected)
543
- throw new Error(NOT_CONNECTED_ERROR);
544
- const factory = TelegramService.TYPING_ACTIONS[action];
545
- if (!factory)
546
- throw new Error(`Unknown typing action: ${action}. Valid: ${Object.keys(TelegramService.TYPING_ACTIONS).join(", ")}`);
547
- const resolved = await this.resolvePeer(chatId);
548
- const peer = await this.client.getInputEntity(resolved);
549
- await this.client.invoke(new Api.messages.SetTyping({ peer, action: factory() }));
550
- }
551
1394
  async getMessageById(chatId, messageId) {
552
1395
  if (!this.client || !this.connected)
553
1396
  throw new Error(NOT_CONNECTED_ERROR);
@@ -588,59 +1431,281 @@ export class TelegramService {
588
1431
  await this.client?.deleteMessages(resolved, messageIds, { revoke: true });
589
1432
  }, `deleteMessages in ${chatId}`);
590
1433
  }
591
- /**
592
- * Resolve a chat by ID, username, or display name.
593
- * Falls back to searching user's dialogs if getEntity() fails.
594
- */
595
- // biome-ignore lint: GramJS has no proper entity union type
596
- async resolveChat(chatId) {
597
- if (!this.client)
1434
+ async getScheduledMessages(chatId) {
1435
+ if (!this.client || !this.connected)
598
1436
  throw new Error(NOT_CONNECTED_ERROR);
599
- // First try direct resolve (numeric ID, username, phone)
600
- try {
601
- return await this.client.getEntity(chatId);
602
- }
603
- catch {
604
- // Fall through to dialog search
605
- }
606
- // Search dialogs by display name
607
- const dialogs = await this.client.getDialogs({ limit: 100 });
608
- const query = chatId.toLowerCase();
609
- // Exact match first
610
- const exact = dialogs.find((d) => d.title?.toLowerCase() === query);
611
- if (exact?.entity)
612
- return exact.entity;
613
- // Partial match
614
- const partial = dialogs.filter((d) => d.title?.toLowerCase().includes(query));
615
- if (partial.length === 1 && partial[0].entity)
616
- return partial[0].entity;
617
- if (partial.length > 1) {
618
- const matches = partial.map((d) => ` ${d.title} (${d.entity?.id?.toString() ?? "?"})`).join("\n");
619
- throw new Error(`Multiple chats match "${chatId}". Use the numeric ID instead:\n${matches}`);
620
- }
621
- throw new Error(`Cannot find chat "${chatId}". Use a numeric ID, @username, or run telegram-search-chats to find it.`);
1437
+ return this.rateLimiter.execute(async () => {
1438
+ const resolved = await this.resolvePeer(chatId);
1439
+ const peer = await this.client?.getInputEntity(resolved);
1440
+ if (!peer)
1441
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1442
+ const result = await this.client?.invoke(new Api.messages.GetScheduledHistory({ peer, hash: bigInt(0) }));
1443
+ if (!result || result instanceof Api.messages.MessagesNotModified)
1444
+ return [];
1445
+ const messages = result
1446
+ .messages;
1447
+ return messages
1448
+ .filter((m) => m instanceof Api.Message)
1449
+ .map((m) => ({
1450
+ id: m.id,
1451
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
1452
+ text: m.message ?? "",
1453
+ media: this.extractMediaInfo(m.media),
1454
+ }));
1455
+ }, `getScheduledMessages in ${chatId}`);
622
1456
  }
623
- /**
624
- * Resolve chatId to a peer string that GramJS methods accept.
625
- * Handles display names by searching dialogs.
626
- */
627
- // biome-ignore lint: GramJS has no proper entity union type
628
- async resolvePeer(chatId) {
629
- // Numeric IDs and @usernames work directly
630
- if (/^-?\d+$/.test(chatId) || chatId.startsWith("@"))
631
- return chatId;
632
- // Everything else — resolve via dialogs
633
- return this.resolveChat(chatId);
1457
+ async deleteScheduledMessages(chatId, messageIds) {
1458
+ if (!this.client || !this.connected)
1459
+ throw new Error(NOT_CONNECTED_ERROR);
1460
+ await this.rateLimiter.execute(async () => {
1461
+ const resolved = await this.resolvePeer(chatId);
1462
+ const peer = await this.client?.getInputEntity(resolved);
1463
+ if (!peer)
1464
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1465
+ await this.client?.invoke(new Api.messages.DeleteScheduledMessages({ peer, id: messageIds }));
1466
+ }, `deleteScheduledMessages in ${chatId}`);
634
1467
  }
635
- async getChatInfo(chatId) {
1468
+ async getReplies(chatId, messageId, limit = 20) {
636
1469
  if (!this.client || !this.connected)
637
1470
  throw new Error(NOT_CONNECTED_ERROR);
638
- const entity = await this.resolveChat(chatId);
639
- if (entity instanceof Api.User) {
640
- const parts = [entity.firstName, entity.lastName].filter(Boolean);
641
- return {
642
- id: entity.id.toString(),
643
- name: parts.join(" ") || "Unknown",
1471
+ return this.rateLimiter.execute(async () => {
1472
+ const resolved = await this.resolvePeer(chatId);
1473
+ const peer = await this.client?.getInputEntity(resolved);
1474
+ if (!peer)
1475
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1476
+ const result = await this.client?.invoke(new Api.messages.GetReplies({ peer, msgId: messageId, limit, hash: bigInt(0) }));
1477
+ if (!result || result instanceof Api.messages.MessagesNotModified)
1478
+ return [];
1479
+ const messages = result
1480
+ .messages;
1481
+ return Promise.all(messages
1482
+ .filter((m) => m instanceof Api.Message)
1483
+ .map(async (m) => ({
1484
+ id: m.id,
1485
+ text: m.message ?? "",
1486
+ sender: await this.resolveSenderName(m.senderId),
1487
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
1488
+ media: this.extractMediaInfo(m.media),
1489
+ reactions: this.extractReactions(m.reactions),
1490
+ })));
1491
+ }, `getReplies for ${messageId} in ${chatId}`);
1492
+ }
1493
+ async getMessageLink(chatId, messageId, thread = false) {
1494
+ if (!this.client || !this.connected)
1495
+ throw new Error(NOT_CONNECTED_ERROR);
1496
+ return this.rateLimiter.execute(async () => {
1497
+ const entity = await this.resolveChat(chatId);
1498
+ if (!(entity instanceof Api.Channel)) {
1499
+ throw new Error("Message links are only available for channels and supergroups");
1500
+ }
1501
+ const result = await this.client?.invoke(new Api.channels.ExportMessageLink({ channel: entity, id: messageId, thread }));
1502
+ if (!result)
1503
+ throw new Error("Failed to export message link");
1504
+ return result.link;
1505
+ }, `getMessageLink for ${messageId} in ${chatId}`);
1506
+ }
1507
+ async getUnreadMentions(chatId, limit = 20) {
1508
+ if (!this.client || !this.connected)
1509
+ throw new Error(NOT_CONNECTED_ERROR);
1510
+ return this.rateLimiter.execute(async () => {
1511
+ const resolved = await this.resolvePeer(chatId);
1512
+ const peer = await this.client?.getInputEntity(resolved);
1513
+ if (!peer)
1514
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1515
+ const result = await this.client?.invoke(new Api.messages.GetUnreadMentions({
1516
+ peer,
1517
+ offsetId: 0,
1518
+ addOffset: 0,
1519
+ limit,
1520
+ maxId: 0,
1521
+ minId: 0,
1522
+ }));
1523
+ if (!result || result instanceof Api.messages.MessagesNotModified)
1524
+ return [];
1525
+ const typedResult = result;
1526
+ const messages = typedResult.messages;
1527
+ const items = await Promise.all(messages
1528
+ .filter((m) => m instanceof Api.Message)
1529
+ .map(async (m) => ({
1530
+ id: m.id,
1531
+ text: m.message ?? "",
1532
+ sender: await this.resolveSenderName(m.senderId),
1533
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
1534
+ media: this.extractMediaInfo(m.media),
1535
+ reactions: this.extractReactions(m.reactions),
1536
+ })));
1537
+ // Only mark all as read when we received the complete set; if truncated, marking all
1538
+ // would silently clear mentions the caller hasn't seen yet.
1539
+ const totalCount = "count" in typedResult ? typedResult.count : items.length;
1540
+ if (items.length > 0 && items.length >= totalCount) {
1541
+ try {
1542
+ await this.client?.invoke(new Api.messages.ReadMentions({ peer }));
1543
+ }
1544
+ catch {
1545
+ // best-effort; don't discard fetched items on mark-read failure
1546
+ }
1547
+ }
1548
+ return items;
1549
+ }, `getUnreadMentions in ${chatId}`);
1550
+ }
1551
+ async getUnreadReactions(chatId, limit = 20) {
1552
+ if (!this.client || !this.connected)
1553
+ throw new Error(NOT_CONNECTED_ERROR);
1554
+ return this.rateLimiter.execute(async () => {
1555
+ const resolved = await this.resolvePeer(chatId);
1556
+ const peer = await this.client?.getInputEntity(resolved);
1557
+ if (!peer)
1558
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1559
+ const result = await this.client?.invoke(new Api.messages.GetUnreadReactions({
1560
+ peer,
1561
+ offsetId: 0,
1562
+ addOffset: 0,
1563
+ limit,
1564
+ maxId: 0,
1565
+ minId: 0,
1566
+ }));
1567
+ if (!result || result instanceof Api.messages.MessagesNotModified)
1568
+ return [];
1569
+ const typedResult = result;
1570
+ const messages = typedResult.messages;
1571
+ const items = await Promise.all(messages
1572
+ .filter((m) => m instanceof Api.Message)
1573
+ .map(async (m) => ({
1574
+ id: m.id,
1575
+ text: m.message ?? "",
1576
+ sender: await this.resolveSenderName(m.senderId),
1577
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
1578
+ media: this.extractMediaInfo(m.media),
1579
+ reactions: this.extractReactions(m.reactions),
1580
+ })));
1581
+ // Only mark all as read when we received the complete set; if truncated, marking all
1582
+ // would silently clear reactions the caller hasn't seen yet.
1583
+ const totalCount = "count" in typedResult ? typedResult.count : items.length;
1584
+ if (items.length > 0 && items.length >= totalCount) {
1585
+ try {
1586
+ await this.client?.invoke(new Api.messages.ReadReactions({ peer }));
1587
+ }
1588
+ catch {
1589
+ // best-effort; don't discard fetched items on mark-read failure
1590
+ }
1591
+ }
1592
+ return items;
1593
+ }, `getUnreadReactions in ${chatId}`);
1594
+ }
1595
+ async translateText(chatId, messageIds, toLang) {
1596
+ if (!this.client || !this.connected)
1597
+ throw new Error(NOT_CONNECTED_ERROR);
1598
+ return this.rateLimiter.execute(async () => {
1599
+ const resolved = await this.resolvePeer(chatId);
1600
+ const peer = await this.client?.getInputEntity(resolved);
1601
+ if (!peer)
1602
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1603
+ const result = await this.client?.invoke(new Api.messages.TranslateText({ peer, id: messageIds, toLang }));
1604
+ if (!result)
1605
+ return [];
1606
+ return result.result.map((t) => (t instanceof Api.TextWithEntities ? t.text : ""));
1607
+ }, `translateText in ${chatId}`);
1608
+ }
1609
+ async sendTyping(chatId, action = "typing") {
1610
+ if (!this.client || !this.connected)
1611
+ throw new Error(NOT_CONNECTED_ERROR);
1612
+ return this.rateLimiter.execute(async () => {
1613
+ let stamped = false;
1614
+ if (action !== "cancel") {
1615
+ const now = Date.now();
1616
+ const last = this.lastTypingAt.get(chatId) ?? 0;
1617
+ if (now - last < 10_000)
1618
+ return;
1619
+ this.lastTypingAt.set(chatId, now);
1620
+ stamped = true;
1621
+ }
1622
+ try {
1623
+ const resolved = await this.resolvePeer(chatId);
1624
+ const peer = await this.client?.getInputEntity(resolved);
1625
+ if (!peer)
1626
+ throw new Error(`Cannot resolve peer for ${chatId}`);
1627
+ let sendAction;
1628
+ switch (action) {
1629
+ case "cancel":
1630
+ sendAction = new Api.SendMessageCancelAction();
1631
+ break;
1632
+ case "upload_photo":
1633
+ sendAction = new Api.SendMessageUploadPhotoAction({ progress: 0 });
1634
+ break;
1635
+ case "upload_document":
1636
+ sendAction = new Api.SendMessageUploadDocumentAction({ progress: 0 });
1637
+ break;
1638
+ default:
1639
+ sendAction = new Api.SendMessageTypingAction();
1640
+ }
1641
+ await this.client?.invoke(new Api.messages.SetTyping({ peer, action: sendAction }));
1642
+ if (action === "cancel") {
1643
+ this.lastTypingAt.delete(chatId);
1644
+ }
1645
+ }
1646
+ catch (err) {
1647
+ if (stamped)
1648
+ this.lastTypingAt.delete(chatId);
1649
+ throw err;
1650
+ }
1651
+ }, `sendTyping in ${chatId}`);
1652
+ }
1653
+ /**
1654
+ * Resolve a chat by ID, username, or display name.
1655
+ * Falls back to searching user's dialogs if getEntity() fails.
1656
+ */
1657
+ // biome-ignore lint: GramJS has no proper entity union type
1658
+ async resolveChat(chatId) {
1659
+ if (!this.client)
1660
+ throw new Error(NOT_CONNECTED_ERROR);
1661
+ // First try direct resolve (numeric ID, username, phone)
1662
+ try {
1663
+ return await this.client.getEntity(chatId);
1664
+ }
1665
+ catch {
1666
+ // Fall through to dialog search
1667
+ }
1668
+ // Search dialogs by display name
1669
+ const dialogs = await this.client.getDialogs({ limit: 100 });
1670
+ const query = chatId.toLowerCase();
1671
+ // Exact match first
1672
+ const exact = dialogs.find((d) => d.title?.toLowerCase() === query);
1673
+ if (exact?.entity)
1674
+ return exact.entity;
1675
+ // Partial match
1676
+ const partial = dialogs.filter((d) => d.title?.toLowerCase().includes(query));
1677
+ if (partial.length === 1 && partial[0].entity)
1678
+ return partial[0].entity;
1679
+ if (partial.length > 1) {
1680
+ const matches = partial.map((d) => ` ${d.title} (${d.entity?.id?.toString() ?? "?"})`).join("\n");
1681
+ throw new Error(`Multiple chats match "${chatId}". Use the numeric ID instead:\n${matches}`);
1682
+ }
1683
+ throw new Error(`Cannot find chat "${chatId}". Use a numeric ID, @username, or run telegram-search-chats to find it.`);
1684
+ }
1685
+ /**
1686
+ * Resolve chatId to a peer string that GramJS methods accept.
1687
+ * Handles display names by searching dialogs.
1688
+ */
1689
+ // biome-ignore lint: GramJS has no proper entity union type
1690
+ async resolvePeer(chatId) {
1691
+ // Normalize '@me' — GramJS only intercepts the plain 'me' string as InputPeerSelf
1692
+ if (chatId === "@me")
1693
+ return "me";
1694
+ // Numeric IDs and @usernames work directly
1695
+ if (/^-?\d+$/.test(chatId) || chatId.startsWith("@"))
1696
+ return chatId;
1697
+ // Everything else — resolve via dialogs
1698
+ return this.resolveChat(chatId);
1699
+ }
1700
+ async getChatInfo(chatId) {
1701
+ if (!this.client || !this.connected)
1702
+ throw new Error(NOT_CONNECTED_ERROR);
1703
+ const entity = await this.resolveChat(chatId);
1704
+ if (entity instanceof Api.User) {
1705
+ const parts = [entity.firstName, entity.lastName].filter(Boolean);
1706
+ return {
1707
+ id: entity.id.toString(),
1708
+ name: parts.join(" ") || "Unknown",
644
1709
  type: "private",
645
1710
  username: entity.username ?? undefined,
646
1711
  isBot: Boolean(entity.bot),
@@ -1118,7 +2183,14 @@ export class TelegramService {
1118
2183
  return "image/png";
1119
2184
  if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
1120
2185
  return "image/gif";
1121
- if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
2186
+ if (buffer[0] === 0x52 &&
2187
+ buffer[1] === 0x49 &&
2188
+ buffer[2] === 0x46 &&
2189
+ buffer[3] === 0x46 &&
2190
+ buffer[8] === 0x57 &&
2191
+ buffer[9] === 0x45 &&
2192
+ buffer[10] === 0x42 &&
2193
+ buffer[11] === 0x50)
1122
2194
  return "image/webp";
1123
2195
  return "image/jpeg"; // Telegram profile photos are almost always JPEG
1124
2196
  }
@@ -1225,7 +2297,7 @@ export class TelegramService {
1225
2297
  for (const r of list.reactions) {
1226
2298
  const userId = r.peerId instanceof Api.PeerUser ? r.peerId.userId.toString() : "";
1227
2299
  if (userId) {
1228
- const name = await this.resolveSenderName(bigInt(Number.parseInt(userId, 10)));
2300
+ const name = await this.resolveSenderName(bigInt(userId));
1229
2301
  users.push({ id: userId, name });
1230
2302
  }
1231
2303
  }
@@ -1240,6 +2312,47 @@ export class TelegramService {
1240
2312
  const total = reactionsOut.reduce((sum, r) => sum + r.count, 0);
1241
2313
  return { reactions: reactionsOut, total };
1242
2314
  }
2315
+ async setDefaultReaction(emoji) {
2316
+ if (!this.client || !this.connected)
2317
+ throw new Error(NOT_CONNECTED_ERROR);
2318
+ await this.rateLimiter.execute(async () => {
2319
+ await this.client?.invoke(new Api.messages.SetDefaultReaction({
2320
+ reaction: new Api.ReactionEmoji({ emoticon: emoji }),
2321
+ }));
2322
+ }, `setDefaultReaction ${emoji}`);
2323
+ }
2324
+ async getTopReactions(limit) {
2325
+ if (!this.client || !this.connected)
2326
+ throw new Error(NOT_CONNECTED_ERROR);
2327
+ return this.rateLimiter.execute(async () => {
2328
+ const result = await this.client?.invoke(new Api.messages.GetTopReactions({ limit, hash: bigInt(0) }));
2329
+ if (!result || result instanceof Api.messages.ReactionsNotModified)
2330
+ return [];
2331
+ const out = [];
2332
+ for (const r of result.reactions) {
2333
+ const emoji = reactionToEmoji(r);
2334
+ if (emoji)
2335
+ out.push({ emoji });
2336
+ }
2337
+ return out;
2338
+ }, "getTopReactions");
2339
+ }
2340
+ async getRecentReactions(limit) {
2341
+ if (!this.client || !this.connected)
2342
+ throw new Error(NOT_CONNECTED_ERROR);
2343
+ return this.rateLimiter.execute(async () => {
2344
+ const result = await this.client?.invoke(new Api.messages.GetRecentReactions({ limit, hash: bigInt(0) }));
2345
+ if (!result || result instanceof Api.messages.ReactionsNotModified)
2346
+ return [];
2347
+ const out = [];
2348
+ for (const r of result.reactions) {
2349
+ const emoji = reactionToEmoji(r);
2350
+ if (emoji)
2351
+ out.push({ emoji });
2352
+ }
2353
+ return out;
2354
+ }, "getRecentReactions");
2355
+ }
1243
2356
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
1244
2357
  if (!this.client || !this.connected)
1245
2358
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1319,7 +2432,8 @@ export class TelegramService {
1319
2432
  async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
1320
2433
  if (!this.client || !this.connected)
1321
2434
  throw new Error(NOT_CONNECTED_ERROR);
1322
- const peer = await this.client.getInputEntity(chatId);
2435
+ const resolved = await this.resolvePeer(chatId);
2436
+ const peer = await this.client.getInputEntity(resolved);
1323
2437
  const result = await this.client.invoke(new Api.messages.GetReplies({
1324
2438
  peer,
1325
2439
  msgId: topicId,
@@ -1376,10 +2490,11 @@ export class TelegramService {
1376
2490
  // Public channel/group by username
1377
2491
  const username = target.replace(/^@/, "").replace(/^https?:\/\/t\.me\//, "");
1378
2492
  const entity = await this.client.getEntity(username);
1379
- if (entity instanceof Api.Channel || entity instanceof Api.Chat) {
1380
- await this.client.invoke(new Api.channels.JoinChannel({
1381
- channel: entity,
1382
- }));
2493
+ if (entity instanceof Api.Chat) {
2494
+ throw new Error("Basic groups cannot be joined by username; use an invite link instead.");
2495
+ }
2496
+ if (entity instanceof Api.Channel) {
2497
+ await this.client.invoke(new Api.channels.JoinChannel({ channel: entity }));
1383
2498
  return {
1384
2499
  id: entity.id.toString(),
1385
2500
  title: entity.title ?? "Unknown",
@@ -1563,7 +2678,7 @@ export class TelegramService {
1563
2678
  await this.client.invoke(new Api.messages.EditChatAbout({ peer: entity, about: options.description }));
1564
2679
  }
1565
2680
  if (options.photoPath) {
1566
- const fileData = readFileSync(options.photoPath);
2681
+ const fileData = await readFile(options.photoPath);
1567
2682
  const uploaded = await this.client.uploadFile({
1568
2683
  file: new CustomFile(options.photoPath, fileData.length, options.photoPath, fileData),
1569
2684
  workers: 1,
@@ -1654,6 +2769,571 @@ export class TelegramService {
1654
2769
  settings: new Api.InputPeerNotifySettings({ muteUntil }),
1655
2770
  }));
1656
2771
  }
2772
+ async archiveChat(chatId, archive) {
2773
+ if (!this.client || !this.connected)
2774
+ throw new Error(NOT_CONNECTED_ERROR);
2775
+ return this.rateLimiter.execute(async () => {
2776
+ const resolved = await this.resolvePeer(chatId);
2777
+ const peer = await this.client?.getInputEntity(resolved);
2778
+ if (!peer)
2779
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2780
+ await this.client?.invoke(new Api.folders.EditPeerFolders({
2781
+ folderPeers: [new Api.InputFolderPeer({ peer, folderId: archive ? 1 : 0 })],
2782
+ }));
2783
+ }, `archiveChat ${chatId}`);
2784
+ }
2785
+ async pinDialog(chatId, pin) {
2786
+ if (!this.client || !this.connected)
2787
+ throw new Error(NOT_CONNECTED_ERROR);
2788
+ return this.rateLimiter.execute(async () => {
2789
+ const resolved = await this.resolvePeer(chatId);
2790
+ const peer = await this.client?.getInputEntity(resolved);
2791
+ if (!peer)
2792
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2793
+ await this.client?.invoke(new Api.messages.ToggleDialogPin({
2794
+ peer: new Api.InputDialogPeer({ peer }),
2795
+ pinned: pin,
2796
+ }));
2797
+ }, `pinDialog ${chatId}`);
2798
+ }
2799
+ async markDialogUnread(chatId, unread) {
2800
+ if (!this.client || !this.connected)
2801
+ throw new Error(NOT_CONNECTED_ERROR);
2802
+ return this.rateLimiter.execute(async () => {
2803
+ const resolved = await this.resolvePeer(chatId);
2804
+ const peer = await this.client?.getInputEntity(resolved);
2805
+ if (!peer)
2806
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2807
+ await this.client?.invoke(new Api.messages.MarkDialogUnread({
2808
+ peer: new Api.InputDialogPeer({ peer }),
2809
+ unread,
2810
+ }));
2811
+ }, `markDialogUnread ${chatId}`);
2812
+ }
2813
+ async getAdminLog(chatId, limit = 20, q) {
2814
+ if (!this.client || !this.connected)
2815
+ throw new Error(NOT_CONNECTED_ERROR);
2816
+ return this.rateLimiter.execute(async () => {
2817
+ const entity = await this.resolveChat(chatId);
2818
+ if (!(entity instanceof Api.Channel)) {
2819
+ throw new Error("Admin log is only available for supergroups and channels");
2820
+ }
2821
+ const result = await this.client?.invoke(new Api.channels.GetAdminLog({
2822
+ channel: entity,
2823
+ q: q ?? "",
2824
+ maxId: bigInt(0),
2825
+ minId: bigInt(0),
2826
+ limit,
2827
+ }));
2828
+ if (!result)
2829
+ return [];
2830
+ const userMap = new Map();
2831
+ for (const u of result.users) {
2832
+ if (u instanceof Api.User)
2833
+ userMap.set(u.id.toString(), u);
2834
+ }
2835
+ const describeUser = (userId) => {
2836
+ const user = userMap.get(userId.toString());
2837
+ if (!user)
2838
+ return userId.toString();
2839
+ const parts = [user.firstName, user.lastName].filter(Boolean);
2840
+ const name = parts.join(" ") || "Unknown";
2841
+ return user.username ? `${name} (@${user.username})` : name;
2842
+ };
2843
+ return result.events.map((event) => ({
2844
+ id: event.id.toString(),
2845
+ date: new Date((event.date ?? 0) * 1000).toISOString(),
2846
+ userId: event.userId.toString(),
2847
+ userName: describeUser(event.userId),
2848
+ action: describeAdminLogAction(event.action),
2849
+ details: describeAdminLogDetails(event.action, describeUser),
2850
+ }));
2851
+ }, `getAdminLog for ${chatId}`);
2852
+ }
2853
+ async setChatPermissions(chatId, permissions) {
2854
+ if (!this.client || !this.connected)
2855
+ throw new Error(NOT_CONNECTED_ERROR);
2856
+ if (Object.values(permissions).every((v) => v === undefined))
2857
+ return;
2858
+ return this.rateLimiter.execute(async () => {
2859
+ const entity = await this.resolveChat(chatId);
2860
+ let currentRights;
2861
+ if (entity instanceof Api.Channel) {
2862
+ const full = await this.client?.invoke(new Api.channels.GetFullChannel({ channel: entity }));
2863
+ const fullChannel = full?.chats?.find((c) => c instanceof Api.Channel && c.id.equals(entity.id));
2864
+ currentRights = fullChannel?.defaultBannedRights ?? undefined;
2865
+ }
2866
+ else if (entity instanceof Api.Chat) {
2867
+ const full = await this.client?.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
2868
+ const fullChat = full?.chats?.find((c) => c instanceof Api.Chat && c.id.equals(entity.id));
2869
+ currentRights = fullChat?.defaultBannedRights ?? undefined;
2870
+ }
2871
+ const peer = await this.client?.getInputEntity(entity);
2872
+ if (!peer)
2873
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2874
+ await this.client?.invoke(new Api.messages.EditChatDefaultBannedRights({
2875
+ peer,
2876
+ bannedRights: new Api.ChatBannedRights({ untilDate: 0, ...mergeBannedRights(currentRights, permissions) }),
2877
+ }));
2878
+ }, `setChatPermissions ${chatId}`);
2879
+ }
2880
+ async setSlowMode(chatId, seconds) {
2881
+ if (!this.client || !this.connected)
2882
+ throw new Error(NOT_CONNECTED_ERROR);
2883
+ const allowed = [0, 10, 30, 60, 300, 900, 3600];
2884
+ if (!allowed.includes(seconds)) {
2885
+ throw new Error(`Invalid slow mode interval. Allowed values: ${allowed.join(", ")} (seconds)`);
2886
+ }
2887
+ return this.rateLimiter.execute(async () => {
2888
+ const entity = await this.resolveChat(chatId);
2889
+ if (!(entity instanceof Api.Channel)) {
2890
+ throw new Error("Slow mode is only available for supergroups");
2891
+ }
2892
+ await this.client?.invoke(new Api.channels.ToggleSlowMode({ channel: entity, seconds }));
2893
+ }, `setSlowMode ${chatId}`);
2894
+ }
2895
+ async toggleChannelSignatures(chatId, enabled) {
2896
+ if (!this.client || !this.connected)
2897
+ throw new Error(NOT_CONNECTED_ERROR);
2898
+ return this.rateLimiter.execute(async () => {
2899
+ const entity = await this.resolveChat(chatId);
2900
+ if (!(entity instanceof Api.Channel)) {
2901
+ throw new Error("Channel signatures are only available for broadcast channels (not groups or supergroups)");
2902
+ }
2903
+ if (entity.megagroup) {
2904
+ throw new Error("Channel signatures are only available for broadcast channels, not supergroups");
2905
+ }
2906
+ await this.client?.invoke(new Api.channels.ToggleSignatures({ channel: entity, signaturesEnabled: enabled }));
2907
+ }, `toggleChannelSignatures ${chatId}`);
2908
+ }
2909
+ async toggleAntiSpam(chatId, enabled) {
2910
+ if (!this.client || !this.connected)
2911
+ throw new Error(NOT_CONNECTED_ERROR);
2912
+ return this.rateLimiter.execute(async () => {
2913
+ const entity = await this.resolveChat(chatId);
2914
+ if (!(entity instanceof Api.Channel)) {
2915
+ throw new Error("Aggressive anti-spam is only available for supergroups");
2916
+ }
2917
+ if (!entity.megagroup) {
2918
+ throw new Error("Aggressive anti-spam is only available for supergroups, not broadcast channels");
2919
+ }
2920
+ await this.client?.invoke(new Api.channels.ToggleAntiSpam({ channel: entity, enabled }));
2921
+ }, `toggleAntiSpam ${chatId}`);
2922
+ }
2923
+ async toggleForumMode(chatId, enabled) {
2924
+ if (!this.client || !this.connected)
2925
+ throw new Error(NOT_CONNECTED_ERROR);
2926
+ return this.rateLimiter.execute(async () => {
2927
+ const entity = await this.resolveChat(chatId);
2928
+ if (!(entity instanceof Api.Channel)) {
2929
+ throw new Error("Forum mode is only available for supergroups");
2930
+ }
2931
+ if (!entity.megagroup) {
2932
+ throw new Error("Forum mode is only available for supergroups, not broadcast channels");
2933
+ }
2934
+ await this.client?.invoke(new Api.channels.ToggleForum({ channel: entity, enabled }));
2935
+ }, `toggleForumMode ${chatId}`);
2936
+ }
2937
+ async togglePrehistoryHidden(chatId, hidden) {
2938
+ if (!this.client || !this.connected)
2939
+ throw new Error(NOT_CONNECTED_ERROR);
2940
+ return this.rateLimiter.execute(async () => {
2941
+ const entity = await this.resolveChat(chatId);
2942
+ if (!(entity instanceof Api.Channel)) {
2943
+ throw new Error("Prehistory visibility is only available for supergroups");
2944
+ }
2945
+ if (!entity.megagroup) {
2946
+ throw new Error("Prehistory visibility is only available for supergroups, not broadcast channels");
2947
+ }
2948
+ await this.client?.invoke(new Api.channels.TogglePreHistoryHidden({ channel: entity, enabled: hidden }));
2949
+ }, `togglePrehistoryHidden ${chatId}`);
2950
+ }
2951
+ async setChatAvailableReactions(chatId, reactions) {
2952
+ if (!this.client || !this.connected)
2953
+ throw new Error(NOT_CONNECTED_ERROR);
2954
+ return this.rateLimiter.execute(async () => {
2955
+ const entity = await this.resolveChat(chatId);
2956
+ if (!(entity instanceof Api.Channel) && !(entity instanceof Api.Chat)) {
2957
+ throw new Error("Chat reactions can only be configured for groups, supergroups, and channels");
2958
+ }
2959
+ let availableReactions;
2960
+ if (reactions.type === "all") {
2961
+ availableReactions = new Api.ChatReactionsAll({ allowCustom: reactions.allowCustom });
2962
+ }
2963
+ else if (reactions.type === "none") {
2964
+ availableReactions = new Api.ChatReactionsNone();
2965
+ }
2966
+ else {
2967
+ if (reactions.emoji.length === 0) {
2968
+ throw new Error('reactions.emoji must be non-empty when type is "some" (use type:"none" to disable)');
2969
+ }
2970
+ availableReactions = new Api.ChatReactionsSome({
2971
+ reactions: reactions.emoji.map((emoticon) => new Api.ReactionEmoji({ emoticon })),
2972
+ });
2973
+ }
2974
+ await this.client?.invoke(new Api.messages.SetChatAvailableReactions({ peer: entity, availableReactions }));
2975
+ }, `setChatAvailableReactions ${chatId}`);
2976
+ }
2977
+ async approveChatJoinRequest(chatId, userId, approved) {
2978
+ if (!this.client || !this.connected)
2979
+ throw new Error(NOT_CONNECTED_ERROR);
2980
+ return this.rateLimiter.execute(async () => {
2981
+ const entity = await this.resolveChat(chatId);
2982
+ if (!(entity instanceof Api.Channel)) {
2983
+ throw new Error("Join request approval is only supported for supergroups and channels, not basic groups");
2984
+ }
2985
+ const user = await this.client?.getEntity(userId);
2986
+ if (!(user instanceof Api.User)) {
2987
+ throw new Error("Target is not a user");
2988
+ }
2989
+ const inputUser = new Api.InputUser({ userId: user.id, accessHash: user.accessHash ?? bigInt.zero });
2990
+ await this.client?.invoke(new Api.messages.HideChatJoinRequest({ peer: entity, userId: inputUser, approved }));
2991
+ }, `approveChatJoinRequest ${chatId}/${userId}`);
2992
+ }
2993
+ async getInlineBotResults(bot, chatId, query, offset) {
2994
+ if (!this.client || !this.connected)
2995
+ throw new Error(NOT_CONNECTED_ERROR);
2996
+ return this.rateLimiter.execute(async () => {
2997
+ const peer = await this.resolveChat(chatId);
2998
+ const botEntity = await this.client?.getEntity(bot);
2999
+ if (!(botEntity instanceof Api.User)) {
3000
+ throw new Error(`'${bot}' is not a user/bot`);
3001
+ }
3002
+ if (!botEntity.bot) {
3003
+ throw new Error(`'${bot}' is not a bot (inline queries require a bot account)`);
3004
+ }
3005
+ const inputBot = new Api.InputUser({
3006
+ userId: botEntity.id,
3007
+ accessHash: botEntity.accessHash ?? bigInt.zero,
3008
+ });
3009
+ const result = await this.client?.invoke(new Api.messages.GetInlineBotResults({
3010
+ bot: inputBot,
3011
+ peer,
3012
+ query,
3013
+ offset: offset ?? "",
3014
+ }));
3015
+ if (!result)
3016
+ throw new Error("No inline bot results returned");
3017
+ return {
3018
+ queryId: result.queryId.toString(),
3019
+ nextOffset: result.nextOffset,
3020
+ cacheTime: result.cacheTime,
3021
+ gallery: result.gallery === true,
3022
+ results: result.results.map((r) => {
3023
+ if (r instanceof Api.BotInlineResult) {
3024
+ return { id: r.id, type: r.type, title: r.title, description: r.description, url: r.url };
3025
+ }
3026
+ const mr = r;
3027
+ return { id: mr.id, type: mr.type, title: mr.title, description: mr.description };
3028
+ }),
3029
+ };
3030
+ }, `getInlineBotResults via ${bot}`);
3031
+ }
3032
+ async sendInlineBotResult(chatId, queryId, resultId, options) {
3033
+ if (!this.client || !this.connected)
3034
+ throw new Error(NOT_CONNECTED_ERROR);
3035
+ return this.rateLimiter.execute(async () => {
3036
+ const peer = await this.resolveChat(chatId);
3037
+ const randomId = bigInt(Math.floor(Math.random() * 1e15));
3038
+ const replyTo = options?.replyTo ? new Api.InputReplyToMessage({ replyToMsgId: options.replyTo }) : undefined;
3039
+ const result = await this.client?.invoke(new Api.messages.SendInlineBotResult({
3040
+ peer,
3041
+ queryId: bigInt(queryId),
3042
+ id: resultId,
3043
+ randomId,
3044
+ ...(replyTo ? { replyTo } : {}),
3045
+ ...(options?.silent ? { silent: true } : {}),
3046
+ ...(options?.hideVia ? { hideVia: true } : {}),
3047
+ ...(options?.clearDraft ? { clearDraft: true } : {}),
3048
+ }));
3049
+ if (!result)
3050
+ throw new Error("No response from SendInlineBotResult");
3051
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
3052
+ for (const update of result.updates) {
3053
+ if (update instanceof Api.UpdateMessageID && update.randomId?.equals(randomId)) {
3054
+ return { messageId: update.id };
3055
+ }
3056
+ }
3057
+ }
3058
+ if (result instanceof Api.UpdateShortSentMessage) {
3059
+ return { messageId: result.id };
3060
+ }
3061
+ return { messageId: 0 };
3062
+ }, `sendInlineBotResult ${resultId} to ${chatId}`);
3063
+ }
3064
+ async pressButton(chatId, messageId, options) {
3065
+ if (!this.client || !this.connected)
3066
+ throw new Error(NOT_CONNECTED_ERROR);
3067
+ return this.rateLimiter.execute(async () => {
3068
+ const entity = await this.resolveChat(chatId);
3069
+ let data;
3070
+ if (options.buttonIndex) {
3071
+ const { row, column } = options.buttonIndex;
3072
+ const messages = await this.client?.getMessages(entity, { ids: [messageId] });
3073
+ const msg = messages?.[0];
3074
+ if (!msg)
3075
+ throw new Error(`Message ${messageId} not found in ${chatId}`);
3076
+ const markup = msg.replyMarkup;
3077
+ if (!markup)
3078
+ throw new Error(`Message ${messageId} has no reply markup`);
3079
+ if (!(markup instanceof Api.ReplyInlineMarkup)) {
3080
+ throw new Error(`Message ${messageId} reply markup is ${markup.className} (only ReplyInlineMarkup has callable buttons)`);
3081
+ }
3082
+ const rowEntry = markup.rows[row];
3083
+ if (!rowEntry)
3084
+ throw new Error(`Row ${row} out of bounds (message has ${markup.rows.length} rows)`);
3085
+ const button = rowEntry.buttons[column];
3086
+ if (!button) {
3087
+ throw new Error(`Column ${column} out of bounds in row ${row} (row has ${rowEntry.buttons.length} buttons)`);
3088
+ }
3089
+ if (!(button instanceof Api.KeyboardButtonCallback)) {
3090
+ throw new Error(`Button at (${row},${column}) is ${button.className}, not callable — use the appropriate tool for URL/switch-inline/game buttons`);
3091
+ }
3092
+ if (button.requiresPassword) {
3093
+ throw new Error(`Button at (${row},${column}) requires 2FA password confirmation — not supported by telegram-press-button`);
3094
+ }
3095
+ data = Buffer.from(button.data);
3096
+ }
3097
+ else if (options.data !== undefined) {
3098
+ data = Buffer.from(options.data, "base64");
3099
+ }
3100
+ else {
3101
+ throw new Error("Either buttonIndex or data must be provided");
3102
+ }
3103
+ const answer = await this.client?.invoke(new Api.messages.GetBotCallbackAnswer({
3104
+ peer: entity,
3105
+ msgId: messageId,
3106
+ data,
3107
+ }));
3108
+ if (!answer)
3109
+ throw new Error("No callback answer returned");
3110
+ return {
3111
+ alert: answer.alert,
3112
+ hasUrl: answer.hasUrl,
3113
+ nativeUi: answer.nativeUi,
3114
+ message: answer.message,
3115
+ url: answer.url,
3116
+ cacheTime: answer.cacheTime,
3117
+ };
3118
+ }, `pressButton ${chatId}/${messageId}`);
3119
+ }
3120
+ async getMessageButtons(chatId, messageId) {
3121
+ if (!this.client || !this.connected)
3122
+ throw new Error(NOT_CONNECTED_ERROR);
3123
+ return this.rateLimiter.execute(async () => {
3124
+ const entity = await this.resolveChat(chatId);
3125
+ const messages = await this.client?.getMessages(entity, { ids: [messageId] });
3126
+ const msg = messages?.[0];
3127
+ if (!msg)
3128
+ throw new Error(`Message ${messageId} not found in ${chatId}`);
3129
+ const markup = msg.replyMarkup;
3130
+ if (!markup) {
3131
+ return { markupType: "none", buttons: [] };
3132
+ }
3133
+ if (!(markup instanceof Api.ReplyInlineMarkup) && !(markup instanceof Api.ReplyKeyboardMarkup)) {
3134
+ return { markupType: markup.className, buttons: [] };
3135
+ }
3136
+ const buttons = [];
3137
+ markup.rows.forEach((rowEntry, row) => {
3138
+ rowEntry.buttons.forEach((button, col) => {
3139
+ buttons.push(describeKeyboardButton(button, row, col));
3140
+ });
3141
+ });
3142
+ return { markupType: markup.className, buttons };
3143
+ }, `getMessageButtons ${chatId}/${messageId}`);
3144
+ }
3145
+ async getBroadcastStats(chatId, options) {
3146
+ if (!this.client || !this.connected)
3147
+ throw new Error(NOT_CONNECTED_ERROR);
3148
+ return this.rateLimiter.execute(async () => {
3149
+ const entity = await this.resolveChat(chatId);
3150
+ if (!(entity instanceof Api.Channel)) {
3151
+ throw new Error("Broadcast stats are only available for channels");
3152
+ }
3153
+ if (entity.megagroup) {
3154
+ throw new Error("Broadcast stats are only available for broadcast channels, not supergroups (use telegram-get-megagroup-stats)");
3155
+ }
3156
+ let result;
3157
+ try {
3158
+ const response = await this.client?.invoke(new Api.stats.GetBroadcastStats({ channel: entity, dark: options?.dark }));
3159
+ if (!response) {
3160
+ throw new Error("channel has no stats (may require Telegram Premium admin)");
3161
+ }
3162
+ result = response;
3163
+ }
3164
+ catch (e) {
3165
+ const msg = e.message ?? String(e);
3166
+ if (/CHAT_ADMIN_REQUIRED|ADMIN_RANK_INVALID/i.test(msg)) {
3167
+ throw new Error("Access denied: channel stats require admin rights (and may require Telegram Premium)");
3168
+ }
3169
+ if (/STATS_UNAVAILABLE|BROADCAST_REQUIRED|PARTICIPANTS_TOO_FEW/i.test(msg)) {
3170
+ throw new Error("channel has no stats (may require Telegram Premium admin)");
3171
+ }
3172
+ throw e;
3173
+ }
3174
+ return summarizeBroadcastStats(result, options?.includeGraphs === true);
3175
+ }, `getBroadcastStats ${chatId}`);
3176
+ }
3177
+ async getMegagroupStats(chatId, options) {
3178
+ if (!this.client || !this.connected)
3179
+ throw new Error(NOT_CONNECTED_ERROR);
3180
+ return this.rateLimiter.execute(async () => {
3181
+ const entity = await this.resolveChat(chatId);
3182
+ if (!(entity instanceof Api.Channel)) {
3183
+ throw new Error("Megagroup stats are only available for supergroups");
3184
+ }
3185
+ if (!entity.megagroup) {
3186
+ throw new Error("Megagroup stats are only available for supergroups, not broadcast channels (use telegram-get-broadcast-stats)");
3187
+ }
3188
+ let result;
3189
+ try {
3190
+ const response = await this.client?.invoke(new Api.stats.GetMegagroupStats({ channel: entity, dark: options?.dark }));
3191
+ if (!response) {
3192
+ throw new Error("supergroup has no stats yet (needs more activity/members)");
3193
+ }
3194
+ result = response;
3195
+ }
3196
+ catch (e) {
3197
+ const msg = e.message ?? String(e);
3198
+ if (/CHAT_ADMIN_REQUIRED|ADMIN_RANK_INVALID/i.test(msg)) {
3199
+ throw new Error("Access denied: supergroup stats require admin rights");
3200
+ }
3201
+ if (/STATS_UNAVAILABLE|PARTICIPANTS_TOO_FEW|MEGAGROUP_REQUIRED/i.test(msg)) {
3202
+ throw new Error("supergroup has no stats yet (needs more activity/members)");
3203
+ }
3204
+ throw e;
3205
+ }
3206
+ return summarizeMegagroupStats(result, options?.includeGraphs === true);
3207
+ }, `getMegagroupStats ${chatId}`, { throwOnFloodWait: true });
3208
+ }
3209
+ async getUpdatesState() {
3210
+ if (!this.client || !this.connected)
3211
+ throw new Error(NOT_CONNECTED_ERROR);
3212
+ return this.rateLimiter.execute(async () => {
3213
+ const state = await this.client?.invoke(new Api.updates.GetState());
3214
+ if (!state)
3215
+ throw new Error("updates.GetState returned no state");
3216
+ return {
3217
+ pts: state.pts,
3218
+ qts: state.qts,
3219
+ date: state.date,
3220
+ seq: state.seq,
3221
+ unreadCount: state.unreadCount,
3222
+ };
3223
+ }, "getUpdatesState");
3224
+ }
3225
+ async getUpdates(cursor) {
3226
+ if (!this.client || !this.connected)
3227
+ throw new Error(NOT_CONNECTED_ERROR);
3228
+ const ptsLimit = Math.min(cursor.ptsLimit ?? 100, 1000);
3229
+ const ptsTotalLimit = Math.min(cursor.ptsTotalLimit ?? 1000, 1000);
3230
+ return this.rateLimiter.execute(async () => {
3231
+ const diff = await this.client?.invoke(new Api.updates.GetDifference({
3232
+ pts: cursor.pts,
3233
+ date: cursor.date,
3234
+ qts: cursor.qts,
3235
+ ptsLimit,
3236
+ ptsTotalLimit,
3237
+ }));
3238
+ if (!diff)
3239
+ throw new Error("updates.GetDifference returned nothing");
3240
+ return summarizeUpdatesDifference(diff, cursor);
3241
+ }, "getUpdates");
3242
+ }
3243
+ async getChannelUpdates(chatId, cursor) {
3244
+ if (!this.client || !this.connected)
3245
+ throw new Error(NOT_CONNECTED_ERROR);
3246
+ const limit = Math.min(cursor.limit ?? 100, 1_000);
3247
+ return this.rateLimiter.execute(async () => {
3248
+ const entity = await this.resolveChat(chatId);
3249
+ if (!(entity instanceof Api.Channel)) {
3250
+ throw new Error("Channel updates are only available for channels/supergroups");
3251
+ }
3252
+ const diff = await this.client?.invoke(new Api.updates.GetChannelDifference({
3253
+ channel: entity,
3254
+ filter: new Api.ChannelMessagesFilterEmpty(),
3255
+ pts: cursor.pts,
3256
+ limit,
3257
+ force: cursor.force,
3258
+ }));
3259
+ if (!diff)
3260
+ throw new Error("updates.GetChannelDifference returned nothing");
3261
+ return summarizeChannelDifference(diff, entity.id.toString(), cursor.pts);
3262
+ }, `getChannelUpdates ${chatId}`);
3263
+ }
3264
+ async createForumTopic(chatId, title, iconColor, iconEmojiId) {
3265
+ if (!this.client || !this.connected)
3266
+ throw new Error(NOT_CONNECTED_ERROR);
3267
+ return this.rateLimiter.execute(async () => {
3268
+ const entity = await this.resolveChat(chatId);
3269
+ if (!(entity instanceof Api.Channel) || !entity.forum) {
3270
+ throw new Error("Forum topics are only available in forum supergroups");
3271
+ }
3272
+ const randomId = bigInt(Math.floor(Math.random() * 1e15));
3273
+ const result = await this.client?.invoke(new Api.channels.CreateForumTopic({
3274
+ channel: entity,
3275
+ title,
3276
+ iconColor,
3277
+ iconEmojiId: iconEmojiId ? bigInt(iconEmojiId) : undefined,
3278
+ randomId,
3279
+ }));
3280
+ let topicId = 0;
3281
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
3282
+ for (const update of result.updates) {
3283
+ if (update instanceof Api.UpdateNewChannelMessage &&
3284
+ update.message instanceof Api.MessageService &&
3285
+ update.message.action instanceof Api.MessageActionTopicCreate) {
3286
+ topicId = update.message.id;
3287
+ break;
3288
+ }
3289
+ }
3290
+ if (topicId === 0) {
3291
+ for (const update of result.updates) {
3292
+ if (update instanceof Api.UpdateMessageID && update.randomId?.equals(randomId)) {
3293
+ topicId = update.id;
3294
+ break;
3295
+ }
3296
+ }
3297
+ }
3298
+ }
3299
+ if (topicId === 0) {
3300
+ throw new Error("Failed to determine created topic ID");
3301
+ }
3302
+ return { id: topicId, title };
3303
+ }, `createForumTopic ${chatId}`);
3304
+ }
3305
+ async editForumTopic(chatId, topicId, options) {
3306
+ if (!this.client || !this.connected)
3307
+ throw new Error(NOT_CONNECTED_ERROR);
3308
+ return this.rateLimiter.execute(async () => {
3309
+ const entity = await this.resolveChat(chatId);
3310
+ if (!(entity instanceof Api.Channel) || !entity.forum) {
3311
+ throw new Error("Forum topics are only available in forum supergroups");
3312
+ }
3313
+ await this.client?.invoke(new Api.channels.EditForumTopic({
3314
+ channel: entity,
3315
+ topicId,
3316
+ title: options.title,
3317
+ iconEmojiId: options.iconEmojiId ? bigInt(options.iconEmojiId) : undefined,
3318
+ closed: options.closed,
3319
+ hidden: options.hidden,
3320
+ }));
3321
+ }, `editForumTopic ${chatId}/${topicId}`);
3322
+ }
3323
+ async deleteForumTopic(chatId, topicId) {
3324
+ if (!this.client || !this.connected)
3325
+ throw new Error(NOT_CONNECTED_ERROR);
3326
+ return this.rateLimiter.execute(async () => {
3327
+ const entity = await this.resolveChat(chatId);
3328
+ if (!(entity instanceof Api.Channel) || !entity.forum) {
3329
+ throw new Error("Forum topics are only available in forum supergroups");
3330
+ }
3331
+ await this.client?.invoke(new Api.channels.DeleteTopicHistory({
3332
+ channel: entity,
3333
+ topMsgId: topicId,
3334
+ }));
3335
+ }, `deleteForumTopic ${chatId}/${topicId}`);
3336
+ }
1657
3337
  async exportInviteLink(chatId, options) {
1658
3338
  if (!this.client || !this.connected)
1659
3339
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1686,7 +3366,7 @@ export class TelegramService {
1686
3366
  .filter((inv) => inv instanceof Api.ChatInviteExported)
1687
3367
  .map((inv) => {
1688
3368
  const expiredByDate = inv.expireDate ? inv.expireDate < Math.floor(Date.now() / 1000) : false;
1689
- const expiredByUsage = inv.usageLimit !== undefined && inv.usage !== undefined ? inv.usage >= inv.usageLimit : false;
3369
+ const expiredByUsage = inv.usageLimit != null && inv.usageLimit > 0 && inv.usage != null ? inv.usage >= inv.usageLimit : false;
1690
3370
  return {
1691
3371
  link: inv.link,
1692
3372
  title: inv.title,
@@ -1713,7 +3393,7 @@ export class TelegramService {
1713
3393
  const result = await this.client.invoke(new Api.messages.GetDialogFilters());
1714
3394
  const filters = "filters" in result ? result.filters : [];
1715
3395
  return filters
1716
- .filter((f) => f instanceof Api.DialogFilter)
3396
+ .filter((f) => f instanceof Api.DialogFilter || f instanceof Api.DialogFilterChatlist)
1717
3397
  .map((f) => ({
1718
3398
  id: f.id,
1719
3399
  title: typeof f.title === "string" ? f.title : f.title.text,
@@ -1925,6 +3605,182 @@ export class TelegramService {
1925
3605
  });
1926
3606
  }, `sendSticker to ${chatId}`);
1927
3607
  }
3608
+ async saveDraft(chatId, text, replyTo) {
3609
+ if (!this.client || !this.connected)
3610
+ throw new Error(NOT_CONNECTED_ERROR);
3611
+ await this.rateLimiter.execute(async () => {
3612
+ const resolved = await this.resolvePeer(chatId);
3613
+ const peer = await this.client?.getInputEntity(resolved);
3614
+ if (!peer)
3615
+ throw new Error(`Cannot resolve peer for ${chatId}`);
3616
+ const effectiveReplyTo = text === "" ? undefined : replyTo;
3617
+ await this.client?.invoke(new Api.messages.SaveDraft({
3618
+ peer,
3619
+ message: text,
3620
+ ...(effectiveReplyTo ? { replyTo: new Api.InputReplyToMessage({ replyToMsgId: effectiveReplyTo }) } : {}),
3621
+ }));
3622
+ }, `saveDraft in ${chatId}`);
3623
+ }
3624
+ async getAllDrafts() {
3625
+ if (!this.client || !this.connected)
3626
+ throw new Error(NOT_CONNECTED_ERROR);
3627
+ return this.rateLimiter.execute(async () => {
3628
+ const result = await this.client?.invoke(new Api.messages.GetAllDrafts());
3629
+ if (!result)
3630
+ return [];
3631
+ const updates = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.updates : [];
3632
+ const users = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.users : [];
3633
+ const chats = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.chats : [];
3634
+ const userMap = new Map();
3635
+ for (const u of users) {
3636
+ if (u instanceof Api.User)
3637
+ userMap.set(u.id.toString(), u);
3638
+ }
3639
+ const chatMap = new Map();
3640
+ for (const c of chats) {
3641
+ if (c instanceof Api.Chat || c instanceof Api.Channel)
3642
+ chatMap.set(c.id.toString(), c);
3643
+ }
3644
+ const resolvePeerTitle = (peer) => {
3645
+ if (peer instanceof Api.PeerUser) {
3646
+ const user = userMap.get(peer.userId.toString());
3647
+ if (user) {
3648
+ const parts = [user.firstName, user.lastName].filter(Boolean);
3649
+ const name = parts.join(" ") || "Unknown";
3650
+ return {
3651
+ id: peer.userId.toString(),
3652
+ title: user.username ? `${name} (@${user.username})` : name,
3653
+ };
3654
+ }
3655
+ return { id: peer.userId.toString(), title: peer.userId.toString() };
3656
+ }
3657
+ if (peer instanceof Api.PeerChat) {
3658
+ const chat = chatMap.get(peer.chatId.toString());
3659
+ return {
3660
+ id: peer.chatId.toString(),
3661
+ title: chat?.title ?? peer.chatId.toString(),
3662
+ };
3663
+ }
3664
+ if (peer instanceof Api.PeerChannel) {
3665
+ const channel = chatMap.get(peer.channelId.toString());
3666
+ return {
3667
+ id: peer.channelId.toString(),
3668
+ title: channel?.title ?? peer.channelId.toString(),
3669
+ };
3670
+ }
3671
+ return { id: "unknown", title: "unknown" };
3672
+ };
3673
+ const drafts = [];
3674
+ for (const update of updates) {
3675
+ if (update instanceof Api.UpdateDraftMessage && update.draft instanceof Api.DraftMessage) {
3676
+ const { id, title } = resolvePeerTitle(update.peer);
3677
+ drafts.push({
3678
+ chatId: id,
3679
+ chatTitle: title,
3680
+ text: update.draft.message ?? "",
3681
+ date: new Date((update.draft.date ?? 0) * 1000).toISOString(),
3682
+ });
3683
+ }
3684
+ }
3685
+ return drafts;
3686
+ }, "getAllDrafts");
3687
+ }
3688
+ async clearAllDrafts() {
3689
+ if (!this.client || !this.connected)
3690
+ throw new Error(NOT_CONNECTED_ERROR);
3691
+ await this.rateLimiter.execute(async () => {
3692
+ await this.client?.invoke(new Api.messages.ClearAllDrafts());
3693
+ }, "clearAllDrafts");
3694
+ }
3695
+ async getSavedDialogs(limit) {
3696
+ if (!this.client || !this.connected)
3697
+ throw new Error(NOT_CONNECTED_ERROR);
3698
+ return this.rateLimiter.execute(async () => {
3699
+ const result = await this.client?.invoke(new Api.messages.GetSavedDialogs({
3700
+ offsetDate: 0,
3701
+ offsetId: 0,
3702
+ offsetPeer: new Api.InputPeerEmpty(),
3703
+ limit,
3704
+ hash: bigInt(0),
3705
+ }));
3706
+ if (!result || result instanceof Api.messages.SavedDialogsNotModified)
3707
+ return [];
3708
+ const userMap = new Map();
3709
+ for (const u of result.users) {
3710
+ if (u instanceof Api.User)
3711
+ userMap.set(u.id.toString(), u);
3712
+ }
3713
+ const chatMap = new Map();
3714
+ for (const c of result.chats) {
3715
+ if (c instanceof Api.Chat || c instanceof Api.Channel)
3716
+ chatMap.set(c.id.toString(), c);
3717
+ }
3718
+ const resolvePeerTitle = (peer) => {
3719
+ if (peer instanceof Api.PeerUser) {
3720
+ const user = userMap.get(peer.userId.toString());
3721
+ if (user) {
3722
+ const parts = [user.firstName, user.lastName].filter(Boolean);
3723
+ const name = parts.join(" ") || "Unknown";
3724
+ return {
3725
+ id: peer.userId.toString(),
3726
+ title: user.username ? `${name} (@${user.username})` : name,
3727
+ };
3728
+ }
3729
+ return { id: peer.userId.toString(), title: peer.userId.toString() };
3730
+ }
3731
+ if (peer instanceof Api.PeerChat) {
3732
+ const chat = chatMap.get(peer.chatId.toString());
3733
+ return { id: peer.chatId.toString(), title: chat?.title ?? peer.chatId.toString() };
3734
+ }
3735
+ if (peer instanceof Api.PeerChannel) {
3736
+ const channel = chatMap.get(peer.channelId.toString());
3737
+ return { id: peer.channelId.toString(), title: channel?.title ?? peer.channelId.toString() };
3738
+ }
3739
+ return { id: "unknown", title: "unknown" };
3740
+ };
3741
+ const dialogs = [];
3742
+ for (const d of result.dialogs) {
3743
+ if (d instanceof Api.SavedDialog) {
3744
+ const { id, title } = resolvePeerTitle(d.peer);
3745
+ dialogs.push({
3746
+ peerId: id,
3747
+ peerTitle: title,
3748
+ lastMsgId: d.topMessage,
3749
+ });
3750
+ }
3751
+ }
3752
+ return dialogs;
3753
+ }, "getSavedDialogs");
3754
+ }
3755
+ async getWebPreview(url) {
3756
+ if (!this.client || !this.connected)
3757
+ throw new Error(NOT_CONNECTED_ERROR);
3758
+ return this.rateLimiter.execute(async () => {
3759
+ const result = await this.client?.invoke(new Api.messages.GetWebPagePreview({ message: url }));
3760
+ if (!result)
3761
+ return null;
3762
+ const media = result.media;
3763
+ if (!(media instanceof Api.MessageMediaWebPage))
3764
+ return null;
3765
+ const page = media.webpage;
3766
+ if (page instanceof Api.WebPageEmpty) {
3767
+ return { type: "empty", url: page.url };
3768
+ }
3769
+ if (page instanceof Api.WebPagePending) {
3770
+ return { type: "pending", url: page.url };
3771
+ }
3772
+ if (page instanceof Api.WebPage) {
3773
+ return {
3774
+ type: page.type ?? "article",
3775
+ url: page.url,
3776
+ title: page.title,
3777
+ description: page.description,
3778
+ siteName: page.siteName,
3779
+ };
3780
+ }
3781
+ return null;
3782
+ }, "getWebPreview");
3783
+ }
1928
3784
  async getRecentStickers() {
1929
3785
  if (!this.client || !this.connected)
1930
3786
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1944,4 +3800,216 @@ export class TelegramService {
1944
3800
  emoji: emojiMap.get(doc.id.toString()) || "",
1945
3801
  }));
1946
3802
  }
3803
+ async getAllStories(options) {
3804
+ if (!this.client || !this.connected)
3805
+ throw new Error(NOT_CONNECTED_ERROR);
3806
+ return this.rateLimiter.execute(async () => {
3807
+ const response = await this.client?.invoke(new Api.stories.GetAllStories({
3808
+ next: options?.next,
3809
+ hidden: options?.hidden,
3810
+ state: options?.state,
3811
+ }));
3812
+ if (!response)
3813
+ throw new Error("stories.GetAllStories returned nothing");
3814
+ return summarizeAllStories(response);
3815
+ }, "getAllStories");
3816
+ }
3817
+ async getPeerStories(chatId) {
3818
+ if (!this.client || !this.connected)
3819
+ throw new Error(NOT_CONNECTED_ERROR);
3820
+ const peer = await this.resolvePeer(chatId);
3821
+ return this.rateLimiter.execute(async () => {
3822
+ const response = await this.client?.invoke(new Api.stories.GetPeerStories({ peer }));
3823
+ if (!response)
3824
+ throw new Error("stories.GetPeerStories returned nothing");
3825
+ return summarizePeerStories(response.stories);
3826
+ }, `getPeerStories ${chatId}`);
3827
+ }
3828
+ async getStoriesById(chatId, ids) {
3829
+ if (!this.client || !this.connected)
3830
+ throw new Error(NOT_CONNECTED_ERROR);
3831
+ const peer = await this.resolvePeer(chatId);
3832
+ return this.rateLimiter.execute(async () => {
3833
+ const response = await this.client?.invoke(new Api.stories.GetStoriesByID({ peer, id: ids }));
3834
+ if (!response)
3835
+ throw new Error("stories.GetStoriesByID returned nothing");
3836
+ return summarizeStoriesById(response);
3837
+ }, `getStoriesById ${chatId}`);
3838
+ }
3839
+ async getStoryViewsList(chatId, options) {
3840
+ if (!this.client || !this.connected)
3841
+ throw new Error(NOT_CONNECTED_ERROR);
3842
+ const peer = await this.resolvePeer(chatId);
3843
+ return this.rateLimiter.execute(async () => {
3844
+ const response = await this.client?.invoke(new Api.stories.GetStoryViewsList({
3845
+ peer,
3846
+ id: options.id,
3847
+ q: options.q,
3848
+ justContacts: options.justContacts,
3849
+ reactionsFirst: options.reactionsFirst,
3850
+ forwardsFirst: options.forwardsFirst,
3851
+ offset: options.offset ?? "",
3852
+ limit: options.limit ?? 50,
3853
+ }));
3854
+ if (!response)
3855
+ throw new Error("stories.GetStoryViewsList returned nothing");
3856
+ return summarizeStoryViewsList(response);
3857
+ }, `getStoryViewsList ${chatId}/${options.id}`);
3858
+ }
3859
+ async getMyBoosts() {
3860
+ if (!this.client || !this.connected)
3861
+ throw new Error(NOT_CONNECTED_ERROR);
3862
+ return this.rateLimiter.execute(async () => {
3863
+ const response = await this.client?.invoke(new Api.premium.GetMyBoosts());
3864
+ if (!response)
3865
+ throw new Error("premium.GetMyBoosts returned nothing");
3866
+ return summarizeMyBoosts(response);
3867
+ }, "getMyBoosts");
3868
+ }
3869
+ async getBoostsStatus(chatId) {
3870
+ if (!this.client || !this.connected)
3871
+ throw new Error(NOT_CONNECTED_ERROR);
3872
+ const peer = await this.resolvePeer(chatId);
3873
+ return this.rateLimiter.execute(async () => {
3874
+ const response = await this.client?.invoke(new Api.premium.GetBoostsStatus({ peer }));
3875
+ if (!response)
3876
+ throw new Error("premium.GetBoostsStatus returned nothing");
3877
+ return summarizeBoostsStatus(response);
3878
+ }, `getBoostsStatus ${chatId}`);
3879
+ }
3880
+ async getBoostsList(chatId, options = {}) {
3881
+ if (!this.client || !this.connected)
3882
+ throw new Error(NOT_CONNECTED_ERROR);
3883
+ const peer = await this.resolvePeer(chatId);
3884
+ return this.rateLimiter.execute(async () => {
3885
+ const response = await this.client?.invoke(new Api.premium.GetBoostsList({
3886
+ peer,
3887
+ gifts: options.gifts,
3888
+ offset: options.offset ?? "",
3889
+ limit: options.limit ?? 50,
3890
+ }));
3891
+ if (!response)
3892
+ throw new Error("premium.GetBoostsList returned nothing");
3893
+ return summarizeBoostsList(response);
3894
+ }, `getBoostsList ${chatId}`);
3895
+ }
3896
+ async getBusinessChatLinks() {
3897
+ if (!this.client || !this.connected)
3898
+ throw new Error(NOT_CONNECTED_ERROR);
3899
+ return this.rateLimiter.execute(async () => {
3900
+ const response = await this.client?.invoke(new Api.account.GetBusinessChatLinks());
3901
+ if (!response)
3902
+ throw new Error("account.GetBusinessChatLinks returned nothing");
3903
+ return summarizeBusinessChatLinks(response);
3904
+ }, "getBusinessChatLinks");
3905
+ }
3906
+ async getGroupCall(chatId, options = {}) {
3907
+ if (!this.client || !this.connected)
3908
+ throw new Error(NOT_CONNECTED_ERROR);
3909
+ return this.rateLimiter.execute(async () => {
3910
+ const call = await this.resolveInputGroupCall(chatId);
3911
+ const response = await this.client?.invoke(new Api.phone.GetGroupCall({ call, limit: options.limit ?? 0 }));
3912
+ if (!response)
3913
+ throw new Error("phone.GetGroupCall returned nothing");
3914
+ return summarizeGroupCall(response);
3915
+ }, `getGroupCall ${chatId}`);
3916
+ }
3917
+ async getGroupCallParticipants(chatId, options = {}) {
3918
+ if (!this.client || !this.connected)
3919
+ throw new Error(NOT_CONNECTED_ERROR);
3920
+ return this.rateLimiter.execute(async () => {
3921
+ const call = await this.resolveInputGroupCall(chatId);
3922
+ const ids = [];
3923
+ for (const id of options.ids ?? []) {
3924
+ ids.push(await this.resolvePeer(id));
3925
+ }
3926
+ const response = await this.client?.invoke(new Api.phone.GetGroupParticipants({
3927
+ call,
3928
+ ids,
3929
+ sources: options.sources ?? [],
3930
+ offset: options.offset ?? "",
3931
+ limit: options.limit ?? 100,
3932
+ }));
3933
+ if (!response)
3934
+ throw new Error("phone.GetGroupParticipants returned nothing");
3935
+ return summarizeGroupCallParticipants(response);
3936
+ }, `getGroupCallParticipants ${chatId}`);
3937
+ }
3938
+ async getStarsStatus(chatId) {
3939
+ if (!this.client || !this.connected)
3940
+ throw new Error(NOT_CONNECTED_ERROR);
3941
+ const peer = await this.resolvePeer(chatId);
3942
+ return this.rateLimiter.execute(async () => {
3943
+ const response = await this.client?.invoke(new Api.payments.GetStarsStatus({ peer }));
3944
+ if (!response)
3945
+ throw new Error("payments.GetStarsStatus returned nothing");
3946
+ return summarizeStarsStatus(response);
3947
+ }, `getStarsStatus ${chatId}`);
3948
+ }
3949
+ async getStarsTransactions(chatId, options = {}) {
3950
+ if (!this.client || !this.connected)
3951
+ throw new Error(NOT_CONNECTED_ERROR);
3952
+ const peer = await this.resolvePeer(chatId);
3953
+ return this.rateLimiter.execute(async () => {
3954
+ const response = await this.client?.invoke(new Api.payments.GetStarsTransactions({
3955
+ peer,
3956
+ inbound: options.inbound,
3957
+ outbound: options.outbound,
3958
+ ascending: options.ascending,
3959
+ subscriptionId: options.subscriptionId,
3960
+ offset: options.offset ?? "",
3961
+ limit: options.limit ?? 50,
3962
+ }));
3963
+ if (!response)
3964
+ throw new Error("payments.GetStarsTransactions returned nothing");
3965
+ return summarizeStarsStatus(response);
3966
+ }, `getStarsTransactions ${chatId}`);
3967
+ }
3968
+ async getQuickReplies(hash) {
3969
+ if (!this.client || !this.connected)
3970
+ throw new Error(NOT_CONNECTED_ERROR);
3971
+ return this.rateLimiter.execute(async () => {
3972
+ const response = await this.client?.invoke(new Api.messages.GetQuickReplies({ hash: hash ? bigInt(hash) : bigInt(0) }));
3973
+ if (!response)
3974
+ throw new Error("messages.GetQuickReplies returned nothing");
3975
+ return summarizeQuickReplies(response);
3976
+ }, "getQuickReplies");
3977
+ }
3978
+ async getQuickReplyMessages(shortcutId, options = {}) {
3979
+ if (!this.client || !this.connected)
3980
+ throw new Error(NOT_CONNECTED_ERROR);
3981
+ return this.rateLimiter.execute(async () => {
3982
+ const response = await this.client?.invoke(new Api.messages.GetQuickReplyMessages({
3983
+ shortcutId,
3984
+ id: options.ids,
3985
+ hash: options.hash ? bigInt(options.hash) : bigInt(0),
3986
+ }));
3987
+ if (!response)
3988
+ throw new Error("messages.GetQuickReplyMessages returned nothing");
3989
+ return summarizeQuickReplyMessages(response);
3990
+ }, `getQuickReplyMessages ${shortcutId}`);
3991
+ }
3992
+ async resolveInputGroupCall(chatId) {
3993
+ const entity = await this.resolveChat(chatId);
3994
+ let call;
3995
+ if (entity instanceof Api.Channel) {
3996
+ const full = await this.client?.invoke(new Api.channels.GetFullChannel({ channel: entity }));
3997
+ if (full?.fullChat instanceof Api.ChannelFull) {
3998
+ call = full.fullChat.call;
3999
+ }
4000
+ }
4001
+ else if (entity instanceof Api.Chat) {
4002
+ const full = await this.client?.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
4003
+ if (full?.fullChat instanceof Api.ChatFull) {
4004
+ call = full.fullChat.call;
4005
+ }
4006
+ }
4007
+ else {
4008
+ throw new Error("Group calls are only available for groups/supergroups/channels");
4009
+ }
4010
+ if (!call) {
4011
+ throw new Error(`No active group call in chat ${chatId}`);
4012
+ }
4013
+ return call;
4014
+ }
1947
4015
  }