Validate / validate (push) Successful in 9m52s
- the poll loop now clears its error status after the next successful getUpdates, not before the retry, so a slow retry never shows as healthy - recovery is reported only when getUpdates itself failed - a send that may have reached Telegram is reported as unconfirmed instead of failed - README and CHANGELOG describe the states, toasts, queue preview and spinner text
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
/**
|
|
* Telegram operator notices
|
|
* Zones: telegram ui, tui
|
|
* Owns the one-off host toasts (owner paired, connection recovered, reply not delivered) and their throttling
|
|
*/
|
|
|
|
import {
|
|
sanitizeTelegramStatusText,
|
|
summarizeTelegramStatusError,
|
|
} from "./status.ts";
|
|
|
|
export type TelegramNoticeLevel = "info" | "warning" | "error";
|
|
|
|
/** A failed delivery raises at most one notice per this window. */
|
|
export const TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS = 30_000;
|
|
|
|
export interface TelegramNoticeRuntimeDeps<TContext> {
|
|
/** Current session context, when the bridge has one. */
|
|
getContext: () => TContext | undefined;
|
|
notify: (ctx: TContext, message: string, level: TelegramNoticeLevel) => void;
|
|
recordRuntimeEvent: (
|
|
category: string,
|
|
error: unknown,
|
|
details?: Record<string, unknown>,
|
|
) => void;
|
|
now?: () => number;
|
|
}
|
|
|
|
export interface TelegramNoticeRuntime<TContext> {
|
|
/** The first Telegram user was accepted as the bridge owner. */
|
|
paired: (userId: number, ctx: TContext) => void;
|
|
/** Polling works again after at least one failed attempt. */
|
|
recovered: (ctx: TContext) => void;
|
|
/** A message could not be delivered; throttled, uses the current context. */
|
|
deliveryFailed: (error: unknown) => void;
|
|
}
|
|
|
|
function describeTelegramDeliveryFailure(error: unknown): {
|
|
message: string;
|
|
level: TelegramNoticeLevel;
|
|
} {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const summary = summarizeTelegramStatusError(message);
|
|
if (summary?.deliveryUnknown) {
|
|
return {
|
|
message: "Telegram: reply delivery unconfirmed (check the chat)",
|
|
level: "warning",
|
|
};
|
|
}
|
|
if (summary) {
|
|
return {
|
|
message: `Telegram: reply not delivered (${summary.label})`,
|
|
level: summary.kind === "fatal" ? "error" : "warning",
|
|
};
|
|
}
|
|
const raw = sanitizeTelegramStatusText(message);
|
|
const reason = raw.length > 80 ? `${raw.slice(0, 79)}…` : raw;
|
|
return { message: `Telegram: reply not delivered (${reason})`, level: "error" };
|
|
}
|
|
|
|
/**
|
|
* Toasts never propagate failures into the path that raised them: a stale
|
|
* session context only produces a recorded runtime event.
|
|
*/
|
|
export function createTelegramNoticeRuntime<TContext>(
|
|
deps: TelegramNoticeRuntimeDeps<TContext>,
|
|
): TelegramNoticeRuntime<TContext> {
|
|
const now = deps.now ?? Date.now;
|
|
let lastDeliveryNoticeAtMs: number | undefined;
|
|
const show = (
|
|
ctx: TContext,
|
|
message: string,
|
|
level: TelegramNoticeLevel,
|
|
): void => {
|
|
try {
|
|
deps.notify(ctx, message, level);
|
|
} catch (error) {
|
|
deps.recordRuntimeEvent("status", error, { phase: "notice" });
|
|
}
|
|
};
|
|
return {
|
|
paired(userId, ctx) {
|
|
show(ctx, `Telegram paired with user ${userId}`, "info");
|
|
},
|
|
recovered(ctx) {
|
|
show(ctx, "Telegram reconnected", "info");
|
|
},
|
|
deliveryFailed(error) {
|
|
const ctx = deps.getContext();
|
|
if (ctx === undefined) return;
|
|
const nowMs = now();
|
|
if (
|
|
lastDeliveryNoticeAtMs !== undefined &&
|
|
nowMs - lastDeliveryNoticeAtMs <
|
|
TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS
|
|
) {
|
|
return;
|
|
}
|
|
lastDeliveryNoticeAtMs = nowMs;
|
|
const failure = describeTelegramDeliveryFailure(error);
|
|
show(ctx, failure.message, failure.level);
|
|
},
|
|
};
|
|
}
|