@llblab/pi-telegram 0.20.6 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -5
- package/BACKLOG.md +3 -45
- package/CHANGELOG.md +16 -0
- package/README.md +7 -1
- package/api/activity.ts +15 -0
- package/api/commands.ts +1 -1
- package/api/delivery.ts +21 -0
- package/api/inbound.ts +1 -1
- package/api/keyboard.ts +1 -1
- package/api/outbound.ts +1 -1
- package/api/sections.ts +1 -1
- package/api/status.ts +2 -2
- package/api/updates.ts +1 -1
- package/api/voice.ts +1 -1
- package/docs/README.md +2 -0
- package/docs/activity.md +294 -0
- package/docs/architecture.md +7 -1
- package/docs/delivery.md +224 -0
- package/docs/public-api.md +92 -1
- package/docs/sections.md +6 -5
- package/index.ts +53 -0
- package/lib/activity.ts +683 -0
- package/lib/bindings.ts +60 -8
- package/lib/bus-api.ts +2 -1
- package/lib/bus-leader.ts +4 -1
- package/lib/bus.ts +38 -0
- package/lib/delivery.ts +800 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/model.ts +3 -1
- package/lib/pi.ts +21 -5
- package/lib/routing.ts +18 -3
- package/lib/sections.ts +1 -1
- package/lib/status.ts +2 -2
- package/lib/updates.ts +4 -4
- package/package.json +6 -4
package/lib/delivery.ts
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram target-aware operational delivery and logical message lifecycle
|
|
3
|
+
* Zones: telegram delivery, extension API, runtime binding
|
|
4
|
+
* 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
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { markTelegramBusAggregateDelivery } from "./bus.ts";
|
|
8
|
+
import {
|
|
9
|
+
assertTelegramInlineKeyboardCallbackData,
|
|
10
|
+
type TelegramInlineKeyboardMarkup,
|
|
11
|
+
} from "./keyboard.ts";
|
|
12
|
+
import {
|
|
13
|
+
buildTelegramReplyParameters,
|
|
14
|
+
renderTelegramMessage,
|
|
15
|
+
} from "./replies.ts";
|
|
16
|
+
import {
|
|
17
|
+
getTelegramTargetThreadParams,
|
|
18
|
+
type TelegramTarget,
|
|
19
|
+
} from "./target.ts";
|
|
20
|
+
import type { TelegramBridgeApiRuntime } from "./telegram-api.ts";
|
|
21
|
+
|
|
22
|
+
const TELEGRAM_DELIVERY_RUNTIME_KEY = "__piTelegramDeliveryRuntime__";
|
|
23
|
+
|
|
24
|
+
export type TelegramDeliveryParseMode = "plain" | "html" | "markdown";
|
|
25
|
+
|
|
26
|
+
export interface TelegramDeliveryView {
|
|
27
|
+
text: string;
|
|
28
|
+
parseMode?: TelegramDeliveryParseMode;
|
|
29
|
+
replyMarkup?: TelegramInlineKeyboardMarkup;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type TelegramDeliveryTarget = TelegramTarget;
|
|
33
|
+
|
|
34
|
+
export type TelegramDeliveryScope =
|
|
35
|
+
| { kind: "active-turn" }
|
|
36
|
+
| { kind: "instance" }
|
|
37
|
+
| { kind: "aggregate" }
|
|
38
|
+
| { kind: "target"; target: TelegramDeliveryTarget };
|
|
39
|
+
|
|
40
|
+
export interface TelegramDeliveryHandle {
|
|
41
|
+
readonly target: TelegramDeliveryTarget;
|
|
42
|
+
readonly messageIds: readonly number[];
|
|
43
|
+
readonly generation: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TelegramDeliveryFailureReason =
|
|
47
|
+
| "runtime-unavailable"
|
|
48
|
+
| "target-unavailable"
|
|
49
|
+
| "target-unauthorized"
|
|
50
|
+
| "stale-handle"
|
|
51
|
+
| "invalid-view"
|
|
52
|
+
| "transport-failed";
|
|
53
|
+
|
|
54
|
+
export type TelegramDeliveryResult<T> =
|
|
55
|
+
| { ok: true; value: T }
|
|
56
|
+
| {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: TelegramDeliveryFailureReason;
|
|
59
|
+
message: string;
|
|
60
|
+
/** Successfully materialized state that callers may edit or delete to recover. */
|
|
61
|
+
partial?: T;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface SendTelegramViewOptions {
|
|
65
|
+
scope: TelegramDeliveryScope;
|
|
66
|
+
replyToMessageId?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type TelegramDeliveryChatAction =
|
|
70
|
+
| "typing"
|
|
71
|
+
| "upload_document"
|
|
72
|
+
| "upload_photo"
|
|
73
|
+
| "record_voice";
|
|
74
|
+
|
|
75
|
+
/** @internal */
|
|
76
|
+
export interface TelegramDeliveryRuntime {
|
|
77
|
+
readonly generation: string;
|
|
78
|
+
shutdown: () => void;
|
|
79
|
+
sendView: (
|
|
80
|
+
view: TelegramDeliveryView,
|
|
81
|
+
options: SendTelegramViewOptions,
|
|
82
|
+
) => Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
83
|
+
editView: (
|
|
84
|
+
handle: TelegramDeliveryHandle,
|
|
85
|
+
view: TelegramDeliveryView,
|
|
86
|
+
) => Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
|
|
87
|
+
deleteView: (
|
|
88
|
+
handle: TelegramDeliveryHandle,
|
|
89
|
+
) => Promise<TelegramDeliveryResult<void>>;
|
|
90
|
+
sendChatAction: (
|
|
91
|
+
action: TelegramDeliveryChatAction,
|
|
92
|
+
scope: TelegramDeliveryScope,
|
|
93
|
+
) => Promise<TelegramDeliveryResult<void>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @internal */
|
|
97
|
+
export interface TelegramDeliveryTargetResolverDeps {
|
|
98
|
+
getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
|
|
99
|
+
getInstanceTarget: () => TelegramDeliveryTarget | undefined;
|
|
100
|
+
getAggregateTarget: () => TelegramDeliveryTarget | undefined;
|
|
101
|
+
isExplicitTargetAuthorized: (target: TelegramDeliveryTarget) => boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @internal */
|
|
105
|
+
export interface TelegramDeliveryRenderedChunk {
|
|
106
|
+
text: string;
|
|
107
|
+
parseMode: TelegramDeliveryParseMode;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @internal */
|
|
111
|
+
export interface TelegramDeliveryTransportOptions {
|
|
112
|
+
replyToMessageId?: number;
|
|
113
|
+
replyMarkup?: TelegramInlineKeyboardMarkup | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @internal */
|
|
117
|
+
export interface TelegramDeliveryRuntimeDeps
|
|
118
|
+
extends TelegramDeliveryTargetResolverDeps {
|
|
119
|
+
generation: string;
|
|
120
|
+
renderView: (
|
|
121
|
+
view: TelegramDeliveryView,
|
|
122
|
+
) => readonly TelegramDeliveryRenderedChunk[];
|
|
123
|
+
sendChunk: (
|
|
124
|
+
target: TelegramDeliveryTarget,
|
|
125
|
+
chunk: TelegramDeliveryRenderedChunk,
|
|
126
|
+
options: TelegramDeliveryTransportOptions,
|
|
127
|
+
) => Promise<number>;
|
|
128
|
+
editChunk: (
|
|
129
|
+
target: TelegramDeliveryTarget,
|
|
130
|
+
messageId: number,
|
|
131
|
+
chunk: TelegramDeliveryRenderedChunk,
|
|
132
|
+
options: TelegramDeliveryTransportOptions,
|
|
133
|
+
) => Promise<void>;
|
|
134
|
+
deleteMessage: (
|
|
135
|
+
target: TelegramDeliveryTarget,
|
|
136
|
+
messageId: number,
|
|
137
|
+
) => Promise<void>;
|
|
138
|
+
sendChatAction: (
|
|
139
|
+
target: TelegramDeliveryTarget,
|
|
140
|
+
action: TelegramDeliveryChatAction,
|
|
141
|
+
) => Promise<void>;
|
|
142
|
+
recordFailure?: (
|
|
143
|
+
operation: "send" | "edit" | "delete" | "chat-action",
|
|
144
|
+
error: unknown,
|
|
145
|
+
target?: TelegramDeliveryTarget,
|
|
146
|
+
) => void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @internal */
|
|
150
|
+
export interface TelegramBridgeDeliveryRuntimeDeps {
|
|
151
|
+
generation: string;
|
|
152
|
+
getTargetPolicyView: () => TelegramDeliveryTargetPolicyView;
|
|
153
|
+
getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
|
|
154
|
+
api: Pick<
|
|
155
|
+
TelegramBridgeApiRuntime,
|
|
156
|
+
"sendMessage" | "editMessageText" | "deleteMessage" | "sendChatAction"
|
|
157
|
+
>;
|
|
158
|
+
recordOwnership: (input: {
|
|
159
|
+
chatId: number;
|
|
160
|
+
messageId: number;
|
|
161
|
+
target: TelegramDeliveryTarget;
|
|
162
|
+
}) => void;
|
|
163
|
+
recordFailure?: TelegramDeliveryRuntimeDeps["recordFailure"];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @internal */
|
|
167
|
+
export function createTelegramDeliveryLifecycleHooks(
|
|
168
|
+
createRuntime: () => TelegramDeliveryRuntime,
|
|
169
|
+
): {
|
|
170
|
+
onSessionStart: () => Promise<void>;
|
|
171
|
+
onSessionShutdown: () => Promise<void>;
|
|
172
|
+
} {
|
|
173
|
+
let runtime: TelegramDeliveryRuntime | undefined;
|
|
174
|
+
let unbind: ReturnType<typeof bindTelegramDeliveryRuntime> | undefined;
|
|
175
|
+
const stopCurrentRuntime = () => {
|
|
176
|
+
unbind?.();
|
|
177
|
+
unbind = undefined;
|
|
178
|
+
runtime?.shutdown();
|
|
179
|
+
runtime = undefined;
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
onSessionStart: async () => {
|
|
183
|
+
stopCurrentRuntime();
|
|
184
|
+
runtime = createRuntime();
|
|
185
|
+
unbind = bindTelegramDeliveryRuntime(runtime);
|
|
186
|
+
},
|
|
187
|
+
onSessionShutdown: async () => {
|
|
188
|
+
stopCurrentRuntime();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @internal */
|
|
194
|
+
export function createTelegramBridgeDeliveryLifecycleHooks(
|
|
195
|
+
deps: Omit<TelegramBridgeDeliveryRuntimeDeps, "generation"> & {
|
|
196
|
+
generationSeed: string;
|
|
197
|
+
},
|
|
198
|
+
): ReturnType<typeof createTelegramDeliveryLifecycleHooks> {
|
|
199
|
+
let generationSequence = 0;
|
|
200
|
+
return createTelegramDeliveryLifecycleHooks(() =>
|
|
201
|
+
createTelegramBridgeDeliveryRuntime({
|
|
202
|
+
...deps,
|
|
203
|
+
generation: `${deps.generationSeed}:${++generationSequence}`,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface TelegramDeliveryRuntimeRegistry {
|
|
209
|
+
runtime?: TelegramDeliveryRuntime;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getTelegramDeliveryRuntimeRegistry(): TelegramDeliveryRuntimeRegistry {
|
|
213
|
+
const globals = globalThis as Record<string, unknown>;
|
|
214
|
+
const existing = globals[TELEGRAM_DELIVERY_RUNTIME_KEY];
|
|
215
|
+
if (
|
|
216
|
+
existing &&
|
|
217
|
+
typeof existing === "object" &&
|
|
218
|
+
"runtime" in existing
|
|
219
|
+
) {
|
|
220
|
+
return existing as TelegramDeliveryRuntimeRegistry;
|
|
221
|
+
}
|
|
222
|
+
const registry: TelegramDeliveryRuntimeRegistry = {};
|
|
223
|
+
globals[TELEGRAM_DELIVERY_RUNTIME_KEY] = registry;
|
|
224
|
+
return registry;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function failure<T>(
|
|
228
|
+
reason: TelegramDeliveryFailureReason,
|
|
229
|
+
message: string,
|
|
230
|
+
partial?: T,
|
|
231
|
+
): TelegramDeliveryResult<T> {
|
|
232
|
+
return partial === undefined
|
|
233
|
+
? { ok: false, reason, message }
|
|
234
|
+
: { ok: false, reason, message, partial };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @internal */
|
|
238
|
+
export interface TelegramDeliveryTargetPolicyView {
|
|
239
|
+
canDeliver: boolean;
|
|
240
|
+
ownsDirect: boolean;
|
|
241
|
+
allowedChatId?: number;
|
|
242
|
+
followerTarget?: TelegramDeliveryTarget;
|
|
243
|
+
leaderTarget?: TelegramDeliveryTarget;
|
|
244
|
+
liveTargets?: readonly TelegramDeliveryTarget[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** @internal */
|
|
248
|
+
export function resolveTelegramDeliveryInstanceTarget(
|
|
249
|
+
view: TelegramDeliveryTargetPolicyView,
|
|
250
|
+
): TelegramDeliveryTarget | undefined {
|
|
251
|
+
if (!view.canDeliver) return undefined;
|
|
252
|
+
return (
|
|
253
|
+
view.followerTarget ??
|
|
254
|
+
view.leaderTarget ??
|
|
255
|
+
(view.allowedChatId === undefined ? undefined : { chatId: view.allowedChatId })
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @internal */
|
|
260
|
+
export function resolveTelegramDeliveryAggregateTarget(
|
|
261
|
+
view: TelegramDeliveryTargetPolicyView,
|
|
262
|
+
): TelegramDeliveryTarget | undefined {
|
|
263
|
+
return !view.canDeliver || view.allowedChatId === undefined
|
|
264
|
+
? undefined
|
|
265
|
+
: { chatId: view.allowedChatId };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** @internal */
|
|
269
|
+
export function isTelegramDeliveryExplicitTargetAuthorized(
|
|
270
|
+
candidate: TelegramDeliveryTarget,
|
|
271
|
+
view: TelegramDeliveryTargetPolicyView,
|
|
272
|
+
): boolean {
|
|
273
|
+
if (
|
|
274
|
+
!view.canDeliver ||
|
|
275
|
+
view.allowedChatId === undefined ||
|
|
276
|
+
candidate.chatId !== view.allowedChatId
|
|
277
|
+
) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (candidate.threadId === undefined) return true;
|
|
281
|
+
if (view.followerTarget) {
|
|
282
|
+
return areDeliveryTargetsEqual(candidate, view.followerTarget);
|
|
283
|
+
}
|
|
284
|
+
if (!view.ownsDirect) return false;
|
|
285
|
+
if (
|
|
286
|
+
view.leaderTarget &&
|
|
287
|
+
areDeliveryTargetsEqual(candidate, view.leaderTarget)
|
|
288
|
+
) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
return (view.liveTargets ?? []).some(function (target) {
|
|
292
|
+
return areDeliveryTargetsEqual(candidate, target);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function areDeliveryTargetsEqual(
|
|
297
|
+
left: TelegramDeliveryTarget,
|
|
298
|
+
right: TelegramDeliveryTarget,
|
|
299
|
+
): boolean {
|
|
300
|
+
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function cloneTarget(target: TelegramDeliveryTarget): TelegramDeliveryTarget {
|
|
304
|
+
return target.threadId === undefined
|
|
305
|
+
? { chatId: target.chatId }
|
|
306
|
+
: { chatId: target.chatId, threadId: target.threadId };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function targetKey(target: TelegramDeliveryTarget): string {
|
|
310
|
+
return `${target.chatId}:${target.threadId ?? "root"}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveTelegramDeliveryTarget(
|
|
314
|
+
scope: TelegramDeliveryScope,
|
|
315
|
+
deps: TelegramDeliveryTargetResolverDeps,
|
|
316
|
+
): TelegramDeliveryResult<TelegramDeliveryTarget> {
|
|
317
|
+
if (scope.kind === "target") {
|
|
318
|
+
if (!deps.isExplicitTargetAuthorized(scope.target)) {
|
|
319
|
+
return failure(
|
|
320
|
+
"target-unauthorized",
|
|
321
|
+
"Telegram delivery target is not authorized for this runtime.",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return { ok: true, value: cloneTarget(scope.target) };
|
|
325
|
+
}
|
|
326
|
+
const target =
|
|
327
|
+
scope.kind === "active-turn"
|
|
328
|
+
? deps.getActiveTurnTarget()
|
|
329
|
+
: scope.kind === "instance"
|
|
330
|
+
? deps.getInstanceTarget()
|
|
331
|
+
: deps.getAggregateTarget();
|
|
332
|
+
if (!target) {
|
|
333
|
+
return failure(
|
|
334
|
+
"target-unavailable",
|
|
335
|
+
`Telegram delivery ${scope.kind} target is unavailable.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return { ok: true, value: cloneTarget(target) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isValidHandleForRuntime(
|
|
342
|
+
handle: TelegramDeliveryHandle,
|
|
343
|
+
generation: string,
|
|
344
|
+
): boolean {
|
|
345
|
+
return (
|
|
346
|
+
handle.generation === generation &&
|
|
347
|
+
handle.messageIds.length > 0 &&
|
|
348
|
+
handle.messageIds.every(
|
|
349
|
+
(messageId) => Number.isInteger(messageId) && messageId > 0,
|
|
350
|
+
)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function createTelegramDeliveryTargetQueue() {
|
|
355
|
+
const queues = new Map<string, Promise<void>>();
|
|
356
|
+
return async function run<T>(
|
|
357
|
+
target: TelegramDeliveryTarget,
|
|
358
|
+
operation: () => Promise<T>,
|
|
359
|
+
): Promise<T> {
|
|
360
|
+
const key = targetKey(target);
|
|
361
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
362
|
+
const current = previous.then(operation, operation);
|
|
363
|
+
const settled = current.then(
|
|
364
|
+
() => {},
|
|
365
|
+
() => {},
|
|
366
|
+
);
|
|
367
|
+
queues.set(key, settled);
|
|
368
|
+
try {
|
|
369
|
+
return await current;
|
|
370
|
+
} finally {
|
|
371
|
+
if (queues.get(key) === settled) queues.delete(key);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getChunkTransportOptions(
|
|
377
|
+
view: TelegramDeliveryView,
|
|
378
|
+
index: number,
|
|
379
|
+
chunkCount: number,
|
|
380
|
+
replyToMessageId?: number,
|
|
381
|
+
editing = false,
|
|
382
|
+
): TelegramDeliveryTransportOptions {
|
|
383
|
+
const isFirst = index === 0;
|
|
384
|
+
const isLast = index === chunkCount - 1;
|
|
385
|
+
return {
|
|
386
|
+
...(isFirst && replyToMessageId !== undefined ? { replyToMessageId } : {}),
|
|
387
|
+
...(isLast
|
|
388
|
+
? { replyMarkup: view.replyMarkup ?? (editing ? null : undefined) }
|
|
389
|
+
: editing
|
|
390
|
+
? { replyMarkup: null }
|
|
391
|
+
: {}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @internal */
|
|
396
|
+
export function createTelegramDeliveryRuntime(
|
|
397
|
+
deps: TelegramDeliveryRuntimeDeps,
|
|
398
|
+
): TelegramDeliveryRuntime {
|
|
399
|
+
let active = true;
|
|
400
|
+
const runForTarget = createTelegramDeliveryTargetQueue();
|
|
401
|
+
const render = (
|
|
402
|
+
view: TelegramDeliveryView,
|
|
403
|
+
): TelegramDeliveryResult<readonly TelegramDeliveryRenderedChunk[]> => {
|
|
404
|
+
const chunks = deps.renderView(view);
|
|
405
|
+
if (
|
|
406
|
+
chunks.length === 0 ||
|
|
407
|
+
chunks.some(
|
|
408
|
+
(chunk) =>
|
|
409
|
+
typeof chunk.text !== "string" || chunk.text.trim().length === 0,
|
|
410
|
+
)
|
|
411
|
+
) {
|
|
412
|
+
return failure("invalid-view", "Telegram delivery view rendered no content.");
|
|
413
|
+
}
|
|
414
|
+
return { ok: true, value: chunks };
|
|
415
|
+
};
|
|
416
|
+
const transportFailure = <T>(
|
|
417
|
+
operation: "send" | "edit" | "delete" | "chat-action",
|
|
418
|
+
error: unknown,
|
|
419
|
+
target?: TelegramDeliveryTarget,
|
|
420
|
+
partial?: T,
|
|
421
|
+
): TelegramDeliveryResult<T> => {
|
|
422
|
+
deps.recordFailure?.(operation, error, target);
|
|
423
|
+
return failure(
|
|
424
|
+
"transport-failed",
|
|
425
|
+
`Telegram delivery ${operation} failed.`,
|
|
426
|
+
partial,
|
|
427
|
+
);
|
|
428
|
+
};
|
|
429
|
+
const inactive = <T>(): TelegramDeliveryResult<T> =>
|
|
430
|
+
failure(
|
|
431
|
+
"runtime-unavailable",
|
|
432
|
+
"Telegram delivery runtime generation is inactive.",
|
|
433
|
+
);
|
|
434
|
+
const createHandle = (
|
|
435
|
+
target: TelegramDeliveryTarget,
|
|
436
|
+
messageIds: readonly number[],
|
|
437
|
+
): TelegramDeliveryHandle => ({
|
|
438
|
+
target: cloneTarget(target),
|
|
439
|
+
messageIds: [...messageIds],
|
|
440
|
+
generation: deps.generation,
|
|
441
|
+
});
|
|
442
|
+
const resolveHandleTarget = (
|
|
443
|
+
handle: TelegramDeliveryHandle,
|
|
444
|
+
): TelegramDeliveryResult<TelegramDeliveryTarget> => {
|
|
445
|
+
if (!active) return inactive();
|
|
446
|
+
if (!isValidHandleForRuntime(handle, deps.generation)) {
|
|
447
|
+
return failure(
|
|
448
|
+
"stale-handle",
|
|
449
|
+
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return resolveTelegramDeliveryTarget(
|
|
453
|
+
{ kind: "target", target: handle.target },
|
|
454
|
+
deps,
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
return {
|
|
458
|
+
generation: deps.generation,
|
|
459
|
+
shutdown() {
|
|
460
|
+
active = false;
|
|
461
|
+
},
|
|
462
|
+
async sendView(view, options) {
|
|
463
|
+
if (!active) return inactive();
|
|
464
|
+
const resolved = resolveTelegramDeliveryTarget(options.scope, deps);
|
|
465
|
+
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
466
|
+
const rendered = render(view);
|
|
467
|
+
if (!rendered.ok) return failure(rendered.reason, rendered.message);
|
|
468
|
+
const target = resolved.value;
|
|
469
|
+
return runForTarget(target, async () => {
|
|
470
|
+
if (!active) return inactive();
|
|
471
|
+
const messageIds: number[] = [];
|
|
472
|
+
try {
|
|
473
|
+
for (const [index, chunk] of rendered.value.entries()) {
|
|
474
|
+
if (!active) return inactive();
|
|
475
|
+
const messageId = await deps.sendChunk(
|
|
476
|
+
target,
|
|
477
|
+
chunk,
|
|
478
|
+
getChunkTransportOptions(
|
|
479
|
+
view,
|
|
480
|
+
index,
|
|
481
|
+
rendered.value.length,
|
|
482
|
+
options.replyToMessageId,
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
messageIds.push(messageId);
|
|
486
|
+
if (!active) return inactive();
|
|
487
|
+
}
|
|
488
|
+
return { ok: true, value: createHandle(target, messageIds) };
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return active
|
|
491
|
+
? transportFailure(
|
|
492
|
+
"send",
|
|
493
|
+
error,
|
|
494
|
+
target,
|
|
495
|
+
messageIds.length > 0
|
|
496
|
+
? createHandle(target, messageIds)
|
|
497
|
+
: undefined,
|
|
498
|
+
)
|
|
499
|
+
: inactive();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
async editView(handle, view) {
|
|
504
|
+
const resolved = resolveHandleTarget(handle);
|
|
505
|
+
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
506
|
+
const rendered = render(view);
|
|
507
|
+
if (!rendered.ok) return failure(rendered.reason, rendered.message);
|
|
508
|
+
const target = resolved.value;
|
|
509
|
+
return runForTarget(target, async () => {
|
|
510
|
+
if (!active) return inactive();
|
|
511
|
+
const visibleMessageIds = [...handle.messageIds];
|
|
512
|
+
try {
|
|
513
|
+
const sharedCount = Math.min(
|
|
514
|
+
visibleMessageIds.length,
|
|
515
|
+
rendered.value.length,
|
|
516
|
+
);
|
|
517
|
+
for (let index = 0; index < sharedCount; index += 1) {
|
|
518
|
+
if (!active) return inactive();
|
|
519
|
+
await deps.editChunk(
|
|
520
|
+
target,
|
|
521
|
+
visibleMessageIds[index]!,
|
|
522
|
+
rendered.value[index]!,
|
|
523
|
+
getChunkTransportOptions(
|
|
524
|
+
view,
|
|
525
|
+
index,
|
|
526
|
+
rendered.value.length,
|
|
527
|
+
undefined,
|
|
528
|
+
true,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
if (!active) return inactive();
|
|
532
|
+
}
|
|
533
|
+
for (
|
|
534
|
+
let index = sharedCount;
|
|
535
|
+
index < rendered.value.length;
|
|
536
|
+
index += 1
|
|
537
|
+
) {
|
|
538
|
+
if (!active) return inactive();
|
|
539
|
+
visibleMessageIds.push(
|
|
540
|
+
await deps.sendChunk(
|
|
541
|
+
target,
|
|
542
|
+
rendered.value[index]!,
|
|
543
|
+
getChunkTransportOptions(view, index, rendered.value.length),
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
if (!active) return inactive();
|
|
547
|
+
}
|
|
548
|
+
const removedMessageIds = visibleMessageIds.slice(
|
|
549
|
+
rendered.value.length,
|
|
550
|
+
);
|
|
551
|
+
for (const messageId of removedMessageIds) {
|
|
552
|
+
if (!active) return inactive();
|
|
553
|
+
await deps.deleteMessage(target, messageId);
|
|
554
|
+
visibleMessageIds.splice(visibleMessageIds.indexOf(messageId), 1);
|
|
555
|
+
if (!active) return inactive();
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
ok: true,
|
|
559
|
+
value: createHandle(target, visibleMessageIds),
|
|
560
|
+
};
|
|
561
|
+
} catch (error) {
|
|
562
|
+
return active
|
|
563
|
+
? transportFailure(
|
|
564
|
+
"edit",
|
|
565
|
+
error,
|
|
566
|
+
target,
|
|
567
|
+
createHandle(target, visibleMessageIds),
|
|
568
|
+
)
|
|
569
|
+
: inactive();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
async deleteView(handle) {
|
|
574
|
+
const resolved = resolveHandleTarget(handle);
|
|
575
|
+
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
576
|
+
const target = resolved.value;
|
|
577
|
+
return runForTarget(target, async () => {
|
|
578
|
+
if (!active) return inactive();
|
|
579
|
+
try {
|
|
580
|
+
for (const messageId of handle.messageIds) {
|
|
581
|
+
if (!active) return inactive();
|
|
582
|
+
await deps.deleteMessage(target, messageId);
|
|
583
|
+
if (!active) return inactive();
|
|
584
|
+
}
|
|
585
|
+
return { ok: true, value: undefined };
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return active ? transportFailure("delete", error, target) : inactive();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
},
|
|
591
|
+
async sendChatAction(action, scope) {
|
|
592
|
+
if (!active) return inactive();
|
|
593
|
+
const resolved = resolveTelegramDeliveryTarget(scope, deps);
|
|
594
|
+
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
595
|
+
const target = resolved.value;
|
|
596
|
+
return runForTarget(target, async () => {
|
|
597
|
+
if (!active) return inactive();
|
|
598
|
+
try {
|
|
599
|
+
await deps.sendChatAction(target, action);
|
|
600
|
+
if (!active) return inactive();
|
|
601
|
+
return { ok: true, value: undefined };
|
|
602
|
+
} catch (error) {
|
|
603
|
+
return active
|
|
604
|
+
? transportFailure("chat-action", error, target)
|
|
605
|
+
: inactive();
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** @internal */
|
|
613
|
+
export function createTelegramBridgeDeliveryRuntime(
|
|
614
|
+
deps: TelegramBridgeDeliveryRuntimeDeps,
|
|
615
|
+
): TelegramDeliveryRuntime {
|
|
616
|
+
const getPolicyView = deps.getTargetPolicyView;
|
|
617
|
+
return createTelegramDeliveryRuntime({
|
|
618
|
+
generation: deps.generation,
|
|
619
|
+
getActiveTurnTarget: deps.getActiveTurnTarget,
|
|
620
|
+
getInstanceTarget() {
|
|
621
|
+
return resolveTelegramDeliveryInstanceTarget(getPolicyView());
|
|
622
|
+
},
|
|
623
|
+
getAggregateTarget() {
|
|
624
|
+
return resolveTelegramDeliveryAggregateTarget(getPolicyView());
|
|
625
|
+
},
|
|
626
|
+
isExplicitTargetAuthorized(target) {
|
|
627
|
+
return isTelegramDeliveryExplicitTargetAuthorized(target, getPolicyView());
|
|
628
|
+
},
|
|
629
|
+
renderView(view) {
|
|
630
|
+
assertTelegramInlineKeyboardCallbackData(view.replyMarkup);
|
|
631
|
+
return renderTelegramMessage(view.text, {
|
|
632
|
+
mode: view.parseMode ?? "plain",
|
|
633
|
+
}).map(function (chunk) {
|
|
634
|
+
return {
|
|
635
|
+
text: chunk.text,
|
|
636
|
+
parseMode: chunk.parseMode === "HTML" ? "html" : "plain",
|
|
637
|
+
} as const;
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
async sendChunk(target, chunk, options) {
|
|
641
|
+
const replyParameters = buildTelegramReplyParameters(
|
|
642
|
+
target.chatId,
|
|
643
|
+
options.replyToMessageId,
|
|
644
|
+
target,
|
|
645
|
+
);
|
|
646
|
+
const body = {
|
|
647
|
+
chat_id: target.chatId,
|
|
648
|
+
text: chunk.text,
|
|
649
|
+
...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
|
|
650
|
+
...getTelegramTargetThreadParams(target),
|
|
651
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
652
|
+
...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
|
653
|
+
};
|
|
654
|
+
const sent = await deps.api.sendMessage(
|
|
655
|
+
target.threadId === undefined
|
|
656
|
+
? markTelegramBusAggregateDelivery(body)
|
|
657
|
+
: body,
|
|
658
|
+
);
|
|
659
|
+
deps.recordOwnership({
|
|
660
|
+
chatId: target.chatId,
|
|
661
|
+
messageId: sent.message_id,
|
|
662
|
+
target,
|
|
663
|
+
});
|
|
664
|
+
return sent.message_id;
|
|
665
|
+
},
|
|
666
|
+
async editChunk(target, messageId, chunk, options) {
|
|
667
|
+
await deps.api.editMessageText({
|
|
668
|
+
chat_id: target.chatId,
|
|
669
|
+
message_id: messageId,
|
|
670
|
+
text: chunk.text,
|
|
671
|
+
...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
|
|
672
|
+
reply_markup:
|
|
673
|
+
options.replyMarkup === null
|
|
674
|
+
? { inline_keyboard: [] }
|
|
675
|
+
: options.replyMarkup,
|
|
676
|
+
});
|
|
677
|
+
},
|
|
678
|
+
deleteMessage(target, messageId) {
|
|
679
|
+
return deps.api.deleteMessage(target.chatId, messageId);
|
|
680
|
+
},
|
|
681
|
+
async sendChatAction(target, action) {
|
|
682
|
+
await deps.api.sendChatAction(target.chatId, action, {
|
|
683
|
+
message_thread_id: target.threadId,
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
recordFailure: deps.recordFailure,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function getBoundTelegramDeliveryRuntime():
|
|
691
|
+
| TelegramDeliveryRuntime
|
|
692
|
+
| TelegramDeliveryResult<never> {
|
|
693
|
+
const runtime = getTelegramDeliveryRuntimeRegistry().runtime;
|
|
694
|
+
return (
|
|
695
|
+
runtime ??
|
|
696
|
+
failure(
|
|
697
|
+
"runtime-unavailable",
|
|
698
|
+
"Telegram delivery runtime is unavailable in this Pi session.",
|
|
699
|
+
)
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function isFailure(
|
|
704
|
+
value: TelegramDeliveryRuntime | TelegramDeliveryResult<never>,
|
|
705
|
+
): value is TelegramDeliveryResult<never> {
|
|
706
|
+
return "ok" in value;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function validateView<T>(
|
|
710
|
+
view: TelegramDeliveryView,
|
|
711
|
+
): TelegramDeliveryResult<T> | undefined {
|
|
712
|
+
if (typeof view.text !== "string" || view.text.trim().length === 0) {
|
|
713
|
+
return failure("invalid-view", "Telegram delivery view text is empty.");
|
|
714
|
+
}
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function runDeliveryOperation<T>(
|
|
719
|
+
operation: (runtime: TelegramDeliveryRuntime) => Promise<TelegramDeliveryResult<T>>,
|
|
720
|
+
): Promise<TelegramDeliveryResult<T>> {
|
|
721
|
+
const runtime = getBoundTelegramDeliveryRuntime();
|
|
722
|
+
if (isFailure(runtime)) return runtime;
|
|
723
|
+
try {
|
|
724
|
+
return await operation(runtime);
|
|
725
|
+
} catch {
|
|
726
|
+
return failure("transport-failed", "Telegram delivery failed.");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** @internal */
|
|
731
|
+
export function bindTelegramDeliveryRuntime(
|
|
732
|
+
runtime: TelegramDeliveryRuntime,
|
|
733
|
+
): () => void {
|
|
734
|
+
const registry = getTelegramDeliveryRuntimeRegistry();
|
|
735
|
+
if (registry.runtime !== runtime) registry.runtime?.shutdown();
|
|
736
|
+
registry.runtime = runtime;
|
|
737
|
+
return () => {
|
|
738
|
+
if (registry.runtime === runtime) registry.runtime = undefined;
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** @internal */
|
|
743
|
+
export function clearTelegramDeliveryRuntime(): void {
|
|
744
|
+
const registry = getTelegramDeliveryRuntimeRegistry();
|
|
745
|
+
registry.runtime?.shutdown();
|
|
746
|
+
registry.runtime = undefined;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export async function sendTelegramView(
|
|
750
|
+
view: TelegramDeliveryView,
|
|
751
|
+
options: SendTelegramViewOptions,
|
|
752
|
+
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>> {
|
|
753
|
+
const invalid = validateView<TelegramDeliveryHandle>(view);
|
|
754
|
+
if (invalid) return invalid;
|
|
755
|
+
return runDeliveryOperation((runtime) => runtime.sendView(view, options));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export async function editTelegramView(
|
|
759
|
+
handle: TelegramDeliveryHandle,
|
|
760
|
+
view: TelegramDeliveryView,
|
|
761
|
+
): Promise<TelegramDeliveryResult<TelegramDeliveryHandle>> {
|
|
762
|
+
const invalid = validateView<TelegramDeliveryHandle>(view);
|
|
763
|
+
if (invalid) return invalid;
|
|
764
|
+
return runDeliveryOperation((runtime) => {
|
|
765
|
+
if (runtime.generation !== handle.generation) {
|
|
766
|
+
return Promise.resolve(
|
|
767
|
+
failure(
|
|
768
|
+
"stale-handle",
|
|
769
|
+
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
770
|
+
),
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
return runtime.editView(handle, view);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export async function deleteTelegramView(
|
|
778
|
+
handle: TelegramDeliveryHandle,
|
|
779
|
+
): Promise<TelegramDeliveryResult<void>> {
|
|
780
|
+
return runDeliveryOperation((runtime) => {
|
|
781
|
+
if (runtime.generation !== handle.generation) {
|
|
782
|
+
return Promise.resolve(
|
|
783
|
+
failure(
|
|
784
|
+
"stale-handle",
|
|
785
|
+
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
786
|
+
),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return runtime.deleteView(handle);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export async function sendTelegramChatAction(
|
|
794
|
+
action: TelegramDeliveryChatAction,
|
|
795
|
+
options: { scope: TelegramDeliveryScope },
|
|
796
|
+
): Promise<TelegramDeliveryResult<void>> {
|
|
797
|
+
return runDeliveryOperation((runtime) =>
|
|
798
|
+
runtime.sendChatAction(action, options.scope),
|
|
799
|
+
);
|
|
800
|
+
}
|