@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.
- package/CHANGELOG.md +67 -1
- package/README.md +45 -13
- package/dist/__tests__/admin-log.test.d.ts +1 -0
- package/dist/__tests__/admin-log.test.js +41 -0
- package/dist/__tests__/approve-join-request.test.d.ts +1 -0
- package/dist/__tests__/approve-join-request.test.js +107 -0
- package/dist/__tests__/boosts.test.d.ts +1 -0
- package/dist/__tests__/boosts.test.js +310 -0
- package/dist/__tests__/broadcast-stats.test.d.ts +1 -0
- package/dist/__tests__/broadcast-stats.test.js +172 -0
- package/dist/__tests__/business-chat-links.test.d.ts +1 -0
- package/dist/__tests__/business-chat-links.test.js +102 -0
- package/dist/__tests__/get-message-buttons.test.d.ts +1 -0
- package/dist/__tests__/get-message-buttons.test.js +122 -0
- package/dist/__tests__/group-calls.test.d.ts +1 -0
- package/dist/__tests__/group-calls.test.js +503 -0
- package/dist/__tests__/inline-query-send.test.d.ts +1 -0
- package/dist/__tests__/inline-query-send.test.js +94 -0
- package/dist/__tests__/inline-query.test.d.ts +1 -0
- package/dist/__tests__/inline-query.test.js +115 -0
- package/dist/__tests__/megagroup-stats.test.d.ts +1 -0
- package/dist/__tests__/megagroup-stats.test.js +166 -0
- package/dist/__tests__/press-button.test.d.ts +1 -0
- package/dist/__tests__/press-button.test.js +123 -0
- package/dist/__tests__/quick-replies.test.d.ts +1 -0
- package/dist/__tests__/quick-replies.test.js +245 -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/__tests__/set-chat-reactions.test.d.ts +1 -0
- package/dist/__tests__/set-chat-reactions.test.js +129 -0
- package/dist/__tests__/stars-status.test.d.ts +1 -0
- package/dist/__tests__/stars-status.test.js +205 -0
- package/dist/__tests__/stars-transactions.test.d.ts +1 -0
- package/dist/__tests__/stars-transactions.test.js +82 -0
- package/dist/__tests__/stories.test.d.ts +1 -0
- package/dist/__tests__/stories.test.js +361 -0
- package/dist/__tests__/toggle-anti-spam.test.d.ts +1 -0
- package/dist/__tests__/toggle-anti-spam.test.js +80 -0
- package/dist/__tests__/toggle-channel-signatures.test.d.ts +1 -0
- package/dist/__tests__/toggle-channel-signatures.test.js +80 -0
- package/dist/__tests__/toggle-forum-mode.test.d.ts +1 -0
- package/dist/__tests__/toggle-forum-mode.test.js +80 -0
- package/dist/__tests__/toggle-prehistory-hidden.test.d.ts +1 -0
- package/dist/__tests__/toggle-prehistory-hidden.test.js +80 -0
- package/dist/__tests__/updates.test.d.ts +1 -0
- package/dist/__tests__/updates.test.js +221 -0
- package/dist/rate-limiter.d.ts +8 -2
- package/dist/rate-limiter.js +15 -8
- package/dist/telegram-client.d.ts +711 -2
- package/dist/telegram-client.js +2167 -99
- package/dist/tools/account.js +108 -0
- package/dist/tools/boosts.d.ts +3 -0
- package/dist/tools/boosts.js +65 -0
- package/dist/tools/chats.js +388 -1
- package/dist/tools/group-calls.d.ts +4 -0
- package/dist/tools/group-calls.js +77 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/media.js +120 -1
- package/dist/tools/messages.js +379 -0
- package/dist/tools/quick-replies.d.ts +4 -0
- package/dist/tools/quick-replies.js +58 -0
- package/dist/tools/reactions.js +102 -1
- package/dist/tools/stars.d.ts +4 -0
- package/dist/tools/stars.js +71 -0
- package/dist/tools/stories.d.ts +3 -0
- package/dist/tools/stories.js +107 -0
- package/package.json +1 -1
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,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
|
-
|
|
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
|
-
|
|
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;
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
1468
|
+
async getReplies(chatId, messageId, limit = 20) {
|
|
636
1469
|
if (!this.client || !this.connected)
|
|
637
1470
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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 &&
|
|
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(
|
|
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
|
|
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.
|
|
1380
|
-
|
|
1381
|
-
|
|
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 =
|
|
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
|
|
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
|
}
|