@overpod/mcp-telegram 1.24.1 → 1.25.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.
@@ -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,137 @@ 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
+ const BANNED_RIGHT_FLAGS = [
138
+ "sendMessages",
139
+ "sendMedia",
140
+ "sendStickers",
141
+ "sendGifs",
142
+ "sendPolls",
143
+ "sendInline",
144
+ "embedLinks",
145
+ "changeInfo",
146
+ "inviteUsers",
147
+ "pinMessages",
148
+ ];
149
+ // Newer granular flags not exposed in ChatPermissions input but must be preserved from currentRights
150
+ const EXTRA_BANNED_RIGHT_FLAGS = [
151
+ "sendGames",
152
+ "manageTopics",
153
+ "sendPhotos",
154
+ "sendVideos",
155
+ "sendRoundvideos",
156
+ "sendAudios",
157
+ "sendVoices",
158
+ "sendDocs",
159
+ "sendPlain",
160
+ ];
161
+ export function mergeBannedRights(current, permissions) {
162
+ const result = {};
163
+ for (const flag of BANNED_RIGHT_FLAGS) {
164
+ const userValue = permissions[flag];
165
+ if (userValue !== undefined) {
166
+ result[flag] = !userValue;
167
+ }
168
+ else {
169
+ result[flag] = Boolean(current?.[flag]);
170
+ }
171
+ }
172
+ // Preserve newer granular flags from existing rights so they are not silently cleared
173
+ for (const flag of EXTRA_BANNED_RIGHT_FLAGS) {
174
+ result[flag] = Boolean(current?.[flag]);
175
+ }
176
+ return result;
177
+ }
47
178
  export class TelegramService {
48
179
  client = null;
49
180
  apiId;
@@ -52,6 +183,7 @@ export class TelegramService {
52
183
  connected = false;
53
184
  sessionPath;
54
185
  rateLimiter = new RateLimiter();
186
+ lastTypingAt = new Map();
55
187
  lastError = "";
56
188
  get sessionDir() {
57
189
  return dirname(this.sessionPath);
@@ -311,24 +443,12 @@ export class TelegramService {
311
443
  return this.rateLimiter.execute(async () => {
312
444
  const resolved = await this.resolvePeer(chatId);
313
445
  if (topicId) {
314
- const peer = await this.client?.getInputEntity(resolved);
315
- const result = await this.client?.invoke(new Api.messages.SendMessage({
316
- peer,
446
+ return await this.client?.sendMessage(resolved, {
317
447
  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;
448
+ topMsgId: topicId,
449
+ ...(replyTo ? { replyTo } : {}),
450
+ ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
451
+ });
332
452
  }
333
453
  return await this.client?.sendMessage(resolved, {
334
454
  message: text,
@@ -386,7 +506,14 @@ export class TelegramService {
386
506
  return "image/png";
387
507
  if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
388
508
  return "image/gif";
389
- if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
509
+ if (buffer[0] === 0x52 &&
510
+ buffer[1] === 0x49 &&
511
+ buffer[2] === 0x46 &&
512
+ buffer[3] === 0x46 &&
513
+ buffer[8] === 0x57 &&
514
+ buffer[9] === 0x45 &&
515
+ buffer[10] === 0x42 &&
516
+ buffer[11] === 0x50)
390
517
  return "image/webp";
391
518
  // Fall back to document mimeType
392
519
  const m = media;
@@ -526,28 +653,6 @@ export class TelegramService {
526
653
  throw new Error(NOT_CONNECTED_ERROR);
527
654
  await this.client.markAsRead(chatId);
528
655
  }
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
656
  async getMessageById(chatId, messageId) {
552
657
  if (!this.client || !this.connected)
553
658
  throw new Error(NOT_CONNECTED_ERROR);
@@ -588,6 +693,225 @@ export class TelegramService {
588
693
  await this.client?.deleteMessages(resolved, messageIds, { revoke: true });
589
694
  }, `deleteMessages in ${chatId}`);
590
695
  }
696
+ async getScheduledMessages(chatId) {
697
+ if (!this.client || !this.connected)
698
+ throw new Error(NOT_CONNECTED_ERROR);
699
+ return this.rateLimiter.execute(async () => {
700
+ const resolved = await this.resolvePeer(chatId);
701
+ const peer = await this.client?.getInputEntity(resolved);
702
+ if (!peer)
703
+ throw new Error(`Cannot resolve peer for ${chatId}`);
704
+ const result = await this.client?.invoke(new Api.messages.GetScheduledHistory({ peer, hash: bigInt(0) }));
705
+ if (!result || result instanceof Api.messages.MessagesNotModified)
706
+ return [];
707
+ const messages = result
708
+ .messages;
709
+ return messages
710
+ .filter((m) => m instanceof Api.Message)
711
+ .map((m) => ({
712
+ id: m.id,
713
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
714
+ text: m.message ?? "",
715
+ media: this.extractMediaInfo(m.media),
716
+ }));
717
+ }, `getScheduledMessages in ${chatId}`);
718
+ }
719
+ async deleteScheduledMessages(chatId, messageIds) {
720
+ if (!this.client || !this.connected)
721
+ throw new Error(NOT_CONNECTED_ERROR);
722
+ await this.rateLimiter.execute(async () => {
723
+ const resolved = await this.resolvePeer(chatId);
724
+ const peer = await this.client?.getInputEntity(resolved);
725
+ if (!peer)
726
+ throw new Error(`Cannot resolve peer for ${chatId}`);
727
+ await this.client?.invoke(new Api.messages.DeleteScheduledMessages({ peer, id: messageIds }));
728
+ }, `deleteScheduledMessages in ${chatId}`);
729
+ }
730
+ async getReplies(chatId, messageId, limit = 20) {
731
+ if (!this.client || !this.connected)
732
+ throw new Error(NOT_CONNECTED_ERROR);
733
+ return this.rateLimiter.execute(async () => {
734
+ const resolved = await this.resolvePeer(chatId);
735
+ const peer = await this.client?.getInputEntity(resolved);
736
+ if (!peer)
737
+ throw new Error(`Cannot resolve peer for ${chatId}`);
738
+ const result = await this.client?.invoke(new Api.messages.GetReplies({ peer, msgId: messageId, limit, hash: bigInt(0) }));
739
+ if (!result || result instanceof Api.messages.MessagesNotModified)
740
+ return [];
741
+ const messages = result
742
+ .messages;
743
+ return Promise.all(messages
744
+ .filter((m) => m instanceof Api.Message)
745
+ .map(async (m) => ({
746
+ id: m.id,
747
+ text: m.message ?? "",
748
+ sender: await this.resolveSenderName(m.senderId),
749
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
750
+ media: this.extractMediaInfo(m.media),
751
+ reactions: this.extractReactions(m.reactions),
752
+ })));
753
+ }, `getReplies for ${messageId} in ${chatId}`);
754
+ }
755
+ async getMessageLink(chatId, messageId, thread = false) {
756
+ if (!this.client || !this.connected)
757
+ throw new Error(NOT_CONNECTED_ERROR);
758
+ return this.rateLimiter.execute(async () => {
759
+ const entity = await this.resolveChat(chatId);
760
+ if (!(entity instanceof Api.Channel)) {
761
+ throw new Error("Message links are only available for channels and supergroups");
762
+ }
763
+ const result = await this.client?.invoke(new Api.channels.ExportMessageLink({ channel: entity, id: messageId, thread }));
764
+ if (!result)
765
+ throw new Error("Failed to export message link");
766
+ return result.link;
767
+ }, `getMessageLink for ${messageId} in ${chatId}`);
768
+ }
769
+ async getUnreadMentions(chatId, limit = 20) {
770
+ if (!this.client || !this.connected)
771
+ throw new Error(NOT_CONNECTED_ERROR);
772
+ return this.rateLimiter.execute(async () => {
773
+ const resolved = await this.resolvePeer(chatId);
774
+ const peer = await this.client?.getInputEntity(resolved);
775
+ if (!peer)
776
+ throw new Error(`Cannot resolve peer for ${chatId}`);
777
+ const result = await this.client?.invoke(new Api.messages.GetUnreadMentions({
778
+ peer,
779
+ offsetId: 0,
780
+ addOffset: 0,
781
+ limit,
782
+ maxId: 0,
783
+ minId: 0,
784
+ }));
785
+ if (!result || result instanceof Api.messages.MessagesNotModified)
786
+ return [];
787
+ const typedResult = result;
788
+ const messages = typedResult.messages;
789
+ const items = await Promise.all(messages
790
+ .filter((m) => m instanceof Api.Message)
791
+ .map(async (m) => ({
792
+ id: m.id,
793
+ text: m.message ?? "",
794
+ sender: await this.resolveSenderName(m.senderId),
795
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
796
+ media: this.extractMediaInfo(m.media),
797
+ reactions: this.extractReactions(m.reactions),
798
+ })));
799
+ // Only mark all as read when we received the complete set; if truncated, marking all
800
+ // would silently clear mentions the caller hasn't seen yet.
801
+ const totalCount = "count" in typedResult ? typedResult.count : items.length;
802
+ if (items.length > 0 && items.length >= totalCount) {
803
+ try {
804
+ await this.client?.invoke(new Api.messages.ReadMentions({ peer }));
805
+ }
806
+ catch {
807
+ // best-effort; don't discard fetched items on mark-read failure
808
+ }
809
+ }
810
+ return items;
811
+ }, `getUnreadMentions in ${chatId}`);
812
+ }
813
+ async getUnreadReactions(chatId, limit = 20) {
814
+ if (!this.client || !this.connected)
815
+ throw new Error(NOT_CONNECTED_ERROR);
816
+ return this.rateLimiter.execute(async () => {
817
+ const resolved = await this.resolvePeer(chatId);
818
+ const peer = await this.client?.getInputEntity(resolved);
819
+ if (!peer)
820
+ throw new Error(`Cannot resolve peer for ${chatId}`);
821
+ const result = await this.client?.invoke(new Api.messages.GetUnreadReactions({
822
+ peer,
823
+ offsetId: 0,
824
+ addOffset: 0,
825
+ limit,
826
+ maxId: 0,
827
+ minId: 0,
828
+ }));
829
+ if (!result || result instanceof Api.messages.MessagesNotModified)
830
+ return [];
831
+ const typedResult = result;
832
+ const messages = typedResult.messages;
833
+ const items = await Promise.all(messages
834
+ .filter((m) => m instanceof Api.Message)
835
+ .map(async (m) => ({
836
+ id: m.id,
837
+ text: m.message ?? "",
838
+ sender: await this.resolveSenderName(m.senderId),
839
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
840
+ media: this.extractMediaInfo(m.media),
841
+ reactions: this.extractReactions(m.reactions),
842
+ })));
843
+ // Only mark all as read when we received the complete set; if truncated, marking all
844
+ // would silently clear reactions the caller hasn't seen yet.
845
+ const totalCount = "count" in typedResult ? typedResult.count : items.length;
846
+ if (items.length > 0 && items.length >= totalCount) {
847
+ try {
848
+ await this.client?.invoke(new Api.messages.ReadReactions({ peer }));
849
+ }
850
+ catch {
851
+ // best-effort; don't discard fetched items on mark-read failure
852
+ }
853
+ }
854
+ return items;
855
+ }, `getUnreadReactions in ${chatId}`);
856
+ }
857
+ async translateText(chatId, messageIds, toLang) {
858
+ if (!this.client || !this.connected)
859
+ throw new Error(NOT_CONNECTED_ERROR);
860
+ return this.rateLimiter.execute(async () => {
861
+ const resolved = await this.resolvePeer(chatId);
862
+ const peer = await this.client?.getInputEntity(resolved);
863
+ if (!peer)
864
+ throw new Error(`Cannot resolve peer for ${chatId}`);
865
+ const result = await this.client?.invoke(new Api.messages.TranslateText({ peer, id: messageIds, toLang }));
866
+ if (!result)
867
+ return [];
868
+ return result.result.map((t) => (t instanceof Api.TextWithEntities ? t.text : ""));
869
+ }, `translateText in ${chatId}`);
870
+ }
871
+ async sendTyping(chatId, action = "typing") {
872
+ if (!this.client || !this.connected)
873
+ throw new Error(NOT_CONNECTED_ERROR);
874
+ return this.rateLimiter.execute(async () => {
875
+ let stamped = false;
876
+ if (action !== "cancel") {
877
+ const now = Date.now();
878
+ const last = this.lastTypingAt.get(chatId) ?? 0;
879
+ if (now - last < 10_000)
880
+ return;
881
+ this.lastTypingAt.set(chatId, now);
882
+ stamped = true;
883
+ }
884
+ try {
885
+ const resolved = await this.resolvePeer(chatId);
886
+ const peer = await this.client?.getInputEntity(resolved);
887
+ if (!peer)
888
+ throw new Error(`Cannot resolve peer for ${chatId}`);
889
+ let sendAction;
890
+ switch (action) {
891
+ case "cancel":
892
+ sendAction = new Api.SendMessageCancelAction();
893
+ break;
894
+ case "upload_photo":
895
+ sendAction = new Api.SendMessageUploadPhotoAction({ progress: 0 });
896
+ break;
897
+ case "upload_document":
898
+ sendAction = new Api.SendMessageUploadDocumentAction({ progress: 0 });
899
+ break;
900
+ default:
901
+ sendAction = new Api.SendMessageTypingAction();
902
+ }
903
+ await this.client?.invoke(new Api.messages.SetTyping({ peer, action: sendAction }));
904
+ if (action === "cancel") {
905
+ this.lastTypingAt.delete(chatId);
906
+ }
907
+ }
908
+ catch (err) {
909
+ if (stamped)
910
+ this.lastTypingAt.delete(chatId);
911
+ throw err;
912
+ }
913
+ }, `sendTyping in ${chatId}`);
914
+ }
591
915
  /**
592
916
  * Resolve a chat by ID, username, or display name.
593
917
  * Falls back to searching user's dialogs if getEntity() fails.
@@ -1118,7 +1442,14 @@ export class TelegramService {
1118
1442
  return "image/png";
1119
1443
  if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
1120
1444
  return "image/gif";
1121
- if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
1445
+ if (buffer[0] === 0x52 &&
1446
+ buffer[1] === 0x49 &&
1447
+ buffer[2] === 0x46 &&
1448
+ buffer[3] === 0x46 &&
1449
+ buffer[8] === 0x57 &&
1450
+ buffer[9] === 0x45 &&
1451
+ buffer[10] === 0x42 &&
1452
+ buffer[11] === 0x50)
1122
1453
  return "image/webp";
1123
1454
  return "image/jpeg"; // Telegram profile photos are almost always JPEG
1124
1455
  }
@@ -1225,7 +1556,7 @@ export class TelegramService {
1225
1556
  for (const r of list.reactions) {
1226
1557
  const userId = r.peerId instanceof Api.PeerUser ? r.peerId.userId.toString() : "";
1227
1558
  if (userId) {
1228
- const name = await this.resolveSenderName(bigInt(Number.parseInt(userId, 10)));
1559
+ const name = await this.resolveSenderName(bigInt(userId));
1229
1560
  users.push({ id: userId, name });
1230
1561
  }
1231
1562
  }
@@ -1240,6 +1571,47 @@ export class TelegramService {
1240
1571
  const total = reactionsOut.reduce((sum, r) => sum + r.count, 0);
1241
1572
  return { reactions: reactionsOut, total };
1242
1573
  }
1574
+ async setDefaultReaction(emoji) {
1575
+ if (!this.client || !this.connected)
1576
+ throw new Error(NOT_CONNECTED_ERROR);
1577
+ await this.rateLimiter.execute(async () => {
1578
+ await this.client?.invoke(new Api.messages.SetDefaultReaction({
1579
+ reaction: new Api.ReactionEmoji({ emoticon: emoji }),
1580
+ }));
1581
+ }, `setDefaultReaction ${emoji}`);
1582
+ }
1583
+ async getTopReactions(limit) {
1584
+ if (!this.client || !this.connected)
1585
+ throw new Error(NOT_CONNECTED_ERROR);
1586
+ return this.rateLimiter.execute(async () => {
1587
+ const result = await this.client?.invoke(new Api.messages.GetTopReactions({ limit, hash: bigInt(0) }));
1588
+ if (!result || result instanceof Api.messages.ReactionsNotModified)
1589
+ return [];
1590
+ const out = [];
1591
+ for (const r of result.reactions) {
1592
+ const emoji = reactionToEmoji(r);
1593
+ if (emoji)
1594
+ out.push({ emoji });
1595
+ }
1596
+ return out;
1597
+ }, "getTopReactions");
1598
+ }
1599
+ async getRecentReactions(limit) {
1600
+ if (!this.client || !this.connected)
1601
+ throw new Error(NOT_CONNECTED_ERROR);
1602
+ return this.rateLimiter.execute(async () => {
1603
+ const result = await this.client?.invoke(new Api.messages.GetRecentReactions({ limit, hash: bigInt(0) }));
1604
+ if (!result || result instanceof Api.messages.ReactionsNotModified)
1605
+ return [];
1606
+ const out = [];
1607
+ for (const r of result.reactions) {
1608
+ const emoji = reactionToEmoji(r);
1609
+ if (emoji)
1610
+ out.push({ emoji });
1611
+ }
1612
+ return out;
1613
+ }, "getRecentReactions");
1614
+ }
1243
1615
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
1244
1616
  if (!this.client || !this.connected)
1245
1617
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1319,7 +1691,8 @@ export class TelegramService {
1319
1691
  async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
1320
1692
  if (!this.client || !this.connected)
1321
1693
  throw new Error(NOT_CONNECTED_ERROR);
1322
- const peer = await this.client.getInputEntity(chatId);
1694
+ const resolved = await this.resolvePeer(chatId);
1695
+ const peer = await this.client.getInputEntity(resolved);
1323
1696
  const result = await this.client.invoke(new Api.messages.GetReplies({
1324
1697
  peer,
1325
1698
  msgId: topicId,
@@ -1376,10 +1749,11 @@ export class TelegramService {
1376
1749
  // Public channel/group by username
1377
1750
  const username = target.replace(/^@/, "").replace(/^https?:\/\/t\.me\//, "");
1378
1751
  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
- }));
1752
+ if (entity instanceof Api.Chat) {
1753
+ throw new Error("Basic groups cannot be joined by username; use an invite link instead.");
1754
+ }
1755
+ if (entity instanceof Api.Channel) {
1756
+ await this.client.invoke(new Api.channels.JoinChannel({ channel: entity }));
1383
1757
  return {
1384
1758
  id: entity.id.toString(),
1385
1759
  title: entity.title ?? "Unknown",
@@ -1563,7 +1937,7 @@ export class TelegramService {
1563
1937
  await this.client.invoke(new Api.messages.EditChatAbout({ peer: entity, about: options.description }));
1564
1938
  }
1565
1939
  if (options.photoPath) {
1566
- const fileData = readFileSync(options.photoPath);
1940
+ const fileData = await readFile(options.photoPath);
1567
1941
  const uploaded = await this.client.uploadFile({
1568
1942
  file: new CustomFile(options.photoPath, fileData.length, options.photoPath, fileData),
1569
1943
  workers: 1,
@@ -1654,6 +2028,202 @@ export class TelegramService {
1654
2028
  settings: new Api.InputPeerNotifySettings({ muteUntil }),
1655
2029
  }));
1656
2030
  }
2031
+ async archiveChat(chatId, archive) {
2032
+ if (!this.client || !this.connected)
2033
+ throw new Error(NOT_CONNECTED_ERROR);
2034
+ return this.rateLimiter.execute(async () => {
2035
+ const resolved = await this.resolvePeer(chatId);
2036
+ const peer = await this.client?.getInputEntity(resolved);
2037
+ if (!peer)
2038
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2039
+ await this.client?.invoke(new Api.folders.EditPeerFolders({
2040
+ folderPeers: [new Api.InputFolderPeer({ peer, folderId: archive ? 1 : 0 })],
2041
+ }));
2042
+ }, `archiveChat ${chatId}`);
2043
+ }
2044
+ async pinDialog(chatId, pin) {
2045
+ if (!this.client || !this.connected)
2046
+ throw new Error(NOT_CONNECTED_ERROR);
2047
+ return this.rateLimiter.execute(async () => {
2048
+ const resolved = await this.resolvePeer(chatId);
2049
+ const peer = await this.client?.getInputEntity(resolved);
2050
+ if (!peer)
2051
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2052
+ await this.client?.invoke(new Api.messages.ToggleDialogPin({
2053
+ peer: new Api.InputDialogPeer({ peer }),
2054
+ pinned: pin,
2055
+ }));
2056
+ }, `pinDialog ${chatId}`);
2057
+ }
2058
+ async markDialogUnread(chatId, unread) {
2059
+ if (!this.client || !this.connected)
2060
+ throw new Error(NOT_CONNECTED_ERROR);
2061
+ return this.rateLimiter.execute(async () => {
2062
+ const resolved = await this.resolvePeer(chatId);
2063
+ const peer = await this.client?.getInputEntity(resolved);
2064
+ if (!peer)
2065
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2066
+ await this.client?.invoke(new Api.messages.MarkDialogUnread({
2067
+ peer: new Api.InputDialogPeer({ peer }),
2068
+ unread,
2069
+ }));
2070
+ }, `markDialogUnread ${chatId}`);
2071
+ }
2072
+ async getAdminLog(chatId, limit = 20, q) {
2073
+ if (!this.client || !this.connected)
2074
+ throw new Error(NOT_CONNECTED_ERROR);
2075
+ return this.rateLimiter.execute(async () => {
2076
+ const entity = await this.resolveChat(chatId);
2077
+ if (!(entity instanceof Api.Channel)) {
2078
+ throw new Error("Admin log is only available for supergroups and channels");
2079
+ }
2080
+ const result = await this.client?.invoke(new Api.channels.GetAdminLog({
2081
+ channel: entity,
2082
+ q: q ?? "",
2083
+ maxId: bigInt(0),
2084
+ minId: bigInt(0),
2085
+ limit,
2086
+ }));
2087
+ if (!result)
2088
+ return [];
2089
+ const userMap = new Map();
2090
+ for (const u of result.users) {
2091
+ if (u instanceof Api.User)
2092
+ userMap.set(u.id.toString(), u);
2093
+ }
2094
+ const describeUser = (userId) => {
2095
+ const user = userMap.get(userId.toString());
2096
+ if (!user)
2097
+ return userId.toString();
2098
+ const parts = [user.firstName, user.lastName].filter(Boolean);
2099
+ const name = parts.join(" ") || "Unknown";
2100
+ return user.username ? `${name} (@${user.username})` : name;
2101
+ };
2102
+ return result.events.map((event) => ({
2103
+ id: event.id.toString(),
2104
+ date: new Date((event.date ?? 0) * 1000).toISOString(),
2105
+ userId: event.userId.toString(),
2106
+ userName: describeUser(event.userId),
2107
+ action: describeAdminLogAction(event.action),
2108
+ details: describeAdminLogDetails(event.action, describeUser),
2109
+ }));
2110
+ }, `getAdminLog for ${chatId}`);
2111
+ }
2112
+ async setChatPermissions(chatId, permissions) {
2113
+ if (!this.client || !this.connected)
2114
+ throw new Error(NOT_CONNECTED_ERROR);
2115
+ if (Object.values(permissions).every((v) => v === undefined))
2116
+ return;
2117
+ return this.rateLimiter.execute(async () => {
2118
+ const entity = await this.resolveChat(chatId);
2119
+ let currentRights;
2120
+ if (entity instanceof Api.Channel) {
2121
+ const full = await this.client?.invoke(new Api.channels.GetFullChannel({ channel: entity }));
2122
+ const fullChannel = full?.chats?.find((c) => c instanceof Api.Channel && c.id.equals(entity.id));
2123
+ currentRights = fullChannel?.defaultBannedRights ?? undefined;
2124
+ }
2125
+ else if (entity instanceof Api.Chat) {
2126
+ const full = await this.client?.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
2127
+ const fullChat = full?.chats?.find((c) => c instanceof Api.Chat && c.id.equals(entity.id));
2128
+ currentRights = fullChat?.defaultBannedRights ?? undefined;
2129
+ }
2130
+ const peer = await this.client?.getInputEntity(entity);
2131
+ if (!peer)
2132
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2133
+ await this.client?.invoke(new Api.messages.EditChatDefaultBannedRights({
2134
+ peer,
2135
+ bannedRights: new Api.ChatBannedRights({ untilDate: 0, ...mergeBannedRights(currentRights, permissions) }),
2136
+ }));
2137
+ }, `setChatPermissions ${chatId}`);
2138
+ }
2139
+ async setSlowMode(chatId, seconds) {
2140
+ if (!this.client || !this.connected)
2141
+ throw new Error(NOT_CONNECTED_ERROR);
2142
+ const allowed = [0, 10, 30, 60, 300, 900, 3600];
2143
+ if (!allowed.includes(seconds)) {
2144
+ throw new Error(`Invalid slow mode interval. Allowed values: ${allowed.join(", ")} (seconds)`);
2145
+ }
2146
+ return this.rateLimiter.execute(async () => {
2147
+ const entity = await this.resolveChat(chatId);
2148
+ if (!(entity instanceof Api.Channel)) {
2149
+ throw new Error("Slow mode is only available for supergroups");
2150
+ }
2151
+ await this.client?.invoke(new Api.channels.ToggleSlowMode({ channel: entity, seconds }));
2152
+ }, `setSlowMode ${chatId}`);
2153
+ }
2154
+ async createForumTopic(chatId, title, iconColor, iconEmojiId) {
2155
+ if (!this.client || !this.connected)
2156
+ throw new Error(NOT_CONNECTED_ERROR);
2157
+ return this.rateLimiter.execute(async () => {
2158
+ const entity = await this.resolveChat(chatId);
2159
+ if (!(entity instanceof Api.Channel) || !entity.forum) {
2160
+ throw new Error("Forum topics are only available in forum supergroups");
2161
+ }
2162
+ const randomId = bigInt(Math.floor(Math.random() * 1e15));
2163
+ const result = await this.client?.invoke(new Api.channels.CreateForumTopic({
2164
+ channel: entity,
2165
+ title,
2166
+ iconColor,
2167
+ iconEmojiId: iconEmojiId ? bigInt(iconEmojiId) : undefined,
2168
+ randomId,
2169
+ }));
2170
+ let topicId = 0;
2171
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
2172
+ for (const update of result.updates) {
2173
+ if (update instanceof Api.UpdateNewChannelMessage &&
2174
+ update.message instanceof Api.MessageService &&
2175
+ update.message.action instanceof Api.MessageActionTopicCreate) {
2176
+ topicId = update.message.id;
2177
+ break;
2178
+ }
2179
+ }
2180
+ if (topicId === 0) {
2181
+ for (const update of result.updates) {
2182
+ if (update instanceof Api.UpdateMessageID && update.randomId?.equals(randomId)) {
2183
+ topicId = update.id;
2184
+ break;
2185
+ }
2186
+ }
2187
+ }
2188
+ }
2189
+ if (topicId === 0) {
2190
+ throw new Error("Failed to determine created topic ID");
2191
+ }
2192
+ return { id: topicId, title };
2193
+ }, `createForumTopic ${chatId}`);
2194
+ }
2195
+ async editForumTopic(chatId, topicId, options) {
2196
+ if (!this.client || !this.connected)
2197
+ throw new Error(NOT_CONNECTED_ERROR);
2198
+ return this.rateLimiter.execute(async () => {
2199
+ const entity = await this.resolveChat(chatId);
2200
+ if (!(entity instanceof Api.Channel) || !entity.forum) {
2201
+ throw new Error("Forum topics are only available in forum supergroups");
2202
+ }
2203
+ await this.client?.invoke(new Api.channels.EditForumTopic({
2204
+ channel: entity,
2205
+ topicId,
2206
+ title: options.title,
2207
+ iconEmojiId: options.iconEmojiId ? bigInt(options.iconEmojiId) : undefined,
2208
+ closed: options.closed,
2209
+ hidden: options.hidden,
2210
+ }));
2211
+ }, `editForumTopic ${chatId}/${topicId}`);
2212
+ }
2213
+ async deleteForumTopic(chatId, topicId) {
2214
+ if (!this.client || !this.connected)
2215
+ throw new Error(NOT_CONNECTED_ERROR);
2216
+ return this.rateLimiter.execute(async () => {
2217
+ const entity = await this.resolveChat(chatId);
2218
+ if (!(entity instanceof Api.Channel) || !entity.forum) {
2219
+ throw new Error("Forum topics are only available in forum supergroups");
2220
+ }
2221
+ await this.client?.invoke(new Api.channels.DeleteTopicHistory({
2222
+ channel: entity,
2223
+ topMsgId: topicId,
2224
+ }));
2225
+ }, `deleteForumTopic ${chatId}/${topicId}`);
2226
+ }
1657
2227
  async exportInviteLink(chatId, options) {
1658
2228
  if (!this.client || !this.connected)
1659
2229
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1686,7 +2256,7 @@ export class TelegramService {
1686
2256
  .filter((inv) => inv instanceof Api.ChatInviteExported)
1687
2257
  .map((inv) => {
1688
2258
  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;
2259
+ const expiredByUsage = inv.usageLimit != null && inv.usageLimit > 0 && inv.usage != null ? inv.usage >= inv.usageLimit : false;
1690
2260
  return {
1691
2261
  link: inv.link,
1692
2262
  title: inv.title,
@@ -1713,7 +2283,7 @@ export class TelegramService {
1713
2283
  const result = await this.client.invoke(new Api.messages.GetDialogFilters());
1714
2284
  const filters = "filters" in result ? result.filters : [];
1715
2285
  return filters
1716
- .filter((f) => f instanceof Api.DialogFilter)
2286
+ .filter((f) => f instanceof Api.DialogFilter || f instanceof Api.DialogFilterChatlist)
1717
2287
  .map((f) => ({
1718
2288
  id: f.id,
1719
2289
  title: typeof f.title === "string" ? f.title : f.title.text,
@@ -1925,6 +2495,182 @@ export class TelegramService {
1925
2495
  });
1926
2496
  }, `sendSticker to ${chatId}`);
1927
2497
  }
2498
+ async saveDraft(chatId, text, replyTo) {
2499
+ if (!this.client || !this.connected)
2500
+ throw new Error(NOT_CONNECTED_ERROR);
2501
+ await this.rateLimiter.execute(async () => {
2502
+ const resolved = await this.resolvePeer(chatId);
2503
+ const peer = await this.client?.getInputEntity(resolved);
2504
+ if (!peer)
2505
+ throw new Error(`Cannot resolve peer for ${chatId}`);
2506
+ const effectiveReplyTo = text === "" ? undefined : replyTo;
2507
+ await this.client?.invoke(new Api.messages.SaveDraft({
2508
+ peer,
2509
+ message: text,
2510
+ ...(effectiveReplyTo ? { replyTo: new Api.InputReplyToMessage({ replyToMsgId: effectiveReplyTo }) } : {}),
2511
+ }));
2512
+ }, `saveDraft in ${chatId}`);
2513
+ }
2514
+ async getAllDrafts() {
2515
+ if (!this.client || !this.connected)
2516
+ throw new Error(NOT_CONNECTED_ERROR);
2517
+ return this.rateLimiter.execute(async () => {
2518
+ const result = await this.client?.invoke(new Api.messages.GetAllDrafts());
2519
+ if (!result)
2520
+ return [];
2521
+ const updates = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.updates : [];
2522
+ const users = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.users : [];
2523
+ const chats = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.chats : [];
2524
+ const userMap = new Map();
2525
+ for (const u of users) {
2526
+ if (u instanceof Api.User)
2527
+ userMap.set(u.id.toString(), u);
2528
+ }
2529
+ const chatMap = new Map();
2530
+ for (const c of chats) {
2531
+ if (c instanceof Api.Chat || c instanceof Api.Channel)
2532
+ chatMap.set(c.id.toString(), c);
2533
+ }
2534
+ const resolvePeerTitle = (peer) => {
2535
+ if (peer instanceof Api.PeerUser) {
2536
+ const user = userMap.get(peer.userId.toString());
2537
+ if (user) {
2538
+ const parts = [user.firstName, user.lastName].filter(Boolean);
2539
+ const name = parts.join(" ") || "Unknown";
2540
+ return {
2541
+ id: peer.userId.toString(),
2542
+ title: user.username ? `${name} (@${user.username})` : name,
2543
+ };
2544
+ }
2545
+ return { id: peer.userId.toString(), title: peer.userId.toString() };
2546
+ }
2547
+ if (peer instanceof Api.PeerChat) {
2548
+ const chat = chatMap.get(peer.chatId.toString());
2549
+ return {
2550
+ id: peer.chatId.toString(),
2551
+ title: chat?.title ?? peer.chatId.toString(),
2552
+ };
2553
+ }
2554
+ if (peer instanceof Api.PeerChannel) {
2555
+ const channel = chatMap.get(peer.channelId.toString());
2556
+ return {
2557
+ id: peer.channelId.toString(),
2558
+ title: channel?.title ?? peer.channelId.toString(),
2559
+ };
2560
+ }
2561
+ return { id: "unknown", title: "unknown" };
2562
+ };
2563
+ const drafts = [];
2564
+ for (const update of updates) {
2565
+ if (update instanceof Api.UpdateDraftMessage && update.draft instanceof Api.DraftMessage) {
2566
+ const { id, title } = resolvePeerTitle(update.peer);
2567
+ drafts.push({
2568
+ chatId: id,
2569
+ chatTitle: title,
2570
+ text: update.draft.message ?? "",
2571
+ date: new Date((update.draft.date ?? 0) * 1000).toISOString(),
2572
+ });
2573
+ }
2574
+ }
2575
+ return drafts;
2576
+ }, "getAllDrafts");
2577
+ }
2578
+ async clearAllDrafts() {
2579
+ if (!this.client || !this.connected)
2580
+ throw new Error(NOT_CONNECTED_ERROR);
2581
+ await this.rateLimiter.execute(async () => {
2582
+ await this.client?.invoke(new Api.messages.ClearAllDrafts());
2583
+ }, "clearAllDrafts");
2584
+ }
2585
+ async getSavedDialogs(limit) {
2586
+ if (!this.client || !this.connected)
2587
+ throw new Error(NOT_CONNECTED_ERROR);
2588
+ return this.rateLimiter.execute(async () => {
2589
+ const result = await this.client?.invoke(new Api.messages.GetSavedDialogs({
2590
+ offsetDate: 0,
2591
+ offsetId: 0,
2592
+ offsetPeer: new Api.InputPeerEmpty(),
2593
+ limit,
2594
+ hash: bigInt(0),
2595
+ }));
2596
+ if (!result || result instanceof Api.messages.SavedDialogsNotModified)
2597
+ return [];
2598
+ const userMap = new Map();
2599
+ for (const u of result.users) {
2600
+ if (u instanceof Api.User)
2601
+ userMap.set(u.id.toString(), u);
2602
+ }
2603
+ const chatMap = new Map();
2604
+ for (const c of result.chats) {
2605
+ if (c instanceof Api.Chat || c instanceof Api.Channel)
2606
+ chatMap.set(c.id.toString(), c);
2607
+ }
2608
+ const resolvePeerTitle = (peer) => {
2609
+ if (peer instanceof Api.PeerUser) {
2610
+ const user = userMap.get(peer.userId.toString());
2611
+ if (user) {
2612
+ const parts = [user.firstName, user.lastName].filter(Boolean);
2613
+ const name = parts.join(" ") || "Unknown";
2614
+ return {
2615
+ id: peer.userId.toString(),
2616
+ title: user.username ? `${name} (@${user.username})` : name,
2617
+ };
2618
+ }
2619
+ return { id: peer.userId.toString(), title: peer.userId.toString() };
2620
+ }
2621
+ if (peer instanceof Api.PeerChat) {
2622
+ const chat = chatMap.get(peer.chatId.toString());
2623
+ return { id: peer.chatId.toString(), title: chat?.title ?? peer.chatId.toString() };
2624
+ }
2625
+ if (peer instanceof Api.PeerChannel) {
2626
+ const channel = chatMap.get(peer.channelId.toString());
2627
+ return { id: peer.channelId.toString(), title: channel?.title ?? peer.channelId.toString() };
2628
+ }
2629
+ return { id: "unknown", title: "unknown" };
2630
+ };
2631
+ const dialogs = [];
2632
+ for (const d of result.dialogs) {
2633
+ if (d instanceof Api.SavedDialog) {
2634
+ const { id, title } = resolvePeerTitle(d.peer);
2635
+ dialogs.push({
2636
+ peerId: id,
2637
+ peerTitle: title,
2638
+ lastMsgId: d.topMessage,
2639
+ });
2640
+ }
2641
+ }
2642
+ return dialogs;
2643
+ }, "getSavedDialogs");
2644
+ }
2645
+ async getWebPreview(url) {
2646
+ if (!this.client || !this.connected)
2647
+ throw new Error(NOT_CONNECTED_ERROR);
2648
+ return this.rateLimiter.execute(async () => {
2649
+ const result = await this.client?.invoke(new Api.messages.GetWebPagePreview({ message: url }));
2650
+ if (!result)
2651
+ return null;
2652
+ const media = result.media;
2653
+ if (!(media instanceof Api.MessageMediaWebPage))
2654
+ return null;
2655
+ const page = media.webpage;
2656
+ if (page instanceof Api.WebPageEmpty) {
2657
+ return { type: "empty", url: page.url };
2658
+ }
2659
+ if (page instanceof Api.WebPagePending) {
2660
+ return { type: "pending", url: page.url };
2661
+ }
2662
+ if (page instanceof Api.WebPage) {
2663
+ return {
2664
+ type: page.type ?? "article",
2665
+ url: page.url,
2666
+ title: page.title,
2667
+ description: page.description,
2668
+ siteName: page.siteName,
2669
+ };
2670
+ }
2671
+ return null;
2672
+ }, "getWebPreview");
2673
+ }
1928
2674
  async getRecentStickers() {
1929
2675
  if (!this.client || !this.connected)
1930
2676
  throw new Error(NOT_CONNECTED_ERROR);