telegram: centralize api error logging
This commit is contained in:
41
src/telegram/api-logging.ts
Normal file
41
src/telegram/api-logging.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { danger } from "../globals.js";
|
||||||
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
|
export type TelegramApiLogger = (message: string) => void;
|
||||||
|
|
||||||
|
type TelegramApiLoggingParams<T> = {
|
||||||
|
operation: string;
|
||||||
|
fn: () => Promise<T>;
|
||||||
|
runtime?: RuntimeEnv;
|
||||||
|
logger?: TelegramApiLogger;
|
||||||
|
shouldLog?: (err: unknown) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackLogger = createSubsystemLogger("telegram/api");
|
||||||
|
|
||||||
|
function resolveTelegramApiLogger(runtime?: RuntimeEnv, logger?: TelegramApiLogger) {
|
||||||
|
if (logger) return logger;
|
||||||
|
if (runtime?.error) return runtime.error;
|
||||||
|
return (message: string) => fallbackLogger.error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withTelegramApiErrorLogging<T>({
|
||||||
|
operation,
|
||||||
|
fn,
|
||||||
|
runtime,
|
||||||
|
logger,
|
||||||
|
shouldLog,
|
||||||
|
}: TelegramApiLoggingParams<T>): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (err) {
|
||||||
|
if (!shouldLog || shouldLog(err)) {
|
||||||
|
const errText = formatErrorMessage(err);
|
||||||
|
const log = resolveTelegramApiLogger(runtime, logger);
|
||||||
|
log(danger(`telegram ${operation} failed: ${errText}`));
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { loadConfig } from "../config/config.js";
|
|||||||
import { writeConfigFile } from "../config/io.js";
|
import { writeConfigFile } from "../config/io.js";
|
||||||
import { danger, logVerbose, warn } from "../globals.js";
|
import { danger, logVerbose, warn } from "../globals.js";
|
||||||
import { resolveMedia } from "./bot/delivery.js";
|
import { resolveMedia } from "./bot/delivery.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import { resolveTelegramForumThreadId } from "./bot/helpers.js";
|
import { resolveTelegramForumThreadId } from "./bot/helpers.js";
|
||||||
import type { TelegramMessage } from "./bot/types.js";
|
import type { TelegramMessage } from "./bot/types.js";
|
||||||
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
|
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
|
||||||
@@ -180,7 +181,11 @@ export const registerTelegramHandlers = ({
|
|||||||
if (!callback) return;
|
if (!callback) return;
|
||||||
if (shouldSkipUpdate(ctx)) return;
|
if (shouldSkipUpdate(ctx)) return;
|
||||||
// Answer immediately to prevent Telegram from retrying while we process
|
// Answer immediately to prevent Telegram from retrying while we process
|
||||||
await bot.api.answerCallbackQuery(callback.id).catch(() => {});
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "answerCallbackQuery",
|
||||||
|
runtime,
|
||||||
|
fn: () => bot.api.answerCallbackQuery(callback.id),
|
||||||
|
}).catch(() => {});
|
||||||
try {
|
try {
|
||||||
const data = (callback.data ?? "").trim();
|
const data = (callback.data ?? "").trim();
|
||||||
const callbackMessage = callback.message;
|
const callbackMessage = callback.message;
|
||||||
@@ -577,11 +582,14 @@ export const registerTelegramHandlers = ({
|
|||||||
const errMsg = String(mediaErr);
|
const errMsg = String(mediaErr);
|
||||||
if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
|
if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
|
||||||
const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
|
const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
|
||||||
await bot.api
|
await withTelegramApiErrorLogging({
|
||||||
.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
|
operation: "sendMessage",
|
||||||
|
runtime,
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
|
||||||
reply_to_message_id: msg.message_id,
|
reply_to_message_id: msg.message_id,
|
||||||
})
|
}),
|
||||||
.catch(() => {});
|
}).catch(() => {});
|
||||||
logger.warn({ chatId, error: errMsg }, "media exceeds size limit");
|
logger.warn({ chatId, error: errMsg }, "media exceeds size limit");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reac
|
|||||||
import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
|
import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
|
||||||
import { resolveControlCommandGate } from "../channels/command-gating.js";
|
import { resolveControlCommandGate } from "../channels/command-gating.js";
|
||||||
import { logInboundDrop } from "../channels/logging.js";
|
import { logInboundDrop } from "../channels/logging.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import {
|
import {
|
||||||
buildGroupLabel,
|
buildGroupLabel,
|
||||||
buildSenderLabel,
|
buildSenderLabel,
|
||||||
@@ -165,16 +166,19 @@ export const buildTelegramMessageContext = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendTyping = async () => {
|
const sendTyping = async () => {
|
||||||
await bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId));
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendChatAction",
|
||||||
|
fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId)),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendRecordVoice = async () => {
|
const sendRecordVoice = async () => {
|
||||||
try {
|
try {
|
||||||
await bot.api.sendChatAction(
|
await withTelegramApiErrorLogging({
|
||||||
chatId,
|
operation: "sendChatAction",
|
||||||
"record_voice",
|
fn: () =>
|
||||||
buildTypingThreadParams(resolvedThreadId),
|
bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(resolvedThreadId)),
|
||||||
);
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`);
|
logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -227,7 +231,10 @@ export const buildTelegramMessageContext = async ({
|
|||||||
},
|
},
|
||||||
"telegram pairing request",
|
"telegram pairing request",
|
||||||
);
|
);
|
||||||
await bot.api.sendMessage(
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(
|
||||||
chatId,
|
chatId,
|
||||||
[
|
[
|
||||||
"Clawdbot: access not configured.",
|
"Clawdbot: access not configured.",
|
||||||
@@ -239,7 +246,8 @@ export const buildTelegramMessageContext = async ({
|
|||||||
"Ask the bot owner to approve with:",
|
"Ask the bot owner to approve with:",
|
||||||
formatCliCommand("clawdbot pairing approve telegram <code>"),
|
formatCliCommand("clawdbot pairing approve telegram <code>"),
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
||||||
@@ -408,7 +416,10 @@ export const buildTelegramMessageContext = async ({
|
|||||||
typeof api.setMessageReaction === "function" ? api.setMessageReaction.bind(api) : null;
|
typeof api.setMessageReaction === "function" ? api.setMessageReaction.bind(api) : null;
|
||||||
const ackReactionPromise =
|
const ackReactionPromise =
|
||||||
shouldAckReaction() && msg.message_id && reactionApi
|
shouldAckReaction() && msg.message_id && reactionApi
|
||||||
? reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji: ackReaction }]).then(
|
? withTelegramApiErrorLogging({
|
||||||
|
operation: "setMessageReaction",
|
||||||
|
fn: () => reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji: ackReaction }]),
|
||||||
|
}).then(
|
||||||
() => true,
|
() => true,
|
||||||
(err) => {
|
(err) => {
|
||||||
logVerbose(`telegram react failed for chat ${chatId}: ${String(err)}`);
|
logVerbose(`telegram react failed for chat ${chatId}: ${String(err)}`);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/pr
|
|||||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||||
import { danger, logVerbose } from "../globals.js";
|
import { danger, logVerbose } from "../globals.js";
|
||||||
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import {
|
import {
|
||||||
normalizeTelegramCommandName,
|
normalizeTelegramCommandName,
|
||||||
TELEGRAM_COMMAND_NAME_PATTERN,
|
TELEGRAM_COMMAND_NAME_PATTERN,
|
||||||
@@ -134,11 +135,17 @@ async function resolveTelegramCommandAuth(params: {
|
|||||||
const senderUsername = msg.from?.username ?? "";
|
const senderUsername = msg.from?.username ?? "";
|
||||||
|
|
||||||
if (isGroup && groupConfig?.enabled === false) {
|
if (isGroup && groupConfig?.enabled === false) {
|
||||||
await bot.api.sendMessage(chatId, "This group is disabled.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "This group is disabled."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isGroup && topicConfig?.enabled === false) {
|
if (isGroup && topicConfig?.enabled === false) {
|
||||||
await bot.api.sendMessage(chatId, "This topic is disabled.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "This topic is disabled."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (requireAuth && isGroup && hasGroupAllowOverride) {
|
if (requireAuth && isGroup && hasGroupAllowOverride) {
|
||||||
@@ -150,7 +157,10 @@ async function resolveTelegramCommandAuth(params: {
|
|||||||
senderUsername,
|
senderUsername,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "You are not authorized to use this command."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,7 +169,10 @@ async function resolveTelegramCommandAuth(params: {
|
|||||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||||
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
|
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
|
||||||
if (groupPolicy === "disabled") {
|
if (groupPolicy === "disabled") {
|
||||||
await bot.api.sendMessage(chatId, "Telegram group commands are disabled.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "Telegram group commands are disabled."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (groupPolicy === "allowlist" && requireAuth) {
|
if (groupPolicy === "allowlist" && requireAuth) {
|
||||||
@@ -171,13 +184,19 @@ async function resolveTelegramCommandAuth(params: {
|
|||||||
senderUsername,
|
senderUsername,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "You are not authorized to use this command."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const groupAllowlist = resolveGroupPolicy(chatId);
|
const groupAllowlist = resolveGroupPolicy(chatId);
|
||||||
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
||||||
await bot.api.sendMessage(chatId, "This group is not allowed.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "This group is not allowed."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +216,10 @@ async function resolveTelegramCommandAuth(params: {
|
|||||||
modeWhenAccessGroupsOff: "configured",
|
modeWhenAccessGroupsOff: "configured",
|
||||||
});
|
});
|
||||||
if (requireAuth && !commandAuthorized) {
|
if (requireAuth && !commandAuthorized) {
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "You are not authorized to use this command."),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,9 +322,11 @@ export const registerTelegramNativeCommands = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (allCommands.length > 0) {
|
if (allCommands.length > 0) {
|
||||||
bot.api.setMyCommands(allCommands).catch((err) => {
|
void withTelegramApiErrorLogging({
|
||||||
runtime.error?.(danger(`telegram setMyCommands failed: ${String(err)}`));
|
operation: "setMyCommands",
|
||||||
});
|
runtime,
|
||||||
|
fn: () => bot.api.setMyCommands(allCommands),
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
if (typeof (bot as unknown as { command?: unknown }).command !== "function") {
|
if (typeof (bot as unknown as { command?: unknown }).command !== "function") {
|
||||||
logVerbose("telegram: bot.command unavailable; skipping native handlers");
|
logVerbose("telegram: bot.command unavailable; skipping native handlers");
|
||||||
@@ -376,9 +400,14 @@ export const registerTelegramNativeCommands = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const replyMarkup = buildInlineKeyboard(rows);
|
const replyMarkup = buildInlineKeyboard(rows);
|
||||||
await bot.api.sendMessage(chatId, title, {
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
runtime,
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(chatId, title, {
|
||||||
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||||
...(resolvedThreadId != null ? { message_thread_id: resolvedThreadId } : {}),
|
...(resolvedThreadId != null ? { message_thread_id: resolvedThreadId } : {}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -492,7 +521,11 @@ export const registerTelegramNativeCommands = ({
|
|||||||
const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`;
|
const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`;
|
||||||
const match = matchPluginCommand(commandBody);
|
const match = matchPluginCommand(commandBody);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
await bot.api.sendMessage(chatId, "Command not found.");
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
runtime,
|
||||||
|
fn: () => bot.api.sendMessage(chatId, "Command not found."),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auth = await resolveTelegramCommandAuth({
|
const auth = await resolveTelegramCommandAuth({
|
||||||
@@ -543,8 +576,10 @@ export const registerTelegramNativeCommands = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (nativeDisabledExplicit) {
|
} else if (nativeDisabledExplicit) {
|
||||||
bot.api.setMyCommands([]).catch((err) => {
|
void withTelegramApiErrorLogging({
|
||||||
runtime.error?.(danger(`telegram clear commands failed: ${String(err)}`));
|
operation: "setMyCommands",
|
||||||
});
|
runtime,
|
||||||
|
fn: () => bot.api.setMyCommands([]),
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
|
|||||||
import { formatUncaughtError } from "../infra/errors.js";
|
import { formatUncaughtError } from "../infra/errors.js";
|
||||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
import { getChildLogger } from "../logging.js";
|
import { getChildLogger } from "../logging.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||||
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -261,7 +262,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
}
|
}
|
||||||
if (typeof botHasTopicsEnabled === "boolean") return botHasTopicsEnabled;
|
if (typeof botHasTopicsEnabled === "boolean") return botHasTopicsEnabled;
|
||||||
try {
|
try {
|
||||||
const me = (await bot.api.getMe()) as { has_topics_enabled?: boolean };
|
const me = (await withTelegramApiErrorLogging({
|
||||||
|
operation: "getMe",
|
||||||
|
runtime,
|
||||||
|
fn: () => bot.api.getMe(),
|
||||||
|
})) as { has_topics_enabled?: boolean };
|
||||||
botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
|
botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`telegram getMe failed: ${String(err)}`);
|
logVerbose(`telegram getMe failed: ${String(err)}`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
markdownToTelegramHtml,
|
markdownToTelegramHtml,
|
||||||
renderTelegramHtmlText,
|
renderTelegramHtmlText,
|
||||||
} from "../format.js";
|
} from "../format.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "../api-logging.js";
|
||||||
import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js";
|
import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js";
|
||||||
import { splitTelegramCaption } from "../caption.js";
|
import { splitTelegramCaption } from "../caption.js";
|
||||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||||
@@ -25,24 +26,6 @@ import type { TelegramContext } from "./types.js";
|
|||||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||||
const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/;
|
const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/;
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a Telegram API call with error logging. Ensures network failures are
|
|
||||||
* logged with context before propagating, preventing silent unhandled rejections.
|
|
||||||
*/
|
|
||||||
async function withMediaErrorHandler<T>(
|
|
||||||
operation: string,
|
|
||||||
runtime: RuntimeEnv,
|
|
||||||
fn: () => Promise<T>,
|
|
||||||
): Promise<T> {
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} catch (err) {
|
|
||||||
const errText = formatErrorMessage(err);
|
|
||||||
runtime.error?.(danger(`telegram ${operation} failed: ${errText}`));
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deliverReplies(params: {
|
export async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@@ -164,17 +147,23 @@ export async function deliverReplies(params: {
|
|||||||
mediaParams.message_thread_id = threadParams.message_thread_id;
|
mediaParams.message_thread_id = threadParams.message_thread_id;
|
||||||
}
|
}
|
||||||
if (isGif) {
|
if (isGif) {
|
||||||
await withMediaErrorHandler("sendAnimation", runtime, () =>
|
await withTelegramApiErrorLogging({
|
||||||
bot.api.sendAnimation(chatId, file, { ...mediaParams }),
|
operation: "sendAnimation",
|
||||||
);
|
runtime,
|
||||||
|
fn: () => bot.api.sendAnimation(chatId, file, { ...mediaParams }),
|
||||||
|
});
|
||||||
} else if (kind === "image") {
|
} else if (kind === "image") {
|
||||||
await withMediaErrorHandler("sendPhoto", runtime, () =>
|
await withTelegramApiErrorLogging({
|
||||||
bot.api.sendPhoto(chatId, file, { ...mediaParams }),
|
operation: "sendPhoto",
|
||||||
);
|
runtime,
|
||||||
|
fn: () => bot.api.sendPhoto(chatId, file, { ...mediaParams }),
|
||||||
|
});
|
||||||
} else if (kind === "video") {
|
} else if (kind === "video") {
|
||||||
await withMediaErrorHandler("sendVideo", runtime, () =>
|
await withTelegramApiErrorLogging({
|
||||||
bot.api.sendVideo(chatId, file, { ...mediaParams }),
|
operation: "sendVideo",
|
||||||
);
|
runtime,
|
||||||
|
fn: () => bot.api.sendVideo(chatId, file, { ...mediaParams }),
|
||||||
|
});
|
||||||
} else if (kind === "audio") {
|
} else if (kind === "audio") {
|
||||||
const { useVoice } = resolveTelegramVoiceSend({
|
const { useVoice } = resolveTelegramVoiceSend({
|
||||||
wantsVoice: reply.audioAsVoice === true, // default false (backward compatible)
|
wantsVoice: reply.audioAsVoice === true, // default false (backward compatible)
|
||||||
@@ -187,9 +176,12 @@ export async function deliverReplies(params: {
|
|||||||
// Switch typing indicator to record_voice before sending.
|
// Switch typing indicator to record_voice before sending.
|
||||||
await params.onVoiceRecording?.();
|
await params.onVoiceRecording?.();
|
||||||
try {
|
try {
|
||||||
await withMediaErrorHandler("sendVoice", runtime, () =>
|
await withTelegramApiErrorLogging({
|
||||||
bot.api.sendVoice(chatId, file, { ...mediaParams }),
|
operation: "sendVoice",
|
||||||
);
|
runtime,
|
||||||
|
shouldLog: (err) => !isVoiceMessagesForbidden(err),
|
||||||
|
fn: () => bot.api.sendVoice(chatId, file, { ...mediaParams }),
|
||||||
|
});
|
||||||
} catch (voiceErr) {
|
} catch (voiceErr) {
|
||||||
// Fall back to text if voice messages are forbidden in this chat.
|
// Fall back to text if voice messages are forbidden in this chat.
|
||||||
// This happens when the recipient has Telegram Premium privacy settings
|
// This happens when the recipient has Telegram Premium privacy settings
|
||||||
@@ -222,14 +214,18 @@ export async function deliverReplies(params: {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Audio file - displays with metadata (title, duration) - DEFAULT
|
// Audio file - displays with metadata (title, duration) - DEFAULT
|
||||||
await withMediaErrorHandler("sendAudio", runtime, () =>
|
await withTelegramApiErrorLogging({
|
||||||
bot.api.sendAudio(chatId, file, { ...mediaParams }),
|
operation: "sendAudio",
|
||||||
);
|
runtime,
|
||||||
|
fn: () => bot.api.sendAudio(chatId, file, { ...mediaParams }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await withMediaErrorHandler("sendDocument", runtime, () =>
|
await withTelegramApiErrorLogging({
|
||||||
bot.api.sendDocument(chatId, file, { ...mediaParams }),
|
operation: "sendDocument",
|
||||||
);
|
runtime,
|
||||||
|
fn: () => bot.api.sendDocument(chatId, file, { ...mediaParams }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
@@ -371,11 +367,17 @@ async function sendTelegramText(
|
|||||||
const textMode = opts?.textMode ?? "markdown";
|
const textMode = opts?.textMode ?? "markdown";
|
||||||
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
||||||
try {
|
try {
|
||||||
const res = await bot.api.sendMessage(chatId, htmlText, {
|
const res = await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
runtime,
|
||||||
|
shouldLog: (err) => !PARSE_ERR_RE.test(formatErrorMessage(err)),
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(chatId, htmlText, {
|
||||||
parse_mode: "HTML",
|
parse_mode: "HTML",
|
||||||
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
||||||
...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}),
|
...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}),
|
||||||
...baseParams,
|
...baseParams,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return res.message_id;
|
return res.message_id;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -383,10 +385,15 @@ async function sendTelegramText(
|
|||||||
if (PARSE_ERR_RE.test(errText)) {
|
if (PARSE_ERR_RE.test(errText)) {
|
||||||
runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`);
|
runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`);
|
||||||
const fallbackText = opts?.plainText ?? text;
|
const fallbackText = opts?.plainText ?? text;
|
||||||
const res = await bot.api.sendMessage(chatId, fallbackText, {
|
const res = await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
runtime,
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(chatId, fallbackText, {
|
||||||
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
||||||
...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}),
|
...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}),
|
||||||
...baseParams,
|
...baseParams,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return res.message_id;
|
return res.message_id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { type ApiClientOptions, Bot, HttpError, InputFile } from "grammy";
|
|||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { logVerbose } from "../globals.js";
|
import { logVerbose } from "../globals.js";
|
||||||
import { recordChannelActivity } from "../infra/channel-activity.js";
|
import { recordChannelActivity } from "../infra/channel-activity.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js";
|
import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js";
|
||||||
import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js";
|
import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js";
|
||||||
import type { RetryConfig } from "../infra/retry.js";
|
import type { RetryConfig } from "../infra/retry.js";
|
||||||
@@ -210,7 +211,10 @@ export async function sendMessageTelegram(
|
|||||||
});
|
});
|
||||||
const logHttpError = createTelegramHttpLogger(cfg);
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
request(fn, label).catch((err) => {
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
logHttpError(label ?? "request", err);
|
logHttpError(label ?? "request", err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -442,7 +446,10 @@ export async function reactMessageTelegram(
|
|||||||
});
|
});
|
||||||
const logHttpError = createTelegramHttpLogger(cfg);
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
request(fn, label).catch((err) => {
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
logHttpError(label ?? "request", err);
|
logHttpError(label ?? "request", err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -492,7 +499,10 @@ export async function deleteMessageTelegram(
|
|||||||
});
|
});
|
||||||
const logHttpError = createTelegramHttpLogger(cfg);
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
request(fn, label).catch((err) => {
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
logHttpError(label ?? "request", err);
|
logHttpError(label ?? "request", err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -537,7 +547,10 @@ export async function editMessageTelegram(
|
|||||||
});
|
});
|
||||||
const logHttpError = createTelegramHttpLogger(cfg);
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
request(fn, label).catch((err) => {
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
logHttpError(label ?? "request", err);
|
logHttpError(label ?? "request", err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type ApiClientOptions, Bot } from "grammy";
|
import { type ApiClientOptions, Bot } from "grammy";
|
||||||
import type { TelegramNetworkConfig } from "../config/types.telegram.js";
|
import type { TelegramNetworkConfig } from "../config/types.telegram.js";
|
||||||
import { resolveTelegramFetch } from "./fetch.js";
|
import { resolveTelegramFetch } from "./fetch.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
|
|
||||||
export async function setTelegramWebhook(opts: {
|
export async function setTelegramWebhook(opts: {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -14,9 +15,13 @@ export async function setTelegramWebhook(opts: {
|
|||||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||||
: undefined;
|
: undefined;
|
||||||
const bot = new Bot(opts.token, client ? { client } : undefined);
|
const bot = new Bot(opts.token, client ? { client } : undefined);
|
||||||
await bot.api.setWebhook(opts.url, {
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "setWebhook",
|
||||||
|
fn: () =>
|
||||||
|
bot.api.setWebhook(opts.url, {
|
||||||
secret_token: opts.secret,
|
secret_token: opts.secret,
|
||||||
drop_pending_updates: opts.dropPendingUpdates ?? false,
|
drop_pending_updates: opts.dropPendingUpdates ?? false,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,5 +34,8 @@ export async function deleteTelegramWebhook(opts: {
|
|||||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||||
: undefined;
|
: undefined;
|
||||||
const bot = new Bot(opts.token, client ? { client } : undefined);
|
const bot = new Bot(opts.token, client ? { client } : undefined);
|
||||||
await bot.api.deleteWebhook();
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "deleteWebhook",
|
||||||
|
fn: () => bot.api.deleteWebhook(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "../logging/diagnostic.js";
|
} from "../logging/diagnostic.js";
|
||||||
import { resolveTelegramAllowedUpdates } from "./allowed-updates.js";
|
import { resolveTelegramAllowedUpdates } from "./allowed-updates.js";
|
||||||
import { createTelegramBot } from "./bot.js";
|
import { createTelegramBot } from "./bot.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
|
|
||||||
export async function startTelegramWebhook(opts: {
|
export async function startTelegramWebhook(opts: {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -97,9 +98,14 @@ export async function startTelegramWebhook(opts: {
|
|||||||
const publicUrl =
|
const publicUrl =
|
||||||
opts.publicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
opts.publicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
||||||
|
|
||||||
await bot.api.setWebhook(publicUrl, {
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "setWebhook",
|
||||||
|
runtime,
|
||||||
|
fn: () =>
|
||||||
|
bot.api.setWebhook(publicUrl, {
|
||||||
secret_token: opts.secret,
|
secret_token: opts.secret,
|
||||||
allowed_updates: resolveTelegramAllowedUpdates(),
|
allowed_updates: resolveTelegramAllowedUpdates(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve) => server.listen(port, host, resolve));
|
await new Promise<void>((resolve) => server.listen(port, host, resolve));
|
||||||
|
|||||||
Reference in New Issue
Block a user