@overpod/mcp-telegram 1.24.0 → 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.
- package/CHANGELOG.md +82 -26
- package/README.md +37 -11
- package/dist/__tests__/admin-log.test.d.ts +1 -0
- package/dist/__tests__/admin-log.test.js +41 -0
- package/dist/__tests__/reactions.test.d.ts +1 -0
- package/dist/__tests__/reactions.test.js +23 -0
- package/dist/__tests__/set-chat-permissions-merge.test.d.ts +1 -0
- package/dist/__tests__/set-chat-permissions-merge.test.js +107 -0
- package/dist/telegram-client.d.ts +131 -2
- package/dist/telegram-client.js +797 -51
- package/dist/tools/account.js +92 -0
- package/dist/tools/chats.js +238 -1
- package/dist/tools/media.js +120 -1
- package/dist/tools/messages.js +187 -0
- package/dist/tools/reactions.js +59 -1
- package/dist/tools/stickers.js +2 -4
- package/package.json +13 -9
package/dist/telegram-client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
replyTo
|
|
320
|
-
|
|
321
|
-
|
|
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 &&
|
|
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 &&
|
|
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(
|
|
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
|
|
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.
|
|
1380
|
-
|
|
1381
|
-
|
|
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 =
|
|
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
|
|
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);
|