@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,406 lines (1,390 loc) • 60 kB
JavaScript
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import { M as logVerbose, O as danger } from "./utils-CP9YLh6M.js";
import { t as createSubsystemLogger } from "./subsystem-BCQGGxdd.js";
import { t as isTruthyEnvValue } from "./env-VriqyjXT.js";
import { i as loadConfig } from "./config-PQiujvsf.js";
import { h as normalizeMimeType, p as isGifMedia, u as getFileExtension, v as mediaKindFromMime } from "./image-ops-CI1VknD1.js";
import { n as redactSensitiveText } from "./redact-B7Mjvk0c.js";
import { n as formatErrorMessage, r as formatUncaughtError, t as extractErrorCode } from "./errors-kfGqPQ4b.js";
import { o as resolveTelegramAccount } from "./normalize-Ci8UyR3Z.js";
import { n as resolveMarkdownTableMode } from "./markdown-tables-B9VGUdDc.js";
import { a as loadWebMedia, n as markdownToIR, t as chunkMarkdownIR } from "./ir-24qCLTKH.js";
import { t as renderMarkdownWithMarkers } from "./render-CXDO_kgw.js";
import { n as normalizePollInput } from "./polls-CdvQ578Y.js";
import { i as createTelegramRetryRunner, n as recordChannelActivity } from "./channel-activity-DynIQUB_.js";
import { t as resolveFetch } from "./fetch-Bu5zBCce.js";
import { i as readChannelAllowFromStore } from "./pairing-store-T5DpOfoL.js";
import { t as makeProxyFetch } from "./proxy-BiOWrea2.js";
import process$1 from "node:process";
import * as net$1 from "node:net";
import { Bot, HttpError, InputFile } from "grammy";
//#region src/telegram/targets.ts
function stripTelegramInternalPrefixes(to) {
let trimmed = to.trim();
let strippedTelegramPrefix = false;
while (true) {
const next = (() => {
if (/^(telegram|tg):/i.test(trimmed)) {
strippedTelegramPrefix = true;
return trimmed.replace(/^(telegram|tg):/i, "").trim();
}
if (strippedTelegramPrefix && /^group:/i.test(trimmed)) return trimmed.replace(/^group:/i, "").trim();
return trimmed;
})();
if (next === trimmed) return trimmed;
trimmed = next;
}
}
/**
* Parse a Telegram delivery target into chatId and optional topic/thread ID.
*
* Supported formats:
* - `chatId` (plain chat ID, t.me link, @username, or internal prefixes like `telegram:...`)
* - `chatId:topicId` (numeric topic/thread ID)
* - `chatId:topic:topicId` (explicit topic marker; preferred)
*/
function resolveTelegramChatType(chatId) {
const trimmed = chatId.trim();
if (!trimmed) return "unknown";
if (/^-?\d+$/.test(trimmed)) return trimmed.startsWith("-") ? "group" : "direct";
return "unknown";
}
function parseTelegramTarget(to) {
const normalized = stripTelegramInternalPrefixes(to);
const topicMatch = /^(.+?):topic:(\d+)$/.exec(normalized);
if (topicMatch) return {
chatId: topicMatch[1],
messageThreadId: Number.parseInt(topicMatch[2], 10),
chatType: resolveTelegramChatType(topicMatch[1])
};
const colonMatch = /^(.+):(\d+)$/.exec(normalized);
if (colonMatch) return {
chatId: colonMatch[1],
messageThreadId: Number.parseInt(colonMatch[2], 10),
chatType: resolveTelegramChatType(colonMatch[1])
};
return {
chatId: normalized,
chatType: resolveTelegramChatType(normalized)
};
}
function resolveTelegramTargetChatType(target) {
return parseTelegramTarget(target).chatType;
}
//#endregion
//#region src/media/audio.ts
const TELEGRAM_VOICE_AUDIO_EXTENSIONS = new Set([
".oga",
".ogg",
".opus",
".mp3",
".m4a"
]);
/**
* MIME types compatible with voice messages.
* Telegram sendVoice supports OGG/Opus, MP3, and M4A.
* https://core.telegram.org/bots/api#sendvoice
*/
const TELEGRAM_VOICE_MIME_TYPES = new Set([
"audio/ogg",
"audio/opus",
"audio/mpeg",
"audio/mp3",
"audio/mp4",
"audio/x-m4a",
"audio/m4a"
]);
function isTelegramVoiceCompatibleAudio(opts) {
const mime = normalizeMimeType(opts.contentType);
if (mime && TELEGRAM_VOICE_MIME_TYPES.has(mime)) return true;
const fileName = opts.fileName?.trim();
if (!fileName) return false;
const ext = getFileExtension(fileName);
if (!ext) return false;
return TELEGRAM_VOICE_AUDIO_EXTENSIONS.has(ext);
}
/**
* Backward-compatible alias used across plugin/runtime call sites.
* Keeps existing behavior while making Telegram-specific policy explicit.
*/
function isVoiceCompatibleAudio(opts) {
return isTelegramVoiceCompatibleAudio(opts);
}
//#endregion
//#region src/infra/diagnostic-flags.ts
const DIAGNOSTICS_ENV = "OPENCLAW_DIAGNOSTICS";
function normalizeFlag(value) {
return value.trim().toLowerCase();
}
function parseEnvFlags(raw) {
if (!raw) return [];
const trimmed = raw.trim();
if (!trimmed) return [];
const lowered = trimmed.toLowerCase();
if ([
"0",
"false",
"off",
"none"
].includes(lowered)) return [];
if ([
"1",
"true",
"all",
"*"
].includes(lowered)) return ["*"];
return trimmed.split(/[,\s]+/).map(normalizeFlag).filter(Boolean);
}
function uniqueFlags(flags) {
const seen = /* @__PURE__ */ new Set();
const out = [];
for (const flag of flags) {
const normalized = normalizeFlag(flag);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
out.push(normalized);
}
return out;
}
function resolveDiagnosticFlags(cfg, env = process.env) {
const configFlags = Array.isArray(cfg?.diagnostics?.flags) ? cfg?.diagnostics?.flags : [];
const envFlags = parseEnvFlags(env[DIAGNOSTICS_ENV]);
return uniqueFlags([...configFlags, ...envFlags]);
}
function matchesDiagnosticFlag(flag, enabledFlags) {
const target = normalizeFlag(flag);
if (!target) return false;
for (const raw of enabledFlags) {
const enabled = normalizeFlag(raw);
if (!enabled) continue;
if (enabled === "*" || enabled === "all") return true;
if (enabled.endsWith(".*")) {
const prefix = enabled.slice(0, -2);
if (target === prefix || target.startsWith(`${prefix}.`)) return true;
}
if (enabled.endsWith("*")) {
const prefix = enabled.slice(0, -1);
if (target.startsWith(prefix)) return true;
}
if (enabled === target) return true;
}
return false;
}
function isDiagnosticFlagEnabled(flag, cfg, env = process.env) {
return matchesDiagnosticFlag(flag, resolveDiagnosticFlags(cfg, env));
}
//#endregion
//#region src/telegram/api-logging.ts
const fallbackLogger = createSubsystemLogger("telegram/api");
function resolveTelegramApiLogger(runtime, logger) {
if (logger) return logger;
if (runtime?.error) return runtime.error;
return (message) => fallbackLogger.error(message);
}
async function withTelegramApiErrorLogging({ operation, fn, runtime, logger, shouldLog }) {
try {
return await fn();
} catch (err) {
if (!shouldLog || shouldLog(err)) {
const errText = formatErrorMessage(err);
resolveTelegramApiLogger(runtime, logger)(danger(`telegram ${operation} failed: ${errText}`));
}
throw err;
}
}
//#endregion
//#region src/channels/location.ts
function resolveLocation(location) {
const source = location.source ?? (location.isLive ? "live" : location.name || location.address ? "place" : "pin");
const isLive = Boolean(location.isLive ?? source === "live");
return {
...location,
source,
isLive
};
}
function formatAccuracy(accuracy) {
if (!Number.isFinite(accuracy)) return "";
return ` ±${Math.round(accuracy ?? 0)}m`;
}
function formatCoords(latitude, longitude) {
return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
}
function formatLocationText(location) {
const resolved = resolveLocation(location);
const coords = formatCoords(resolved.latitude, resolved.longitude);
const accuracy = formatAccuracy(resolved.accuracy);
const caption = resolved.caption?.trim();
let header = "";
if (resolved.source === "live" || resolved.isLive) header = `🛰 Live location: ${coords}${accuracy}`;
else if (resolved.name || resolved.address) header = `📍 ${[resolved.name, resolved.address].filter(Boolean).join(" — ")} (${coords}${accuracy})`;
else header = `📍 ${coords}${accuracy}`;
return caption ? `${header}\n${caption}` : header;
}
function toLocationContext(location) {
const resolved = resolveLocation(location);
return {
LocationLat: resolved.latitude,
LocationLon: resolved.longitude,
LocationAccuracy: resolved.accuracy,
LocationName: resolved.name,
LocationAddress: resolved.address,
LocationSource: resolved.source,
LocationIsLive: resolved.isLive
};
}
//#endregion
//#region src/channels/allow-from.ts
function mergeAllowFromSources(params) {
return [...params.allowFrom ?? [], ...params.storeAllowFrom ?? []].map((value) => String(value).trim()).filter(Boolean);
}
function firstDefined(...values) {
for (const value of values) if (typeof value !== "undefined") return value;
}
function isSenderIdAllowed(allow, senderId, allowWhenEmpty) {
if (!allow.hasEntries) return allowWhenEmpty;
if (allow.hasWildcard) return true;
if (!senderId) return false;
return allow.entries.includes(senderId);
}
//#endregion
//#region src/telegram/bot-access.ts
const warnedInvalidEntries = /* @__PURE__ */ new Set();
function warnInvalidAllowFromEntries(entries) {
if (process.env.VITEST || false) return;
for (const entry of entries) {
if (warnedInvalidEntries.has(entry)) continue;
warnedInvalidEntries.add(entry);
console.warn([
"[telegram] Invalid allowFrom entry:",
JSON.stringify(entry),
"- allowFrom/groupAllowFrom authorization requires numeric Telegram sender IDs only.",
"If you had \"@username\" entries, re-run onboarding (it resolves @username to IDs) or replace them manually."
].join(" "));
}
}
const normalizeAllowFrom = (list) => {
const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
const hasWildcard = entries.includes("*");
const normalized = entries.filter((value) => value !== "*").map((value) => value.replace(/^(telegram|tg):/i, ""));
const invalidEntries = normalized.filter((value) => !/^\d+$/.test(value));
if (invalidEntries.length > 0) warnInvalidAllowFromEntries([...new Set(invalidEntries)]);
return {
entries: normalized.filter((value) => /^\d+$/.test(value)),
hasWildcard,
hasEntries: entries.length > 0,
invalidEntries
};
};
const normalizeAllowFromWithStore = (params) => normalizeAllowFrom(mergeAllowFromSources(params));
const isSenderAllowed = (params) => {
const { allow, senderId } = params;
return isSenderIdAllowed(allow, senderId, true);
};
const resolveSenderAllowMatch = (params) => {
const { allow, senderId } = params;
if (allow.hasWildcard) return {
allowed: true,
matchKey: "*",
matchSource: "wildcard"
};
if (!allow.hasEntries) return { allowed: false };
if (senderId && allow.entries.includes(senderId)) return {
allowed: true,
matchKey: senderId,
matchSource: "id"
};
return { allowed: false };
};
//#endregion
//#region src/telegram/bot/helpers.ts
const TELEGRAM_GENERAL_TOPIC_ID = 1;
async function resolveTelegramGroupAllowFromContext(params) {
const resolvedThreadId = resolveTelegramForumThreadId({
isForum: params.isForum,
messageThreadId: params.messageThreadId
});
const storeAllowFrom = await readChannelAllowFromStore("telegram", process.env, params.accountId).catch(() => []);
const { groupConfig, topicConfig } = params.resolveTelegramGroupConfig(params.chatId, resolvedThreadId);
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
return {
resolvedThreadId,
storeAllowFrom,
groupConfig,
topicConfig,
groupAllowOverride,
effectiveGroupAllow: normalizeAllowFromWithStore({
allowFrom: groupAllowOverride ?? params.groupAllowFrom,
storeAllowFrom
}),
hasGroupAllowOverride: typeof groupAllowOverride !== "undefined"
};
}
/**
* Resolve the thread ID for Telegram forum topics.
* For non-forum groups, returns undefined even if messageThreadId is present
* (reply threads in regular groups should not create separate sessions).
* For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
*/
function resolveTelegramForumThreadId(params) {
if (!params.isForum) return;
if (params.messageThreadId == null) return TELEGRAM_GENERAL_TOPIC_ID;
return params.messageThreadId;
}
function resolveTelegramThreadSpec(params) {
if (params.isGroup) return {
id: resolveTelegramForumThreadId({
isForum: params.isForum,
messageThreadId: params.messageThreadId
}),
scope: params.isForum ? "forum" : "none"
};
if (params.messageThreadId == null) return { scope: "dm" };
return {
id: params.messageThreadId,
scope: "dm"
};
}
/**
* Build thread params for Telegram API calls (messages, media).
*
* IMPORTANT: Thread IDs behave differently based on chat type:
* - DMs (private chats): Include message_thread_id when present (DM topics)
* - Forum topics: Skip thread_id=1 (General topic), include others
* - Regular groups: Thread IDs are ignored by Telegram
*
* General forum topic (id=1) must be treated like a regular supergroup send:
* Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
*
* @param thread - Thread specification with ID and scope
* @returns API params object or undefined if thread_id should be omitted
*/
function buildTelegramThreadParams(thread) {
if (thread?.id == null) return;
const normalized = Math.trunc(thread.id);
if (thread.scope === "dm") return normalized > 0 ? { message_thread_id: normalized } : void 0;
if (normalized === TELEGRAM_GENERAL_TOPIC_ID) return;
return { message_thread_id: normalized };
}
/**
* Build thread params for typing indicators (sendChatAction).
* Empirically, General topic (id=1) needs message_thread_id for typing to appear.
*/
function buildTypingThreadParams(messageThreadId) {
if (messageThreadId == null) return;
return { message_thread_id: Math.trunc(messageThreadId) };
}
function resolveTelegramStreamMode(telegramCfg) {
const raw = telegramCfg?.streamMode?.trim().toLowerCase();
if (raw === "off" || raw === "partial" || raw === "block") return raw;
return "partial";
}
function buildTelegramGroupPeerId(chatId, messageThreadId) {
return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId);
}
function buildTelegramGroupFrom(chatId, messageThreadId) {
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
}
/**
* Build parentPeer for forum topic binding inheritance.
* When a message comes from a forum topic, the peer ID includes the topic suffix
* (e.g., `-1001234567890:topic:99`). To allow bindings configured for the base
* group ID to match, we provide the parent group as `parentPeer` so the routing
* layer can fall back to it when the exact peer doesn't match.
*/
function buildTelegramParentPeer(params) {
if (!params.isGroup || params.resolvedThreadId == null) return;
return {
kind: "group",
id: String(params.chatId)
};
}
function buildSenderName(msg) {
return [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || msg.from?.username || void 0;
}
function resolveTelegramMediaPlaceholder(msg) {
if (!msg) return;
if (msg.photo) return "<media:image>";
if (msg.video || msg.video_note) return "<media:video>";
if (msg.audio || msg.voice) return "<media:audio>";
if (msg.document) return "<media:document>";
if (msg.sticker) return "<media:sticker>";
}
function buildSenderLabel(msg, senderId) {
const name = buildSenderName(msg);
const username = msg.from?.username ? `@${msg.from.username}` : void 0;
let label = name;
if (name && username) label = `${name} (${username})`;
else if (!name && username) label = username;
const fallbackId = (senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : void 0) ?? (msg.from?.id != null ? String(msg.from.id) : void 0);
const idPart = fallbackId ? `id:${fallbackId}` : void 0;
if (label && idPart) return `${label} ${idPart}`;
if (label) return label;
return idPart ?? "id:unknown";
}
function buildGroupLabel(msg, chatId, messageThreadId) {
const title = msg.chat?.title;
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
if (title) return `${title} id:${chatId}${topicSuffix}`;
return `group:${chatId}${topicSuffix}`;
}
function hasBotMention(msg, botUsername) {
if ((msg.text ?? msg.caption ?? "").toLowerCase().includes(`@${botUsername}`)) return true;
const entities = msg.entities ?? msg.caption_entities ?? [];
for (const ent of entities) {
if (ent.type !== "mention") continue;
if ((msg.text ?? msg.caption ?? "").slice(ent.offset, ent.offset + ent.length).toLowerCase() === `@${botUsername}`) return true;
}
return false;
}
function expandTextLinks(text, entities) {
if (!text || !entities?.length) return text;
const textLinks = entities.filter((entity) => entity.type === "text_link" && Boolean(entity.url)).toSorted((a, b) => b.offset - a.offset);
if (textLinks.length === 0) return text;
let result = text;
for (const entity of textLinks) {
const markdown = `[${text.slice(entity.offset, entity.offset + entity.length)}](${entity.url})`;
result = result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
}
return result;
}
function resolveTelegramReplyId(raw) {
if (!raw) return;
const parsed = Number(raw);
if (!Number.isFinite(parsed)) return;
return parsed;
}
function describeReplyTarget(msg) {
const reply = msg.reply_to_message;
const externalReply = msg.external_reply;
const quoteText = msg.quote?.text ?? externalReply?.quote?.text;
let body = "";
let kind = "reply";
if (typeof quoteText === "string") {
body = quoteText.trim();
if (body) kind = "quote";
}
const replyLike = reply ?? externalReply;
if (!body && replyLike) {
body = (replyLike.text ?? replyLike.caption ?? "").trim();
if (!body) {
body = resolveTelegramMediaPlaceholder(replyLike) ?? "";
if (!body) {
const locationData = extractTelegramLocation(replyLike);
if (locationData) body = formatLocationText(locationData);
}
}
}
if (!body) return null;
const senderLabel = (replyLike ? buildSenderName(replyLike) : void 0) ?? "unknown sender";
return {
id: replyLike?.message_id ? String(replyLike.message_id) : void 0,
sender: senderLabel,
body,
kind
};
}
function normalizeForwardedUserLabel(user) {
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
const username = user.username?.trim() || void 0;
const id = String(user.id);
return {
display: (name && username ? `${name} (@${username})` : name || (username ? `@${username}` : void 0)) || `user:${id}`,
name: name || void 0,
username,
id
};
}
function normalizeForwardedChatLabel(chat, fallbackKind) {
const title = chat.title?.trim() || void 0;
const username = chat.username?.trim() || void 0;
const id = String(chat.id);
return {
display: title || (username ? `@${username}` : void 0) || `${fallbackKind}:${id}`,
title,
username,
id
};
}
function buildForwardedContextFromUser(params) {
const { display, name, username, id } = normalizeForwardedUserLabel(params.user);
if (!display) return null;
return {
from: display,
date: params.date,
fromType: params.type,
fromId: id,
fromUsername: username,
fromTitle: name
};
}
function buildForwardedContextFromHiddenName(params) {
const trimmed = params.name?.trim();
if (!trimmed) return null;
return {
from: trimmed,
date: params.date,
fromType: params.type,
fromTitle: trimmed
};
}
function buildForwardedContextFromChat(params) {
const fallbackKind = params.type === "channel" ? "channel" : "chat";
const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
if (!display) return null;
const signature = params.signature?.trim() || void 0;
const from = signature ? `${display} (${signature})` : display;
const chatType = params.chat.type?.trim() || void 0;
return {
from,
date: params.date,
fromType: params.type,
fromId: id,
fromUsername: username,
fromTitle: title,
fromSignature: signature,
fromChatType: chatType,
fromMessageId: params.messageId
};
}
function resolveForwardOrigin(origin) {
switch (origin.type) {
case "user": return buildForwardedContextFromUser({
user: origin.sender_user,
date: origin.date,
type: "user"
});
case "hidden_user": return buildForwardedContextFromHiddenName({
name: origin.sender_user_name,
date: origin.date,
type: "hidden_user"
});
case "chat": return buildForwardedContextFromChat({
chat: origin.sender_chat,
date: origin.date,
type: "chat",
signature: origin.author_signature
});
case "channel": return buildForwardedContextFromChat({
chat: origin.chat,
date: origin.date,
type: "channel",
signature: origin.author_signature,
messageId: origin.message_id
});
default: return null;
}
}
/** Extract forwarded message origin info from Telegram message. */
function normalizeForwardedContext(msg) {
if (!msg.forward_origin) return null;
return resolveForwardOrigin(msg.forward_origin);
}
function extractTelegramLocation(msg) {
const { venue, location } = msg;
if (venue) return {
latitude: venue.location.latitude,
longitude: venue.location.longitude,
accuracy: venue.location.horizontal_accuracy,
name: venue.title,
address: venue.address,
source: "place",
isLive: false
};
if (location) {
const isLive = typeof location.live_period === "number" && location.live_period > 0;
return {
latitude: location.latitude,
longitude: location.longitude,
accuracy: location.horizontal_accuracy,
source: isLive ? "live" : "pin",
isLive
};
}
return null;
}
//#endregion
//#region src/telegram/caption.ts
const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
function splitTelegramCaption(text) {
const trimmed = text?.trim() ?? "";
if (!trimmed) return {
caption: void 0,
followUpText: void 0
};
if (trimmed.length > TELEGRAM_MAX_CAPTION_LENGTH) return {
caption: void 0,
followUpText: trimmed
};
return {
caption: trimmed,
followUpText: void 0
};
}
//#endregion
//#region src/telegram/network-config.ts
const TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV = "OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY";
const TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV = "OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY";
function resolveTelegramAutoSelectFamilyDecision(params) {
const env = params?.env ?? process$1.env;
const nodeMajor = typeof params?.nodeMajor === "number" ? params.nodeMajor : Number(process$1.versions.node.split(".")[0]);
if (isTruthyEnvValue(env[TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV])) return {
value: true,
source: `env:${TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV}`
};
if (isTruthyEnvValue(env[TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV])) return {
value: false,
source: `env:${TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV}`
};
if (typeof params?.network?.autoSelectFamily === "boolean") return {
value: params.network.autoSelectFamily,
source: "config"
};
if (Number.isFinite(nodeMajor) && nodeMajor >= 22) return {
value: true,
source: "default-node22"
};
return { value: null };
}
//#endregion
//#region src/telegram/fetch.ts
let appliedAutoSelectFamily = null;
const log = createSubsystemLogger("telegram/network");
function applyTelegramNetworkWorkarounds(network) {
const decision = resolveTelegramAutoSelectFamilyDecision({ network });
if (decision.value === null || decision.value === appliedAutoSelectFamily) return;
appliedAutoSelectFamily = decision.value;
if (typeof net$1.setDefaultAutoSelectFamily === "function") try {
net$1.setDefaultAutoSelectFamily(decision.value);
const label = decision.source ? ` (${decision.source})` : "";
log.info(`telegram: autoSelectFamily=${decision.value}${label}`);
} catch {}
}
function resolveTelegramFetch(proxyFetch, options) {
applyTelegramNetworkWorkarounds(options?.network);
if (proxyFetch) return resolveFetch(proxyFetch);
const fetchImpl = resolveFetch();
if (!fetchImpl) throw new Error("fetch is not available; set channels.telegram.proxy in config");
return fetchImpl;
}
//#endregion
//#region src/telegram/format.ts
function escapeHtml(text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function escapeHtmlAttr(text) {
return escapeHtml(text).replace(/"/g, """);
}
/**
* File extensions that share TLDs and commonly appear in code/documentation.
* These are wrapped in <code> tags to prevent Telegram from generating
* spurious domain registrar previews.
*
* Only includes extensions that are:
* 1. Commonly used as file extensions in code/docs
* 2. Rarely used as intentional domain references
*
* Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
*/
const FILE_EXTENSIONS_WITH_TLD = new Set([
"md",
"go",
"py",
"pl",
"sh",
"am",
"at",
"be",
"cc"
]);
/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
function isAutoLinkedFileRef(href, label) {
if (href.replace(/^https?:\/\//i, "") !== label) return false;
const dotIndex = label.lastIndexOf(".");
if (dotIndex < 1) return false;
const ext = label.slice(dotIndex + 1).toLowerCase();
if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) return false;
const segments = label.split("/");
if (segments.length > 1) {
for (let i = 0; i < segments.length - 1; i++) if (segments[i].includes(".")) return false;
}
return true;
}
function buildTelegramLink(link, text) {
const href = link.href.trim();
if (!href) return null;
if (link.start === link.end) return null;
if (isAutoLinkedFileRef(href, text.slice(link.start, link.end))) return null;
const safeHref = escapeHtmlAttr(href);
return {
start: link.start,
end: link.end,
open: `<a href="${safeHref}">`,
close: "</a>"
};
}
function renderTelegramHtml(ir) {
return renderMarkdownWithMarkers(ir, {
styleMarkers: {
bold: {
open: "<b>",
close: "</b>"
},
italic: {
open: "<i>",
close: "</i>"
},
strikethrough: {
open: "<s>",
close: "</s>"
},
code: {
open: "<code>",
close: "</code>"
},
code_block: {
open: "<pre><code>",
close: "</code></pre>"
},
spoiler: {
open: "<tg-spoiler>",
close: "</tg-spoiler>"
},
blockquote: {
open: "<blockquote>",
close: "</blockquote>"
}
},
escapeText: escapeHtml,
buildLink: buildTelegramLink
});
}
function markdownToTelegramHtml(markdown, options = {}) {
const html = renderTelegramHtml(markdownToIR(markdown ?? "", {
linkify: true,
enableSpoilers: true,
headingStyle: "none",
blockquotePrefix: "",
tableMode: options.tableMode
}));
if (options.wrapFileRefs !== false) return wrapFileReferencesInHtml(html);
return html;
}
/**
* Wraps standalone file references (with TLD extensions) in <code> tags.
* This prevents Telegram from treating them as URLs and generating
* irrelevant domain registrar previews.
*
* Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
* Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
*/
/** Escape regex metacharacters in a string */
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
const FILE_REFERENCE_PATTERN = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
const ORPHANED_TLD_PATTERN = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`, "g");
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
function wrapStandaloneFileRef(match, prefix, filename) {
if (filename.startsWith("//")) return match;
if (/https?:\/\/$/i.test(prefix)) return match;
return `${prefix}<code>${escapeHtml(filename)}</code>`;
}
function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) return text;
return text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef).replace(ORPHANED_TLD_PATTERN, (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
}
function wrapFileReferencesInHtml(html) {
AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
if (!isAutoLinkedFileRef(`http://${label}`, label)) return _match;
return `<code>${escapeHtml(label)}</code>`;
});
let codeDepth = 0;
let preDepth = 0;
let anchorDepth = 0;
let result = "";
let lastIndex = 0;
HTML_TAG_PATTERN.lastIndex = 0;
let match;
while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
const tagStart = match.index;
const tagEnd = HTML_TAG_PATTERN.lastIndex;
const isClosing = match[1] === "</";
const tagName = match[2].toLowerCase();
const textBefore = deLinkified.slice(lastIndex, tagStart);
result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
if (tagName === "code") codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
else if (tagName === "pre") preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
else if (tagName === "a") anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
result += deLinkified.slice(tagStart, tagEnd);
lastIndex = tagEnd;
}
const remainingText = deLinkified.slice(lastIndex);
result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
return result;
}
function renderTelegramHtmlText(text, options = {}) {
if ((options.textMode ?? "markdown") === "html") return text;
return markdownToTelegramHtml(text, { tableMode: options.tableMode });
}
function markdownToTelegramChunks(markdown, limit, options = {}) {
return chunkMarkdownIR(markdownToIR(markdown ?? "", {
linkify: true,
enableSpoilers: true,
headingStyle: "none",
blockquotePrefix: "",
tableMode: options.tableMode
}), limit).map((chunk) => ({
html: wrapFileReferencesInHtml(renderTelegramHtml(chunk)),
text: chunk.text
}));
}
//#endregion
//#region src/telegram/network-errors.ts
const RECOVERABLE_ERROR_CODES = new Set([
"ECONNRESET",
"ECONNREFUSED",
"EPIPE",
"ETIMEDOUT",
"ESOCKETTIMEDOUT",
"ENETUNREACH",
"EHOSTUNREACH",
"ENOTFOUND",
"EAI_AGAIN",
"UND_ERR_CONNECT_TIMEOUT",
"UND_ERR_HEADERS_TIMEOUT",
"UND_ERR_BODY_TIMEOUT",
"UND_ERR_SOCKET",
"UND_ERR_ABORTED",
"ECONNABORTED",
"ERR_NETWORK"
]);
const RECOVERABLE_ERROR_NAMES = new Set([
"AbortError",
"TimeoutError",
"ConnectTimeoutError",
"HeadersTimeoutError",
"BodyTimeoutError"
]);
const RECOVERABLE_MESSAGE_SNIPPETS = [
"fetch failed",
"typeerror: fetch failed",
"undici",
"network error",
"network request",
"client network socket disconnected",
"socket hang up",
"getaddrinfo",
"timeout",
"timed out"
];
function normalizeCode(code) {
return code?.trim().toUpperCase() ?? "";
}
function getErrorName(err) {
if (!err || typeof err !== "object") return "";
return "name" in err ? String(err.name) : "";
}
function getErrorCode(err) {
const direct = extractErrorCode(err);
if (direct) return direct;
if (!err || typeof err !== "object") return;
const errno = err.errno;
if (typeof errno === "string") return errno;
if (typeof errno === "number") return String(errno);
}
function collectErrorCandidates(err) {
const queue = [err];
const seen = /* @__PURE__ */ new Set();
const candidates = [];
while (queue.length > 0) {
const current = queue.shift();
if (current == null || seen.has(current)) continue;
seen.add(current);
candidates.push(current);
if (typeof current === "object") {
const cause = current.cause;
if (cause && !seen.has(cause)) queue.push(cause);
const reason = current.reason;
if (reason && !seen.has(reason)) queue.push(reason);
const errors = current.errors;
if (Array.isArray(errors)) {
for (const nested of errors) if (nested && !seen.has(nested)) queue.push(nested);
}
if (getErrorName(current) === "HttpError") {
const wrappedError = current.error;
if (wrappedError && !seen.has(wrappedError)) queue.push(wrappedError);
}
}
}
return candidates;
}
function isRecoverableTelegramNetworkError(err, options = {}) {
if (!err) return false;
const allowMessageMatch = typeof options.allowMessageMatch === "boolean" ? options.allowMessageMatch : options.context !== "send";
for (const candidate of collectErrorCandidates(err)) {
const code = normalizeCode(getErrorCode(candidate));
if (code && RECOVERABLE_ERROR_CODES.has(code)) return true;
const name = getErrorName(candidate);
if (name && RECOVERABLE_ERROR_NAMES.has(name)) return true;
if (allowMessageMatch) {
const message = formatErrorMessage(candidate).toLowerCase();
if (message && RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) return true;
}
}
return false;
}
//#endregion
//#region src/telegram/sent-message-cache.ts
/**
* In-memory cache of sent message IDs per chat.
* Used to identify bot's own messages for reaction filtering ("own" mode).
*/
const TTL_MS = 1440 * 60 * 1e3;
const sentMessages = /* @__PURE__ */ new Map();
function getChatKey(chatId) {
return String(chatId);
}
function cleanupExpired(entry) {
const now = Date.now();
for (const [msgId, timestamp] of entry.timestamps) if (now - timestamp > TTL_MS) entry.timestamps.delete(msgId);
}
/**
* Record a message ID as sent by the bot.
*/
function recordSentMessage(chatId, messageId) {
const key = getChatKey(chatId);
let entry = sentMessages.get(key);
if (!entry) {
entry = { timestamps: /* @__PURE__ */ new Map() };
sentMessages.set(key, entry);
}
entry.timestamps.set(messageId, Date.now());
if (entry.timestamps.size > 100) cleanupExpired(entry);
}
/**
* Check if a message was sent by the bot.
*/
function wasSentByBot(chatId, messageId) {
const key = getChatKey(chatId);
const entry = sentMessages.get(key);
if (!entry) return false;
cleanupExpired(entry);
return entry.timestamps.has(messageId);
}
//#endregion
//#region src/telegram/voice.ts
function resolveTelegramVoiceDecision(opts) {
if (!opts.wantsVoice) return { useVoice: false };
if (isTelegramVoiceCompatibleAudio(opts)) return { useVoice: true };
return {
useVoice: false,
reason: `media is ${opts.contentType ?? "unknown"} (${opts.fileName ?? "unknown"})`
};
}
function resolveTelegramVoiceSend(opts) {
const decision = resolveTelegramVoiceDecision(opts);
if (decision.reason && opts.logFallback) opts.logFallback(`Telegram voice requested but ${decision.reason}; sending as audio file instead.`);
return { useVoice: decision.useVoice };
}
//#endregion
//#region src/telegram/send.ts
var send_exports = /* @__PURE__ */ __exportAll({
buildInlineKeyboard: () => buildInlineKeyboard,
createForumTopicTelegram: () => createForumTopicTelegram,
deleteMessageTelegram: () => deleteMessageTelegram,
editMessageTelegram: () => editMessageTelegram,
reactMessageTelegram: () => reactMessageTelegram,
sendMessageTelegram: () => sendMessageTelegram,
sendPollTelegram: () => sendPollTelegram,
sendStickerTelegram: () => sendStickerTelegram
});
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
const MESSAGE_NOT_MODIFIED_RE = /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i;
const diagLogger = createSubsystemLogger("telegram/diagnostic");
function createTelegramHttpLogger(cfg) {
if (!isDiagnosticFlagEnabled("telegram.http", cfg)) return () => {};
return (label, err) => {
if (!(err instanceof HttpError)) return;
const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
diagLogger.warn(`telegram http error (${label}): ${detail}`);
};
}
function resolveTelegramClientOptions(account) {
const proxyUrl = account.config.proxy?.trim();
const fetchImpl = resolveTelegramFetch(proxyUrl ? makeProxyFetch(proxyUrl) : void 0, { network: account.config.network });
const timeoutSeconds = typeof account.config.timeoutSeconds === "number" && Number.isFinite(account.config.timeoutSeconds) ? Math.max(1, Math.floor(account.config.timeoutSeconds)) : void 0;
return fetchImpl || timeoutSeconds ? {
...fetchImpl ? { fetch: fetchImpl } : {},
...timeoutSeconds ? { timeoutSeconds } : {}
} : void 0;
}
function resolveToken(explicit, params) {
if (explicit?.trim()) return explicit.trim();
if (!params.token) throw new Error(`Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
return params.token.trim();
}
function normalizeChatId(to) {
const trimmed = to.trim();
if (!trimmed) throw new Error("Recipient is required for Telegram sends");
let normalized = stripTelegramInternalPrefixes(trimmed);
const m = /^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ?? /^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
if (m?.[1]) normalized = `@${m[1]}`;
if (!normalized) throw new Error("Recipient is required for Telegram sends");
if (normalized.startsWith("@")) return normalized;
if (/^-?\d+$/.test(normalized)) return normalized;
if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) return `@${normalized}`;
return normalized;
}
function normalizeMessageId(raw) {
if (typeof raw === "number" && Number.isFinite(raw)) return Math.trunc(raw);
if (typeof raw === "string") {
const value = raw.trim();
if (!value) throw new Error("Message id is required for Telegram actions");
const parsed = Number.parseInt(value, 10);
if (Number.isFinite(parsed)) return parsed;
}
throw new Error("Message id is required for Telegram actions");
}
function isTelegramThreadNotFoundError(err) {
return THREAD_NOT_FOUND_RE.test(formatErrorMessage(err));
}
function isTelegramMessageNotModifiedError(err) {
return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
}
function hasMessageThreadIdParam(params) {
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;
}
function removeMessageThreadIdParam(params) {
if (!params || !hasMessageThreadIdParam(params)) return params;
const next = { ...params };
delete next.message_thread_id;
return Object.keys(next).length > 0 ? next : void 0;
}
function isTelegramHtmlParseError(err) {
return PARSE_ERR_RE.test(formatErrorMessage(err));
}
function buildTelegramThreadReplyParams(params) {
const messageThreadId = params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
const threadScope = params.chatType === "direct" ? "dm" : "forum";
const threadIdParams = buildTelegramThreadParams(messageThreadId != null ? {
id: messageThreadId,
scope: threadScope
} : void 0);
const threadParams = 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(params) {
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`);
}
}
function resolveTelegramApiContext(opts) {
const cfg = opts.cfg ?? loadConfig();
const account = resolveTelegramAccount({
cfg,
accountId: opts.accountId
});
const token = resolveToken(opts.token, account);
const client = resolveTelegramClientOptions(account);
return {
cfg,
account,
api: opts.api ?? new Bot(token, client ? { client } : void 0).api
};
}
function createTelegramRequestWithDiag(params) {
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 (fn, label, options) => {
const runRequest = () => request(fn, label);
return (params.useApiErrorLogging === false ? runRequest() : withTelegramApiErrorLogging({
operation: label ?? "request",
fn: runRequest,
...options?.shouldLog ? { shouldLog: options.shouldLog } : {}
})).catch((err) => {
logHttpError(label ?? "request", err);
throw err;
});
};
}
function wrapTelegramChatNotFoundError(err, params) {
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(params, label, verbose, attempt) {
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)}`);
return await attempt(removeMessageThreadIdParam(params), `${label}-threadless`);
}
}
function createRequestWithChatNotFound(params) {
return async (fn, label) => params.requestWithDiag(fn, label).catch((err) => {
throw wrapTelegramChatNotFoundError(err, {
chatId: params.chatId,
input: params.input
});
});
}
function buildInlineKeyboard(buttons) {
if (!buttons?.length) return;
const rows = buttons.map((row) => row.filter((button) => button?.text && button?.callback_data).map((button) => ({
text: button.text,
callback_data: button.callback_data,
...button.style ? { style: button.style } : {}
}))).filter((row) => row.length > 0);
if (rows.length === 0) return;
return { inline_keyboard: rows };
}
async function sendMessageTelegram(to, text, opts = {}) {
const { cfg, account, api } = resolveTelegramApiContext(opts);
const target = parseTelegramTarget(to);
const chatId = normalizeChatId(target.chatId);
const mediaUrl = opts.mediaUrl?.trim();
const replyMarkup = buildInlineKeyboard(opts.buttons);
const threadParams = buildTelegramThreadReplyParams({
targetMessageThreadId: target.messageThreadId,
messageThreadId: opts.messageThreadId,
chatType: target.chatType,
replyToMessageId: opts.replyToMessageId,
quoteText: opts.quoteText
});
const hasThreadParams = Object.keys(threadParams).length > 0;
const requestWithChatNotFound = createRequestWithChatNotFound({
requestWithDiag: createTelegramRequestWithDiag({
cfg,
account,
retry: opts.retry,
verbose: opts.verbose,
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
}),
chatId,
input: to
});
const textMode = opts.textMode ?? "markdown";
const tableMode = resolveMarkdownTableMode({
cfg,
channel: "telegram",
accountId: account.accountId
});
const renderHtmlText = (value) => renderTelegramHtmlText(value, {
textMode,
tableMode
});
const linkPreviewOptions = account.config.linkPreview ?? true ? void 0 : { is_disabled: true };
const sendTelegramText = async (rawText, params, fallbackText) => {
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;
const hasBaseParams = Object.keys(baseParams).length > 0;
const sendParams = {
parse_mode: "HTML",
...baseParams,
...opts.silent === true ? { disable_notification: true } : {}
};
return await withTelegramHtmlParseFallback({
label,
verbose: opts.verbose,
requestHtml: (retryLabel) => requestWithChatNotFound(() => api.sendMessage(chatId, htmlText, sendParams), retryLabel),
requestPlain: (retryLabel) => {
const plainParams = hasBaseParams ? baseParams : void 0;
return requestWithChatNotFound(() => plainParams ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams) : api.sendMessage(chatId, fallbackText ?? rawText), retryLabel);
}
});
});
};
if (mediaUrl) {
const media = await loadWebMedia(mediaUrl, {
maxBytes: opts.maxBytes,
localRoots: opts.mediaLocalRoots
});
const kind = mediaKindFromMime(media.contentType ?? void 0);
const isGif = isGifMedia({
contentType: media.contentType,
fileName: media.fileName
});
const isVideoNote = kind === "video" && opts.asVideoNote === true;
const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
const file = new InputFile(media.buffer, fileName);
let caption;
let followUpText;
if (isVideoNote) {
caption = void 0;
followUpText = text.trim() ? text : void 0;
} else {
const split = splitTelegramCaption(text);
caption = split.caption;
followUpText = split.followUpText;
}
const htmlCaption = caption ? renderHtmlText(caption) : void 0;
const needsSeparateText = Boolean(followUpText);
const baseMediaParams = {
...hasThreadParams ? threadParams : {},
...!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}
};
const mediaParams = {
...htmlCaption ? {
caption: htmlCaption,
parse_mode: "HTML"
} : {},
...baseMediaParams,
...opts.silent === true ? { disable_notification: true } : {}
};
const sendMedia = async (label, sender) => await withTelegramThreadFallback(mediaParams, label, opts.verbose, async (effectiveParams, retryLabel) => requestWithChatNotFound(() => sender(effectiveParams), retryLabel));
const mediaSender = (() => {
if (isGif) return {
label: "animation",
sender: (effectiveParams) => api.sendAnimation(chatId, file, effectiveParams)
};
if (kind === "image") return {
label: "photo",
sender: (effectiveParams) => api.sendPhoto(chatId, file, effectiveParams)
};
if (kind === "video") {
if (isVideoNote) return {
label: "video_note",
sender: (effectiveParams) => api.sendVideoNote(chatId, file, effectiveParams)
};
return {
label: "video",
sender: (effectiveParams) => api.sendVideo(chatId, file, effectiveParams)
};
}
if (kind === "audio") {
const { useVoice } = resolveTelegramVoiceSend({
wantsVoice: opts.asVoice === true,
contentType: media.contentType,
fileName,
logFallback: logVerbose
});
if (useVoice) return {
label: "voice",
sender: (effectiveParams) => api.sendVoice(chatId, file, effectiveParams)
};
return {
label: "audio",
sender: (effectiveParams) => api.sendAudio(chatId, file, effectiveParams)
};
}
return {
label: "document",
sender: (effectiveParams) => api.sendDocument(chatId, file, effectiveParams)
};
})();
const result = await sendMedia(mediaSender.label, mediaSender.sender);
const mediaMessageId = 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"
});
if (needsSeparateText && followUpText) {
const textParams = hasThreadParams || replyMarkup ? {
...threadParams,
...replyMarkup ? { reply_markup: replyMarkup } : {}
} : void 0;
const textRes = await sendTelegramText(followUpText, textParams);
return {
messageId: String(textRes?.message_id ?? mediaMessageId),
chatId: resolvedChatId
};
}
return {
messageId: mediaMessageId,
chatId: resolvedChatId
};
}
if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");
const res = await sendTelegramText(text, hasThreadParams || replyMarkup ? {
...threadParams,
...replyMarkup ? { reply_markup: replyMarkup } : {}
} : void 0, opts.plainText);
const messageId = String(res?.message_id ?? "unknown");
if (res?.message_id) recordSentMessage(chatId, res.message_id);
recordChannelActivity({
channel: "telegram",
accountId: account.accountId,
direction: "outbound"
});
return {
messageId,
chatId: String(res?.chat?.id ?? chatId)
};
}
async function reactMessageTelegram(chatIdInput, messageIdInput, emoji, opts = {}) {
const { cfg, account, api } = resolveTelegramApiContext(opts);
const chatId = normalizeChatId(String(chatIdInput));
const messageId = normalizeMessageId(messageIdInput);
const requestWithDiag = createTelegramRequestWithDiag({
cfg,
account,
retry: opts.retry,
verbose: opts.verbose,
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
});
const remove = opts.remove === true;
const trimmedEmoji = emoji.trim();
const reactions = remove || !trimmedEmoji ? [] : [{
type: "emoji",
emoji: trimmedEmoji
}];
if (typeof api.setMessageReaction !== "function") throw new Error("Telegram reactions are unavailable in this bot API.");
try {
await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (/REACTION_INVALID/i.test(msg)) return {
ok: false,
warning: `Reaction unavailable: ${trimmedEmoji}`
};
throw err;
}
return { ok: true };
}
async function deleteMessageTelegram(chatIdInput, messageIdInput, opts = {}) {
const { cfg, account, api } = resolveTelegramApiContext(opts);
const chatId = normalizeChatId(String(chatIdInput));
const messageId = normalizeMessageId(messageIdInput);
await createTelegramRequestWithDiag({
cfg,
account,
retry: opts.retry,
verbose: opts.verbose,
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
})(() => ap