@overpod/mcp-telegram 1.28.0 → 1.32.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 +153 -0
- package/README.md +22 -9
- package/dist/master.js +5 -0
- package/dist/telegram-client.d.ts +271 -6
- package/dist/telegram-client.js +1143 -17
- package/dist/telegram-helpers.d.ts +111 -1
- package/dist/telegram-helpers.js +270 -0
- package/dist/tools/account.js +221 -6
- package/dist/tools/auth.js +26 -1
- package/dist/tools/business.d.ts +3 -0
- package/dist/tools/business.js +333 -0
- package/dist/tools/fact-check.d.ts +3 -0
- package/dist/tools/fact-check.js +72 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/messages.js +183 -4
- package/dist/tools/reactions.js +62 -0
- package/dist/tools/send-media.d.ts +3 -0
- package/dist/tools/send-media.js +259 -0
- package/dist/tools/shared.d.ts +28 -0
- package/dist/tools/shared.js +59 -0
- package/dist/tools/stories.js +303 -1
- package/dist/tools/transcribe.d.ts +3 -0
- package/dist/tools/transcribe.js +75 -0
- package/package.json +1 -1
package/dist/telegram-client.js
CHANGED
|
@@ -10,8 +10,8 @@ import { CustomFile } from "telegram/client/uploads.js";
|
|
|
10
10
|
import { StringSession } from "telegram/sessions/index.js";
|
|
11
11
|
import { Api } from "telegram/tl/index.js";
|
|
12
12
|
import { RateLimiter } from "./rate-limiter.js";
|
|
13
|
-
import { describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, mergeBannedRights, reactionToEmoji, summarizeAllStories, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeGroupCall, summarizeGroupCallParticipants, summarizeMegagroupStats, summarizeMyBoosts, summarizePeerStories, summarizeQuickReplies, summarizeQuickReplyMessages, summarizeStarsStatus, summarizeStoriesById, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
|
|
14
|
-
export { describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, mergeBannedRights, peerToCompact, reactionToEmoji, summarizeAllStories, summarizeBoost, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLink, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeGroupCall, summarizeGroupCallInfo, summarizeGroupCallParticipant, summarizeGroupCallParticipants, summarizeMegagroupStats, summarizeMyBoost, summarizeMyBoosts, summarizePeerStories, summarizePrepaidGiveaway, summarizeQuickReplies, summarizeQuickReply, summarizeQuickReplyMessage, summarizeQuickReplyMessages, summarizeStarsAmount, summarizeStarsStatus, summarizeStarsSubscription, summarizeStarsTransaction, summarizeStarsTransactionPeer, summarizeStoriesById, summarizeStoryItem, summarizeStoryView, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
|
|
13
|
+
import { buildReplyTo, buildStoryPrivacyRules, describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, detectMediaType, extractDiceResult, extractMessageId, extractPeerId, extractPollMediaFromUpdates, extractStoryIdFromUpdates, generateRandomBigInt, mergeBannedRights, reactionToEmoji, summarizeAllStories, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLink, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeDiscussionMessage, summarizeEmojiStatus, summarizeGroupCall, summarizeGroupCallParticipants, summarizeGroupsForDiscussion, summarizeMegagroupStats, summarizeMyBoosts, summarizePeer, summarizePeerStories, summarizePoll, summarizeQuickReplies, summarizeQuickReplyMessages, summarizeReadParticipants, summarizeReportResult, summarizeStarsStatus, summarizeStoriesById, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
|
|
14
|
+
export { buildStoryPrivacyRules, describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, detectMediaType, extractPeerId, extractPollMediaFromUpdates, extractStoryIdFromUpdates, mergeBannedRights, peerToCompact, reactionToEmoji, summarizeAllStories, summarizeBoost, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLink, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeDiscussionMessage, summarizeEmojiStatus, summarizeGroupCall, summarizeGroupCallInfo, summarizeGroupCallParticipant, summarizeGroupCallParticipants, summarizeGroupsForDiscussion, summarizeMegagroupStats, summarizeMyBoost, summarizeMyBoosts, summarizePeer, summarizePeerStories, summarizePoll, summarizePrepaidGiveaway, summarizeQuickReplies, summarizeQuickReply, summarizeQuickReplyMessage, summarizeQuickReplyMessages, summarizeReadParticipants, summarizeReportResult, summarizeStarsAmount, summarizeStarsStatus, summarizeStarsSubscription, summarizeStarsTransaction, summarizeStarsTransactionPeer, summarizeStoriesById, summarizeStoryItem, summarizeStoryView, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const LEGACY_SESSION_FILE = join(__dirname, "..", ".telegram-session");
|
|
17
17
|
const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
|
|
@@ -60,6 +60,9 @@ export class TelegramService {
|
|
|
60
60
|
get sessionDir() {
|
|
61
61
|
return dirname(this.sessionPath);
|
|
62
62
|
}
|
|
63
|
+
hasLocalSession() {
|
|
64
|
+
return existsSync(this.sessionPath);
|
|
65
|
+
}
|
|
63
66
|
// ─── Session & Auth ────────────────────────────────────────────────────────
|
|
64
67
|
getClient() {
|
|
65
68
|
return this.client;
|
|
@@ -195,25 +198,42 @@ export class TelegramService {
|
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
201
|
+
* Terminates the session on Telegram servers, destroys the client, and clears
|
|
202
|
+
* local session (in-memory + file). Returns true only when server-side revoke
|
|
203
|
+
* confirmed. False means server revoke could not be confirmed — local wipe
|
|
204
|
+
* was still attempted. Throws if local file removal failed so callers can
|
|
205
|
+
* surface the partial state instead of silently misreporting success.
|
|
200
206
|
*/
|
|
201
207
|
async logOut() {
|
|
202
|
-
|
|
208
|
+
const wipeLocalOrThrow = async () => {
|
|
209
|
+
await this.clearSession();
|
|
210
|
+
if (existsSync(this.sessionPath)) {
|
|
211
|
+
throw new Error(`Local session file still present after clearSession: ${this.sessionPath}`);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
if (!this.client || !this.connected) {
|
|
215
|
+
if (existsSync(this.sessionPath))
|
|
216
|
+
await wipeLocalOrThrow();
|
|
203
217
|
return false;
|
|
218
|
+
}
|
|
219
|
+
const client = this.client;
|
|
220
|
+
let revoked = false;
|
|
204
221
|
try {
|
|
205
|
-
await
|
|
206
|
-
|
|
207
|
-
this.connected = false;
|
|
208
|
-
this.sessionString = "";
|
|
209
|
-
this.client = null;
|
|
210
|
-
return true;
|
|
222
|
+
await client.invoke(new Api.auth.LogOut());
|
|
223
|
+
revoked = true;
|
|
211
224
|
}
|
|
212
225
|
catch (error) {
|
|
213
|
-
console.error("[telegram]
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
console.error("[telegram] auth.LogOut failed:", error);
|
|
227
|
+
}
|
|
228
|
+
// destroy() failure must NOT mask a successful server revoke — log and continue.
|
|
229
|
+
try {
|
|
230
|
+
await client.destroy();
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.error("[telegram] client.destroy failed during logOut:", err);
|
|
216
234
|
}
|
|
235
|
+
await wipeLocalOrThrow();
|
|
236
|
+
return revoked;
|
|
217
237
|
}
|
|
218
238
|
isConnected() {
|
|
219
239
|
return this.connected;
|
|
@@ -353,20 +373,60 @@ export class TelegramService {
|
|
|
353
373
|
firstName: user.firstName ?? undefined,
|
|
354
374
|
};
|
|
355
375
|
}
|
|
356
|
-
async sendMessage(chatId, text, replyTo, parseMode, topicId) {
|
|
376
|
+
async sendMessage(chatId, text, replyTo, parseMode, topicId, extra) {
|
|
357
377
|
if (!this.client || !this.connected)
|
|
358
378
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
379
|
+
const client = this.client;
|
|
359
380
|
return this.rateLimiter.execute(async () => {
|
|
360
381
|
const resolved = await this.resolvePeer(chatId);
|
|
382
|
+
// Raw path: high-level client.sendMessage does not support quoteText/effect.
|
|
383
|
+
// Fall back to messages.SendMessage when either is requested.
|
|
384
|
+
if (extra?.quoteText || extra?.effect) {
|
|
385
|
+
if (extra.quoteText && !replyTo) {
|
|
386
|
+
throw new Error("quoteText requires replyTo — provide the message ID of the message you are quoting");
|
|
387
|
+
}
|
|
388
|
+
const replyToObj = extra.quoteText
|
|
389
|
+
? new Api.InputReplyToMessage({
|
|
390
|
+
replyToMsgId: replyTo,
|
|
391
|
+
topMsgId: topicId,
|
|
392
|
+
quoteText: extra.quoteText,
|
|
393
|
+
})
|
|
394
|
+
: buildReplyTo(replyTo, topicId);
|
|
395
|
+
// GramJS parses md/html via the internal `_parseMessageText` helper. We feature-detect
|
|
396
|
+
// it so a future GramJS rename surfaces a clear error instead of silently sending plain.
|
|
397
|
+
let parsedText = text;
|
|
398
|
+
let entities;
|
|
399
|
+
if (parseMode) {
|
|
400
|
+
// biome-ignore lint/suspicious/noExplicitAny: GramJS internal helper, no public typing
|
|
401
|
+
const parser = client._parseMessageText;
|
|
402
|
+
if (typeof parser !== "function") {
|
|
403
|
+
throw new Error("GramJS version incompatible: parseMode not supported in quoteText/effect code path. Omit parseMode or upgrade GramJS.");
|
|
404
|
+
}
|
|
405
|
+
[parsedText, entities] = await parser.call(client, text, parseMode === "html" ? "html" : "md");
|
|
406
|
+
}
|
|
407
|
+
const result = await client.invoke(new Api.messages.SendMessage({
|
|
408
|
+
peer: resolved,
|
|
409
|
+
message: parsedText,
|
|
410
|
+
randomId: generateRandomBigInt(),
|
|
411
|
+
...(replyToObj ? { replyTo: replyToObj } : {}),
|
|
412
|
+
...(entities?.length ? { entities } : {}),
|
|
413
|
+
...(extra.effect ? { effect: bigInt(extra.effect) } : {}),
|
|
414
|
+
}));
|
|
415
|
+
const id = extractMessageId(result);
|
|
416
|
+
if (id === undefined)
|
|
417
|
+
throw new Error("Telegram did not return a message ID for sendMessage");
|
|
418
|
+
// Return a minimal UpdateShortSentMessage — it only carries `id`, avoiding fake peerId/date.
|
|
419
|
+
return new Api.UpdateShortSentMessage({ id, pts: 0, ptsCount: 0, date: Math.floor(Date.now() / 1000) });
|
|
420
|
+
}
|
|
361
421
|
if (topicId) {
|
|
362
|
-
return await
|
|
422
|
+
return await client.sendMessage(resolved, {
|
|
363
423
|
message: text,
|
|
364
424
|
topMsgId: topicId,
|
|
365
425
|
...(replyTo ? { replyTo } : {}),
|
|
366
426
|
...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
|
|
367
427
|
});
|
|
368
428
|
}
|
|
369
|
-
return await
|
|
429
|
+
return await client.sendMessage(resolved, {
|
|
370
430
|
message: text,
|
|
371
431
|
...(replyTo ? { replyTo } : {}),
|
|
372
432
|
...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
|
|
@@ -381,6 +441,188 @@ export class TelegramService {
|
|
|
381
441
|
await this.client?.sendFile(resolved, { file: filePath, caption });
|
|
382
442
|
}, `sendFile to ${chatId}`);
|
|
383
443
|
}
|
|
444
|
+
async sendVoice(chatId, filePath, opts = {}) {
|
|
445
|
+
if (!this.client || !this.connected)
|
|
446
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
447
|
+
const client = this.client;
|
|
448
|
+
return this.rateLimiter.execute(async () => {
|
|
449
|
+
const resolved = await this.resolvePeer(chatId);
|
|
450
|
+
// Duration is intentionally auto-detected by GramJS from the audio file —
|
|
451
|
+
// letting the AI override it would mis-report playback length in the Telegram UI.
|
|
452
|
+
const message = await client.sendFile(resolved, {
|
|
453
|
+
file: filePath,
|
|
454
|
+
voiceNote: true,
|
|
455
|
+
caption: opts.caption,
|
|
456
|
+
parseMode: opts.parseMode,
|
|
457
|
+
...(opts.replyTo ? { replyTo: opts.replyTo } : {}),
|
|
458
|
+
...(opts.topicId ? { topMsgId: opts.topicId } : {}),
|
|
459
|
+
});
|
|
460
|
+
return { id: message.id };
|
|
461
|
+
}, `sendVoice to ${chatId}`);
|
|
462
|
+
}
|
|
463
|
+
async sendVideoNote(chatId, filePath, opts = {}) {
|
|
464
|
+
if (!this.client || !this.connected)
|
|
465
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
466
|
+
const client = this.client;
|
|
467
|
+
return this.rateLimiter.execute(async () => {
|
|
468
|
+
const resolved = await this.resolvePeer(chatId);
|
|
469
|
+
const attributes = opts.duration || opts.length
|
|
470
|
+
? [
|
|
471
|
+
new Api.DocumentAttributeVideo({
|
|
472
|
+
roundMessage: true,
|
|
473
|
+
duration: opts.duration ?? 0,
|
|
474
|
+
w: opts.length ?? 0,
|
|
475
|
+
h: opts.length ?? 0,
|
|
476
|
+
}),
|
|
477
|
+
]
|
|
478
|
+
: undefined;
|
|
479
|
+
const message = await client.sendFile(resolved, {
|
|
480
|
+
file: filePath,
|
|
481
|
+
videoNote: true,
|
|
482
|
+
...(opts.replyTo ? { replyTo: opts.replyTo } : {}),
|
|
483
|
+
...(opts.topicId ? { topMsgId: opts.topicId } : {}),
|
|
484
|
+
...(attributes ? { attributes } : {}),
|
|
485
|
+
});
|
|
486
|
+
return { id: message.id };
|
|
487
|
+
}, `sendVideoNote to ${chatId}`);
|
|
488
|
+
}
|
|
489
|
+
async sendContact(chatId, phone, firstName, opts = {}) {
|
|
490
|
+
if (!this.client || !this.connected)
|
|
491
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
492
|
+
const client = this.client;
|
|
493
|
+
return this.rateLimiter.execute(async () => {
|
|
494
|
+
const resolved = await this.resolvePeer(chatId);
|
|
495
|
+
const media = new Api.InputMediaContact({
|
|
496
|
+
phoneNumber: phone,
|
|
497
|
+
firstName,
|
|
498
|
+
lastName: opts.lastName ?? "",
|
|
499
|
+
vcard: opts.vcard ?? "",
|
|
500
|
+
});
|
|
501
|
+
const result = await client.invoke(new Api.messages.SendMedia({
|
|
502
|
+
peer: resolved,
|
|
503
|
+
media,
|
|
504
|
+
message: "",
|
|
505
|
+
randomId: generateRandomBigInt(),
|
|
506
|
+
replyTo: buildReplyTo(opts.replyTo, opts.topicId),
|
|
507
|
+
}));
|
|
508
|
+
const id = extractMessageId(result);
|
|
509
|
+
if (id === undefined)
|
|
510
|
+
throw new Error("Telegram did not return a message ID for sendContact");
|
|
511
|
+
return { id };
|
|
512
|
+
}, `sendContact to ${chatId}`);
|
|
513
|
+
}
|
|
514
|
+
async sendDice(chatId, emoji, opts = {}) {
|
|
515
|
+
if (!this.client || !this.connected)
|
|
516
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
517
|
+
const client = this.client;
|
|
518
|
+
return this.rateLimiter.execute(async () => {
|
|
519
|
+
const resolved = await this.resolvePeer(chatId);
|
|
520
|
+
const result = await client.invoke(new Api.messages.SendMedia({
|
|
521
|
+
peer: resolved,
|
|
522
|
+
media: new Api.InputMediaDice({ emoticon: emoji }),
|
|
523
|
+
message: "",
|
|
524
|
+
randomId: generateRandomBigInt(),
|
|
525
|
+
replyTo: buildReplyTo(opts.replyTo, opts.topicId),
|
|
526
|
+
}));
|
|
527
|
+
const dice = extractDiceResult(result);
|
|
528
|
+
if (!dice) {
|
|
529
|
+
const id = extractMessageId(result);
|
|
530
|
+
if (id === undefined)
|
|
531
|
+
throw new Error("Telegram did not return a message ID for sendDice");
|
|
532
|
+
return { id };
|
|
533
|
+
}
|
|
534
|
+
return dice;
|
|
535
|
+
}, `sendDice to ${chatId}`);
|
|
536
|
+
}
|
|
537
|
+
async sendLocation(chatId, latitude, longitude, opts = {}) {
|
|
538
|
+
if (!this.client || !this.connected)
|
|
539
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
540
|
+
const client = this.client;
|
|
541
|
+
return this.rateLimiter.execute(async () => {
|
|
542
|
+
const resolved = await this.resolvePeer(chatId);
|
|
543
|
+
const geoPoint = new Api.InputGeoPoint({
|
|
544
|
+
lat: latitude,
|
|
545
|
+
long: longitude,
|
|
546
|
+
...(opts.accuracyRadius !== undefined ? { accuracyRadius: opts.accuracyRadius } : {}),
|
|
547
|
+
});
|
|
548
|
+
const media = opts.livePeriod
|
|
549
|
+
? new Api.InputMediaGeoLive({
|
|
550
|
+
geoPoint,
|
|
551
|
+
period: opts.livePeriod,
|
|
552
|
+
...(opts.heading !== undefined ? { heading: opts.heading } : {}),
|
|
553
|
+
...(opts.proximityRadius !== undefined ? { proximityNotificationRadius: opts.proximityRadius } : {}),
|
|
554
|
+
})
|
|
555
|
+
: new Api.InputMediaGeoPoint({ geoPoint });
|
|
556
|
+
const result = await client.invoke(new Api.messages.SendMedia({
|
|
557
|
+
peer: resolved,
|
|
558
|
+
media,
|
|
559
|
+
message: "",
|
|
560
|
+
randomId: generateRandomBigInt(),
|
|
561
|
+
replyTo: buildReplyTo(opts.replyTo, opts.topicId),
|
|
562
|
+
}));
|
|
563
|
+
const id = extractMessageId(result);
|
|
564
|
+
if (id === undefined)
|
|
565
|
+
throw new Error("Telegram did not return a message ID for sendLocation");
|
|
566
|
+
return { id };
|
|
567
|
+
}, `sendLocation to ${chatId}`);
|
|
568
|
+
}
|
|
569
|
+
async sendVenue(chatId, latitude, longitude, title, address, opts = {}) {
|
|
570
|
+
if (!this.client || !this.connected)
|
|
571
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
572
|
+
const client = this.client;
|
|
573
|
+
return this.rateLimiter.execute(async () => {
|
|
574
|
+
const resolved = await this.resolvePeer(chatId);
|
|
575
|
+
const media = new Api.InputMediaVenue({
|
|
576
|
+
geoPoint: new Api.InputGeoPoint({ lat: latitude, long: longitude }),
|
|
577
|
+
title,
|
|
578
|
+
address,
|
|
579
|
+
provider: opts.provider ?? "foursquare",
|
|
580
|
+
venueId: opts.venueId ?? "",
|
|
581
|
+
venueType: opts.venueType ?? "",
|
|
582
|
+
});
|
|
583
|
+
const result = await client.invoke(new Api.messages.SendMedia({
|
|
584
|
+
peer: resolved,
|
|
585
|
+
media,
|
|
586
|
+
message: "",
|
|
587
|
+
randomId: generateRandomBigInt(),
|
|
588
|
+
replyTo: buildReplyTo(opts.replyTo, opts.topicId),
|
|
589
|
+
}));
|
|
590
|
+
const id = extractMessageId(result);
|
|
591
|
+
if (id === undefined)
|
|
592
|
+
throw new Error("Telegram did not return a message ID for sendVenue");
|
|
593
|
+
return { id };
|
|
594
|
+
}, `sendVenue to ${chatId}`);
|
|
595
|
+
}
|
|
596
|
+
async sendAlbum(chatId, items, opts = {}) {
|
|
597
|
+
if (!this.client || !this.connected)
|
|
598
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
599
|
+
if (items.length < 2 || items.length > 10) {
|
|
600
|
+
throw new Error("Album requires 2-10 items");
|
|
601
|
+
}
|
|
602
|
+
const client = this.client;
|
|
603
|
+
return this.rateLimiter.execute(async () => {
|
|
604
|
+
const resolved = await this.resolvePeer(chatId);
|
|
605
|
+
// Album-level caption lands on the first item; per-item captions stay as provided.
|
|
606
|
+
const captions = items.map((it, i) => (i === 0 ? (opts.caption ?? it.caption ?? "") : (it.caption ?? "")));
|
|
607
|
+
// GramJS sendFile auto-detects `file: string[]` and takes the _sendAlbum path,
|
|
608
|
+
// which invokes messages.UploadMedia per item + messages.SendMultiMedia.
|
|
609
|
+
const result = (await client.sendFile(resolved, {
|
|
610
|
+
file: items.map((it) => it.filePath),
|
|
611
|
+
caption: captions,
|
|
612
|
+
parseMode: opts.parseMode,
|
|
613
|
+
...(opts.replyTo ? { replyTo: opts.replyTo } : {}),
|
|
614
|
+
...(opts.topicId ? { topMsgId: opts.topicId } : {}),
|
|
615
|
+
}));
|
|
616
|
+
const ids = Array.isArray(result)
|
|
617
|
+
? result.filter((m) => m instanceof Api.Message).map((m) => m.id)
|
|
618
|
+
: result instanceof Api.Message
|
|
619
|
+
? [result.id]
|
|
620
|
+
: [];
|
|
621
|
+
if (ids.length === 0)
|
|
622
|
+
throw new Error("Telegram did not return any message IDs for sendAlbum");
|
|
623
|
+
return { ids };
|
|
624
|
+
}, `sendAlbum to ${chatId}`);
|
|
625
|
+
}
|
|
384
626
|
async downloadMedia(chatId, messageId, downloadPath) {
|
|
385
627
|
if (!this.client || !this.connected)
|
|
386
628
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -1593,6 +1835,246 @@ export class TelegramService {
|
|
|
1593
1835
|
}
|
|
1594
1836
|
return 0;
|
|
1595
1837
|
}
|
|
1838
|
+
// ─── Poll interaction ──────────────────────────────────────────────────────
|
|
1839
|
+
async sendPollVote(chatId, messageId, optionIndexes) {
|
|
1840
|
+
if (!this.client || !this.connected)
|
|
1841
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1842
|
+
const peer = await this.resolvePeer(chatId);
|
|
1843
|
+
const client = this.client;
|
|
1844
|
+
return this.rateLimiter.execute(async () => {
|
|
1845
|
+
const options = optionIndexes.map((i) => Buffer.from([i]));
|
|
1846
|
+
const result = await client.invoke(new Api.messages.SendVote({ peer, msgId: messageId, options }));
|
|
1847
|
+
const pollMedia = extractPollMediaFromUpdates(result);
|
|
1848
|
+
const results = pollMedia?.results;
|
|
1849
|
+
const poll = pollMedia?.poll;
|
|
1850
|
+
return {
|
|
1851
|
+
totalVoters: results?.totalVoters ?? 0,
|
|
1852
|
+
chosenLabels: optionIndexes.map((i) => {
|
|
1853
|
+
const answer = poll?.answers?.[i];
|
|
1854
|
+
return answer ? answer.text.text : `#${i}`;
|
|
1855
|
+
}),
|
|
1856
|
+
isRetracted: optionIndexes.length === 0,
|
|
1857
|
+
};
|
|
1858
|
+
}, `sendPollVote ${chatId}/${messageId}`);
|
|
1859
|
+
}
|
|
1860
|
+
async getPollResults(chatId, messageId) {
|
|
1861
|
+
if (!this.client || !this.connected)
|
|
1862
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1863
|
+
const peer = await this.resolvePeer(chatId);
|
|
1864
|
+
const client = this.client;
|
|
1865
|
+
return this.rateLimiter.execute(async () => {
|
|
1866
|
+
const msgs = await client.getMessages(peer, { ids: [messageId] });
|
|
1867
|
+
if (!(msgs[0]?.media instanceof Api.MessageMediaPoll)) {
|
|
1868
|
+
throw new Error("Message is not a poll");
|
|
1869
|
+
}
|
|
1870
|
+
const pollMedia = msgs[0].media;
|
|
1871
|
+
// Refresh results from server
|
|
1872
|
+
try {
|
|
1873
|
+
await client.invoke(new Api.messages.GetPollResults({ peer, msgId: messageId }));
|
|
1874
|
+
}
|
|
1875
|
+
catch {
|
|
1876
|
+
// ignore — use whatever is in the message
|
|
1877
|
+
}
|
|
1878
|
+
return summarizePoll(pollMedia.poll, pollMedia.results);
|
|
1879
|
+
}, `getPollResults ${chatId}/${messageId}`);
|
|
1880
|
+
}
|
|
1881
|
+
async getPollVoters(chatId, messageId, opts) {
|
|
1882
|
+
if (!this.client || !this.connected)
|
|
1883
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1884
|
+
const peer = await this.resolvePeer(chatId);
|
|
1885
|
+
const client = this.client;
|
|
1886
|
+
return this.rateLimiter.execute(async () => {
|
|
1887
|
+
const optionIndex = opts?.optionIndex;
|
|
1888
|
+
const result = (await client.invoke(new Api.messages.GetPollVotes({
|
|
1889
|
+
peer,
|
|
1890
|
+
id: messageId,
|
|
1891
|
+
option: optionIndex !== undefined ? Buffer.from([optionIndex]) : undefined,
|
|
1892
|
+
offset: opts?.offset,
|
|
1893
|
+
limit: opts?.limit ?? 20,
|
|
1894
|
+
})));
|
|
1895
|
+
// Build user map
|
|
1896
|
+
const userMap = new Map();
|
|
1897
|
+
for (const u of result.users ?? []) {
|
|
1898
|
+
const user = u;
|
|
1899
|
+
const id = user.id?.toString() ?? "";
|
|
1900
|
+
userMap.set(id, {
|
|
1901
|
+
name: [user.firstName, user.lastName].filter(Boolean).join(" ") || undefined,
|
|
1902
|
+
username: user.username ?? undefined,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
const voters = (result.votes ?? []).map((v) => {
|
|
1906
|
+
const vote = v;
|
|
1907
|
+
const peerId = extractPeerId(vote.peer);
|
|
1908
|
+
const info = userMap.get(peerId) ?? {};
|
|
1909
|
+
let options = [];
|
|
1910
|
+
if ("option" in vote && vote.option) {
|
|
1911
|
+
options = [Buffer.from(vote.option).toString("hex")];
|
|
1912
|
+
}
|
|
1913
|
+
else if ("options" in vote && vote.options) {
|
|
1914
|
+
options = vote.options.map((o) => Buffer.from(o).toString("hex"));
|
|
1915
|
+
}
|
|
1916
|
+
return {
|
|
1917
|
+
peerId,
|
|
1918
|
+
name: info.name,
|
|
1919
|
+
username: info.username,
|
|
1920
|
+
options,
|
|
1921
|
+
date: vote.date,
|
|
1922
|
+
};
|
|
1923
|
+
});
|
|
1924
|
+
return { total: result.count, nextOffset: result.nextOffset, voters };
|
|
1925
|
+
}, `getPollVoters ${chatId}/${messageId}`);
|
|
1926
|
+
}
|
|
1927
|
+
async closePoll(chatId, messageId) {
|
|
1928
|
+
if (!this.client || !this.connected)
|
|
1929
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1930
|
+
const peer = await this.resolvePeer(chatId);
|
|
1931
|
+
const client = this.client;
|
|
1932
|
+
return this.rateLimiter.execute(async () => {
|
|
1933
|
+
// Step 1: fetch existing poll
|
|
1934
|
+
const msgs = await client.getMessages(peer, { ids: [messageId] });
|
|
1935
|
+
if (!(msgs[0]?.media instanceof Api.MessageMediaPoll)) {
|
|
1936
|
+
throw new Error("Message is not a poll");
|
|
1937
|
+
}
|
|
1938
|
+
const pollMedia = msgs[0].media;
|
|
1939
|
+
const originalPoll = pollMedia.poll;
|
|
1940
|
+
// Step 2: build closed poll (preserve all flags)
|
|
1941
|
+
const closedPoll = new Api.Poll({
|
|
1942
|
+
id: originalPoll.id,
|
|
1943
|
+
question: originalPoll.question,
|
|
1944
|
+
answers: originalPoll.answers,
|
|
1945
|
+
closed: true,
|
|
1946
|
+
publicVoters: originalPoll.publicVoters,
|
|
1947
|
+
multipleChoice: originalPoll.multipleChoice,
|
|
1948
|
+
quiz: originalPoll.quiz,
|
|
1949
|
+
closePeriod: originalPoll.closePeriod,
|
|
1950
|
+
closeDate: originalPoll.closeDate,
|
|
1951
|
+
});
|
|
1952
|
+
// Step 3: edit message to close poll
|
|
1953
|
+
const result = await client.invoke(new Api.messages.EditMessage({
|
|
1954
|
+
peer,
|
|
1955
|
+
id: messageId,
|
|
1956
|
+
media: new Api.InputMediaPoll({ poll: closedPoll }),
|
|
1957
|
+
}));
|
|
1958
|
+
// Extract updated voter count from result updates
|
|
1959
|
+
const pollInfo = extractPollMediaFromUpdates(result);
|
|
1960
|
+
const totalVoters = pollInfo?.results?.totalVoters ?? pollMedia.results?.totalVoters ?? 0;
|
|
1961
|
+
return { totalVoters };
|
|
1962
|
+
}, `closePoll ${chatId}/${messageId}`);
|
|
1963
|
+
}
|
|
1964
|
+
// ─── Audio transcription ───────────────────────────────────────────────────
|
|
1965
|
+
async transcribeAudio(chatId, messageId) {
|
|
1966
|
+
if (!this.client || !this.connected)
|
|
1967
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1968
|
+
const peer = await this.resolvePeer(chatId);
|
|
1969
|
+
const client = this.client;
|
|
1970
|
+
return this.rateLimiter.execute(async () => {
|
|
1971
|
+
const result = (await client.invoke(new Api.messages.TranscribeAudio({ peer, msgId: messageId })));
|
|
1972
|
+
return {
|
|
1973
|
+
transcriptionId: result.transcriptionId.toString(),
|
|
1974
|
+
text: result.text ?? "",
|
|
1975
|
+
pending: result.pending ?? false,
|
|
1976
|
+
trialRemainsNum: result.trialRemainsNum,
|
|
1977
|
+
trialRemainsUntilDate: result.trialRemainsUntilDate,
|
|
1978
|
+
};
|
|
1979
|
+
}, `transcribeAudio ${chatId}/${messageId}`);
|
|
1980
|
+
}
|
|
1981
|
+
async rateTranscription(chatId, messageId, transcriptionId, good) {
|
|
1982
|
+
if (!this.client || !this.connected)
|
|
1983
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1984
|
+
const peer = await this.resolvePeer(chatId);
|
|
1985
|
+
const client = this.client;
|
|
1986
|
+
await this.rateLimiter.execute(async () => {
|
|
1987
|
+
await client.invoke(new Api.messages.RateTranscribedAudio({
|
|
1988
|
+
peer,
|
|
1989
|
+
msgId: messageId,
|
|
1990
|
+
transcriptionId: bigInt(transcriptionId),
|
|
1991
|
+
good,
|
|
1992
|
+
}));
|
|
1993
|
+
}, `rateTranscription ${chatId}/${messageId}`);
|
|
1994
|
+
}
|
|
1995
|
+
// ─── Fact-check ────────────────────────────────────────────────────────────
|
|
1996
|
+
async getFactCheck(chatId, messageIds) {
|
|
1997
|
+
if (!this.client || !this.connected)
|
|
1998
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1999
|
+
const peer = await this.resolvePeer(chatId);
|
|
2000
|
+
const client = this.client;
|
|
2001
|
+
return this.rateLimiter.execute(async () => {
|
|
2002
|
+
const result = (await client.invoke(new Api.messages.GetFactCheck({ peer, msgId: messageIds })));
|
|
2003
|
+
return result.map((fc, i) => ({
|
|
2004
|
+
messageId: messageIds[i],
|
|
2005
|
+
needCheck: fc.needCheck ?? false,
|
|
2006
|
+
country: fc.country,
|
|
2007
|
+
text: fc.text?.text,
|
|
2008
|
+
hash: fc.hash.toString(),
|
|
2009
|
+
}));
|
|
2010
|
+
}, `getFactCheck ${chatId}`);
|
|
2011
|
+
}
|
|
2012
|
+
async editFactCheck(chatId, messageId, text, _opts) {
|
|
2013
|
+
if (!this.client || !this.connected)
|
|
2014
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
2015
|
+
const peer = await this.resolvePeer(chatId);
|
|
2016
|
+
const client = this.client;
|
|
2017
|
+
await this.rateLimiter.execute(async () => {
|
|
2018
|
+
// Build TextWithEntities — basic plain text (no entity parsing for fact-checks)
|
|
2019
|
+
const textObj = new Api.TextWithEntities({ text, entities: [] });
|
|
2020
|
+
await client.invoke(new Api.messages.EditFactCheck({ peer, msgId: messageId, text: textObj }));
|
|
2021
|
+
}, `editFactCheck ${chatId}/${messageId}`);
|
|
2022
|
+
}
|
|
2023
|
+
async deleteFactCheck(chatId, messageId) {
|
|
2024
|
+
if (!this.client || !this.connected)
|
|
2025
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
2026
|
+
const peer = await this.resolvePeer(chatId);
|
|
2027
|
+
const client = this.client;
|
|
2028
|
+
await this.rateLimiter.execute(async () => {
|
|
2029
|
+
await client.invoke(new Api.messages.DeleteFactCheck({ peer, msgId: messageId }));
|
|
2030
|
+
}, `deleteFactCheck ${chatId}/${messageId}`);
|
|
2031
|
+
}
|
|
2032
|
+
// ─── Paid reactions ────────────────────────────────────────────────────────
|
|
2033
|
+
async sendPaidReaction(chatId, messageId, count, opts) {
|
|
2034
|
+
if (!this.client || !this.connected)
|
|
2035
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
2036
|
+
const peer = await this.resolvePeer(chatId);
|
|
2037
|
+
const client = this.client;
|
|
2038
|
+
return this.rateLimiter.execute(async () => {
|
|
2039
|
+
const randomId = generateRandomBigInt();
|
|
2040
|
+
const params = { peer, msgId: messageId, count, randomId };
|
|
2041
|
+
if (opts?.private !== undefined)
|
|
2042
|
+
params.private = opts.private;
|
|
2043
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic params for optional `private` field
|
|
2044
|
+
await client.invoke(new Api.messages.SendPaidReaction(params));
|
|
2045
|
+
return { count };
|
|
2046
|
+
}, `sendPaidReaction ${chatId}/${messageId}`);
|
|
2047
|
+
}
|
|
2048
|
+
async togglePaidReactionPrivacy(chatId, messageId, privateFlag) {
|
|
2049
|
+
if (!this.client || !this.connected)
|
|
2050
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
2051
|
+
const peer = await this.resolvePeer(chatId);
|
|
2052
|
+
const client = this.client;
|
|
2053
|
+
await this.rateLimiter.execute(async () => {
|
|
2054
|
+
await client.invoke(new Api.messages.TogglePaidReactionPrivacy({ peer, msgId: messageId, private: privateFlag }));
|
|
2055
|
+
}, `togglePaidReactionPrivacy ${chatId}/${messageId}`);
|
|
2056
|
+
}
|
|
2057
|
+
async getPaidReactionPrivacy() {
|
|
2058
|
+
if (!this.client || !this.connected)
|
|
2059
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
2060
|
+
const client = this.client;
|
|
2061
|
+
return this.rateLimiter.execute(async () => {
|
|
2062
|
+
const result = await client.invoke(new Api.messages.GetPaidReactionPrivacy());
|
|
2063
|
+
let list = [];
|
|
2064
|
+
if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
|
|
2065
|
+
list = result.updates;
|
|
2066
|
+
}
|
|
2067
|
+
else if (result instanceof Api.UpdateShort) {
|
|
2068
|
+
list = [result.update];
|
|
2069
|
+
}
|
|
2070
|
+
for (const u of list) {
|
|
2071
|
+
if (u instanceof Api.UpdatePaidReactionPrivacy) {
|
|
2072
|
+
return { private: Boolean(u.private) };
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return { private: false };
|
|
2076
|
+
}, "getPaidReactionPrivacy");
|
|
2077
|
+
}
|
|
1596
2078
|
async getForumTopics(chatId, limit = 100) {
|
|
1597
2079
|
if (!this.client || !this.connected)
|
|
1598
2080
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -3105,6 +3587,412 @@ export class TelegramService {
|
|
|
3105
3587
|
return summarizeBusinessChatLinks(response);
|
|
3106
3588
|
}, "getBusinessChatLinks");
|
|
3107
3589
|
}
|
|
3590
|
+
// ─── Profile write (v1.32.0) ───────────────────────────────────────────────
|
|
3591
|
+
async setEmojiStatus(opts) {
|
|
3592
|
+
if (!this.client || !this.connected)
|
|
3593
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3594
|
+
const client = this.client;
|
|
3595
|
+
return this.rateLimiter.execute(async () => {
|
|
3596
|
+
let emojiStatus;
|
|
3597
|
+
if (opts.collectibleId) {
|
|
3598
|
+
emojiStatus = new Api.InputEmojiStatusCollectible({
|
|
3599
|
+
collectibleId: bigInt(opts.collectibleId),
|
|
3600
|
+
until: opts.untilUnix,
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
else if (opts.documentId) {
|
|
3604
|
+
emojiStatus = new Api.EmojiStatus({
|
|
3605
|
+
documentId: bigInt(opts.documentId),
|
|
3606
|
+
until: opts.untilUnix,
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
else {
|
|
3610
|
+
emojiStatus = new Api.EmojiStatusEmpty();
|
|
3611
|
+
}
|
|
3612
|
+
await client.invoke(new Api.account.UpdateEmojiStatus({ emojiStatus }));
|
|
3613
|
+
}, "setEmojiStatus");
|
|
3614
|
+
}
|
|
3615
|
+
async listEmojiStatuses(kind, limit) {
|
|
3616
|
+
if (!this.client || !this.connected)
|
|
3617
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3618
|
+
const client = this.client;
|
|
3619
|
+
return this.rateLimiter.execute(async () => {
|
|
3620
|
+
const hash = bigInt(0);
|
|
3621
|
+
let resp;
|
|
3622
|
+
if (kind === "recent") {
|
|
3623
|
+
resp = await client.invoke(new Api.account.GetRecentEmojiStatuses({ hash }));
|
|
3624
|
+
}
|
|
3625
|
+
else if (kind === "channel_default") {
|
|
3626
|
+
resp = await client.invoke(new Api.account.GetChannelDefaultEmojiStatuses({ hash }));
|
|
3627
|
+
}
|
|
3628
|
+
else if (kind === "collectible") {
|
|
3629
|
+
resp = await client.invoke(new Api.account.GetCollectibleEmojiStatuses({ hash }));
|
|
3630
|
+
}
|
|
3631
|
+
else {
|
|
3632
|
+
resp = await client.invoke(new Api.account.GetDefaultEmojiStatuses({ hash }));
|
|
3633
|
+
}
|
|
3634
|
+
if (resp.className === "account.EmojiStatusesNotModified")
|
|
3635
|
+
return [];
|
|
3636
|
+
const statuses = resp.statuses ?? [];
|
|
3637
|
+
return statuses.slice(0, limit).map(summarizeEmojiStatus);
|
|
3638
|
+
}, `listEmojiStatuses ${kind}`);
|
|
3639
|
+
}
|
|
3640
|
+
async clearRecentEmojiStatuses() {
|
|
3641
|
+
if (!this.client || !this.connected)
|
|
3642
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3643
|
+
const client = this.client;
|
|
3644
|
+
return this.rateLimiter.execute(async () => {
|
|
3645
|
+
await client.invoke(new Api.account.ClearRecentEmojiStatuses());
|
|
3646
|
+
}, "clearRecentEmojiStatuses");
|
|
3647
|
+
}
|
|
3648
|
+
async setProfileColor(opts) {
|
|
3649
|
+
if (!this.client || !this.connected)
|
|
3650
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3651
|
+
const client = this.client;
|
|
3652
|
+
return this.rateLimiter.execute(async () => {
|
|
3653
|
+
await client.invoke(new Api.account.UpdateColor({
|
|
3654
|
+
forProfile: opts.forProfile || undefined,
|
|
3655
|
+
color: opts.color,
|
|
3656
|
+
backgroundEmojiId: opts.backgroundEmojiId ? bigInt(opts.backgroundEmojiId) : undefined,
|
|
3657
|
+
}));
|
|
3658
|
+
}, "setProfileColor");
|
|
3659
|
+
}
|
|
3660
|
+
async setBirthday(opts) {
|
|
3661
|
+
if (!this.client || !this.connected)
|
|
3662
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3663
|
+
const client = this.client;
|
|
3664
|
+
return this.rateLimiter.execute(async () => {
|
|
3665
|
+
const birthday = opts.clear || !opts.day || !opts.month
|
|
3666
|
+
? undefined
|
|
3667
|
+
: new Api.Birthday({ day: opts.day, month: opts.month, year: opts.year });
|
|
3668
|
+
await client.invoke(new Api.account.UpdateBirthday({ birthday }));
|
|
3669
|
+
}, "setBirthday");
|
|
3670
|
+
}
|
|
3671
|
+
async setPersonalChannel(opts) {
|
|
3672
|
+
if (!this.client || !this.connected)
|
|
3673
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3674
|
+
const client = this.client;
|
|
3675
|
+
return this.rateLimiter.execute(async () => {
|
|
3676
|
+
if (opts.clear) {
|
|
3677
|
+
await client.invoke(new Api.account.UpdatePersonalChannel({ channel: new Api.InputChannelEmpty() }));
|
|
3678
|
+
return null;
|
|
3679
|
+
}
|
|
3680
|
+
const entity = await client.getInputEntity(opts.channelId ?? "");
|
|
3681
|
+
if (!(entity instanceof Api.InputPeerChannel)) {
|
|
3682
|
+
throw new Error(`Not a channel: ${opts.channelId}`);
|
|
3683
|
+
}
|
|
3684
|
+
const channel = new Api.InputChannel({
|
|
3685
|
+
channelId: entity.channelId,
|
|
3686
|
+
accessHash: entity.accessHash,
|
|
3687
|
+
});
|
|
3688
|
+
await client.invoke(new Api.account.UpdatePersonalChannel({ channel }));
|
|
3689
|
+
const info = await client.getEntity(entity);
|
|
3690
|
+
return "title" in info ? info.title : (opts.channelId ?? "");
|
|
3691
|
+
}, `setPersonalChannel ${opts.channelId ?? "clear"}`);
|
|
3692
|
+
}
|
|
3693
|
+
async setProfilePhoto(opts) {
|
|
3694
|
+
if (!this.client || !this.connected)
|
|
3695
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3696
|
+
const client = this.client;
|
|
3697
|
+
return this.rateLimiter.execute(async () => {
|
|
3698
|
+
const inputFile = await client.uploadFile({
|
|
3699
|
+
file: new CustomFile(opts.filePath.split("/").pop() ?? "upload", (await import("node:fs")).statSync(opts.filePath).size, opts.filePath),
|
|
3700
|
+
workers: 4,
|
|
3701
|
+
});
|
|
3702
|
+
const request = opts.isVideo
|
|
3703
|
+
? new Api.photos.UploadProfilePhoto({
|
|
3704
|
+
fallback: opts.fallback || undefined,
|
|
3705
|
+
video: inputFile,
|
|
3706
|
+
videoStartTs: opts.videoStartTs,
|
|
3707
|
+
})
|
|
3708
|
+
: new Api.photos.UploadProfilePhoto({
|
|
3709
|
+
fallback: opts.fallback || undefined,
|
|
3710
|
+
file: inputFile,
|
|
3711
|
+
});
|
|
3712
|
+
const result = await client.invoke(request);
|
|
3713
|
+
const photo = result.photo;
|
|
3714
|
+
return { id: photo.id.toString() };
|
|
3715
|
+
}, "setProfilePhoto");
|
|
3716
|
+
}
|
|
3717
|
+
async deleteProfilePhotos(photoIds) {
|
|
3718
|
+
if (!this.client || !this.connected)
|
|
3719
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3720
|
+
const client = this.client;
|
|
3721
|
+
return this.rateLimiter.execute(async () => {
|
|
3722
|
+
const me = await client.getMe();
|
|
3723
|
+
const all = await client.invoke(new Api.photos.GetUserPhotos({
|
|
3724
|
+
userId: me.id,
|
|
3725
|
+
offset: 0,
|
|
3726
|
+
maxId: bigInt(0),
|
|
3727
|
+
limit: 100,
|
|
3728
|
+
}));
|
|
3729
|
+
const byId = new Map();
|
|
3730
|
+
for (const p of all.photos) {
|
|
3731
|
+
if (p instanceof Api.Photo)
|
|
3732
|
+
byId.set(p.id.toString(), p);
|
|
3733
|
+
}
|
|
3734
|
+
const inputs = [];
|
|
3735
|
+
const missing = [];
|
|
3736
|
+
for (const pid of photoIds) {
|
|
3737
|
+
const photo = byId.get(pid);
|
|
3738
|
+
if (!photo) {
|
|
3739
|
+
missing.push(pid);
|
|
3740
|
+
continue;
|
|
3741
|
+
}
|
|
3742
|
+
inputs.push(new Api.InputPhoto({
|
|
3743
|
+
id: photo.id,
|
|
3744
|
+
accessHash: photo.accessHash,
|
|
3745
|
+
fileReference: photo.fileReference,
|
|
3746
|
+
}));
|
|
3747
|
+
}
|
|
3748
|
+
if (inputs.length === 0) {
|
|
3749
|
+
throw new Error(`No matching photos found. Missing IDs: ${missing.join(", ")}`);
|
|
3750
|
+
}
|
|
3751
|
+
const deletedIds = await client.invoke(new Api.photos.DeletePhotos({ id: inputs }));
|
|
3752
|
+
return { deleted: deletedIds.map((x) => x.toString()), missing };
|
|
3753
|
+
}, "deleteProfilePhotos");
|
|
3754
|
+
}
|
|
3755
|
+
// ─── Business write (v1.32.0) ──────────────────────────────────────────────
|
|
3756
|
+
async buildBusinessRecipients(opts) {
|
|
3757
|
+
const flags = {};
|
|
3758
|
+
switch (opts.audience) {
|
|
3759
|
+
case "all_new":
|
|
3760
|
+
flags.contacts = true;
|
|
3761
|
+
flags.nonContacts = true;
|
|
3762
|
+
break;
|
|
3763
|
+
case "contacts_only":
|
|
3764
|
+
flags.contacts = true;
|
|
3765
|
+
break;
|
|
3766
|
+
case "non_contacts":
|
|
3767
|
+
flags.nonContacts = true;
|
|
3768
|
+
break;
|
|
3769
|
+
case "existing_only":
|
|
3770
|
+
flags.existingChats = true;
|
|
3771
|
+
break;
|
|
3772
|
+
}
|
|
3773
|
+
if (opts.includeUsers?.length) {
|
|
3774
|
+
flags.users = await this.resolveInputUsers(opts.includeUsers);
|
|
3775
|
+
}
|
|
3776
|
+
else if (opts.excludeUsers?.length) {
|
|
3777
|
+
flags.users = await this.resolveInputUsers(opts.excludeUsers);
|
|
3778
|
+
flags.excludeSelected = true;
|
|
3779
|
+
}
|
|
3780
|
+
return new Api.InputBusinessRecipients({ ...flags });
|
|
3781
|
+
}
|
|
3782
|
+
async resolveInputUsers(ids) {
|
|
3783
|
+
if (!this.client)
|
|
3784
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3785
|
+
const client = this.client;
|
|
3786
|
+
const result = [];
|
|
3787
|
+
for (const id of ids) {
|
|
3788
|
+
const entity = await client.getInputEntity(id);
|
|
3789
|
+
if (entity instanceof Api.InputPeerUser) {
|
|
3790
|
+
result.push(new Api.InputUser({ userId: entity.userId, accessHash: entity.accessHash }));
|
|
3791
|
+
}
|
|
3792
|
+
else {
|
|
3793
|
+
throw new Error(`Not a user: ${id}`);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
return result;
|
|
3797
|
+
}
|
|
3798
|
+
async parseEntities(text, parseMode) {
|
|
3799
|
+
if (!this.client || !parseMode)
|
|
3800
|
+
return { text };
|
|
3801
|
+
// biome-ignore lint/suspicious/noExplicitAny: internal GramJS API
|
|
3802
|
+
const parser = this.client._parseMessageText?.bind(this.client);
|
|
3803
|
+
if (!parser)
|
|
3804
|
+
return { text };
|
|
3805
|
+
const [parsedText, entities] = await parser(text, parseMode === "md" ? "markdown" : "html");
|
|
3806
|
+
return { text: parsedText, entities: entities?.length ? entities : undefined };
|
|
3807
|
+
}
|
|
3808
|
+
async setBusinessWorkHours(opts) {
|
|
3809
|
+
if (!this.client || !this.connected)
|
|
3810
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3811
|
+
const client = this.client;
|
|
3812
|
+
return this.rateLimiter.execute(async () => {
|
|
3813
|
+
if (opts.clear) {
|
|
3814
|
+
await client.invoke(new Api.account.UpdateBusinessWorkHours({}));
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
const dayOffsets = {
|
|
3818
|
+
mon: 0,
|
|
3819
|
+
tue: 1,
|
|
3820
|
+
wed: 2,
|
|
3821
|
+
thu: 3,
|
|
3822
|
+
fri: 4,
|
|
3823
|
+
sat: 5,
|
|
3824
|
+
sun: 6,
|
|
3825
|
+
};
|
|
3826
|
+
const weeklyOpen = (opts.schedule ?? []).map((s) => {
|
|
3827
|
+
const [fh, fm] = s.openFrom.split(":").map(Number);
|
|
3828
|
+
const [th, tm] = s.openTo.split(":").map(Number);
|
|
3829
|
+
const base = (dayOffsets[s.day] ?? 0) * 1440;
|
|
3830
|
+
return new Api.BusinessWeeklyOpen({
|
|
3831
|
+
startMinute: base + fh * 60 + fm,
|
|
3832
|
+
endMinute: base + th * 60 + tm,
|
|
3833
|
+
});
|
|
3834
|
+
});
|
|
3835
|
+
await client.invoke(new Api.account.UpdateBusinessWorkHours({
|
|
3836
|
+
businessWorkHours: new Api.BusinessWorkHours({
|
|
3837
|
+
openNow: opts.openNow,
|
|
3838
|
+
timezoneId: opts.timezone ?? "",
|
|
3839
|
+
weeklyOpen,
|
|
3840
|
+
}),
|
|
3841
|
+
}));
|
|
3842
|
+
}, "setBusinessWorkHours");
|
|
3843
|
+
}
|
|
3844
|
+
async setBusinessLocation(opts) {
|
|
3845
|
+
if (!this.client || !this.connected)
|
|
3846
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3847
|
+
const client = this.client;
|
|
3848
|
+
return this.rateLimiter.execute(async () => {
|
|
3849
|
+
if (opts.clear) {
|
|
3850
|
+
await client.invoke(new Api.account.UpdateBusinessLocation({}));
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
const geoPoint = opts.latitude !== undefined && opts.longitude !== undefined
|
|
3854
|
+
? new Api.InputGeoPoint({ lat: opts.latitude, long: opts.longitude })
|
|
3855
|
+
: undefined;
|
|
3856
|
+
await client.invoke(new Api.account.UpdateBusinessLocation({ geoPoint, address: opts.address }));
|
|
3857
|
+
}, "setBusinessLocation");
|
|
3858
|
+
}
|
|
3859
|
+
async setBusinessGreeting(opts) {
|
|
3860
|
+
if (!this.client || !this.connected)
|
|
3861
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3862
|
+
const client = this.client;
|
|
3863
|
+
return this.rateLimiter.execute(async () => {
|
|
3864
|
+
if (opts.clear) {
|
|
3865
|
+
await client.invoke(new Api.account.UpdateBusinessGreetingMessage({}));
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
const recipients = await this.buildBusinessRecipients({
|
|
3869
|
+
audience: opts.audience,
|
|
3870
|
+
includeUsers: opts.includeUsers,
|
|
3871
|
+
excludeUsers: opts.excludeUsers,
|
|
3872
|
+
});
|
|
3873
|
+
await client.invoke(new Api.account.UpdateBusinessGreetingMessage({
|
|
3874
|
+
message: new Api.InputBusinessGreetingMessage({
|
|
3875
|
+
shortcutId: opts.shortcutId ?? 0,
|
|
3876
|
+
recipients,
|
|
3877
|
+
noActivityDays: opts.noActivityDays,
|
|
3878
|
+
}),
|
|
3879
|
+
}));
|
|
3880
|
+
}, "setBusinessGreeting");
|
|
3881
|
+
}
|
|
3882
|
+
async setBusinessAway(opts) {
|
|
3883
|
+
if (!this.client || !this.connected)
|
|
3884
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3885
|
+
const client = this.client;
|
|
3886
|
+
return this.rateLimiter.execute(async () => {
|
|
3887
|
+
if (opts.clear) {
|
|
3888
|
+
await client.invoke(new Api.account.UpdateBusinessAwayMessage({}));
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
const scheduleObj = opts.schedule === "always"
|
|
3892
|
+
? new Api.BusinessAwayMessageScheduleAlways()
|
|
3893
|
+
: opts.schedule === "outside_hours"
|
|
3894
|
+
? new Api.BusinessAwayMessageScheduleOutsideWorkHours()
|
|
3895
|
+
: new Api.BusinessAwayMessageScheduleCustom({
|
|
3896
|
+
startDate: opts.customFrom ?? 0,
|
|
3897
|
+
endDate: opts.customTo ?? 0,
|
|
3898
|
+
});
|
|
3899
|
+
const recipients = await this.buildBusinessRecipients({
|
|
3900
|
+
audience: opts.audience,
|
|
3901
|
+
includeUsers: opts.includeUsers,
|
|
3902
|
+
excludeUsers: opts.excludeUsers,
|
|
3903
|
+
});
|
|
3904
|
+
await client.invoke(new Api.account.UpdateBusinessAwayMessage({
|
|
3905
|
+
message: new Api.InputBusinessAwayMessage({
|
|
3906
|
+
offlineOnly: opts.offlineOnly || undefined,
|
|
3907
|
+
shortcutId: opts.shortcutId ?? 0,
|
|
3908
|
+
schedule: scheduleObj,
|
|
3909
|
+
recipients,
|
|
3910
|
+
}),
|
|
3911
|
+
}));
|
|
3912
|
+
}, "setBusinessAway");
|
|
3913
|
+
}
|
|
3914
|
+
async setBusinessIntro(opts) {
|
|
3915
|
+
if (!this.client || !this.connected)
|
|
3916
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3917
|
+
const client = this.client;
|
|
3918
|
+
return this.rateLimiter.execute(async () => {
|
|
3919
|
+
if (opts.clear) {
|
|
3920
|
+
await client.invoke(new Api.account.UpdateBusinessIntro({}));
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3923
|
+
const sticker = opts.stickerId && opts.stickerAccessHash && opts.stickerFileReference
|
|
3924
|
+
? new Api.InputDocument({
|
|
3925
|
+
id: bigInt(opts.stickerId),
|
|
3926
|
+
accessHash: bigInt(opts.stickerAccessHash),
|
|
3927
|
+
fileReference: Buffer.from(opts.stickerFileReference, "hex"),
|
|
3928
|
+
})
|
|
3929
|
+
: undefined;
|
|
3930
|
+
await client.invoke(new Api.account.UpdateBusinessIntro({
|
|
3931
|
+
intro: new Api.InputBusinessIntro({
|
|
3932
|
+
title: opts.title ?? "",
|
|
3933
|
+
description: opts.description ?? "",
|
|
3934
|
+
sticker,
|
|
3935
|
+
}),
|
|
3936
|
+
}));
|
|
3937
|
+
}, "setBusinessIntro");
|
|
3938
|
+
}
|
|
3939
|
+
async createBusinessChatLink(opts) {
|
|
3940
|
+
if (!this.client || !this.connected)
|
|
3941
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3942
|
+
const client = this.client;
|
|
3943
|
+
return this.rateLimiter.execute(async () => {
|
|
3944
|
+
const { text, entities } = await this.parseEntities(opts.message, opts.parseMode);
|
|
3945
|
+
const result = await client.invoke(new Api.account.CreateBusinessChatLink({
|
|
3946
|
+
link: new Api.InputBusinessChatLink({
|
|
3947
|
+
message: text,
|
|
3948
|
+
entities,
|
|
3949
|
+
title: opts.title,
|
|
3950
|
+
}),
|
|
3951
|
+
}));
|
|
3952
|
+
const summary = summarizeBusinessChatLink(result);
|
|
3953
|
+
const slug = result.link.split("/").pop() ?? "";
|
|
3954
|
+
return { ...summary, slug };
|
|
3955
|
+
}, "createBusinessChatLink");
|
|
3956
|
+
}
|
|
3957
|
+
async editBusinessChatLink(opts) {
|
|
3958
|
+
if (!this.client || !this.connected)
|
|
3959
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3960
|
+
const client = this.client;
|
|
3961
|
+
return this.rateLimiter.execute(async () => {
|
|
3962
|
+
const { text, entities } = await this.parseEntities(opts.message, opts.parseMode);
|
|
3963
|
+
const result = await client.invoke(new Api.account.EditBusinessChatLink({
|
|
3964
|
+
slug: opts.slug,
|
|
3965
|
+
link: new Api.InputBusinessChatLink({
|
|
3966
|
+
message: text,
|
|
3967
|
+
entities,
|
|
3968
|
+
title: opts.title,
|
|
3969
|
+
}),
|
|
3970
|
+
}));
|
|
3971
|
+
return summarizeBusinessChatLink(result);
|
|
3972
|
+
}, `editBusinessChatLink ${opts.slug}`);
|
|
3973
|
+
}
|
|
3974
|
+
async deleteBusinessChatLink(slug) {
|
|
3975
|
+
if (!this.client || !this.connected)
|
|
3976
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3977
|
+
const client = this.client;
|
|
3978
|
+
return this.rateLimiter.execute(async () => {
|
|
3979
|
+
await client.invoke(new Api.account.DeleteBusinessChatLink({ slug }));
|
|
3980
|
+
}, `deleteBusinessChatLink ${slug}`);
|
|
3981
|
+
}
|
|
3982
|
+
async resolveBusinessChatLink(slug) {
|
|
3983
|
+
if (!this.client || !this.connected)
|
|
3984
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
3985
|
+
const client = this.client;
|
|
3986
|
+
return this.rateLimiter.execute(async () => {
|
|
3987
|
+
const result = await client.invoke(new Api.account.ResolveBusinessChatLink({ slug }));
|
|
3988
|
+
const r = result;
|
|
3989
|
+
return {
|
|
3990
|
+
peer: summarizePeer(r.peer),
|
|
3991
|
+
message: r.message,
|
|
3992
|
+
entityCount: r.entities?.length ?? 0,
|
|
3993
|
+
};
|
|
3994
|
+
}, `resolveBusinessChatLink ${slug}`);
|
|
3995
|
+
}
|
|
3108
3996
|
// ─── Group calls ───────────────────────────────────────────────────────────
|
|
3109
3997
|
async getGroupCall(chatId, options = {}) {
|
|
3110
3998
|
if (!this.client || !this.connected)
|
|
@@ -3194,6 +4082,244 @@ export class TelegramService {
|
|
|
3194
4082
|
return summarizeQuickReplyMessages(response);
|
|
3195
4083
|
}, `getQuickReplyMessages ${shortcutId}`);
|
|
3196
4084
|
}
|
|
4085
|
+
// ─── Stories Write ─────────────────────────────────────────────────────────
|
|
4086
|
+
async sendStory(chatId, filePath, opts) {
|
|
4087
|
+
if (!this.client || !this.connected)
|
|
4088
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4089
|
+
const client = this.client;
|
|
4090
|
+
const peer = await this.resolvePeer(chatId);
|
|
4091
|
+
return this.rateLimiter.execute(async () => {
|
|
4092
|
+
const inputPeer = await client.getInputEntity(peer);
|
|
4093
|
+
const fileData = await readFile(filePath);
|
|
4094
|
+
const uploaded = await client.uploadFile({
|
|
4095
|
+
file: new CustomFile(filePath, fileData.length, filePath, fileData),
|
|
4096
|
+
workers: 4,
|
|
4097
|
+
});
|
|
4098
|
+
const mediaType = opts.type ?? detectMediaType(filePath);
|
|
4099
|
+
const media = mediaType === "photo"
|
|
4100
|
+
? new Api.InputMediaUploadedPhoto({ file: uploaded })
|
|
4101
|
+
: new Api.InputMediaUploadedDocument({
|
|
4102
|
+
file: uploaded,
|
|
4103
|
+
mimeType: "video/mp4",
|
|
4104
|
+
attributes: [new Api.DocumentAttributeVideo({ duration: 0, w: 0, h: 0, supportsStreaming: true })],
|
|
4105
|
+
});
|
|
4106
|
+
const privacyRules = buildStoryPrivacyRules(opts.privacy, opts.allowUserIds, opts.disallowUserIds);
|
|
4107
|
+
let caption = opts.caption;
|
|
4108
|
+
let entities;
|
|
4109
|
+
if (opts.caption && opts.parseMode) {
|
|
4110
|
+
// biome-ignore lint/suspicious/noExplicitAny: GramJS internal helper, no public typing
|
|
4111
|
+
const parser = client._parseMessageText;
|
|
4112
|
+
if (typeof parser === "function") {
|
|
4113
|
+
[caption, entities] = await parser.call(client, opts.caption, opts.parseMode === "html" ? "html" : "md");
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
const result = await client.invoke(new Api.stories.SendStory({
|
|
4117
|
+
peer: inputPeer,
|
|
4118
|
+
media,
|
|
4119
|
+
privacyRules,
|
|
4120
|
+
caption,
|
|
4121
|
+
...(entities?.length ? { entities } : {}),
|
|
4122
|
+
randomId: generateRandomBigInt(),
|
|
4123
|
+
period: opts.period ?? 86400,
|
|
4124
|
+
pinned: opts.pinned,
|
|
4125
|
+
noforwards: opts.noforwards,
|
|
4126
|
+
}));
|
|
4127
|
+
const id = extractStoryIdFromUpdates(result) || undefined;
|
|
4128
|
+
return { id, period: opts.period ?? 86400 };
|
|
4129
|
+
}, `sendStory ${chatId}`);
|
|
4130
|
+
}
|
|
4131
|
+
async editStory(chatId, storyId, opts) {
|
|
4132
|
+
if (!this.client || !this.connected)
|
|
4133
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4134
|
+
const client = this.client;
|
|
4135
|
+
const peer = await this.resolvePeer(chatId);
|
|
4136
|
+
return this.rateLimiter.execute(async () => {
|
|
4137
|
+
const inputPeer = await client.getInputEntity(peer);
|
|
4138
|
+
const changed = [];
|
|
4139
|
+
let media;
|
|
4140
|
+
if (opts.filePath) {
|
|
4141
|
+
changed.push("media");
|
|
4142
|
+
const fileData = await readFile(opts.filePath);
|
|
4143
|
+
const uploaded = await client.uploadFile({
|
|
4144
|
+
file: new CustomFile(opts.filePath, fileData.length, opts.filePath, fileData),
|
|
4145
|
+
workers: 4,
|
|
4146
|
+
});
|
|
4147
|
+
const mediaType = opts.type ?? detectMediaType(opts.filePath);
|
|
4148
|
+
media =
|
|
4149
|
+
mediaType === "photo"
|
|
4150
|
+
? new Api.InputMediaUploadedPhoto({ file: uploaded })
|
|
4151
|
+
: new Api.InputMediaUploadedDocument({
|
|
4152
|
+
file: uploaded,
|
|
4153
|
+
mimeType: "video/mp4",
|
|
4154
|
+
attributes: [new Api.DocumentAttributeVideo({ duration: 0, w: 0, h: 0, supportsStreaming: true })],
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
let caption = opts.caption;
|
|
4158
|
+
let entities;
|
|
4159
|
+
if (opts.caption !== undefined) {
|
|
4160
|
+
changed.push("caption");
|
|
4161
|
+
if (opts.caption && opts.parseMode) {
|
|
4162
|
+
// biome-ignore lint/suspicious/noExplicitAny: GramJS internal helper, no public typing
|
|
4163
|
+
const parser = client._parseMessageText;
|
|
4164
|
+
if (typeof parser === "function") {
|
|
4165
|
+
[caption, entities] = await parser.call(client, opts.caption, opts.parseMode === "html" ? "html" : "md");
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
let privacyRules;
|
|
4170
|
+
if (opts.privacy) {
|
|
4171
|
+
changed.push("privacy");
|
|
4172
|
+
privacyRules = buildStoryPrivacyRules(opts.privacy, opts.allowUserIds, opts.disallowUserIds);
|
|
4173
|
+
}
|
|
4174
|
+
await client.invoke(new Api.stories.EditStory({
|
|
4175
|
+
peer: inputPeer,
|
|
4176
|
+
id: storyId,
|
|
4177
|
+
...(media ? { media } : {}),
|
|
4178
|
+
...(caption !== undefined ? { caption } : {}),
|
|
4179
|
+
...(entities?.length ? { entities } : {}),
|
|
4180
|
+
...(privacyRules ? { privacyRules } : {}),
|
|
4181
|
+
}));
|
|
4182
|
+
return { changed };
|
|
4183
|
+
}, `editStory ${chatId}/${storyId}`);
|
|
4184
|
+
}
|
|
4185
|
+
async deleteStories(chatId, ids) {
|
|
4186
|
+
if (!this.client || !this.connected)
|
|
4187
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4188
|
+
const peer = await this.resolvePeer(chatId);
|
|
4189
|
+
return this.rateLimiter.execute(async () => {
|
|
4190
|
+
const deleted = await this.client?.invoke(new Api.stories.DeleteStories({ peer, id: ids }));
|
|
4191
|
+
return { deleted: deleted ?? [] };
|
|
4192
|
+
}, `deleteStories ${chatId}`);
|
|
4193
|
+
}
|
|
4194
|
+
async sendStoryReaction(chatId, storyId, emoji, addToRecent) {
|
|
4195
|
+
if (!this.client || !this.connected)
|
|
4196
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4197
|
+
const peer = await this.resolvePeer(chatId);
|
|
4198
|
+
return this.rateLimiter.execute(async () => {
|
|
4199
|
+
const reaction = emoji === "" ? new Api.ReactionEmpty() : new Api.ReactionEmoji({ emoticon: emoji });
|
|
4200
|
+
await this.client?.invoke(new Api.stories.SendReaction({ peer, storyId, reaction, addToRecent }));
|
|
4201
|
+
}, `sendStoryReaction ${chatId}/${storyId}`);
|
|
4202
|
+
}
|
|
4203
|
+
async exportStoryLink(chatId, storyId) {
|
|
4204
|
+
if (!this.client || !this.connected)
|
|
4205
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4206
|
+
const peer = await this.resolvePeer(chatId);
|
|
4207
|
+
return this.rateLimiter.execute(async () => {
|
|
4208
|
+
const result = await this.client?.invoke(new Api.stories.ExportStoryLink({ peer, id: storyId }));
|
|
4209
|
+
if (!result)
|
|
4210
|
+
throw new Error("stories.ExportStoryLink returned nothing");
|
|
4211
|
+
return { link: result.link };
|
|
4212
|
+
}, `exportStoryLink ${chatId}/${storyId}`);
|
|
4213
|
+
}
|
|
4214
|
+
async readStories(chatId, maxId) {
|
|
4215
|
+
if (!this.client || !this.connected)
|
|
4216
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4217
|
+
const peer = await this.resolvePeer(chatId);
|
|
4218
|
+
return this.rateLimiter.execute(async () => {
|
|
4219
|
+
const ids = await this.client?.invoke(new Api.stories.ReadStories({ peer, maxId }));
|
|
4220
|
+
return { ids: ids ?? [] };
|
|
4221
|
+
}, `readStories ${chatId}/${maxId}`);
|
|
4222
|
+
}
|
|
4223
|
+
async toggleStoryPinned(chatId, ids, pinned) {
|
|
4224
|
+
if (!this.client || !this.connected)
|
|
4225
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4226
|
+
const peer = await this.resolvePeer(chatId);
|
|
4227
|
+
return this.rateLimiter.execute(async () => {
|
|
4228
|
+
const affected = await this.client?.invoke(new Api.stories.TogglePinned({ peer, id: ids, pinned }));
|
|
4229
|
+
return { affected: affected ?? [] };
|
|
4230
|
+
}, `toggleStoryPinned ${chatId}`);
|
|
4231
|
+
}
|
|
4232
|
+
async toggleStoryPinnedToTop(chatId, ids) {
|
|
4233
|
+
if (!this.client || !this.connected)
|
|
4234
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4235
|
+
const peer = await this.resolvePeer(chatId);
|
|
4236
|
+
return this.rateLimiter.execute(async () => {
|
|
4237
|
+
await this.client?.invoke(new Api.stories.TogglePinnedToTop({ peer, id: ids }));
|
|
4238
|
+
return { ok: true };
|
|
4239
|
+
}, `toggleStoryPinnedToTop ${chatId}`);
|
|
4240
|
+
}
|
|
4241
|
+
async activateStealthMode(past, future) {
|
|
4242
|
+
if (!this.client || !this.connected)
|
|
4243
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4244
|
+
return this.rateLimiter.execute(async () => {
|
|
4245
|
+
await this.client?.invoke(new Api.stories.ActivateStealthMode({ past: past ?? false, future: future ?? false }));
|
|
4246
|
+
}, "activateStealthMode");
|
|
4247
|
+
}
|
|
4248
|
+
async getStoriesArchive(chatId, offsetId, limit) {
|
|
4249
|
+
if (!this.client || !this.connected)
|
|
4250
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4251
|
+
const peer = await this.resolvePeer(chatId);
|
|
4252
|
+
return this.rateLimiter.execute(async () => {
|
|
4253
|
+
const result = await this.client?.invoke(new Api.stories.GetStoriesArchive({ peer, offsetId, limit }));
|
|
4254
|
+
if (!result)
|
|
4255
|
+
throw new Error("stories.GetStoriesArchive returned nothing");
|
|
4256
|
+
return summarizeStoriesById(result);
|
|
4257
|
+
}, `getStoriesArchive ${chatId}`);
|
|
4258
|
+
}
|
|
4259
|
+
async reportStory(chatId, ids, option, message) {
|
|
4260
|
+
if (!this.client || !this.connected)
|
|
4261
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4262
|
+
const peer = await this.resolvePeer(chatId);
|
|
4263
|
+
return this.rateLimiter.execute(async () => {
|
|
4264
|
+
const optionBytes = Buffer.from(option, "base64");
|
|
4265
|
+
const result = await this.client?.invoke(new Api.stories.Report({ peer, id: ids, option: optionBytes, message }));
|
|
4266
|
+
if (!result)
|
|
4267
|
+
throw new Error("stories.Report returned nothing");
|
|
4268
|
+
return summarizeReportResult(result);
|
|
4269
|
+
}, `reportStory ${chatId}`);
|
|
4270
|
+
}
|
|
4271
|
+
// ─── Discussion & Read Receipts ────────────────────────────────────────────
|
|
4272
|
+
async getDiscussionMessage(chatId, messageId) {
|
|
4273
|
+
if (!this.client || !this.connected)
|
|
4274
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4275
|
+
const peer = await this.resolvePeer(chatId);
|
|
4276
|
+
return this.rateLimiter.execute(async () => {
|
|
4277
|
+
const result = await this.client?.invoke(new Api.messages.GetDiscussionMessage({ peer, msgId: messageId }));
|
|
4278
|
+
if (!result)
|
|
4279
|
+
throw new Error("messages.GetDiscussionMessage returned nothing");
|
|
4280
|
+
return summarizeDiscussionMessage(result);
|
|
4281
|
+
}, `getDiscussionMessage ${chatId}/${messageId}`);
|
|
4282
|
+
}
|
|
4283
|
+
async getGroupsForDiscussion() {
|
|
4284
|
+
if (!this.client || !this.connected)
|
|
4285
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4286
|
+
return this.rateLimiter.execute(async () => {
|
|
4287
|
+
const result = await this.client?.invoke(new Api.channels.GetGroupsForDiscussion());
|
|
4288
|
+
if (!result)
|
|
4289
|
+
throw new Error("channels.GetGroupsForDiscussion returned nothing");
|
|
4290
|
+
return summarizeGroupsForDiscussion(result);
|
|
4291
|
+
}, "getGroupsForDiscussion");
|
|
4292
|
+
}
|
|
4293
|
+
async getMessageReadParticipants(chatId, messageId) {
|
|
4294
|
+
if (!this.client || !this.connected)
|
|
4295
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4296
|
+
const peer = await this.resolvePeer(chatId);
|
|
4297
|
+
return this.rateLimiter.execute(async () => {
|
|
4298
|
+
const result = await this.client?.invoke(new Api.messages.GetMessageReadParticipants({ peer, msgId: messageId }));
|
|
4299
|
+
if (!result)
|
|
4300
|
+
throw new Error("messages.GetMessageReadParticipants returned nothing");
|
|
4301
|
+
return summarizeReadParticipants(result, messageId);
|
|
4302
|
+
}, `getMessageReadParticipants ${chatId}/${messageId}`);
|
|
4303
|
+
}
|
|
4304
|
+
async getOutboxReadDate(chatId, messageId) {
|
|
4305
|
+
if (!this.client || !this.connected)
|
|
4306
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
4307
|
+
const peer = await this.resolvePeer(chatId);
|
|
4308
|
+
return this.rateLimiter.execute(async () => {
|
|
4309
|
+
try {
|
|
4310
|
+
const result = await this.client?.invoke(new Api.messages.GetOutboxReadDate({ peer, msgId: messageId }));
|
|
4311
|
+
if (!result)
|
|
4312
|
+
return { readAt: null };
|
|
4313
|
+
const date = result.date;
|
|
4314
|
+
return { readAt: new Date(date * 1000).toISOString() };
|
|
4315
|
+
}
|
|
4316
|
+
catch (e) {
|
|
4317
|
+
if (/NOT_READ_YET/i.test(e.message ?? ""))
|
|
4318
|
+
return { readAt: null };
|
|
4319
|
+
throw e;
|
|
4320
|
+
}
|
|
4321
|
+
}, `getOutboxReadDate ${chatId}/${messageId}`);
|
|
4322
|
+
}
|
|
3197
4323
|
async resolveInputGroupCall(chatId) {
|
|
3198
4324
|
const entity = await this.resolveChat(chatId);
|
|
3199
4325
|
let call;
|