2026-01-09 20:46:11 +01:00
import type {
InlineKeyboardButton ,
InlineKeyboardMarkup ,
ReactionType ,
ReactionTypeEmoji ,
} from "@grammyjs/types" ;
2026-01-25 10:38:49 +00:00
import { type ApiClientOptions , Bot , HttpError , InputFile } from "grammy" ;
2026-01-07 17:48:19 +00:00
import { loadConfig } from "../config/config.js" ;
2026-02-01 10:03:47 +09:00
import { resolveMarkdownTableMode } from "../config/markdown-tables.js" ;
2026-01-08 14:26:54 +00:00
import { logVerbose } from "../globals.js" ;
2026-01-13 06:16:43 +00:00
import { recordChannelActivity } from "../infra/channel-activity.js" ;
2026-01-25 10:38:49 +00:00
import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js" ;
2026-02-01 10:03:47 +09:00
import { formatErrorMessage , formatUncaughtError } from "../infra/errors.js" ;
2026-01-07 17:48:19 +00:00
import { createTelegramRetryRunner } from "../infra/retry-policy.js" ;
2026-02-17 13:36:48 +09:00
import type { RetryConfig } from "../infra/retry.js" ;
2026-01-25 10:38:49 +00:00
import { redactSensitiveText } from "../logging/redact.js" ;
import { createSubsystemLogger } from "../logging/subsystem.js" ;
2025-12-07 22:46:02 +01:00
import { mediaKindFromMime } from "../media/constants.js" ;
2026-01-06 02:22:09 +00:00
import { isGifMedia } from "../media/mime.js" ;
2026-02-14 18:34:30 +01:00
import { normalizePollInput , type PollInput } from "../polls.js" ;
2025-12-07 22:46:02 +01:00
import { loadWebMedia } from "../web/media.js" ;
2026-01-25 11:41:32 +00:00
import { type ResolvedTelegramAccount , resolveTelegramAccount } from "./accounts.js" ;
2026-02-01 10:03:47 +09:00
import { withTelegramApiErrorLogging } from "./api-logging.js" ;
import { buildTelegramThreadParams } from "./bot/helpers.js" ;
2026-02-17 13:36:48 +09:00
import type { TelegramInlineButtons } from "./button-types.js" ;
2026-02-01 10:03:47 +09:00
import { splitTelegramCaption } from "./caption.js" ;
2026-01-08 04:40:29 +01:00
import { resolveTelegramFetch } from "./fetch.js" ;
2026-01-24 03:39:21 +00:00
import { renderTelegramHtmlText } from "./format.js" ;
2026-01-26 19:24:13 -05:00
import { isRecoverableTelegramNetworkError } from "./network-errors.js" ;
2026-02-16 21:19:54 +05:30
import { recordSentPoll } from "./poll-vote-cache.js" ;
2026-02-01 10:03:47 +09:00
import { makeProxyFetch } from "./proxy.js" ;
2026-01-13 21:13:05 +02:00
import { recordSentMessage } from "./sent-message-cache.js" ;
2026-01-15 18:13:49 +02:00
import { parseTelegramTarget , stripTelegramInternalPrefixes } from "./targets.js" ;
2026-01-10 02:40:41 +01:00
import { resolveTelegramVoiceSend } from "./voice.js" ;
2025-12-07 22:46:02 +01:00
2026-02-17 10:26:36 +09:00
type TelegramApi = Bot [ "api" ] ;
type TelegramApiOverride = Partial < TelegramApi > ;
2025-12-07 22:46:02 +01:00
type TelegramSendOpts = {
token? : string ;
2026-01-08 01:18:37 +01:00
accountId? : string ;
2025-12-07 22:46:02 +01:00
verbose? : boolean ;
mediaUrl? : string ;
2026-02-15 10:53:45 -05:00
mediaLocalRoots? : readonly string [ ] ;
2025-12-07 22:46:02 +01:00
maxBytes? : number ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-01-07 17:48:19 +00:00
retry? : RetryConfig ;
2026-01-15 00:12:29 +00:00
textMode ? : "markdown" | "html" ;
plainText? : string ;
2026-01-04 20:28:29 +01:00
/** Send audio as voice message (voice bubble) instead of audio file. Defaults to false. */
asVoice? : boolean ;
2026-02-09 07:00:57 +00:00
/** Send video as video note (voice bubble) instead of regular video. Defaults to false. */
asVideoNote? : boolean ;
2026-01-27 02:44:13 +05:30
/** Send message silently (no notification). Defaults to false. */
silent? : boolean ;
2026-01-07 03:24:56 -03:00
/** Message ID to reply to (for threading) */
replyToMessageId? : number ;
2026-01-28 00:59:24 +04:00
/** Quote text for Telegram reply_parameters. */
quoteText? : string ;
2026-01-07 03:24:56 -03:00
/** Forum topic thread ID (for forum supergroups) */
messageThreadId? : number ;
2026-01-09 20:46:11 +01:00
/** Inline keyboard buttons (reply markup). */
2026-02-16 22:48:47 +05:30
buttons? : TelegramInlineButtons ;
2025-12-07 22:46:02 +01:00
} ;
type TelegramSendResult = {
messageId : string ;
chatId : string ;
} ;
2026-02-16 14:00:34 +05:30
type TelegramMessageLike = {
message_id? : number ;
chat ? : { id? : string | number } ;
} ;
2026-01-07 04:10:13 +01:00
type TelegramReactionOpts = {
token? : string ;
2026-01-08 01:18:37 +01:00
accountId? : string ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-01-07 04:10:13 +01:00
remove? : boolean ;
2026-01-07 17:48:19 +00:00
verbose? : boolean ;
retry? : RetryConfig ;
2026-01-07 04:10:13 +01:00
} ;
2026-01-14 14:31:43 +00:00
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i ;
2026-02-09 08:35:53 +05:30
const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i ;
2026-02-16 04:18:17 +01:00
const MESSAGE_NOT_MODIFIED_RE =
/400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i ;
2026-02-16 14:00:34 +05:30
const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i ;
2026-01-25 10:38:49 +00:00
const diagLogger = createSubsystemLogger ( "telegram/diagnostic" ) ;
function createTelegramHttpLogger ( cfg : ReturnType < typeof loadConfig > ) {
const enabled = isDiagnosticFlagEnabled ( "telegram.http" , cfg ) ;
if ( ! enabled ) {
return ( ) = > { } ;
}
return ( label : string , err : unknown ) = > {
2026-01-31 16:19:20 +09:00
if ( ! ( err instanceof HttpError ) ) {
return ;
}
2026-01-25 10:38:49 +00:00
const detail = redactSensitiveText ( formatUncaughtError ( err . error ? ? err ) ) ;
diagLogger . warn ( ` telegram http error ( ${ label } ): ${ detail } ` ) ;
} ;
}
2025-12-10 15:55:20 +00:00
2026-01-25 11:41:32 +00:00
function resolveTelegramClientOptions (
account : ResolvedTelegramAccount ,
) : ApiClientOptions | undefined {
const proxyUrl = account . config . proxy ? . trim ( ) ;
const proxyFetch = proxyUrl ? makeProxyFetch ( proxyUrl ) : undefined ;
2026-01-26 19:24:13 -05:00
const fetchImpl = resolveTelegramFetch ( proxyFetch , {
network : account.config.network ,
} ) ;
2026-01-25 11:41:32 +00:00
const timeoutSeconds =
typeof account . config . timeoutSeconds === "number" &&
Number . isFinite ( account . config . timeoutSeconds )
? Math . max ( 1 , Math . floor ( account . config . timeoutSeconds ) )
: undefined ;
return fetchImpl || timeoutSeconds
? {
. . . ( fetchImpl ? { fetch : fetchImpl as unknown as ApiClientOptions [ "fetch" ] } : { } ) ,
. . . ( timeoutSeconds ? { timeoutSeconds } : { } ) ,
}
: undefined ;
}
2026-01-14 14:31:43 +00:00
function resolveToken ( explicit : string | undefined , params : { accountId : string ; token : string } ) {
2026-01-31 16:19:20 +09:00
if ( explicit ? . trim ( ) ) {
return explicit . trim ( ) ;
}
2026-01-08 01:18:37 +01:00
if ( ! params . token ) {
2025-12-08 01:48:53 +01:00
throw new Error (
2026-01-13 06:16:43 +00:00
` Telegram bot token missing for account " ${ params . accountId } " (set channels.telegram.accounts. ${ params . accountId } .botToken/tokenFile or TELEGRAM_BOT_TOKEN for default). ` ,
2025-12-08 01:48:53 +01:00
) ;
2025-12-07 22:46:02 +01:00
}
2026-01-08 01:18:37 +01:00
return params . token . trim ( ) ;
2025-12-07 22:46:02 +01:00
}
function normalizeChatId ( to : string ) : string {
const trimmed = to . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! trimmed ) {
throw new Error ( "Recipient is required for Telegram sends" ) ;
}
2025-12-20 14:21:49 +00:00
// Common internal prefixes that sometimes leak into outbound sends.
// - ctx.To uses `telegram:<id>`
2026-01-02 10:14:58 +01:00
// - group sessions often use `telegram:group:<id>`
2026-01-08 21:38:59 +01:00
let normalized = stripTelegramInternalPrefixes ( trimmed ) ;
2025-12-20 14:21:49 +00:00
// Accept t.me links for public chats/channels.
// (Invite links like `t.me/+...` are not resolvable via Bot API.)
const m =
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i . exec ( normalized ) ? ?
/^t\.me\/([A-Za-z0-9_]+)$/i . exec ( normalized ) ;
2026-01-31 16:19:20 +09:00
if ( m ? . [ 1 ] ) {
normalized = ` @ ${ m [ 1 ] } ` ;
}
2025-12-20 14:21:49 +00:00
2026-01-31 16:19:20 +09:00
if ( ! normalized ) {
throw new Error ( "Recipient is required for Telegram sends" ) ;
}
if ( normalized . startsWith ( "@" ) ) {
return normalized ;
}
if ( /^-?\d+$/ . test ( normalized ) ) {
return normalized ;
}
2025-12-20 14:21:49 +00:00
// If the user passed a username without `@`, assume they meant a public chat/channel.
2026-01-31 16:19:20 +09:00
if ( /^[A-Za-z0-9_]{5,}$/i . test ( normalized ) ) {
return ` @ ${ normalized } ` ;
}
2025-12-20 14:21:49 +00:00
return normalized ;
2025-12-07 22:46:02 +01:00
}
2026-01-07 04:10:13 +01:00
function normalizeMessageId ( raw : string | number ) : number {
if ( typeof raw === "number" && Number . isFinite ( raw ) ) {
return Math . trunc ( raw ) ;
}
if ( typeof raw === "string" ) {
const value = raw . trim ( ) ;
if ( ! value ) {
2026-01-15 00:29:28 +00:00
throw new Error ( "Message id is required for Telegram actions" ) ;
2026-01-07 04:10:13 +01:00
}
const parsed = Number . parseInt ( value , 10 ) ;
2026-01-31 16:19:20 +09:00
if ( Number . isFinite ( parsed ) ) {
return parsed ;
}
2026-01-07 04:10:13 +01:00
}
2026-01-15 00:29:28 +00:00
throw new Error ( "Message id is required for Telegram actions" ) ;
2026-01-07 04:10:13 +01:00
}
2026-02-09 08:35:53 +05:30
function isTelegramThreadNotFoundError ( err : unknown ) : boolean {
return THREAD_NOT_FOUND_RE . test ( formatErrorMessage ( err ) ) ;
}
2026-02-16 04:18:17 +01:00
function isTelegramMessageNotModifiedError ( err : unknown ) : boolean {
return MESSAGE_NOT_MODIFIED_RE . test ( formatErrorMessage ( err ) ) ;
}
2026-02-16 12:01:35 +01:00
/ * *
* Telegram private chats have positive numeric IDs .
* Groups and supergroups have negative IDs ( typically - 100 … for supergroups ) .
* Private chats never support forum topics , so ` message_thread_id ` must
* not be included in API calls targeting them ( # 17242 ) .
* /
function isTelegramPrivateChat ( chatId : string ) : boolean {
const n = Number ( chatId ) ;
return Number . isFinite ( n ) && n > 0 ;
}
2026-02-09 08:35:53 +05:30
function hasMessageThreadIdParam ( params? : Record < string , unknown > ) : boolean {
2026-02-09 08:43:40 +05:30
if ( ! params ) {
return false ;
}
const value = params . message_thread_id ;
if ( typeof value === "number" ) {
return Number . isFinite ( value ) ;
}
if ( typeof value === "string" ) {
return value . trim ( ) . length > 0 ;
}
return false ;
2026-02-09 08:35:53 +05:30
}
function removeMessageThreadIdParam (
params? : Record < string , unknown > ,
) : Record < string , unknown > | undefined {
if ( ! params || ! hasMessageThreadIdParam ( params ) ) {
return params ;
}
const next = { . . . params } ;
delete next . message_thread_id ;
return Object . keys ( next ) . length > 0 ? next : undefined ;
}
2026-02-16 14:00:34 +05:30
function isTelegramHtmlParseError ( err : unknown ) : boolean {
return PARSE_ERR_RE . test ( formatErrorMessage ( err ) ) ;
}
function buildTelegramThreadReplyParams ( params : {
targetMessageThreadId? : number ;
messageThreadId? : number ;
replyToMessageId? : number ;
quoteText? : string ;
} ) : Record < string , unknown > {
const messageThreadId =
params . messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId ;
const threadSpec =
messageThreadId != null ? { id : messageThreadId , scope : "forum" as const } : undefined ;
const threadIdParams = buildTelegramThreadParams ( threadSpec ) ;
const threadParams : Record < string , unknown > = threadIdParams ? { . . . threadIdParams } : { } ;
if ( params . replyToMessageId != null ) {
const replyToMessageId = Math . trunc ( params . replyToMessageId ) ;
if ( params . quoteText ? . trim ( ) ) {
threadParams . reply_parameters = {
message_id : replyToMessageId ,
quote : params.quoteText.trim ( ) ,
} ;
} else {
threadParams . reply_to_message_id = replyToMessageId ;
}
}
return threadParams ;
}
async function withTelegramHtmlParseFallback < T > ( params : {
label : string ;
verbose? : boolean ;
requestHtml : ( label : string ) = > Promise < T > ;
requestPlain : ( label : string ) = > Promise < T > ;
} ) : Promise < T > {
try {
return await params . requestHtml ( params . label ) ;
} catch ( err ) {
if ( ! isTelegramHtmlParseError ( err ) ) {
throw err ;
}
if ( params . verbose ) {
console . warn (
` telegram ${ params . label } failed with HTML parse error, retrying as plain text: ${ formatErrorMessage (
err ,
) } ` ,
) ;
}
return await params . requestPlain ( ` ${ params . label } -plain ` ) ;
}
}
type TelegramApiContext = {
cfg : ReturnType < typeof loadConfig > ;
account : ResolvedTelegramAccount ;
2026-02-17 10:26:36 +09:00
api : TelegramApi ;
2026-02-16 14:00:34 +05:30
} ;
function resolveTelegramApiContext ( opts : {
token? : string ;
accountId? : string ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-02-16 14:00:34 +05:30
cfg? : ReturnType < typeof loadConfig > ;
} ) : TelegramApiContext {
const cfg = opts . cfg ? ? loadConfig ( ) ;
const account = resolveTelegramAccount ( {
cfg ,
accountId : opts.accountId ,
} ) ;
const token = resolveToken ( opts . token , account ) ;
const client = resolveTelegramClientOptions ( account ) ;
2026-02-17 10:26:36 +09:00
const api = ( opts . api ? ? new Bot ( token , client ? { client } : undefined ) . api ) as TelegramApi ;
2026-02-16 14:00:34 +05:30
return { cfg , account , api } ;
}
type TelegramRequestWithDiag = < T > (
fn : ( ) = > Promise < T > ,
label? : string ,
options ? : { shouldLog ? : ( err : unknown ) = > boolean } ,
) = > Promise < T > ;
function createTelegramRequestWithDiag ( params : {
cfg : ReturnType < typeof loadConfig > ;
account : ResolvedTelegramAccount ;
retry? : RetryConfig ;
verbose? : boolean ;
shouldRetry ? : ( err : unknown ) = > boolean ;
useApiErrorLogging? : boolean ;
} ) : TelegramRequestWithDiag {
const request = createTelegramRetryRunner ( {
retry : params.retry ,
configRetry : params.account.config.retry ,
verbose : params.verbose ,
. . . ( params . shouldRetry ? { shouldRetry : params.shouldRetry } : { } ) ,
} ) ;
const logHttpError = createTelegramHttpLogger ( params . cfg ) ;
return < T > (
fn : ( ) = > Promise < T > ,
label? : string ,
options ? : { shouldLog ? : ( err : unknown ) = > boolean } ,
) = > {
const runRequest = ( ) = > request ( fn , label ) ;
const call =
params . useApiErrorLogging === false
? runRequest ( )
: withTelegramApiErrorLogging ( {
operation : label ? ? "request" ,
fn : runRequest ,
. . . ( options ? . shouldLog ? { shouldLog : options.shouldLog } : { } ) ,
} ) ;
return call . catch ( ( err ) = > {
logHttpError ( label ? ? "request" , err ) ;
throw err ;
} ) ;
} ;
}
function wrapTelegramChatNotFoundError ( err : unknown , params : { chatId : string ; input : string } ) {
if ( ! CHAT_NOT_FOUND_RE . test ( formatErrorMessage ( err ) ) ) {
return err ;
}
return new Error (
[
` Telegram send failed: chat not found (chat_id= ${ params . chatId } ). ` ,
"Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token." ,
` Input was: ${ JSON . stringify ( params . input ) } . ` ,
] . join ( " " ) ,
) ;
}
async function withTelegramThreadFallback < T > (
params : Record < string , unknown > | undefined ,
label : string ,
verbose : boolean | undefined ,
attempt : (
effectiveParams : Record < string , unknown > | undefined ,
effectiveLabel : string ,
) = > Promise < T > ,
) : Promise < T > {
try {
return await attempt ( params , label ) ;
} catch ( err ) {
if ( ! hasMessageThreadIdParam ( params ) || ! isTelegramThreadNotFoundError ( err ) ) {
throw err ;
}
if ( verbose ) {
console . warn (
` telegram ${ label } failed with message_thread_id, retrying without thread: ${ formatErrorMessage ( err ) } ` ,
) ;
}
const retriedParams = removeMessageThreadIdParam ( params ) ;
return await attempt ( retriedParams , ` ${ label } -threadless ` ) ;
}
}
function createRequestWithChatNotFound ( params : {
requestWithDiag : TelegramRequestWithDiag ;
chatId : string ;
input : string ;
} ) {
return async < T > ( fn : ( ) = > Promise < T > , label : string ) = >
params . requestWithDiag ( fn , label ) . catch ( ( err ) = > {
throw wrapTelegramChatNotFoundError ( err , {
chatId : params.chatId ,
input : params.input ,
} ) ;
} ) ;
}
2026-01-09 20:46:11 +01:00
export function buildInlineKeyboard (
buttons? : TelegramSendOpts [ "buttons" ] ,
) : InlineKeyboardMarkup | undefined {
2026-01-31 16:19:20 +09:00
if ( ! buttons ? . length ) {
return undefined ;
}
2026-01-09 20:46:11 +01:00
const rows = buttons
. map ( ( row ) = >
row
. filter ( ( button ) = > button ? . text && button ? . callback_data )
. map (
( button ) : InlineKeyboardButton = > ( {
text : button.text ,
callback_data : button.callback_data ,
2026-02-16 22:48:47 +05:30
. . . ( button . style ? { style : button.style } : { } ) ,
2026-01-09 20:46:11 +01:00
} ) ,
) ,
)
. filter ( ( row ) = > row . length > 0 ) ;
2026-01-31 16:19:20 +09:00
if ( rows . length === 0 ) {
return undefined ;
}
2026-01-09 20:46:11 +01:00
return { inline_keyboard : rows } ;
}
2025-12-07 22:46:02 +01:00
export async function sendMessageTelegram (
to : string ,
text : string ,
opts : TelegramSendOpts = { } ,
) : Promise < TelegramSendResult > {
2026-02-16 14:00:34 +05:30
const { cfg , account , api } = resolveTelegramApiContext ( opts ) ;
2026-01-08 21:38:59 +01:00
const target = parseTelegramTarget ( to ) ;
const chatId = normalizeChatId ( target . chatId ) ;
2025-12-07 22:46:02 +01:00
const mediaUrl = opts . mediaUrl ? . trim ( ) ;
2026-01-09 20:46:11 +01:00
const replyMarkup = buildInlineKeyboard ( opts . buttons ) ;
2026-01-07 03:24:56 -03:00
2026-02-16 12:01:35 +01:00
const isPrivate = isTelegramPrivateChat ( chatId ) ;
2026-02-16 14:00:34 +05:30
const threadParams = buildTelegramThreadReplyParams ( {
2026-02-16 12:01:35 +01:00
targetMessageThreadId : isPrivate ? undefined : target . messageThreadId ,
messageThreadId : isPrivate ? undefined : opts . messageThreadId ,
2026-02-16 14:00:34 +05:30
replyToMessageId : opts.replyToMessageId ,
quoteText : opts.quoteText ,
} ) ;
2026-01-07 03:24:56 -03:00
const hasThreadParams = Object . keys ( threadParams ) . length > 0 ;
2026-02-16 14:00:34 +05:30
const requestWithDiag = createTelegramRequestWithDiag ( {
cfg ,
account ,
2026-01-07 17:48:19 +00:00
retry : opts.retry ,
verbose : opts.verbose ,
2026-01-26 19:24:13 -05:00
shouldRetry : ( err ) = > isRecoverableTelegramNetworkError ( err , { context : "send" } ) ,
2026-01-07 17:48:19 +00:00
} ) ;
2026-02-16 14:00:34 +05:30
const requestWithChatNotFound = createRequestWithChatNotFound ( {
requestWithDiag ,
chatId ,
input : to ,
} ) ;
2026-02-09 08:35:53 +05:30
2026-01-24 03:39:21 +00:00
const textMode = opts . textMode ? ? "markdown" ;
const tableMode = resolveMarkdownTableMode ( {
cfg ,
channel : "telegram" ,
accountId : account.accountId ,
} ) ;
const renderHtmlText = ( value : string ) = > renderTelegramHtmlText ( value , { textMode , tableMode } ) ;
2026-01-25 07:55:39 +00:00
// Resolve link preview setting from config (default: enabled).
const linkPreviewEnabled = account . config . linkPreview ? ? true ;
2026-01-25 13:18:05 +08:00
const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled : true } ;
2026-01-24 03:39:21 +00:00
const sendTelegramText = async (
rawText : string ,
params? : Record < string , unknown > ,
fallbackText? : string ,
) = > {
2026-02-16 14:00:34 +05:30
return await withTelegramThreadFallback (
params ,
"message" ,
opts . verbose ,
async ( effectiveParams , label ) = > {
const htmlText = renderHtmlText ( rawText ) ;
const baseParams = effectiveParams ? { . . . effectiveParams } : { } ;
if ( linkPreviewOptions ) {
baseParams . link_preview_options = linkPreviewOptions ;
2026-02-09 08:35:53 +05:30
}
2026-02-16 14:00:34 +05:30
const hasBaseParams = Object . keys ( baseParams ) . length > 0 ;
const sendParams = {
parse_mode : "HTML" as const ,
. . . baseParams ,
. . . ( opts . silent === true ? { disable_notification : true } : { } ) ,
} ;
return await withTelegramHtmlParseFallback ( {
label ,
verbose : opts.verbose ,
requestHtml : ( retryLabel ) = >
requestWithChatNotFound (
( ) = >
api . sendMessage (
chatId ,
htmlText ,
sendParams as Parameters < typeof api.sendMessage > [ 2 ] ,
) ,
retryLabel ,
) ,
requestPlain : ( retryLabel ) = > {
const plainParams = hasBaseParams
? ( baseParams as Parameters < typeof api.sendMessage > [ 2 ] )
: undefined ;
return requestWithChatNotFound (
( ) = >
plainParams
? api . sendMessage ( chatId , fallbackText ? ? rawText , plainParams )
: api . sendMessage ( chatId , fallbackText ? ? rawText ) ,
retryLabel ,
) ;
} ,
} ) ;
} ,
) ;
2026-01-24 03:39:21 +00:00
} ;
2025-12-07 22:46:02 +01:00
if ( mediaUrl ) {
2026-02-15 10:53:45 -05:00
const media = await loadWebMedia ( mediaUrl , {
maxBytes : opts.maxBytes ,
localRoots : opts.mediaLocalRoots ,
} ) ;
2025-12-07 22:46:02 +01:00
const kind = mediaKindFromMime ( media . contentType ? ? undefined ) ;
2026-01-06 02:22:09 +00:00
const isGif = isGifMedia ( {
contentType : media.contentType ,
fileName : media.fileName ,
} ) ;
2026-02-09 07:00:57 +00:00
const isVideoNote = kind === "video" && opts . asVideoNote === true ;
2026-01-14 14:31:43 +00:00
const fileName = media . fileName ? ? ( isGif ? "animation.gif" : inferFilename ( kind ) ) ? ? "file" ;
2026-01-06 02:22:09 +00:00
const file = new InputFile ( media . buffer , fileName ) ;
2026-02-09 07:00:57 +00:00
let caption : string | undefined ;
let followUpText : string | undefined ;
if ( isVideoNote ) {
caption = undefined ;
followUpText = text . trim ( ) ? text : undefined ;
} else {
const split = splitTelegramCaption ( text ) ;
caption = split . caption ;
followUpText = split . followUpText ;
}
2026-01-24 03:39:21 +00:00
const htmlCaption = caption ? renderHtmlText ( caption ) : undefined ;
2026-01-14 15:52:54 +00:00
// If text exceeds Telegram's caption limit, send media without caption
// then send text as a separate follow-up message.
2026-01-17 03:50:05 +00:00
const needsSeparateText = Boolean ( followUpText ) ;
2026-01-14 15:52:54 +00:00
// When splitting, put reply_markup only on the follow-up text (the "main" content),
// not on the media message.
2026-01-24 03:39:21 +00:00
const baseMediaParams = {
. . . ( hasThreadParams ? threadParams : { } ) ,
. . . ( ! needsSeparateText && replyMarkup ? { reply_markup : replyMarkup } : { } ) ,
} ;
const mediaParams = {
2026-02-09 07:00:57 +00:00
. . . ( htmlCaption ? { caption : htmlCaption , parse_mode : "HTML" as const } : { } ) ,
2026-01-24 03:39:21 +00:00
. . . baseMediaParams ,
2026-01-27 02:44:13 +05:30
. . . ( opts . silent === true ? { disable_notification : true } : { } ) ,
2026-01-24 03:39:21 +00:00
} ;
2026-02-16 14:00:34 +05:30
const sendMedia = async (
label : string ,
sender : (
effectiveParams : Record < string , unknown > | undefined ,
) = > Promise < TelegramMessageLike > ,
) = >
await withTelegramThreadFallback (
2026-02-09 08:35:53 +05:30
mediaParams ,
2026-02-16 14:00:34 +05:30
label ,
opts . verbose ,
async ( effectiveParams , retryLabel ) = >
requestWithChatNotFound ( ( ) = > sender ( effectiveParams ) , retryLabel ) ,
2026-01-14 14:31:43 +00:00
) ;
2026-02-16 14:00:34 +05:30
const mediaSender = ( ( ) = > {
if ( isGif ) {
return {
label : "animation" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendAnimation (
chatId ,
file ,
effectiveParams as Parameters < typeof api.sendAnimation > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
2026-02-09 07:00:57 +00:00
}
2026-02-16 14:00:34 +05:30
if ( kind === "image" ) {
return {
label : "photo" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendPhoto (
chatId ,
file ,
effectiveParams as Parameters < typeof api.sendPhoto > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
2026-01-04 20:28:29 +01:00
}
2026-02-16 14:00:34 +05:30
if ( kind === "video" ) {
if ( isVideoNote ) {
return {
label : "video_note" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendVideoNote (
2026-02-09 08:35:53 +05:30
chatId ,
file ,
2026-02-16 14:00:34 +05:30
effectiveParams as Parameters < typeof api.sendVideoNote > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
}
return {
label : "video" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendVideo (
chatId ,
file ,
effectiveParams as Parameters < typeof api.sendVideo > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
}
if ( kind === "audio" ) {
const { useVoice } = resolveTelegramVoiceSend ( {
wantsVoice : opts.asVoice === true , // default false (backward compatible)
contentType : media.contentType ,
fileName ,
logFallback : logVerbose ,
} ) ;
if ( useVoice ) {
return {
label : "voice" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendVoice (
chatId ,
file ,
effectiveParams as Parameters < typeof api.sendVoice > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
}
return {
label : "audio" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendAudio (
chatId ,
file ,
effectiveParams as Parameters < typeof api.sendAudio > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
}
return {
label : "document" ,
sender : ( effectiveParams : Record < string , unknown > | undefined ) = >
api . sendDocument (
chatId ,
file ,
effectiveParams as Parameters < typeof api.sendDocument > [ 2 ] ,
) as Promise < TelegramMessageLike > ,
} ;
} ) ( ) ;
const result = await sendMedia ( mediaSender . label , mediaSender . sender ) ;
2026-01-14 15:52:54 +00:00
const mediaMessageId = String ( result ? . message_id ? ? "unknown" ) ;
const resolvedChatId = String ( result ? . chat ? . id ? ? chatId ) ;
2026-01-13 21:13:05 +02:00
if ( result ? . message_id ) {
recordSentMessage ( chatId , result . message_id ) ;
}
2026-01-13 06:16:43 +00:00
recordChannelActivity ( {
channel : "telegram" ,
2026-01-08 23:48:07 +01:00
accountId : account.accountId ,
direction : "outbound" ,
} ) ;
2026-01-14 15:52:54 +00:00
// If text was too long for a caption, send it as a separate follow-up message.
2026-01-24 03:39:21 +00:00
// Use HTML conversion so markdown renders like captions.
2026-01-17 03:50:05 +00:00
if ( needsSeparateText && followUpText ) {
2026-01-14 15:52:54 +00:00
const textParams =
hasThreadParams || replyMarkup
? {
. . . threadParams ,
. . . ( replyMarkup ? { reply_markup : replyMarkup } : { } ) ,
}
: undefined ;
2026-01-24 03:39:21 +00:00
const textRes = await sendTelegramText ( followUpText , textParams ) ;
2026-01-14 15:52:54 +00:00
// Return the text message ID as the "main" message (it's the actual content).
return {
messageId : String ( textRes ? . message_id ? ? mediaMessageId ) ,
chatId : resolvedChatId ,
} ;
}
return { messageId : mediaMessageId , chatId : resolvedChatId } ;
2025-12-07 22:46:02 +01:00
}
if ( ! text || ! text . trim ( ) ) {
throw new Error ( "Message must be non-empty for Telegram sends" ) ;
}
2026-01-24 03:39:21 +00:00
const textParams =
hasThreadParams || replyMarkup
? {
. . . threadParams ,
. . . ( replyMarkup ? { reply_markup : replyMarkup } : { } ) ,
2026-01-14 14:31:43 +00:00
}
2026-01-24 03:39:21 +00:00
: undefined ;
const res = await sendTelegramText ( text , textParams , opts . plainText ) ;
2025-12-07 22:46:02 +01:00
const messageId = String ( res ? . message_id ? ? "unknown" ) ;
2026-01-13 21:13:05 +02:00
if ( res ? . message_id ) {
recordSentMessage ( chatId , res . message_id ) ;
}
2026-01-13 06:16:43 +00:00
recordChannelActivity ( {
channel : "telegram" ,
2026-01-08 23:48:07 +01:00
accountId : account.accountId ,
direction : "outbound" ,
} ) ;
2025-12-07 22:46:02 +01:00
return { messageId , chatId : String ( res ? . chat ? . id ? ? chatId ) } ;
}
2026-01-07 04:10:13 +01:00
export async function reactMessageTelegram (
chatIdInput : string | number ,
messageIdInput : string | number ,
emoji : string ,
opts : TelegramReactionOpts = { } ,
2026-02-12 14:28:47 +08:00
) : Promise < { ok : true } | { ok : false ; warning : string } > {
2026-02-16 14:00:34 +05:30
const { cfg , account , api } = resolveTelegramApiContext ( opts ) ;
2026-01-07 04:10:13 +01:00
const chatId = normalizeChatId ( String ( chatIdInput ) ) ;
const messageId = normalizeMessageId ( messageIdInput ) ;
2026-02-16 14:00:34 +05:30
const requestWithDiag = createTelegramRequestWithDiag ( {
cfg ,
account ,
2026-01-07 17:48:19 +00:00
retry : opts.retry ,
verbose : opts.verbose ,
2026-01-26 19:24:13 -05:00
shouldRetry : ( err ) = > isRecoverableTelegramNetworkError ( err , { context : "send" } ) ,
2026-01-07 17:48:19 +00:00
} ) ;
2026-01-07 04:10:13 +01:00
const remove = opts . remove === true ;
const trimmedEmoji = emoji . trim ( ) ;
2026-01-07 03:24:56 -03:00
// Build the reaction array. We cast emoji to the grammY union type since
// Telegram validates emoji server-side; invalid emojis fail gracefully.
const reactions : ReactionType [ ] =
remove || ! trimmedEmoji
? [ ]
: [ { type : "emoji" , emoji : trimmedEmoji as ReactionTypeEmoji [ "emoji" ] } ] ;
2026-01-07 04:10:13 +01:00
if ( typeof api . setMessageReaction !== "function" ) {
throw new Error ( "Telegram reactions are unavailable in this bot API." ) ;
}
2026-02-12 14:28:47 +08:00
try {
await requestWithDiag ( ( ) = > api . setMessageReaction ( chatId , messageId , reactions ) , "reaction" ) ;
} catch ( err : unknown ) {
const msg = err instanceof Error ? err.message : String ( err ) ;
if ( /REACTION_INVALID/i . test ( msg ) ) {
return { ok : false as const , warning : ` Reaction unavailable: ${ trimmedEmoji } ` } ;
}
throw err ;
}
2026-01-07 04:10:13 +01:00
return { ok : true } ;
}
2026-01-14 22:20:17 +02:00
type TelegramDeleteOpts = {
token? : string ;
accountId? : string ;
verbose? : boolean ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-01-14 22:20:17 +02:00
retry? : RetryConfig ;
} ;
export async function deleteMessageTelegram (
chatIdInput : string | number ,
messageIdInput : string | number ,
opts : TelegramDeleteOpts = { } ,
) : Promise < { ok : true } > {
2026-02-16 14:00:34 +05:30
const { cfg , account , api } = resolveTelegramApiContext ( opts ) ;
2026-01-14 22:20:17 +02:00
const chatId = normalizeChatId ( String ( chatIdInput ) ) ;
const messageId = normalizeMessageId ( messageIdInput ) ;
2026-02-16 14:00:34 +05:30
const requestWithDiag = createTelegramRequestWithDiag ( {
cfg ,
account ,
2026-01-14 22:20:17 +02:00
retry : opts.retry ,
verbose : opts.verbose ,
2026-01-26 19:24:13 -05:00
shouldRetry : ( err ) = > isRecoverableTelegramNetworkError ( err , { context : "send" } ) ,
2026-01-14 22:20:17 +02:00
} ) ;
2026-01-25 10:38:49 +00:00
await requestWithDiag ( ( ) = > api . deleteMessage ( chatId , messageId ) , "deleteMessage" ) ;
2026-01-14 17:10:16 -08:00
logVerbose ( ` [telegram] Deleted message ${ messageId } from chat ${ chatId } ` ) ;
2026-01-14 22:20:17 +02:00
return { ok : true } ;
}
2026-01-26 15:26:15 -08:00
type TelegramEditOpts = {
token? : string ;
accountId? : string ;
verbose? : boolean ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-01-26 15:26:15 -08:00
retry? : RetryConfig ;
textMode ? : "markdown" | "html" ;
2026-02-15 20:09:10 +05:30
/** Controls whether link previews are shown in the edited message. */
linkPreview? : boolean ;
2026-01-26 15:26:15 -08:00
/** Inline keyboard buttons (reply markup). Pass empty array to remove buttons. */
2026-02-16 22:48:47 +05:30
buttons? : TelegramInlineButtons ;
2026-01-26 15:26:15 -08:00
/** Optional config injection to avoid global loadConfig() (improves testability). */
cfg? : ReturnType < typeof loadConfig > ;
} ;
export async function editMessageTelegram (
chatIdInput : string | number ,
messageIdInput : string | number ,
text : string ,
opts : TelegramEditOpts = { } ,
) : Promise < { ok : true ; messageId : string ; chatId : string } > {
2026-02-16 14:00:34 +05:30
const { cfg , account , api } = resolveTelegramApiContext ( {
. . . opts ,
cfg : opts.cfg ,
2026-01-26 15:26:15 -08:00
} ) ;
const chatId = normalizeChatId ( String ( chatIdInput ) ) ;
const messageId = normalizeMessageId ( messageIdInput ) ;
2026-02-16 14:00:34 +05:30
const requestWithDiag = createTelegramRequestWithDiag ( {
cfg ,
account ,
2026-01-26 15:26:15 -08:00
retry : opts.retry ,
verbose : opts.verbose ,
} ) ;
2026-02-16 14:00:34 +05:30
const requestWithEditShouldLog = < T > (
2026-02-16 04:18:17 +01:00
fn : ( ) = > Promise < T > ,
label? : string ,
shouldLog ? : ( err : unknown ) = > boolean ,
2026-02-16 14:00:34 +05:30
) = > requestWithDiag ( fn , label , shouldLog ? { shouldLog } : undefined ) ;
2026-01-26 15:26:15 -08:00
const textMode = opts . textMode ? ? "markdown" ;
const tableMode = resolveMarkdownTableMode ( {
cfg ,
channel : "telegram" ,
accountId : account.accountId ,
} ) ;
const htmlText = renderTelegramHtmlText ( text , { textMode , tableMode } ) ;
// Reply markup semantics:
// - buttons === undefined → don't send reply_markup (keep existing)
// - buttons is [] (or filters to empty) → send { inline_keyboard: [] } (remove)
// - otherwise → send built inline keyboard
const shouldTouchButtons = opts . buttons !== undefined ;
const builtKeyboard = shouldTouchButtons ? buildInlineKeyboard ( opts . buttons ) : undefined ;
const replyMarkup = shouldTouchButtons ? ( builtKeyboard ? ? { inline_keyboard : [ ] } ) : undefined ;
const editParams : Record < string , unknown > = {
parse_mode : "HTML" ,
} ;
2026-02-15 20:09:10 +05:30
if ( opts . linkPreview === false ) {
editParams . link_preview_options = { is_disabled : true } ;
}
2026-01-26 15:26:15 -08:00
if ( replyMarkup !== undefined ) {
editParams . reply_markup = replyMarkup ;
}
2026-02-16 14:00:34 +05:30
const plainParams : Record < string , unknown > = { } ;
if ( opts . linkPreview === false ) {
plainParams . link_preview_options = { is_disabled : true } ;
}
if ( replyMarkup !== undefined ) {
plainParams . reply_markup = replyMarkup ;
}
2026-01-26 15:26:15 -08:00
2026-02-16 14:00:34 +05:30
try {
await withTelegramHtmlParseFallback ( {
label : "editMessage" ,
verbose : opts.verbose ,
requestHtml : ( retryLabel ) = >
requestWithEditShouldLog (
( ) = > api . editMessageText ( chatId , messageId , htmlText , editParams ) ,
retryLabel ,
( err ) = > ! isTelegramMessageNotModifiedError ( err ) ,
) ,
requestPlain : ( retryLabel ) = >
requestWithEditShouldLog (
( ) = >
Object . keys ( plainParams ) . length > 0
? api . editMessageText ( chatId , messageId , text , plainParams )
: api . editMessageText ( chatId , messageId , text ) ,
retryLabel ,
( plainErr ) = > ! isTelegramMessageNotModifiedError ( plainErr ) ,
) ,
} ) ;
} catch ( err ) {
2026-02-16 04:18:17 +01:00
if ( isTelegramMessageNotModifiedError ( err ) ) {
2026-02-16 14:00:34 +05:30
// no-op: Telegram reports message content unchanged, treat as success
} else {
throw err ;
2026-01-26 15:26:15 -08:00
}
2026-02-16 14:00:34 +05:30
}
2026-01-26 15:26:15 -08:00
logVerbose ( ` [telegram] Edited message ${ messageId } in chat ${ chatId } ` ) ;
return { ok : true , messageId : String ( messageId ) , chatId } ;
}
2025-12-07 22:46:02 +01:00
function inferFilename ( kind : ReturnType < typeof mediaKindFromMime > ) {
switch ( kind ) {
case "image" :
return "image.jpg" ;
case "video" :
return "video.mp4" ;
case "audio" :
return "audio.ogg" ;
default :
return "file.bin" ;
}
}
2026-01-26 22:07:43 +00:00
type TelegramStickerOpts = {
token? : string ;
accountId? : string ;
verbose? : boolean ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-01-26 22:07:43 +00:00
retry? : RetryConfig ;
/** Message ID to reply to (for threading) */
replyToMessageId? : number ;
/** Forum topic thread ID (for forum supergroups) */
messageThreadId? : number ;
} ;
/ * *
* Send a sticker to a Telegram chat by file_id .
* @param to - Chat ID or username ( e . g . , "123456789" or "@username" )
* @param fileId - Telegram file_id of the sticker to send
* @param opts - Optional configuration
* /
export async function sendStickerTelegram (
to : string ,
fileId : string ,
opts : TelegramStickerOpts = { } ,
) : Promise < TelegramSendResult > {
if ( ! fileId ? . trim ( ) ) {
throw new Error ( "Telegram sticker file_id is required" ) ;
}
2026-02-16 14:00:34 +05:30
const { cfg , account , api } = resolveTelegramApiContext ( opts ) ;
2026-01-26 22:07:43 +00:00
const target = parseTelegramTarget ( to ) ;
const chatId = normalizeChatId ( target . chatId ) ;
2026-02-16 12:01:35 +01:00
const isPrivate = isTelegramPrivateChat ( chatId ) ;
2026-02-16 14:00:34 +05:30
const threadParams = buildTelegramThreadReplyParams ( {
2026-02-16 12:01:35 +01:00
targetMessageThreadId : isPrivate ? undefined : target . messageThreadId ,
messageThreadId : isPrivate ? undefined : opts . messageThreadId ,
2026-02-16 14:00:34 +05:30
replyToMessageId : opts.replyToMessageId ,
} ) ;
2026-01-26 22:07:43 +00:00
const hasThreadParams = Object . keys ( threadParams ) . length > 0 ;
2026-02-16 14:00:34 +05:30
const requestWithDiag = createTelegramRequestWithDiag ( {
cfg ,
account ,
2026-01-26 22:07:43 +00:00
retry : opts.retry ,
verbose : opts.verbose ,
2026-02-16 14:00:34 +05:30
useApiErrorLogging : false ,
} ) ;
const requestWithChatNotFound = createRequestWithChatNotFound ( {
requestWithDiag ,
chatId ,
input : to ,
2026-01-26 22:07:43 +00:00
} ) ;
2026-02-09 08:35:53 +05:30
2026-01-26 22:07:43 +00:00
const stickerParams = hasThreadParams ? threadParams : undefined ;
2026-02-16 14:00:34 +05:30
const result = await withTelegramThreadFallback (
2026-02-09 08:35:53 +05:30
stickerParams ,
2026-01-26 22:07:43 +00:00
"sticker" ,
2026-02-16 14:00:34 +05:30
opts . verbose ,
2026-02-09 08:35:53 +05:30
async ( effectiveParams , label ) = >
2026-02-16 14:00:34 +05:30
requestWithChatNotFound ( ( ) = > api . sendSticker ( chatId , fileId . trim ( ) , effectiveParams ) , label ) ,
2026-02-09 08:35:53 +05:30
) ;
2026-01-26 22:07:43 +00:00
const messageId = String ( result ? . message_id ? ? "unknown" ) ;
const resolvedChatId = String ( result ? . chat ? . id ? ? chatId ) ;
if ( result ? . message_id ) {
recordSentMessage ( chatId , result . message_id ) ;
}
recordChannelActivity ( {
channel : "telegram" ,
accountId : account.accountId ,
direction : "outbound" ,
} ) ;
return { messageId , chatId : resolvedChatId } ;
}
2026-02-14 18:34:30 +01:00
type TelegramPollOpts = {
token? : string ;
accountId? : string ;
verbose? : boolean ;
2026-02-17 10:26:36 +09:00
api? : TelegramApiOverride ;
2026-02-14 18:34:30 +01:00
retry? : RetryConfig ;
/** Message ID to reply to (for threading) */
replyToMessageId? : number ;
/** Forum topic thread ID (for forum supergroups) */
messageThreadId? : number ;
/** Send message silently (no notification). Defaults to false. */
silent? : boolean ;
2026-02-16 20:27:26 +05:30
/** Whether votes are anonymous. Defaults to false (public poll). */
2026-02-14 18:34:30 +01:00
isAnonymous? : boolean ;
} ;
/ * *
* Send a poll to a Telegram chat .
* @param to - Chat ID or username ( e . g . , "123456789" or "@username" )
* @param poll - Poll input with question , options , maxSelections , and optional durationHours
* @param opts - Optional configuration
* /
export async function sendPollTelegram (
to : string ,
poll : PollInput ,
opts : TelegramPollOpts = { } ,
) : Promise < { messageId : string ; chatId : string ; pollId? : string } > {
2026-02-16 14:00:34 +05:30
const { cfg , account , api } = resolveTelegramApiContext ( opts ) ;
2026-02-14 18:34:30 +01:00
const target = parseTelegramTarget ( to ) ;
const chatId = normalizeChatId ( target . chatId ) ;
// Normalize the poll input (validates question, options, maxSelections)
const normalizedPoll = normalizePollInput ( poll , { maxOptions : 10 } ) ;
2026-02-16 12:01:35 +01:00
const isPrivate = isTelegramPrivateChat ( chatId ) ;
2026-02-16 14:00:34 +05:30
const threadParams = buildTelegramThreadReplyParams ( {
2026-02-16 12:01:35 +01:00
targetMessageThreadId : isPrivate ? undefined : target . messageThreadId ,
messageThreadId : isPrivate ? undefined : opts . messageThreadId ,
2026-02-16 14:00:34 +05:30
replyToMessageId : opts.replyToMessageId ,
} ) ;
2026-02-14 18:34:30 +01:00
// Build poll options as simple strings (Grammy accepts string[] or InputPollOption[])
const pollOptions = normalizedPoll . options ;
2026-02-16 14:00:34 +05:30
const requestWithDiag = createTelegramRequestWithDiag ( {
cfg ,
account ,
2026-02-14 18:34:30 +01:00
retry : opts.retry ,
verbose : opts.verbose ,
shouldRetry : ( err ) = > isRecoverableTelegramNetworkError ( err , { context : "send" } ) ,
} ) ;
2026-02-16 14:00:34 +05:30
const requestWithChatNotFound = createRequestWithChatNotFound ( {
requestWithDiag ,
chatId ,
input : to ,
} ) ;
2026-02-14 18:34:30 +01:00
const durationSeconds = normalizedPoll . durationSeconds ;
if ( durationSeconds === undefined && normalizedPoll . durationHours !== undefined ) {
throw new Error (
"Telegram poll durationHours is not supported. Use durationSeconds (5-600) instead." ,
) ;
}
if ( durationSeconds !== undefined && ( durationSeconds < 5 || durationSeconds > 600 ) ) {
throw new Error ( "Telegram poll durationSeconds must be between 5 and 600" ) ;
}
// Build poll parameters following Grammy's api.sendPoll signature
// sendPoll(chat_id, question, options, other?, signal?)
const pollParams = {
allows_multiple_answers : normalizedPoll.maxSelections > 1 ,
2026-02-16 20:27:26 +05:30
is_anonymous : opts.isAnonymous ? ? false ,
2026-02-14 18:34:30 +01:00
. . . ( durationSeconds !== undefined ? { open_period : durationSeconds } : { } ) ,
2026-02-16 14:00:34 +05:30
. . . ( Object . keys ( threadParams ) . length > 0 ? threadParams : { } ) ,
2026-02-14 18:34:30 +01:00
. . . ( opts . silent === true ? { disable_notification : true } : { } ) ,
} ;
2026-02-16 14:00:34 +05:30
const result = await withTelegramThreadFallback (
pollParams ,
"poll" ,
opts . verbose ,
async ( effectiveParams , label ) = >
requestWithChatNotFound (
( ) = > api . sendPoll ( chatId , normalizedPoll . question , pollOptions , effectiveParams ) ,
label ,
) ,
2026-02-14 18:34:30 +01:00
) ;
const messageId = String ( result ? . message_id ? ? "unknown" ) ;
const resolvedChatId = String ( result ? . chat ? . id ? ? chatId ) ;
const pollId = result ? . poll ? . id ;
if ( result ? . message_id ) {
recordSentMessage ( chatId , result . message_id ) ;
}
2026-02-16 21:19:54 +05:30
if ( pollId ) {
recordSentPoll ( {
pollId ,
chatId : resolvedChatId ,
question : normalizedPoll.question ,
options : normalizedPoll.options ,
accountId : account.accountId ,
} ) ;
}
2026-02-14 18:34:30 +01:00
recordChannelActivity ( {
channel : "telegram" ,
accountId : account.accountId ,
direction : "outbound" ,
} ) ;
return { messageId , chatId : resolvedChatId , pollId } ;
}