wip: snapshot das alterações não commitadas (2026-10-20)
Validate / validate (push) Failing after 4m58s
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:
1 parent
a56408711e
commit
97f2b723bb
5 files changed
+916
No files matched your search
+104
@@ -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
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user