@overpod/mcp-telegram 1.28.1 → 1.33.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.
@@ -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");
@@ -373,20 +373,60 @@ export class TelegramService {
373
373
  firstName: user.firstName ?? undefined,
374
374
  };
375
375
  }
376
- async sendMessage(chatId, text, replyTo, parseMode, topicId) {
376
+ async sendMessage(chatId, text, replyTo, parseMode, topicId, extra) {
377
377
  if (!this.client || !this.connected)
378
378
  throw new Error(NOT_CONNECTED_ERROR);
379
+ const client = this.client;
379
380
  return this.rateLimiter.execute(async () => {
380
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
+ }
381
421
  if (topicId) {
382
- return await this.client?.sendMessage(resolved, {
422
+ return await client.sendMessage(resolved, {
383
423
  message: text,
384
424
  topMsgId: topicId,
385
425
  ...(replyTo ? { replyTo } : {}),
386
426
  ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
387
427
  });
388
428
  }
389
- return await this.client?.sendMessage(resolved, {
429
+ return await client.sendMessage(resolved, {
390
430
  message: text,
391
431
  ...(replyTo ? { replyTo } : {}),
392
432
  ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
@@ -401,6 +441,188 @@ export class TelegramService {
401
441
  await this.client?.sendFile(resolved, { file: filePath, caption });
402
442
  }, `sendFile to ${chatId}`);
403
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
+ }
404
626
  async downloadMedia(chatId, messageId, downloadPath) {
405
627
  if (!this.client || !this.connected)
406
628
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1613,6 +1835,246 @@ export class TelegramService {
1613
1835
  }
1614
1836
  return 0;
1615
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
+ }
1616
2078
  async getForumTopics(chatId, limit = 100) {
1617
2079
  if (!this.client || !this.connected)
1618
2080
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2627,6 +3089,145 @@ export class TelegramService {
2627
3089
  const peer = await this.client.getInputEntity(resolved);
2628
3090
  await this.client.invoke(new Api.messages.SetHistoryTTL({ peer, period }));
2629
3091
  }
3092
+ // ─── Folder management (v1.33.0) ───────────────────────────────────────────
3093
+ async createFolder(opts) {
3094
+ if (!this.client || !this.connected)
3095
+ throw new Error(NOT_CONNECTED_ERROR);
3096
+ const client = this.client;
3097
+ const filtersResult = await client.invoke(new Api.messages.GetDialogFilters());
3098
+ const existing = "filters" in filtersResult ? filtersResult.filters : [];
3099
+ const usedIds = existing
3100
+ .filter((f) => "id" in f)
3101
+ .map((f) => f.id);
3102
+ let newId = 2;
3103
+ while (usedIds.includes(newId))
3104
+ newId++;
3105
+ const toInput = async (ids) => {
3106
+ const peers = [];
3107
+ for (const id of ids) {
3108
+ const resolved = await this.resolvePeer(id);
3109
+ peers.push(await client.getInputEntity(resolved));
3110
+ }
3111
+ return peers;
3112
+ };
3113
+ const includePeers = await toInput(opts.includePeers ?? []);
3114
+ const excludePeers = await toInput(opts.excludePeers ?? []);
3115
+ const pinnedPeers = await toInput(opts.pinnedPeers ?? []);
3116
+ await client.invoke(new Api.messages.UpdateDialogFilter({
3117
+ id: newId,
3118
+ filter: new Api.DialogFilter({
3119
+ id: newId,
3120
+ title: new Api.TextWithEntities({ text: opts.title, entities: [] }),
3121
+ emoticon: opts.emoticon,
3122
+ contacts: opts.contacts,
3123
+ nonContacts: opts.nonContacts,
3124
+ groups: opts.groups,
3125
+ broadcasts: opts.broadcasts,
3126
+ bots: opts.bots,
3127
+ excludeMuted: opts.excludeMuted,
3128
+ excludeRead: opts.excludeRead,
3129
+ excludeArchived: opts.excludeArchived,
3130
+ pinnedPeers,
3131
+ includePeers,
3132
+ excludePeers,
3133
+ }),
3134
+ }));
3135
+ return newId;
3136
+ }
3137
+ async editFolder(id, opts) {
3138
+ if (!this.client || !this.connected)
3139
+ throw new Error(NOT_CONNECTED_ERROR);
3140
+ const client = this.client;
3141
+ const filtersResult = await client.invoke(new Api.messages.GetDialogFilters());
3142
+ const existing = "filters" in filtersResult ? filtersResult.filters : [];
3143
+ const current = existing.find((f) => f instanceof Api.DialogFilter && f.id === id);
3144
+ if (!current)
3145
+ throw new Error(`Folder with id=${id} not found`);
3146
+ const toInput = async (ids) => {
3147
+ const peers = [];
3148
+ for (const id of ids) {
3149
+ const resolved = await this.resolvePeer(id);
3150
+ peers.push(await client.getInputEntity(resolved));
3151
+ }
3152
+ return peers;
3153
+ };
3154
+ const includePeers = opts.includePeers !== undefined ? await toInput(opts.includePeers) : current.includePeers;
3155
+ const excludePeers = opts.excludePeers !== undefined ? await toInput(opts.excludePeers) : current.excludePeers;
3156
+ const pinnedPeers = opts.pinnedPeers !== undefined ? await toInput(opts.pinnedPeers) : current.pinnedPeers;
3157
+ const titleText = opts.title !== undefined ? opts.title : typeof current.title === "string" ? current.title : current.title.text;
3158
+ await client.invoke(new Api.messages.UpdateDialogFilter({
3159
+ id,
3160
+ filter: new Api.DialogFilter({
3161
+ id,
3162
+ title: new Api.TextWithEntities({ text: titleText, entities: [] }),
3163
+ emoticon: opts.emoticon !== undefined ? opts.emoticon : current.emoticon,
3164
+ contacts: opts.contacts !== undefined ? opts.contacts : current.contacts,
3165
+ nonContacts: opts.nonContacts !== undefined ? opts.nonContacts : current.nonContacts,
3166
+ groups: opts.groups !== undefined ? opts.groups : current.groups,
3167
+ broadcasts: opts.broadcasts !== undefined ? opts.broadcasts : current.broadcasts,
3168
+ bots: opts.bots !== undefined ? opts.bots : current.bots,
3169
+ excludeMuted: opts.excludeMuted !== undefined ? opts.excludeMuted : current.excludeMuted,
3170
+ excludeRead: opts.excludeRead !== undefined ? opts.excludeRead : current.excludeRead,
3171
+ excludeArchived: opts.excludeArchived !== undefined ? opts.excludeArchived : current.excludeArchived,
3172
+ pinnedPeers,
3173
+ includePeers,
3174
+ excludePeers,
3175
+ }),
3176
+ }));
3177
+ }
3178
+ async deleteFolder(id) {
3179
+ if (!this.client || !this.connected)
3180
+ throw new Error(NOT_CONNECTED_ERROR);
3181
+ await this.client.invoke(new Api.messages.UpdateDialogFilter({ id }));
3182
+ }
3183
+ async reorderFolders(ids) {
3184
+ if (!this.client || !this.connected)
3185
+ throw new Error(NOT_CONNECTED_ERROR);
3186
+ await this.client.invoke(new Api.messages.UpdateDialogFiltersOrder({ order: ids }));
3187
+ }
3188
+ async getSuggestedFolders() {
3189
+ if (!this.client || !this.connected)
3190
+ throw new Error(NOT_CONNECTED_ERROR);
3191
+ const result = await this.client.invoke(new Api.messages.GetSuggestedDialogFilters());
3192
+ return result
3193
+ .filter((s) => s.filter instanceof Api.DialogFilter || s.filter instanceof Api.DialogFilterChatlist)
3194
+ .map((s) => ({
3195
+ title: typeof s.filter.title === "string" ? s.filter.title : s.filter.title.text,
3196
+ emoticon: s.filter.emoticon,
3197
+ }));
3198
+ }
3199
+ async toggleDialogFilterTags(enabled) {
3200
+ if (!this.client || !this.connected)
3201
+ throw new Error(NOT_CONNECTED_ERROR);
3202
+ await this.client.invoke(new Api.messages.ToggleDialogFilterTags({ enabled }));
3203
+ }
3204
+ // ─── Global privacy (v1.33.0) ──────────────────────────────────────────────
3205
+ async getGlobalPrivacySettings() {
3206
+ if (!this.client || !this.connected)
3207
+ throw new Error(NOT_CONNECTED_ERROR);
3208
+ const s = await this.client.invoke(new Api.account.GetGlobalPrivacySettings());
3209
+ return {
3210
+ archiveAndMuteNewNoncontactPeers: s.archiveAndMuteNewNoncontactPeers ?? false,
3211
+ keepArchivedUnmuted: s.keepArchivedUnmuted ?? false,
3212
+ keepArchivedFolders: s.keepArchivedFolders ?? false,
3213
+ hideReadMarks: s.hideReadMarks ?? false,
3214
+ newNoncontactPeersRequirePremium: s.newNoncontactPeersRequirePremium ?? false,
3215
+ };
3216
+ }
3217
+ async setGlobalPrivacySettings(opts) {
3218
+ if (!this.client || !this.connected)
3219
+ throw new Error(NOT_CONNECTED_ERROR);
3220
+ const current = await this.client.invoke(new Api.account.GetGlobalPrivacySettings());
3221
+ await this.client.invoke(new Api.account.SetGlobalPrivacySettings({
3222
+ settings: new Api.GlobalPrivacySettings({
3223
+ archiveAndMuteNewNoncontactPeers: opts.archiveAndMuteNewNoncontactPeers ?? current.archiveAndMuteNewNoncontactPeers,
3224
+ keepArchivedUnmuted: opts.keepArchivedUnmuted ?? current.keepArchivedUnmuted,
3225
+ keepArchivedFolders: opts.keepArchivedFolders ?? current.keepArchivedFolders,
3226
+ hideReadMarks: opts.hideReadMarks ?? current.hideReadMarks,
3227
+ newNoncontactPeersRequirePremium: opts.newNoncontactPeersRequirePremium ?? current.newNoncontactPeersRequirePremium,
3228
+ }),
3229
+ }));
3230
+ }
2630
3231
  // ─── Account & privacy ─────────────────────────────────────────────────────
2631
3232
  async getActiveSessions() {
2632
3233
  if (!this.client || !this.connected)
@@ -3125,6 +3726,412 @@ export class TelegramService {
3125
3726
  return summarizeBusinessChatLinks(response);
3126
3727
  }, "getBusinessChatLinks");
3127
3728
  }
3729
+ // ─── Profile write (v1.32.0) ───────────────────────────────────────────────
3730
+ async setEmojiStatus(opts) {
3731
+ if (!this.client || !this.connected)
3732
+ throw new Error(NOT_CONNECTED_ERROR);
3733
+ const client = this.client;
3734
+ return this.rateLimiter.execute(async () => {
3735
+ let emojiStatus;
3736
+ if (opts.collectibleId) {
3737
+ emojiStatus = new Api.InputEmojiStatusCollectible({
3738
+ collectibleId: bigInt(opts.collectibleId),
3739
+ until: opts.untilUnix,
3740
+ });
3741
+ }
3742
+ else if (opts.documentId) {
3743
+ emojiStatus = new Api.EmojiStatus({
3744
+ documentId: bigInt(opts.documentId),
3745
+ until: opts.untilUnix,
3746
+ });
3747
+ }
3748
+ else {
3749
+ emojiStatus = new Api.EmojiStatusEmpty();
3750
+ }
3751
+ await client.invoke(new Api.account.UpdateEmojiStatus({ emojiStatus }));
3752
+ }, "setEmojiStatus");
3753
+ }
3754
+ async listEmojiStatuses(kind, limit) {
3755
+ if (!this.client || !this.connected)
3756
+ throw new Error(NOT_CONNECTED_ERROR);
3757
+ const client = this.client;
3758
+ return this.rateLimiter.execute(async () => {
3759
+ const hash = bigInt(0);
3760
+ let resp;
3761
+ if (kind === "recent") {
3762
+ resp = await client.invoke(new Api.account.GetRecentEmojiStatuses({ hash }));
3763
+ }
3764
+ else if (kind === "channel_default") {
3765
+ resp = await client.invoke(new Api.account.GetChannelDefaultEmojiStatuses({ hash }));
3766
+ }
3767
+ else if (kind === "collectible") {
3768
+ resp = await client.invoke(new Api.account.GetCollectibleEmojiStatuses({ hash }));
3769
+ }
3770
+ else {
3771
+ resp = await client.invoke(new Api.account.GetDefaultEmojiStatuses({ hash }));
3772
+ }
3773
+ if (resp.className === "account.EmojiStatusesNotModified")
3774
+ return [];
3775
+ const statuses = resp.statuses ?? [];
3776
+ return statuses.slice(0, limit).map(summarizeEmojiStatus);
3777
+ }, `listEmojiStatuses ${kind}`);
3778
+ }
3779
+ async clearRecentEmojiStatuses() {
3780
+ if (!this.client || !this.connected)
3781
+ throw new Error(NOT_CONNECTED_ERROR);
3782
+ const client = this.client;
3783
+ return this.rateLimiter.execute(async () => {
3784
+ await client.invoke(new Api.account.ClearRecentEmojiStatuses());
3785
+ }, "clearRecentEmojiStatuses");
3786
+ }
3787
+ async setProfileColor(opts) {
3788
+ if (!this.client || !this.connected)
3789
+ throw new Error(NOT_CONNECTED_ERROR);
3790
+ const client = this.client;
3791
+ return this.rateLimiter.execute(async () => {
3792
+ await client.invoke(new Api.account.UpdateColor({
3793
+ forProfile: opts.forProfile || undefined,
3794
+ color: opts.color,
3795
+ backgroundEmojiId: opts.backgroundEmojiId ? bigInt(opts.backgroundEmojiId) : undefined,
3796
+ }));
3797
+ }, "setProfileColor");
3798
+ }
3799
+ async setBirthday(opts) {
3800
+ if (!this.client || !this.connected)
3801
+ throw new Error(NOT_CONNECTED_ERROR);
3802
+ const client = this.client;
3803
+ return this.rateLimiter.execute(async () => {
3804
+ const birthday = opts.clear || !opts.day || !opts.month
3805
+ ? undefined
3806
+ : new Api.Birthday({ day: opts.day, month: opts.month, year: opts.year });
3807
+ await client.invoke(new Api.account.UpdateBirthday({ birthday }));
3808
+ }, "setBirthday");
3809
+ }
3810
+ async setPersonalChannel(opts) {
3811
+ if (!this.client || !this.connected)
3812
+ throw new Error(NOT_CONNECTED_ERROR);
3813
+ const client = this.client;
3814
+ return this.rateLimiter.execute(async () => {
3815
+ if (opts.clear) {
3816
+ await client.invoke(new Api.account.UpdatePersonalChannel({ channel: new Api.InputChannelEmpty() }));
3817
+ return null;
3818
+ }
3819
+ const entity = await client.getInputEntity(opts.channelId ?? "");
3820
+ if (!(entity instanceof Api.InputPeerChannel)) {
3821
+ throw new Error(`Not a channel: ${opts.channelId}`);
3822
+ }
3823
+ const channel = new Api.InputChannel({
3824
+ channelId: entity.channelId,
3825
+ accessHash: entity.accessHash,
3826
+ });
3827
+ await client.invoke(new Api.account.UpdatePersonalChannel({ channel }));
3828
+ const info = await client.getEntity(entity);
3829
+ return "title" in info ? info.title : (opts.channelId ?? "");
3830
+ }, `setPersonalChannel ${opts.channelId ?? "clear"}`);
3831
+ }
3832
+ async setProfilePhoto(opts) {
3833
+ if (!this.client || !this.connected)
3834
+ throw new Error(NOT_CONNECTED_ERROR);
3835
+ const client = this.client;
3836
+ return this.rateLimiter.execute(async () => {
3837
+ const inputFile = await client.uploadFile({
3838
+ file: new CustomFile(opts.filePath.split("/").pop() ?? "upload", (await import("node:fs")).statSync(opts.filePath).size, opts.filePath),
3839
+ workers: 4,
3840
+ });
3841
+ const request = opts.isVideo
3842
+ ? new Api.photos.UploadProfilePhoto({
3843
+ fallback: opts.fallback || undefined,
3844
+ video: inputFile,
3845
+ videoStartTs: opts.videoStartTs,
3846
+ })
3847
+ : new Api.photos.UploadProfilePhoto({
3848
+ fallback: opts.fallback || undefined,
3849
+ file: inputFile,
3850
+ });
3851
+ const result = await client.invoke(request);
3852
+ const photo = result.photo;
3853
+ return { id: photo.id.toString() };
3854
+ }, "setProfilePhoto");
3855
+ }
3856
+ async deleteProfilePhotos(photoIds) {
3857
+ if (!this.client || !this.connected)
3858
+ throw new Error(NOT_CONNECTED_ERROR);
3859
+ const client = this.client;
3860
+ return this.rateLimiter.execute(async () => {
3861
+ const me = await client.getMe();
3862
+ const all = await client.invoke(new Api.photos.GetUserPhotos({
3863
+ userId: me.id,
3864
+ offset: 0,
3865
+ maxId: bigInt(0),
3866
+ limit: 100,
3867
+ }));
3868
+ const byId = new Map();
3869
+ for (const p of all.photos) {
3870
+ if (p instanceof Api.Photo)
3871
+ byId.set(p.id.toString(), p);
3872
+ }
3873
+ const inputs = [];
3874
+ const missing = [];
3875
+ for (const pid of photoIds) {
3876
+ const photo = byId.get(pid);
3877
+ if (!photo) {
3878
+ missing.push(pid);
3879
+ continue;
3880
+ }
3881
+ inputs.push(new Api.InputPhoto({
3882
+ id: photo.id,
3883
+ accessHash: photo.accessHash,
3884
+ fileReference: photo.fileReference,
3885
+ }));
3886
+ }
3887
+ if (inputs.length === 0) {
3888
+ throw new Error(`No matching photos found. Missing IDs: ${missing.join(", ")}`);
3889
+ }
3890
+ const deletedIds = await client.invoke(new Api.photos.DeletePhotos({ id: inputs }));
3891
+ return { deleted: deletedIds.map((x) => x.toString()), missing };
3892
+ }, "deleteProfilePhotos");
3893
+ }
3894
+ // ─── Business write (v1.32.0) ──────────────────────────────────────────────
3895
+ async buildBusinessRecipients(opts) {
3896
+ const flags = {};
3897
+ switch (opts.audience) {
3898
+ case "all_new":
3899
+ flags.contacts = true;
3900
+ flags.nonContacts = true;
3901
+ break;
3902
+ case "contacts_only":
3903
+ flags.contacts = true;
3904
+ break;
3905
+ case "non_contacts":
3906
+ flags.nonContacts = true;
3907
+ break;
3908
+ case "existing_only":
3909
+ flags.existingChats = true;
3910
+ break;
3911
+ }
3912
+ if (opts.includeUsers?.length) {
3913
+ flags.users = await this.resolveInputUsers(opts.includeUsers);
3914
+ }
3915
+ else if (opts.excludeUsers?.length) {
3916
+ flags.users = await this.resolveInputUsers(opts.excludeUsers);
3917
+ flags.excludeSelected = true;
3918
+ }
3919
+ return new Api.InputBusinessRecipients({ ...flags });
3920
+ }
3921
+ async resolveInputUsers(ids) {
3922
+ if (!this.client)
3923
+ throw new Error(NOT_CONNECTED_ERROR);
3924
+ const client = this.client;
3925
+ const result = [];
3926
+ for (const id of ids) {
3927
+ const entity = await client.getInputEntity(id);
3928
+ if (entity instanceof Api.InputPeerUser) {
3929
+ result.push(new Api.InputUser({ userId: entity.userId, accessHash: entity.accessHash }));
3930
+ }
3931
+ else {
3932
+ throw new Error(`Not a user: ${id}`);
3933
+ }
3934
+ }
3935
+ return result;
3936
+ }
3937
+ async parseEntities(text, parseMode) {
3938
+ if (!this.client || !parseMode)
3939
+ return { text };
3940
+ // biome-ignore lint/suspicious/noExplicitAny: internal GramJS API
3941
+ const parser = this.client._parseMessageText?.bind(this.client);
3942
+ if (!parser)
3943
+ return { text };
3944
+ const [parsedText, entities] = await parser(text, parseMode === "md" ? "markdown" : "html");
3945
+ return { text: parsedText, entities: entities?.length ? entities : undefined };
3946
+ }
3947
+ async setBusinessWorkHours(opts) {
3948
+ if (!this.client || !this.connected)
3949
+ throw new Error(NOT_CONNECTED_ERROR);
3950
+ const client = this.client;
3951
+ return this.rateLimiter.execute(async () => {
3952
+ if (opts.clear) {
3953
+ await client.invoke(new Api.account.UpdateBusinessWorkHours({}));
3954
+ return;
3955
+ }
3956
+ const dayOffsets = {
3957
+ mon: 0,
3958
+ tue: 1,
3959
+ wed: 2,
3960
+ thu: 3,
3961
+ fri: 4,
3962
+ sat: 5,
3963
+ sun: 6,
3964
+ };
3965
+ const weeklyOpen = (opts.schedule ?? []).map((s) => {
3966
+ const [fh, fm] = s.openFrom.split(":").map(Number);
3967
+ const [th, tm] = s.openTo.split(":").map(Number);
3968
+ const base = (dayOffsets[s.day] ?? 0) * 1440;
3969
+ return new Api.BusinessWeeklyOpen({
3970
+ startMinute: base + fh * 60 + fm,
3971
+ endMinute: base + th * 60 + tm,
3972
+ });
3973
+ });
3974
+ await client.invoke(new Api.account.UpdateBusinessWorkHours({
3975
+ businessWorkHours: new Api.BusinessWorkHours({
3976
+ openNow: opts.openNow,
3977
+ timezoneId: opts.timezone ?? "",
3978
+ weeklyOpen,
3979
+ }),
3980
+ }));
3981
+ }, "setBusinessWorkHours");
3982
+ }
3983
+ async setBusinessLocation(opts) {
3984
+ if (!this.client || !this.connected)
3985
+ throw new Error(NOT_CONNECTED_ERROR);
3986
+ const client = this.client;
3987
+ return this.rateLimiter.execute(async () => {
3988
+ if (opts.clear) {
3989
+ await client.invoke(new Api.account.UpdateBusinessLocation({}));
3990
+ return;
3991
+ }
3992
+ const geoPoint = opts.latitude !== undefined && opts.longitude !== undefined
3993
+ ? new Api.InputGeoPoint({ lat: opts.latitude, long: opts.longitude })
3994
+ : undefined;
3995
+ await client.invoke(new Api.account.UpdateBusinessLocation({ geoPoint, address: opts.address }));
3996
+ }, "setBusinessLocation");
3997
+ }
3998
+ async setBusinessGreeting(opts) {
3999
+ if (!this.client || !this.connected)
4000
+ throw new Error(NOT_CONNECTED_ERROR);
4001
+ const client = this.client;
4002
+ return this.rateLimiter.execute(async () => {
4003
+ if (opts.clear) {
4004
+ await client.invoke(new Api.account.UpdateBusinessGreetingMessage({}));
4005
+ return;
4006
+ }
4007
+ const recipients = await this.buildBusinessRecipients({
4008
+ audience: opts.audience,
4009
+ includeUsers: opts.includeUsers,
4010
+ excludeUsers: opts.excludeUsers,
4011
+ });
4012
+ await client.invoke(new Api.account.UpdateBusinessGreetingMessage({
4013
+ message: new Api.InputBusinessGreetingMessage({
4014
+ shortcutId: opts.shortcutId ?? 0,
4015
+ recipients,
4016
+ noActivityDays: opts.noActivityDays,
4017
+ }),
4018
+ }));
4019
+ }, "setBusinessGreeting");
4020
+ }
4021
+ async setBusinessAway(opts) {
4022
+ if (!this.client || !this.connected)
4023
+ throw new Error(NOT_CONNECTED_ERROR);
4024
+ const client = this.client;
4025
+ return this.rateLimiter.execute(async () => {
4026
+ if (opts.clear) {
4027
+ await client.invoke(new Api.account.UpdateBusinessAwayMessage({}));
4028
+ return;
4029
+ }
4030
+ const scheduleObj = opts.schedule === "always"
4031
+ ? new Api.BusinessAwayMessageScheduleAlways()
4032
+ : opts.schedule === "outside_hours"
4033
+ ? new Api.BusinessAwayMessageScheduleOutsideWorkHours()
4034
+ : new Api.BusinessAwayMessageScheduleCustom({
4035
+ startDate: opts.customFrom ?? 0,
4036
+ endDate: opts.customTo ?? 0,
4037
+ });
4038
+ const recipients = await this.buildBusinessRecipients({
4039
+ audience: opts.audience,
4040
+ includeUsers: opts.includeUsers,
4041
+ excludeUsers: opts.excludeUsers,
4042
+ });
4043
+ await client.invoke(new Api.account.UpdateBusinessAwayMessage({
4044
+ message: new Api.InputBusinessAwayMessage({
4045
+ offlineOnly: opts.offlineOnly || undefined,
4046
+ shortcutId: opts.shortcutId ?? 0,
4047
+ schedule: scheduleObj,
4048
+ recipients,
4049
+ }),
4050
+ }));
4051
+ }, "setBusinessAway");
4052
+ }
4053
+ async setBusinessIntro(opts) {
4054
+ if (!this.client || !this.connected)
4055
+ throw new Error(NOT_CONNECTED_ERROR);
4056
+ const client = this.client;
4057
+ return this.rateLimiter.execute(async () => {
4058
+ if (opts.clear) {
4059
+ await client.invoke(new Api.account.UpdateBusinessIntro({}));
4060
+ return;
4061
+ }
4062
+ const sticker = opts.stickerId && opts.stickerAccessHash && opts.stickerFileReference
4063
+ ? new Api.InputDocument({
4064
+ id: bigInt(opts.stickerId),
4065
+ accessHash: bigInt(opts.stickerAccessHash),
4066
+ fileReference: Buffer.from(opts.stickerFileReference, "hex"),
4067
+ })
4068
+ : undefined;
4069
+ await client.invoke(new Api.account.UpdateBusinessIntro({
4070
+ intro: new Api.InputBusinessIntro({
4071
+ title: opts.title ?? "",
4072
+ description: opts.description ?? "",
4073
+ sticker,
4074
+ }),
4075
+ }));
4076
+ }, "setBusinessIntro");
4077
+ }
4078
+ async createBusinessChatLink(opts) {
4079
+ if (!this.client || !this.connected)
4080
+ throw new Error(NOT_CONNECTED_ERROR);
4081
+ const client = this.client;
4082
+ return this.rateLimiter.execute(async () => {
4083
+ const { text, entities } = await this.parseEntities(opts.message, opts.parseMode);
4084
+ const result = await client.invoke(new Api.account.CreateBusinessChatLink({
4085
+ link: new Api.InputBusinessChatLink({
4086
+ message: text,
4087
+ entities,
4088
+ title: opts.title,
4089
+ }),
4090
+ }));
4091
+ const summary = summarizeBusinessChatLink(result);
4092
+ const slug = result.link.split("/").pop() ?? "";
4093
+ return { ...summary, slug };
4094
+ }, "createBusinessChatLink");
4095
+ }
4096
+ async editBusinessChatLink(opts) {
4097
+ if (!this.client || !this.connected)
4098
+ throw new Error(NOT_CONNECTED_ERROR);
4099
+ const client = this.client;
4100
+ return this.rateLimiter.execute(async () => {
4101
+ const { text, entities } = await this.parseEntities(opts.message, opts.parseMode);
4102
+ const result = await client.invoke(new Api.account.EditBusinessChatLink({
4103
+ slug: opts.slug,
4104
+ link: new Api.InputBusinessChatLink({
4105
+ message: text,
4106
+ entities,
4107
+ title: opts.title,
4108
+ }),
4109
+ }));
4110
+ return summarizeBusinessChatLink(result);
4111
+ }, `editBusinessChatLink ${opts.slug}`);
4112
+ }
4113
+ async deleteBusinessChatLink(slug) {
4114
+ if (!this.client || !this.connected)
4115
+ throw new Error(NOT_CONNECTED_ERROR);
4116
+ const client = this.client;
4117
+ return this.rateLimiter.execute(async () => {
4118
+ await client.invoke(new Api.account.DeleteBusinessChatLink({ slug }));
4119
+ }, `deleteBusinessChatLink ${slug}`);
4120
+ }
4121
+ async resolveBusinessChatLink(slug) {
4122
+ if (!this.client || !this.connected)
4123
+ throw new Error(NOT_CONNECTED_ERROR);
4124
+ const client = this.client;
4125
+ return this.rateLimiter.execute(async () => {
4126
+ const result = await client.invoke(new Api.account.ResolveBusinessChatLink({ slug }));
4127
+ const r = result;
4128
+ return {
4129
+ peer: summarizePeer(r.peer),
4130
+ message: r.message,
4131
+ entityCount: r.entities?.length ?? 0,
4132
+ };
4133
+ }, `resolveBusinessChatLink ${slug}`);
4134
+ }
3128
4135
  // ─── Group calls ───────────────────────────────────────────────────────────
3129
4136
  async getGroupCall(chatId, options = {}) {
3130
4137
  if (!this.client || !this.connected)
@@ -3214,6 +4221,244 @@ export class TelegramService {
3214
4221
  return summarizeQuickReplyMessages(response);
3215
4222
  }, `getQuickReplyMessages ${shortcutId}`);
3216
4223
  }
4224
+ // ─── Stories Write ─────────────────────────────────────────────────────────
4225
+ async sendStory(chatId, filePath, opts) {
4226
+ if (!this.client || !this.connected)
4227
+ throw new Error(NOT_CONNECTED_ERROR);
4228
+ const client = this.client;
4229
+ const peer = await this.resolvePeer(chatId);
4230
+ return this.rateLimiter.execute(async () => {
4231
+ const inputPeer = await client.getInputEntity(peer);
4232
+ const fileData = await readFile(filePath);
4233
+ const uploaded = await client.uploadFile({
4234
+ file: new CustomFile(filePath, fileData.length, filePath, fileData),
4235
+ workers: 4,
4236
+ });
4237
+ const mediaType = opts.type ?? detectMediaType(filePath);
4238
+ const media = mediaType === "photo"
4239
+ ? new Api.InputMediaUploadedPhoto({ file: uploaded })
4240
+ : new Api.InputMediaUploadedDocument({
4241
+ file: uploaded,
4242
+ mimeType: "video/mp4",
4243
+ attributes: [new Api.DocumentAttributeVideo({ duration: 0, w: 0, h: 0, supportsStreaming: true })],
4244
+ });
4245
+ const privacyRules = buildStoryPrivacyRules(opts.privacy, opts.allowUserIds, opts.disallowUserIds);
4246
+ let caption = opts.caption;
4247
+ let entities;
4248
+ if (opts.caption && opts.parseMode) {
4249
+ // biome-ignore lint/suspicious/noExplicitAny: GramJS internal helper, no public typing
4250
+ const parser = client._parseMessageText;
4251
+ if (typeof parser === "function") {
4252
+ [caption, entities] = await parser.call(client, opts.caption, opts.parseMode === "html" ? "html" : "md");
4253
+ }
4254
+ }
4255
+ const result = await client.invoke(new Api.stories.SendStory({
4256
+ peer: inputPeer,
4257
+ media,
4258
+ privacyRules,
4259
+ caption,
4260
+ ...(entities?.length ? { entities } : {}),
4261
+ randomId: generateRandomBigInt(),
4262
+ period: opts.period ?? 86400,
4263
+ pinned: opts.pinned,
4264
+ noforwards: opts.noforwards,
4265
+ }));
4266
+ const id = extractStoryIdFromUpdates(result) || undefined;
4267
+ return { id, period: opts.period ?? 86400 };
4268
+ }, `sendStory ${chatId}`);
4269
+ }
4270
+ async editStory(chatId, storyId, opts) {
4271
+ if (!this.client || !this.connected)
4272
+ throw new Error(NOT_CONNECTED_ERROR);
4273
+ const client = this.client;
4274
+ const peer = await this.resolvePeer(chatId);
4275
+ return this.rateLimiter.execute(async () => {
4276
+ const inputPeer = await client.getInputEntity(peer);
4277
+ const changed = [];
4278
+ let media;
4279
+ if (opts.filePath) {
4280
+ changed.push("media");
4281
+ const fileData = await readFile(opts.filePath);
4282
+ const uploaded = await client.uploadFile({
4283
+ file: new CustomFile(opts.filePath, fileData.length, opts.filePath, fileData),
4284
+ workers: 4,
4285
+ });
4286
+ const mediaType = opts.type ?? detectMediaType(opts.filePath);
4287
+ media =
4288
+ mediaType === "photo"
4289
+ ? new Api.InputMediaUploadedPhoto({ file: uploaded })
4290
+ : new Api.InputMediaUploadedDocument({
4291
+ file: uploaded,
4292
+ mimeType: "video/mp4",
4293
+ attributes: [new Api.DocumentAttributeVideo({ duration: 0, w: 0, h: 0, supportsStreaming: true })],
4294
+ });
4295
+ }
4296
+ let caption = opts.caption;
4297
+ let entities;
4298
+ if (opts.caption !== undefined) {
4299
+ changed.push("caption");
4300
+ if (opts.caption && opts.parseMode) {
4301
+ // biome-ignore lint/suspicious/noExplicitAny: GramJS internal helper, no public typing
4302
+ const parser = client._parseMessageText;
4303
+ if (typeof parser === "function") {
4304
+ [caption, entities] = await parser.call(client, opts.caption, opts.parseMode === "html" ? "html" : "md");
4305
+ }
4306
+ }
4307
+ }
4308
+ let privacyRules;
4309
+ if (opts.privacy) {
4310
+ changed.push("privacy");
4311
+ privacyRules = buildStoryPrivacyRules(opts.privacy, opts.allowUserIds, opts.disallowUserIds);
4312
+ }
4313
+ await client.invoke(new Api.stories.EditStory({
4314
+ peer: inputPeer,
4315
+ id: storyId,
4316
+ ...(media ? { media } : {}),
4317
+ ...(caption !== undefined ? { caption } : {}),
4318
+ ...(entities?.length ? { entities } : {}),
4319
+ ...(privacyRules ? { privacyRules } : {}),
4320
+ }));
4321
+ return { changed };
4322
+ }, `editStory ${chatId}/${storyId}`);
4323
+ }
4324
+ async deleteStories(chatId, ids) {
4325
+ if (!this.client || !this.connected)
4326
+ throw new Error(NOT_CONNECTED_ERROR);
4327
+ const peer = await this.resolvePeer(chatId);
4328
+ return this.rateLimiter.execute(async () => {
4329
+ const deleted = await this.client?.invoke(new Api.stories.DeleteStories({ peer, id: ids }));
4330
+ return { deleted: deleted ?? [] };
4331
+ }, `deleteStories ${chatId}`);
4332
+ }
4333
+ async sendStoryReaction(chatId, storyId, emoji, addToRecent) {
4334
+ if (!this.client || !this.connected)
4335
+ throw new Error(NOT_CONNECTED_ERROR);
4336
+ const peer = await this.resolvePeer(chatId);
4337
+ return this.rateLimiter.execute(async () => {
4338
+ const reaction = emoji === "" ? new Api.ReactionEmpty() : new Api.ReactionEmoji({ emoticon: emoji });
4339
+ await this.client?.invoke(new Api.stories.SendReaction({ peer, storyId, reaction, addToRecent }));
4340
+ }, `sendStoryReaction ${chatId}/${storyId}`);
4341
+ }
4342
+ async exportStoryLink(chatId, storyId) {
4343
+ if (!this.client || !this.connected)
4344
+ throw new Error(NOT_CONNECTED_ERROR);
4345
+ const peer = await this.resolvePeer(chatId);
4346
+ return this.rateLimiter.execute(async () => {
4347
+ const result = await this.client?.invoke(new Api.stories.ExportStoryLink({ peer, id: storyId }));
4348
+ if (!result)
4349
+ throw new Error("stories.ExportStoryLink returned nothing");
4350
+ return { link: result.link };
4351
+ }, `exportStoryLink ${chatId}/${storyId}`);
4352
+ }
4353
+ async readStories(chatId, maxId) {
4354
+ if (!this.client || !this.connected)
4355
+ throw new Error(NOT_CONNECTED_ERROR);
4356
+ const peer = await this.resolvePeer(chatId);
4357
+ return this.rateLimiter.execute(async () => {
4358
+ const ids = await this.client?.invoke(new Api.stories.ReadStories({ peer, maxId }));
4359
+ return { ids: ids ?? [] };
4360
+ }, `readStories ${chatId}/${maxId}`);
4361
+ }
4362
+ async toggleStoryPinned(chatId, ids, pinned) {
4363
+ if (!this.client || !this.connected)
4364
+ throw new Error(NOT_CONNECTED_ERROR);
4365
+ const peer = await this.resolvePeer(chatId);
4366
+ return this.rateLimiter.execute(async () => {
4367
+ const affected = await this.client?.invoke(new Api.stories.TogglePinned({ peer, id: ids, pinned }));
4368
+ return { affected: affected ?? [] };
4369
+ }, `toggleStoryPinned ${chatId}`);
4370
+ }
4371
+ async toggleStoryPinnedToTop(chatId, ids) {
4372
+ if (!this.client || !this.connected)
4373
+ throw new Error(NOT_CONNECTED_ERROR);
4374
+ const peer = await this.resolvePeer(chatId);
4375
+ return this.rateLimiter.execute(async () => {
4376
+ await this.client?.invoke(new Api.stories.TogglePinnedToTop({ peer, id: ids }));
4377
+ return { ok: true };
4378
+ }, `toggleStoryPinnedToTop ${chatId}`);
4379
+ }
4380
+ async activateStealthMode(past, future) {
4381
+ if (!this.client || !this.connected)
4382
+ throw new Error(NOT_CONNECTED_ERROR);
4383
+ return this.rateLimiter.execute(async () => {
4384
+ await this.client?.invoke(new Api.stories.ActivateStealthMode({ past: past ?? false, future: future ?? false }));
4385
+ }, "activateStealthMode");
4386
+ }
4387
+ async getStoriesArchive(chatId, offsetId, limit) {
4388
+ if (!this.client || !this.connected)
4389
+ throw new Error(NOT_CONNECTED_ERROR);
4390
+ const peer = await this.resolvePeer(chatId);
4391
+ return this.rateLimiter.execute(async () => {
4392
+ const result = await this.client?.invoke(new Api.stories.GetStoriesArchive({ peer, offsetId, limit }));
4393
+ if (!result)
4394
+ throw new Error("stories.GetStoriesArchive returned nothing");
4395
+ return summarizeStoriesById(result);
4396
+ }, `getStoriesArchive ${chatId}`);
4397
+ }
4398
+ async reportStory(chatId, ids, option, message) {
4399
+ if (!this.client || !this.connected)
4400
+ throw new Error(NOT_CONNECTED_ERROR);
4401
+ const peer = await this.resolvePeer(chatId);
4402
+ return this.rateLimiter.execute(async () => {
4403
+ const optionBytes = Buffer.from(option, "base64");
4404
+ const result = await this.client?.invoke(new Api.stories.Report({ peer, id: ids, option: optionBytes, message }));
4405
+ if (!result)
4406
+ throw new Error("stories.Report returned nothing");
4407
+ return summarizeReportResult(result);
4408
+ }, `reportStory ${chatId}`);
4409
+ }
4410
+ // ─── Discussion & Read Receipts ────────────────────────────────────────────
4411
+ async getDiscussionMessage(chatId, messageId) {
4412
+ if (!this.client || !this.connected)
4413
+ throw new Error(NOT_CONNECTED_ERROR);
4414
+ const peer = await this.resolvePeer(chatId);
4415
+ return this.rateLimiter.execute(async () => {
4416
+ const result = await this.client?.invoke(new Api.messages.GetDiscussionMessage({ peer, msgId: messageId }));
4417
+ if (!result)
4418
+ throw new Error("messages.GetDiscussionMessage returned nothing");
4419
+ return summarizeDiscussionMessage(result);
4420
+ }, `getDiscussionMessage ${chatId}/${messageId}`);
4421
+ }
4422
+ async getGroupsForDiscussion() {
4423
+ if (!this.client || !this.connected)
4424
+ throw new Error(NOT_CONNECTED_ERROR);
4425
+ return this.rateLimiter.execute(async () => {
4426
+ const result = await this.client?.invoke(new Api.channels.GetGroupsForDiscussion());
4427
+ if (!result)
4428
+ throw new Error("channels.GetGroupsForDiscussion returned nothing");
4429
+ return summarizeGroupsForDiscussion(result);
4430
+ }, "getGroupsForDiscussion");
4431
+ }
4432
+ async getMessageReadParticipants(chatId, messageId) {
4433
+ if (!this.client || !this.connected)
4434
+ throw new Error(NOT_CONNECTED_ERROR);
4435
+ const peer = await this.resolvePeer(chatId);
4436
+ return this.rateLimiter.execute(async () => {
4437
+ const result = await this.client?.invoke(new Api.messages.GetMessageReadParticipants({ peer, msgId: messageId }));
4438
+ if (!result)
4439
+ throw new Error("messages.GetMessageReadParticipants returned nothing");
4440
+ return summarizeReadParticipants(result, messageId);
4441
+ }, `getMessageReadParticipants ${chatId}/${messageId}`);
4442
+ }
4443
+ async getOutboxReadDate(chatId, messageId) {
4444
+ if (!this.client || !this.connected)
4445
+ throw new Error(NOT_CONNECTED_ERROR);
4446
+ const peer = await this.resolvePeer(chatId);
4447
+ return this.rateLimiter.execute(async () => {
4448
+ try {
4449
+ const result = await this.client?.invoke(new Api.messages.GetOutboxReadDate({ peer, msgId: messageId }));
4450
+ if (!result)
4451
+ return { readAt: null };
4452
+ const date = result.date;
4453
+ return { readAt: new Date(date * 1000).toISOString() };
4454
+ }
4455
+ catch (e) {
4456
+ if (/NOT_READ_YET/i.test(e.message ?? ""))
4457
+ return { readAt: null };
4458
+ throw e;
4459
+ }
4460
+ }, `getOutboxReadDate ${chatId}/${messageId}`);
4461
+ }
3217
4462
  async resolveInputGroupCall(chatId) {
3218
4463
  const entity = await this.resolveChat(chatId);
3219
4464
  let call;