telegram: centralize api error logging

This commit is contained in:
Shadow
2026-01-26 20:25:06 -06:00
parent 66a5b324a1
commit 9e200068dc
9 changed files with 234 additions and 100 deletions

View 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;
}
}

View File

@@ -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;
} }

View File

@@ -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)}`);

View File

@@ -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(() => {});
} }
}; };

View File

@@ -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)}`);

View File

@@ -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;
} }

View File

@@ -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;
}); });

View File

@@ -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(),
});
} }

View File

@@ -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));