wip: snapshot das alterações não commitadas (2026-10-20)
Validate / validate (push) Failing after 4m58s

Origem: D:/Documents/Github/oh-my-pi-telegram
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
This commit is contained in:
evandro committed 2026-10-20 00:49:42 -03:00
1 parent a56408711e
commit 97f2b723bb
5 files changed
+916

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
/**
* 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);
},
};
}
+134
View File
@@ -0,0 +1,134 @@
/**
* Telegram bridge traffic counters
* Zones: telegram transport, status
* Owns in-memory received/sent message counts and last-activity time for the status bar
*
* This domain is pure: it counts, it does not read config, call Telegram, or import other domains.
*/
export interface TelegramTrafficSnapshot {
/** Authorized messages received from the paired owner since the bridge loaded. */
received: number;
/** Messages the bridge delivered to Telegram since the bridge loaded. */
sent: number;
/** Messages that could not be delivered after the transport retries. */
failed: number;
/** Time of the latest received or sent message, when there was one. */
lastAtMs?: number;
}
export interface TelegramTrafficCounters {
recordReceived: () => void;
recordSent: () => void;
recordFailed: () => void;
snapshot: () => TelegramTrafficSnapshot;
}
/**
* Bot API methods that deliver a message the owner can read. Typing actions,
* drafts, edits, reactions and callback answers are not messages.
*/
const TELEGRAM_OUTBOUND_MESSAGE_METHODS: Readonly<Record<string, true>> = {
sendMessage: true,
sendRichMessage: true,
sendPhoto: true,
sendDocument: true,
sendAudio: true,
sendVideo: true,
sendVoice: true,
sendAnimation: true,
sendVideoNote: true,
sendMediaGroup: true,
sendSticker: true,
};
export function isTelegramOutboundMessageMethod(method: string): boolean {
return Object.hasOwn(TELEGRAM_OUTBOUND_MESSAGE_METHODS, method);
}
export function createTelegramTrafficCounters(
now: () => number = Date.now,
): TelegramTrafficCounters {
let received = 0;
let sent = 0;
let failed = 0;
let lastAtMs: number | undefined;
return {
recordReceived() {
received += 1;
lastAtMs = now();
},
recordSent() {
sent += 1;
lastAtMs = now();
},
recordFailed() {
failed += 1;
},
snapshot: () => ({
received,
sent,
failed,
...(lastAtMs === undefined ? {} : { lastAtMs }),
}),
};
}
export interface TelegramTrafficStatusBridgeDeps<TContext> {
counters: TelegramTrafficCounters;
/** Current session context, when the bridge has one. */
getContext: () => TContext | undefined;
updateStatus: (ctx: TContext) => void;
recordRuntimeEvent: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
/** Told about every message delivery that failed, e.g. to raise a notice. */
onDeliveryFailed?: (error: unknown) => void;
}
export interface TelegramTrafficStatusBridge<TContext> {
/** Observer for successful Bot API calls; counts delivered messages. */
onCallSucceeded: (method: string) => void;
/** Observer for failed Bot API calls; counts undelivered messages. */
onCallFailed: (method: string, error: unknown) => void;
/** Observer for messages accepted from the paired owner. */
onAuthorizedMessage: (ctx: TContext) => void;
}
/**
* Counts traffic and refreshes the status bar so the counters stay current. A
* status refresh failure (for example a stale session context) is recorded and
* never propagates into the send or receive path that triggered it.
*/
export function createTelegramTrafficStatusBridge<TContext>(
deps: TelegramTrafficStatusBridgeDeps<TContext>,
): TelegramTrafficStatusBridge<TContext> {
const refreshStatus = (ctx: TContext): void => {
try {
deps.updateStatus(ctx);
} catch (error) {
deps.recordRuntimeEvent("status", error, { phase: "traffic-refresh" });
}
};
return {
onCallSucceeded(method) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordSent();
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onCallFailed(method, error) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordFailed();
deps.onDeliveryFailed?.(error);
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onAuthorizedMessage(ctx) {
deps.counters.recordReceived();
refreshStatus(ctx);
},
};
}