Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a drop-in extension for can1357/oh-my-pi (omp >= 16.5.2). Changes vs upstream: - Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram - Bump version: 0.22.0 -> 0.22.0-evandro.1 - Update repository/homepage/bugs URLs to evandrodevbr - Add omp field in package.json (displayName + compatibleWith) - README: prepend 'Oh My Pi (omp) Compatibility' section with install commands, verified-components table, upstream lineage, and rationale - CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased - docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram - tests/public-api.test.ts: rename package import paths Gates re-run on the fork: - npm run typecheck -> tsc --noEmit (success) - npm test -> 1229 pass / 0 fail / 1 platform-only skip - npm run audit -> found 0 vulnerabilities - npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz Verified: - omp install --link ./pi-telegram -> Linked OK - omp plugin list -> @evandrodevbr/[email protected].0-evandro.1 Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
888 lines
27 KiB
TypeScript
888 lines
27 KiB
TypeScript
/**
|
|
* Telegram target-aware operational delivery and logical message lifecycle
|
|
* Zones: telegram delivery, extension API, runtime binding
|
|
* Owns the public extension delivery contract, authorized scope resolution, operational rendering adapter, per-target serialization, chunk reconciliation, generation-fenced logical handles, and process-local runtime membrane; composes the established reply renderer with bus-aware Telegram API ports and excludes bot clients, Pi contexts, and consumer-extension policy
|
|
*/
|
|
|
|
import { markTelegramBusAggregateDelivery } from "./bus.ts";
|
|
import {
|
|
assertTelegramInlineKeyboardCallbackData,
|
|
type TelegramInlineKeyboardMarkup,
|
|
} from "./keyboard.ts";
|
|
import {
|
|
buildTelegramReplyParameters,
|
|
renderTelegramMessage,
|
|
} from "./replies.ts";
|
|
import {
|
|
getTelegramTargetThreadParams,
|
|
type TelegramTarget,
|
|
} from "./target.ts";
|
|
import {
|
|
isTelegramApiCommitUnknownError,
|
|
type TelegramBridgeApiRuntime,
|
|
} from "./telegram-api.ts";
|
|
|
|
const TELEGRAM_DELIVERY_RUNTIME_KEY = "__piTelegramDeliveryRuntime__";
|
|
|
|
class TelegramDeliveryTransportGenerationError extends Error {
|
|
constructor() {
|
|
super("Telegram Delivery transport generation is no longer active.");
|
|
this.name = "TelegramDeliveryTransportGenerationError";
|
|
}
|
|
}
|
|
|
|
export type TelegramDeliveryParseMode = "plain" | "html" | "markdown";
|
|
|
|
export interface TelegramDeliveryView {
|
|
text: string;
|
|
parseMode?: TelegramDeliveryParseMode;
|
|
replyMarkup?: TelegramInlineKeyboardMarkup;
|
|
}
|
|
|
|
export type TelegramDeliveryTarget = TelegramTarget;
|
|
|
|
export type TelegramDeliveryScope =
|
|
| { kind: "active-turn" }
|
|
| { kind: "instance" }
|
|
| { kind: "aggregate" }
|
|
| { kind: "target"; target: TelegramDeliveryTarget };
|
|
|
|
export interface TelegramDeliveryHandle {
|
|
readonly target: TelegramDeliveryTarget;
|
|
readonly messageIds: readonly number[];
|
|
readonly generation: string;
|
|
}
|
|
|
|
export type TelegramDeliveryFailureReason =
|
|
| "runtime-unavailable"
|
|
| "target-unavailable"
|
|
| "target-unauthorized"
|
|
| "stale-handle"
|
|
| "invalid-view"
|
|
| "commit-unknown"
|
|
| "transport-failed";
|
|
|
|
export type TelegramDeliveryResult<T> =
|
|
| { ok: true; value: T }
|
|
| {
|
|
ok: false;
|
|
reason: TelegramDeliveryFailureReason;
|
|
message: string;
|
|
/** Successfully materialized state that callers may edit or delete to recover. */
|
|
partial?: T;
|
|
};
|
|
|
|
export interface SendTelegramViewOptions {
|
|
scope: TelegramDeliveryScope;
|
|
replyToMessageId?: number;
|
|
}
|
|
|
|
export type TelegramDeliveryChatAction =
|
|
"typing" | "upload_document" | "upload_photo" | "record_voice";
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryRuntime {
|
|
readonly generation: string;
|
|
shutdown: () => void;
|
|
sendView: (
|
|
view: TelegramDeliveryView,
|
|
options: SendTelegramViewOptions,
|
|
) => Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
editView: (
|
|
handle: TelegramDeliveryHandle,
|
|
view: TelegramDeliveryView,
|
|
) => Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
deleteView: (
|
|
handle: TelegramDeliveryHandle,
|
|
) => Promise<TelegramDeliveryResult<void>>;
|
|
sendChatAction: (
|
|
action: TelegramDeliveryChatAction,
|
|
scope: TelegramDeliveryScope,
|
|
) => Promise<TelegramDeliveryResult<void>>;
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryTargetResolverDeps {
|
|
getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
|
|
getInstanceTarget: () => TelegramDeliveryTarget | undefined;
|
|
getAggregateTarget: () => TelegramDeliveryTarget | undefined;
|
|
isExplicitTargetAuthorized: (target: TelegramDeliveryTarget) => boolean;
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryRenderedChunk {
|
|
text: string;
|
|
parseMode: TelegramDeliveryParseMode;
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryTransportOptions {
|
|
replyToMessageId?: number;
|
|
replyMarkup?: TelegramInlineKeyboardMarkup | null;
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResolverDeps {
|
|
generation: string;
|
|
renderView: (
|
|
view: TelegramDeliveryView,
|
|
) => readonly TelegramDeliveryRenderedChunk[];
|
|
sendChunk: (
|
|
target: TelegramDeliveryTarget,
|
|
chunk: TelegramDeliveryRenderedChunk,
|
|
options: TelegramDeliveryTransportOptions,
|
|
) => Promise<number>;
|
|
editChunk: (
|
|
target: TelegramDeliveryTarget,
|
|
messageId: number,
|
|
chunk: TelegramDeliveryRenderedChunk,
|
|
options: TelegramDeliveryTransportOptions,
|
|
) => Promise<void>;
|
|
deleteMessage: (
|
|
target: TelegramDeliveryTarget,
|
|
messageId: number,
|
|
) => Promise<void>;
|
|
sendChatAction: (
|
|
target: TelegramDeliveryTarget,
|
|
action: TelegramDeliveryChatAction,
|
|
) => Promise<void>;
|
|
recordFailure?: (
|
|
operation: "send" | "edit" | "delete" | "chat-action",
|
|
error: unknown,
|
|
target?: TelegramDeliveryTarget,
|
|
) => void;
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramBridgeDeliveryRuntimeDeps {
|
|
generation: string;
|
|
getTargetPolicyView: () => TelegramDeliveryTargetPolicyView;
|
|
getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
|
|
isTransportActive?: () => boolean;
|
|
api: Pick<
|
|
TelegramBridgeApiRuntime,
|
|
"sendMessage" | "editMessageText" | "deleteMessage" | "sendChatAction"
|
|
>;
|
|
recordOwnership: (input: {
|
|
chatId: number;
|
|
messageId: number;
|
|
target: TelegramDeliveryTarget;
|
|
}) => void;
|
|
recordFailure?: TelegramDeliveryRuntimeDeps["recordFailure"];
|
|
}
|
|
|
|
/** @internal */
|
|
export function createTelegramDeliveryLifecycleHooks(
|
|
createRuntime: () => TelegramDeliveryRuntime,
|
|
): {
|
|
onSessionStart: () => Promise<void>;
|
|
onSessionShutdown: () => Promise<void>;
|
|
} {
|
|
let runtime: TelegramDeliveryRuntime | undefined;
|
|
let unbind: ReturnType<typeof bindTelegramDeliveryRuntime> | undefined;
|
|
const stopCurrentRuntime = () => {
|
|
unbind?.();
|
|
unbind = undefined;
|
|
runtime?.shutdown();
|
|
runtime = undefined;
|
|
};
|
|
return {
|
|
onSessionStart: async () => {
|
|
stopCurrentRuntime();
|
|
runtime = createRuntime();
|
|
unbind = bindTelegramDeliveryRuntime(runtime);
|
|
},
|
|
onSessionShutdown: async () => {
|
|
stopCurrentRuntime();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** @internal */
|
|
export function createTelegramBridgeDeliveryLifecycleHooks<TTransportStamp>(
|
|
deps: Omit<
|
|
TelegramBridgeDeliveryRuntimeDeps,
|
|
"generation" | "isTransportActive"
|
|
> & {
|
|
generationSeed: string;
|
|
getTransportStamp?: () => TTransportStamp;
|
|
isTransportStampActive?: (stamp: TTransportStamp) => boolean;
|
|
},
|
|
): ReturnType<typeof createTelegramDeliveryLifecycleHooks> {
|
|
let generationSequence = 0;
|
|
return createTelegramDeliveryLifecycleHooks(() => {
|
|
const transportStamp = deps.getTransportStamp?.();
|
|
return createTelegramBridgeDeliveryRuntime({
|
|
...deps,
|
|
generation: `${deps.generationSeed}:${++generationSequence}`,
|
|
isTransportActive:
|
|
transportStamp !== undefined && deps.isTransportStampActive
|
|
? () => deps.isTransportStampActive?.(transportStamp) ?? false
|
|
: undefined,
|
|
});
|
|
});
|
|
}
|
|
|
|
interface TelegramDeliveryRuntimeRegistry {
|
|
runtime?: TelegramDeliveryRuntime;
|
|
}
|
|
|
|
function getTelegramDeliveryRuntimeRegistry(): TelegramDeliveryRuntimeRegistry {
|
|
const globals = globalThis as Record<string, unknown>;
|
|
const existing = globals[TELEGRAM_DELIVERY_RUNTIME_KEY];
|
|
if (existing && typeof existing === "object" && "runtime" in existing) {
|
|
return existing as TelegramDeliveryRuntimeRegistry;
|
|
}
|
|
const registry: TelegramDeliveryRuntimeRegistry = {};
|
|
globals[TELEGRAM_DELIVERY_RUNTIME_KEY] = registry;
|
|
return registry;
|
|
}
|
|
|
|
function failure<T>(
|
|
reason: TelegramDeliveryFailureReason,
|
|
message: string,
|
|
partial?: T,
|
|
): TelegramDeliveryResult<T> {
|
|
return partial === undefined
|
|
? { ok: false, reason, message }
|
|
: { ok: false, reason, message, partial };
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryTargetPolicyView {
|
|
canDeliver: boolean;
|
|
ownsDirect: boolean;
|
|
allowedChatId?: number;
|
|
followerTarget?: TelegramDeliveryTarget;
|
|
leaderTarget?: TelegramDeliveryTarget;
|
|
liveTargets?: readonly TelegramDeliveryTarget[];
|
|
}
|
|
|
|
/** @internal */
|
|
export interface TelegramDeliveryTargetPolicyRuntime {
|
|
getTargetPolicyView(): TelegramDeliveryTargetPolicyView;
|
|
getActiveTurnTarget(): TelegramDeliveryTarget | undefined;
|
|
}
|
|
|
|
/** @internal */
|
|
export function createTelegramDeliveryTargetPolicyRuntime(deps: {
|
|
ownsDirect(): boolean;
|
|
isFollowerRegistered(): boolean;
|
|
getAllowedChatId(): number | undefined;
|
|
getFollowerTarget(): TelegramDeliveryTarget | undefined;
|
|
getLeaderTarget(): TelegramDeliveryTarget | undefined;
|
|
listThreadRecords(): readonly { target: TelegramDeliveryTarget }[];
|
|
getActiveTurnTarget(): TelegramDeliveryTarget | undefined;
|
|
getActiveGuestQueryId(): string | undefined;
|
|
}): TelegramDeliveryTargetPolicyRuntime {
|
|
return {
|
|
getTargetPolicyView() {
|
|
const ownsDirect = deps.ownsDirect();
|
|
return {
|
|
canDeliver: ownsDirect || deps.isFollowerRegistered(),
|
|
ownsDirect,
|
|
allowedChatId: deps.getAllowedChatId(),
|
|
followerTarget: deps.getFollowerTarget(),
|
|
leaderTarget: deps.getLeaderTarget(),
|
|
liveTargets: deps.listThreadRecords().map((record) => record.target),
|
|
};
|
|
},
|
|
getActiveTurnTarget() {
|
|
return deps.getActiveGuestQueryId()
|
|
? undefined
|
|
: deps.getActiveTurnTarget();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** @internal */
|
|
export function resolveTelegramDeliveryInstanceTarget(
|
|
view: TelegramDeliveryTargetPolicyView,
|
|
): TelegramDeliveryTarget | undefined {
|
|
if (!view.canDeliver) return undefined;
|
|
return (
|
|
view.followerTarget ??
|
|
view.leaderTarget ??
|
|
(view.allowedChatId === undefined
|
|
? undefined
|
|
: { chatId: view.allowedChatId })
|
|
);
|
|
}
|
|
|
|
/** @internal */
|
|
export function resolveTelegramDeliveryAggregateTarget(
|
|
view: TelegramDeliveryTargetPolicyView,
|
|
): TelegramDeliveryTarget | undefined {
|
|
return !view.canDeliver || view.allowedChatId === undefined
|
|
? undefined
|
|
: { chatId: view.allowedChatId };
|
|
}
|
|
|
|
/** @internal */
|
|
export function isTelegramDeliveryExplicitTargetAuthorized(
|
|
candidate: TelegramDeliveryTarget,
|
|
view: TelegramDeliveryTargetPolicyView,
|
|
): boolean {
|
|
if (
|
|
!view.canDeliver ||
|
|
view.allowedChatId === undefined ||
|
|
candidate.chatId !== view.allowedChatId
|
|
) {
|
|
return false;
|
|
}
|
|
if (candidate.threadId === undefined) return true;
|
|
if (view.followerTarget) {
|
|
return areDeliveryTargetsEqual(candidate, view.followerTarget);
|
|
}
|
|
if (!view.ownsDirect) return false;
|
|
if (
|
|
view.leaderTarget &&
|
|
areDeliveryTargetsEqual(candidate, view.leaderTarget)
|
|
) {
|
|
return true;
|
|
}
|
|
return (view.liveTargets ?? []).some(function (target) {
|
|
return areDeliveryTargetsEqual(candidate, target);
|
|
});
|
|
}
|
|
|
|
function areDeliveryTargetsEqual(
|
|
left: TelegramDeliveryTarget,
|
|
right: TelegramDeliveryTarget,
|
|
): boolean {
|
|
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
}
|
|
|
|
function cloneTarget(target: TelegramDeliveryTarget): TelegramDeliveryTarget {
|
|
return target.threadId === undefined
|
|
? { chatId: target.chatId }
|
|
: { chatId: target.chatId, threadId: target.threadId };
|
|
}
|
|
|
|
function targetKey(target: TelegramDeliveryTarget): string {
|
|
return `${target.chatId}:${target.threadId ?? "root"}`;
|
|
}
|
|
|
|
function resolveTelegramDeliveryTarget(
|
|
scope: TelegramDeliveryScope,
|
|
deps: TelegramDeliveryTargetResolverDeps,
|
|
): TelegramDeliveryResult<TelegramDeliveryTarget> {
|
|
if (scope.kind === "target") {
|
|
if (!deps.isExplicitTargetAuthorized(scope.target)) {
|
|
return failure(
|
|
"target-unauthorized",
|
|
"Telegram delivery target is not authorized for this runtime.",
|
|
);
|
|
}
|
|
return { ok: true, value: cloneTarget(scope.target) };
|
|
}
|
|
const target =
|
|
scope.kind === "active-turn"
|
|
? deps.getActiveTurnTarget()
|
|
: scope.kind === "instance"
|
|
? deps.getInstanceTarget()
|
|
: deps.getAggregateTarget();
|
|
if (!target) {
|
|
return failure(
|
|
"target-unavailable",
|
|
`Telegram delivery ${scope.kind} target is unavailable.`,
|
|
);
|
|
}
|
|
return { ok: true, value: cloneTarget(target) };
|
|
}
|
|
|
|
function createTelegramDeliveryTargetQueue() {
|
|
const queues = new Map<string, Promise<void>>();
|
|
return async function run<T>(
|
|
target: TelegramDeliveryTarget,
|
|
operation: () => Promise<T>,
|
|
): Promise<T> {
|
|
const key = targetKey(target);
|
|
const previous = queues.get(key) ?? Promise.resolve();
|
|
const current = previous.then(operation, operation);
|
|
const settled = current.then(
|
|
() => {},
|
|
() => {},
|
|
);
|
|
queues.set(key, settled);
|
|
try {
|
|
return await current;
|
|
} finally {
|
|
if (queues.get(key) === settled) queues.delete(key);
|
|
}
|
|
};
|
|
}
|
|
|
|
function getChunkTransportOptions(
|
|
view: TelegramDeliveryView,
|
|
index: number,
|
|
chunkCount: number,
|
|
replyToMessageId?: number,
|
|
editing = false,
|
|
): TelegramDeliveryTransportOptions {
|
|
const isFirst = index === 0;
|
|
const isLast = index === chunkCount - 1;
|
|
return {
|
|
...(isFirst && replyToMessageId !== undefined ? { replyToMessageId } : {}),
|
|
...(isLast
|
|
? { replyMarkup: view.replyMarkup ?? (editing ? null : undefined) }
|
|
: editing
|
|
? { replyMarkup: null }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
/** @internal */
|
|
export function createTelegramDeliveryRuntime(
|
|
deps: TelegramDeliveryRuntimeDeps,
|
|
): TelegramDeliveryRuntime {
|
|
let active = true;
|
|
const handleBindings = new WeakMap<
|
|
TelegramDeliveryHandle,
|
|
{ target: TelegramDeliveryTarget; messageIds: readonly number[] }
|
|
>();
|
|
const runForTarget = createTelegramDeliveryTargetQueue();
|
|
const render = (
|
|
view: TelegramDeliveryView,
|
|
): TelegramDeliveryResult<readonly TelegramDeliveryRenderedChunk[]> => {
|
|
const chunks = deps.renderView(view);
|
|
if (
|
|
chunks.length === 0 ||
|
|
chunks.some(
|
|
(chunk) =>
|
|
typeof chunk.text !== "string" || chunk.text.trim().length === 0,
|
|
)
|
|
) {
|
|
return failure(
|
|
"invalid-view",
|
|
"Telegram delivery view rendered no content.",
|
|
);
|
|
}
|
|
return { ok: true, value: chunks };
|
|
};
|
|
const inactive = <T>(): TelegramDeliveryResult<T> =>
|
|
failure(
|
|
"runtime-unavailable",
|
|
"Telegram delivery runtime generation is inactive.",
|
|
);
|
|
const transportFailure = <T>(
|
|
operation: "send" | "edit" | "delete" | "chat-action",
|
|
error: unknown,
|
|
target?: TelegramDeliveryTarget,
|
|
partial?: T,
|
|
): TelegramDeliveryResult<T> => {
|
|
deps.recordFailure?.(operation, error, target);
|
|
if (error instanceof TelegramDeliveryTransportGenerationError) {
|
|
return inactive();
|
|
}
|
|
return failure(
|
|
isTelegramApiCommitUnknownError(error)
|
|
? "commit-unknown"
|
|
: "transport-failed",
|
|
isTelegramApiCommitUnknownError(error)
|
|
? `Telegram delivery ${operation} may have committed before transport failed.`
|
|
: `Telegram delivery ${operation} failed.`,
|
|
partial,
|
|
);
|
|
};
|
|
const createHandle = (
|
|
target: TelegramDeliveryTarget,
|
|
messageIds: readonly number[],
|
|
): TelegramDeliveryHandle => {
|
|
const canonicalTarget = Object.freeze(cloneTarget(target));
|
|
const canonicalMessageIds = Object.freeze([...messageIds]);
|
|
const handle = Object.freeze({
|
|
target: canonicalTarget,
|
|
messageIds: canonicalMessageIds,
|
|
generation: deps.generation,
|
|
});
|
|
handleBindings.set(handle, {
|
|
target: canonicalTarget,
|
|
messageIds: canonicalMessageIds,
|
|
});
|
|
return handle;
|
|
};
|
|
const resolveHandle = (
|
|
handle: TelegramDeliveryHandle,
|
|
): TelegramDeliveryResult<{
|
|
target: TelegramDeliveryTarget;
|
|
messageIds: readonly number[];
|
|
}> => {
|
|
if (!active) return inactive();
|
|
const binding = handleBindings.get(handle);
|
|
if (!binding || handle.generation !== deps.generation) {
|
|
return failure(
|
|
"stale-handle",
|
|
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
);
|
|
}
|
|
const authorized = resolveTelegramDeliveryTarget(
|
|
{ kind: "target", target: binding.target },
|
|
deps,
|
|
);
|
|
if (!authorized.ok) {
|
|
return failure(authorized.reason, authorized.message);
|
|
}
|
|
return { ok: true, value: binding };
|
|
};
|
|
return {
|
|
generation: deps.generation,
|
|
shutdown() {
|
|
active = false;
|
|
},
|
|
async sendView(view, options) {
|
|
if (!active) return inactive();
|
|
const resolved = resolveTelegramDeliveryTarget(options.scope, deps);
|
|
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
const rendered = render(view);
|
|
if (!rendered.ok) return failure(rendered.reason, rendered.message);
|
|
const target = resolved.value;
|
|
return runForTarget(target, async () => {
|
|
if (!active) return inactive();
|
|
const messageIds: number[] = [];
|
|
try {
|
|
for (const [index, chunk] of rendered.value.entries()) {
|
|
if (!active) return inactive();
|
|
const messageId = await deps.sendChunk(
|
|
target,
|
|
chunk,
|
|
getChunkTransportOptions(
|
|
view,
|
|
index,
|
|
rendered.value.length,
|
|
options.replyToMessageId,
|
|
),
|
|
);
|
|
messageIds.push(messageId);
|
|
if (!active) return inactive();
|
|
}
|
|
return { ok: true, value: createHandle(target, messageIds) };
|
|
} catch (error) {
|
|
return active
|
|
? transportFailure(
|
|
"send",
|
|
error,
|
|
target,
|
|
messageIds.length > 0
|
|
? createHandle(target, messageIds)
|
|
: undefined,
|
|
)
|
|
: inactive();
|
|
}
|
|
});
|
|
},
|
|
async editView(handle, view) {
|
|
const resolved = resolveHandle(handle);
|
|
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
const rendered = render(view);
|
|
if (!rendered.ok) return failure(rendered.reason, rendered.message);
|
|
const target = resolved.value.target;
|
|
return runForTarget(target, async () => {
|
|
if (!active) return inactive();
|
|
const visibleMessageIds = [...resolved.value.messageIds];
|
|
try {
|
|
const sharedCount = Math.min(
|
|
visibleMessageIds.length,
|
|
rendered.value.length,
|
|
);
|
|
for (let index = 0; index < sharedCount; index += 1) {
|
|
if (!active) return inactive();
|
|
await deps.editChunk(
|
|
target,
|
|
visibleMessageIds[index]!,
|
|
rendered.value[index]!,
|
|
getChunkTransportOptions(
|
|
view,
|
|
index,
|
|
rendered.value.length,
|
|
undefined,
|
|
true,
|
|
),
|
|
);
|
|
if (!active) return inactive();
|
|
}
|
|
for (
|
|
let index = sharedCount;
|
|
index < rendered.value.length;
|
|
index += 1
|
|
) {
|
|
if (!active) return inactive();
|
|
visibleMessageIds.push(
|
|
await deps.sendChunk(
|
|
target,
|
|
rendered.value[index]!,
|
|
getChunkTransportOptions(view, index, rendered.value.length),
|
|
),
|
|
);
|
|
if (!active) return inactive();
|
|
}
|
|
const removedMessageIds = visibleMessageIds.slice(
|
|
rendered.value.length,
|
|
);
|
|
for (const messageId of removedMessageIds) {
|
|
if (!active) return inactive();
|
|
await deps.deleteMessage(target, messageId);
|
|
visibleMessageIds.splice(visibleMessageIds.indexOf(messageId), 1);
|
|
if (!active) return inactive();
|
|
}
|
|
return {
|
|
ok: true,
|
|
value: createHandle(target, visibleMessageIds),
|
|
};
|
|
} catch (error) {
|
|
return active
|
|
? transportFailure(
|
|
"edit",
|
|
error,
|
|
target,
|
|
createHandle(target, visibleMessageIds),
|
|
)
|
|
: inactive();
|
|
}
|
|
});
|
|
},
|
|
async deleteView(handle) {
|
|
const resolved = resolveHandle(handle);
|
|
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
const target = resolved.value.target;
|
|
return runForTarget(target, async () => {
|
|
if (!active) return inactive();
|
|
try {
|
|
for (const messageId of resolved.value.messageIds) {
|
|
if (!active) return inactive();
|
|
await deps.deleteMessage(target, messageId);
|
|
if (!active) return inactive();
|
|
}
|
|
return { ok: true, value: undefined };
|
|
} catch (error) {
|
|
return active
|
|
? transportFailure("delete", error, target)
|
|
: inactive();
|
|
}
|
|
});
|
|
},
|
|
async sendChatAction(action, scope) {
|
|
if (!active) return inactive();
|
|
const resolved = resolveTelegramDeliveryTarget(scope, deps);
|
|
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
const target = resolved.value;
|
|
return runForTarget(target, async () => {
|
|
if (!active) return inactive();
|
|
try {
|
|
await deps.sendChatAction(target, action);
|
|
if (!active) return inactive();
|
|
return { ok: true, value: undefined };
|
|
} catch (error) {
|
|
return active
|
|
? transportFailure("chat-action", error, target)
|
|
: inactive();
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/** @internal */
|
|
export function createTelegramBridgeDeliveryRuntime(
|
|
deps: TelegramBridgeDeliveryRuntimeDeps,
|
|
): TelegramDeliveryRuntime {
|
|
const getPolicyView = deps.getTargetPolicyView;
|
|
const assertTransportActive = (): void => {
|
|
if (deps.isTransportActive?.() === false) {
|
|
throw new TelegramDeliveryTransportGenerationError();
|
|
}
|
|
};
|
|
return createTelegramDeliveryRuntime({
|
|
generation: deps.generation,
|
|
getActiveTurnTarget: deps.getActiveTurnTarget,
|
|
getInstanceTarget() {
|
|
return resolveTelegramDeliveryInstanceTarget(getPolicyView());
|
|
},
|
|
getAggregateTarget() {
|
|
return resolveTelegramDeliveryAggregateTarget(getPolicyView());
|
|
},
|
|
isExplicitTargetAuthorized(target) {
|
|
return isTelegramDeliveryExplicitTargetAuthorized(
|
|
target,
|
|
getPolicyView(),
|
|
);
|
|
},
|
|
renderView(view) {
|
|
assertTelegramInlineKeyboardCallbackData(view.replyMarkup);
|
|
return renderTelegramMessage(view.text, {
|
|
mode: view.parseMode ?? "plain",
|
|
}).map(function (chunk) {
|
|
return {
|
|
text: chunk.text,
|
|
parseMode: chunk.parseMode === "HTML" ? "html" : "plain",
|
|
} as const;
|
|
});
|
|
},
|
|
async sendChunk(target, chunk, options) {
|
|
assertTransportActive();
|
|
const replyParameters = buildTelegramReplyParameters(
|
|
target.chatId,
|
|
options.replyToMessageId,
|
|
target,
|
|
);
|
|
const body = {
|
|
chat_id: target.chatId,
|
|
text: chunk.text,
|
|
...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
|
|
...getTelegramTargetThreadParams(target),
|
|
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
|
};
|
|
const sent = await deps.api.sendMessage(
|
|
target.threadId === undefined
|
|
? markTelegramBusAggregateDelivery(body)
|
|
: body,
|
|
);
|
|
assertTransportActive();
|
|
deps.recordOwnership({
|
|
chatId: target.chatId,
|
|
messageId: sent.message_id,
|
|
target,
|
|
});
|
|
return sent.message_id;
|
|
},
|
|
async editChunk(target, messageId, chunk, options) {
|
|
assertTransportActive();
|
|
await deps.api.editMessageText({
|
|
chat_id: target.chatId,
|
|
message_id: messageId,
|
|
text: chunk.text,
|
|
...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
|
|
reply_markup:
|
|
options.replyMarkup === null
|
|
? { inline_keyboard: [] }
|
|
: options.replyMarkup,
|
|
});
|
|
},
|
|
deleteMessage(target, messageId) {
|
|
assertTransportActive();
|
|
return deps.api.deleteMessage(target.chatId, messageId);
|
|
},
|
|
async sendChatAction(target, action) {
|
|
assertTransportActive();
|
|
await deps.api.sendChatAction(target.chatId, action, {
|
|
message_thread_id: target.threadId,
|
|
});
|
|
},
|
|
recordFailure: deps.recordFailure,
|
|
});
|
|
}
|
|
|
|
function getBoundTelegramDeliveryRuntime():
|
|
TelegramDeliveryRuntime | TelegramDeliveryResult<never> {
|
|
const runtime = getTelegramDeliveryRuntimeRegistry().runtime;
|
|
return (
|
|
runtime ??
|
|
failure(
|
|
"runtime-unavailable",
|
|
"Telegram delivery runtime is unavailable in this Pi session.",
|
|
)
|
|
);
|
|
}
|
|
|
|
function isFailure(
|
|
value: TelegramDeliveryRuntime | TelegramDeliveryResult<never>,
|
|
): value is TelegramDeliveryResult<never> {
|
|
return "ok" in value;
|
|
}
|
|
|
|
function validateView<T>(
|
|
view: TelegramDeliveryView,
|
|
): TelegramDeliveryResult<T> | undefined {
|
|
if (typeof view.text !== "string" || view.text.trim().length === 0) {
|
|
return failure("invalid-view", "Telegram delivery view text is empty.");
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
async function runDeliveryOperation<T>(
|
|
operation: (
|
|
runtime: TelegramDeliveryRuntime,
|
|
) => Promise<TelegramDeliveryResult<T>>,
|
|
): Promise<TelegramDeliveryResult<T>> {
|
|
const runtime = getBoundTelegramDeliveryRuntime();
|
|
if (isFailure(runtime)) return runtime;
|
|
try {
|
|
return await operation(runtime);
|
|
} catch {
|
|
return failure("transport-failed", "Telegram delivery failed.");
|
|
}
|
|
}
|
|
|
|
/** @internal */
|
|
export function bindTelegramDeliveryRuntime(
|
|
runtime: TelegramDeliveryRuntime,
|
|
): () => void {
|
|
const registry = getTelegramDeliveryRuntimeRegistry();
|
|
if (registry.runtime !== runtime) registry.runtime?.shutdown();
|
|
registry.runtime = runtime;
|
|
return () => {
|
|
if (registry.runtime === runtime) registry.runtime = undefined;
|
|
};
|
|
}
|
|
|
|
/** @internal */
|
|
export function clearTelegramDeliveryRuntime(): void {
|
|
const registry = getTelegramDeliveryRuntimeRegistry();
|
|
registry.runtime?.shutdown();
|
|
registry.runtime = undefined;
|
|
}
|
|
|
|
export async function sendTelegramView(
|
|
view: TelegramDeliveryView,
|
|
options: SendTelegramViewOptions,
|
|
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>> {
|
|
const invalid = validateView<TelegramDeliveryHandle>(view);
|
|
if (invalid) return invalid;
|
|
return runDeliveryOperation((runtime) => runtime.sendView(view, options));
|
|
}
|
|
|
|
export async function editTelegramView(
|
|
handle: TelegramDeliveryHandle,
|
|
view: TelegramDeliveryView,
|
|
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>> {
|
|
const invalid = validateView<TelegramDeliveryHandle>(view);
|
|
if (invalid) return invalid;
|
|
return runDeliveryOperation((runtime) => {
|
|
if (runtime.generation !== handle.generation) {
|
|
return Promise.resolve(
|
|
failure(
|
|
"stale-handle",
|
|
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
),
|
|
);
|
|
}
|
|
return runtime.editView(handle, view);
|
|
});
|
|
}
|
|
|
|
export async function deleteTelegramView(
|
|
handle: TelegramDeliveryHandle,
|
|
): Promise<TelegramDeliveryResult<void>> {
|
|
return runDeliveryOperation((runtime) => {
|
|
if (runtime.generation !== handle.generation) {
|
|
return Promise.resolve(
|
|
failure(
|
|
"stale-handle",
|
|
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
),
|
|
);
|
|
}
|
|
return runtime.deleteView(handle);
|
|
});
|
|
}
|
|
|
|
export async function sendTelegramChatAction(
|
|
action: TelegramDeliveryChatAction,
|
|
options: { scope: TelegramDeliveryScope },
|
|
): Promise<TelegramDeliveryResult<void>> {
|
|
return runDeliveryOperation((runtime) =>
|
|
runtime.sendChatAction(action, options.scope),
|
|
);
|
|
}
|