src/telegram/status.ts
Rama main · réplica de lectura del repositorio original.
/**
* What the Telegram bridge believes about itself, as a value.
*
* The bridge sat dead in production for days: TELEGRAM_WEBHOOK_SECRET was never
* set, readTelegramConfig threw on every inbound email, and the notifier caught
* the throw and moved on so that Telegram could not break mail ingest. Not
* failing the request was right. Failing *silently* was not -- the error went to
* console.error, which nobody reads, and nothing else in the system knew.
*
* So the state is reified here and surfaced in /api/health and `pnpm doctor`,
* exactly as getAuthConfigStatus already does for the auth perimeter. Degrading to a named
* mode is the standard; degrading to a mute broken one is the bug.
*
* Everything here is read from D1. Deliberately: /api/health is what a monitor
* polls to decide whether this worker is alive, and making that answer depend on
* a third party's latency and uptime would turn a Telegram outage into a
* degraded Reccado. The cron already looks at Telegram hourly and writes down
* what it saw; this reads the note.
*/
import {
getRuntimeConfig,
getTelegramWebhookObservation,
getTelegramWebhookRegistration,
RUNTIME_CONFIG_KEYS,
} from "../db/runtime-config";
import { readTelegramConfig } from "./api";
import { resolveTelegramOperators } from "./operators";
import { hasDeliveryErrorSinceRegistration, telegramWebhookUrl } from "./registration";
export type TelegramStatus = {
/**
* off: no bot token, bridge intentionally disabled.
* failing: fully set up, but Telegram is failing to deliver updates that were
* sent after our last registration -- the mode that used to read as "on".
*/
mode: "off" | "partial" | "on" | "failing";
ok: boolean;
/** What is still needed before new mail reaches Telegram. */
missing: string[];
reason: string | null;
/** Where updates should be delivered, once the origin is known. */
webhookUrl: string | null;
/**
* Updates Telegram is holding for us. Null until the cron has looked once.
* A number that climbs is the same failure the error message describes, seen
* from the other side -- and it keeps counting when the message is generic.
*/
pendingUpdateCount: number | null;
};
export async function getTelegramStatus(env: Env): Promise<TelegramStatus> {
const config = readTelegramConfig(env);
if (!config) {
return {
mode: "off",
ok: true,
missing: [],
reason: "Telegram bridge is disabled until TELEGRAM_BOT_TOKEN is set.",
webhookUrl: null,
pendingUpdateCount: null,
};
}
const [origin, adoptedChat, registration, observation, operators] = await Promise.all([
getRuntimeConfig(env.INDEX_DB, RUNTIME_CONFIG_KEYS.deploymentOrigin),
getRuntimeConfig(env.INDEX_DB, RUNTIME_CONFIG_KEYS.telegramChatId),
getTelegramWebhookRegistration(env.INDEX_DB),
getTelegramWebhookObservation(env.INDEX_DB),
resolveTelegramOperators(env),
]);
const missing: string[] = [];
// A token with nobody linked is exactly what "partial" was always for. It used
// to arrive here as a thrown config error, which said the same thing in a
// shape that cost the caller a try/catch and told the operator to edit a file.
if (operators.size === 0) {
missing.push("linked operator (send /start <pairing code> to the bot)");
}
if (!origin) {
missing.push("deployment origin (load the authenticated UI once so the cron can register)");
}
if (!adoptedChat) {
missing.push("adopted chat (send /start to the bot from a linked account)");
}
const webhookUrl = origin ? telegramWebhookUrl(origin) : null;
const pendingUpdateCount = observation?.pendingUpdateCount ?? null;
if (missing.length > 0) {
return {
mode: "partial",
ok: false,
missing,
reason: `Telegram bridge is configured but not yet delivering: ${missing.join("; ")}.`,
webhookUrl,
pendingUpdateCount,
};
}
// No registration or no observation yet means the cron has not completed a pass
// on this deployment; that is a gap in knowledge, not a fault, and raising an
// alarm for it would train the operator to ignore this field.
if (
registration &&
observation &&
hasDeliveryErrorSinceRegistration(registration, observation.lastErrorAt)
) {
const message = observation.lastErrorMessage ?? "no message reported";
return {
mode: "failing",
ok: false,
missing: [],
reason:
`Telegram is failing to deliver webhook updates since ${observation.lastErrorAt}: ` +
`${message} (${pendingUpdateCount ?? 0} update(s) pending).`,
webhookUrl,
pendingUpdateCount,
};
}
return {
mode: "on",
ok: true,
missing: [],
reason: null,
webhookUrl,
pendingUpdateCount,
};
}