@llblab/pi-telegram 0.21.1 → 0.22.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 +6 -4
- package/CHANGELOG.md +474 -452
- package/docs/architecture.md +12 -10
- package/docs/delivery.md +2 -1
- package/docs/locks.md +9 -5
- package/docs/multi-instance-bus.md +5 -5
- package/index.ts +311 -633
- package/lib/bindings.ts +59 -21
- package/lib/bus-api.ts +12 -2
- package/lib/bus-follower.ts +327 -83
- package/lib/bus-leader.ts +201 -121
- package/lib/bus.ts +345 -37
- package/lib/config.ts +168 -28
- package/lib/delivery.ts +148 -61
- package/lib/lifecycle.ts +199 -8
- package/lib/locks.ts +569 -56
- package/lib/logs.ts +178 -19
- package/lib/media.ts +79 -24
- package/lib/model.ts +5 -10
- package/lib/ownership.ts +150 -6
- package/lib/polling.ts +156 -7
- package/lib/preview.ts +15 -3
- package/lib/queue.ts +167 -20
- package/lib/routing.ts +68 -12
- package/lib/sync.ts +105 -32
- package/lib/telegram-api.ts +86 -10
- package/lib/text-groups.ts +71 -15
- package/lib/thread-reconciler.ts +49 -36
- package/lib/threads.ts +505 -118
- package/package.json +1 -1
package/lib/config.ts
CHANGED
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
* Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import {
|
|
9
|
+
chmodSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
statSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
9
18
|
import { resolveAgentDir, resolveTelegramConfigPath } from "./paths.ts";
|
|
10
19
|
|
|
11
20
|
import type { TelegramInboundHandlerConfig } from "./inbound.ts";
|
|
12
21
|
import type { CommandTemplateObjectConfig } from "./command-templates.ts";
|
|
22
|
+
import { withTelegramFileTransaction } from "./locks.ts";
|
|
13
23
|
|
|
14
24
|
const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
|
|
15
25
|
|
|
@@ -136,9 +146,7 @@ export function resolveTelegramActiveProfile(
|
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
/** List defined profile names. */
|
|
139
|
-
export function getTelegramProfileNames(
|
|
140
|
-
config: TelegramConfig,
|
|
141
|
-
): string[] {
|
|
149
|
+
export function getTelegramProfileNames(config: TelegramConfig): string[] {
|
|
142
150
|
return Object.keys(config.profiles ?? {}).sort();
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -251,16 +259,31 @@ export async function readTelegramConfig(
|
|
|
251
259
|
onInvalidConfig?: (recovery: TelegramInvalidConfigRecovery) => void;
|
|
252
260
|
} = {},
|
|
253
261
|
): Promise<TelegramConfig> {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
return withTelegramFileTransaction(`${configPath}.transaction`, () => {
|
|
263
|
+
if (!existsSync(configPath)) return {};
|
|
264
|
+
const identity = statSync(configPath);
|
|
265
|
+
const content = readFileSync(configPath, "utf8");
|
|
266
|
+
try {
|
|
267
|
+
return JSON.parse(content) as TelegramConfig;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const currentIdentity = statSync(configPath);
|
|
270
|
+
if (
|
|
271
|
+
currentIdentity.dev !== identity.dev ||
|
|
272
|
+
currentIdentity.ino !== identity.ino ||
|
|
273
|
+
currentIdentity.size !== identity.size ||
|
|
274
|
+
currentIdentity.mtimeMs !== identity.mtimeMs
|
|
275
|
+
) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Telegram config changed while validating invalid content: ${configPath}`,
|
|
278
|
+
{ cause: error },
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const recoveryPath = getInvalidTelegramConfigRecoveryPath(configPath);
|
|
282
|
+
renameSync(configPath, recoveryPath);
|
|
283
|
+
options.onInvalidConfig?.({ configPath, recoveryPath, error });
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
});
|
|
264
287
|
}
|
|
265
288
|
|
|
266
289
|
export async function writeTelegramConfig(
|
|
@@ -279,7 +302,88 @@ export async function writeTelegramConfig(
|
|
|
279
302
|
await chmod(configPath, 0o600);
|
|
280
303
|
}
|
|
281
304
|
|
|
282
|
-
function
|
|
305
|
+
function isPlainConfigRecord(value: unknown): value is Record<string, unknown> {
|
|
306
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function cloneTelegramConfig<T>(value: T): T {
|
|
310
|
+
return structuredClone(value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function configValuesEqual(left: unknown, right: unknown): boolean {
|
|
314
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function mergeTelegramConfigDelta(
|
|
318
|
+
base: Record<string, unknown>,
|
|
319
|
+
desired: Record<string, unknown>,
|
|
320
|
+
latest: Record<string, unknown>,
|
|
321
|
+
): Record<string, unknown> {
|
|
322
|
+
const merged = cloneTelegramConfig(latest);
|
|
323
|
+
for (const key of new Set([...Object.keys(base), ...Object.keys(desired)])) {
|
|
324
|
+
const baseHas = Object.hasOwn(base, key);
|
|
325
|
+
const desiredHas = Object.hasOwn(desired, key);
|
|
326
|
+
const baseValue = base[key];
|
|
327
|
+
const desiredValue = desired[key];
|
|
328
|
+
if (baseHas === desiredHas && configValuesEqual(baseValue, desiredValue)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (!desiredHas) {
|
|
332
|
+
if (key !== "lastUpdateId") delete merged[key];
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
key === "lastUpdateId" &&
|
|
337
|
+
typeof desiredValue === "number" &&
|
|
338
|
+
typeof merged[key] === "number"
|
|
339
|
+
) {
|
|
340
|
+
merged[key] = Math.max(desiredValue, merged[key] as number);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (
|
|
344
|
+
isPlainConfigRecord(desiredValue) &&
|
|
345
|
+
(!baseHas || isPlainConfigRecord(baseValue))
|
|
346
|
+
) {
|
|
347
|
+
merged[key] = mergeTelegramConfigDelta(
|
|
348
|
+
isPlainConfigRecord(baseValue) ? baseValue : {},
|
|
349
|
+
desiredValue,
|
|
350
|
+
isPlainConfigRecord(merged[key]) ? merged[key] : {},
|
|
351
|
+
);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
merged[key] = cloneTelegramConfig(desiredValue);
|
|
355
|
+
}
|
|
356
|
+
return merged;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function readTelegramConfigForTransaction(configPath: string): TelegramConfig {
|
|
360
|
+
if (!existsSync(configPath)) return {};
|
|
361
|
+
const parsed: unknown = JSON.parse(readFileSync(configPath, "utf8"));
|
|
362
|
+
if (!isPlainConfigRecord(parsed)) {
|
|
363
|
+
throw new Error(`Invalid Telegram config object: ${configPath}`);
|
|
364
|
+
}
|
|
365
|
+
return parsed as TelegramConfig;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function writeTelegramConfigInTransaction(
|
|
369
|
+
agentDir: string,
|
|
370
|
+
configPath: string,
|
|
371
|
+
config: TelegramConfig,
|
|
372
|
+
): void {
|
|
373
|
+
mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
|
374
|
+
const tempConfigPath = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
375
|
+
writeFileSync(tempConfigPath, `${JSON.stringify(config, null, "\t")}\n`, {
|
|
376
|
+
encoding: "utf8",
|
|
377
|
+
mode: 0o600,
|
|
378
|
+
});
|
|
379
|
+
chmodSync(tempConfigPath, 0o600);
|
|
380
|
+
renameSync(tempConfigPath, configPath);
|
|
381
|
+
chmodSync(configPath, 0o600);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function getTelegramProfileFields(
|
|
385
|
+
config: TelegramConfig,
|
|
386
|
+
): TelegramBotProfile | undefined {
|
|
283
387
|
const token = config.botToken?.trim();
|
|
284
388
|
if (!token) return undefined;
|
|
285
389
|
return {
|
|
@@ -332,13 +436,22 @@ function storeTelegramEffectiveConfig(
|
|
|
332
436
|
export function createTelegramConfigStore(
|
|
333
437
|
options: TelegramConfigStoreOptions = {},
|
|
334
438
|
): TelegramConfigStore {
|
|
335
|
-
let config: TelegramConfig = options.initialConfig ?? {};
|
|
439
|
+
let config: TelegramConfig = cloneTelegramConfig(options.initialConfig ?? {});
|
|
440
|
+
let persistedConfig: TelegramConfig = {};
|
|
441
|
+
let mutationVersion = 0;
|
|
442
|
+
let persistQueue: Promise<void> = Promise.resolve();
|
|
336
443
|
let activeProfileName: string | undefined;
|
|
337
444
|
const agentDir = options.agentDir ?? resolveAgentDir();
|
|
338
445
|
const configPath = options.configPath ?? getConfigPath();
|
|
339
|
-
const getEffectiveConfig = () =>
|
|
446
|
+
const getEffectiveConfig = () =>
|
|
447
|
+
applyTelegramProfile(config, activeProfileName);
|
|
340
448
|
const setEffectiveConfig = (nextConfig: TelegramConfig) => {
|
|
341
|
-
config = storeTelegramEffectiveConfig(
|
|
449
|
+
config = storeTelegramEffectiveConfig(
|
|
450
|
+
config,
|
|
451
|
+
nextConfig,
|
|
452
|
+
activeProfileName,
|
|
453
|
+
);
|
|
454
|
+
mutationVersion += 1;
|
|
342
455
|
};
|
|
343
456
|
return {
|
|
344
457
|
get: getEffectiveConfig,
|
|
@@ -379,18 +492,45 @@ export function createTelegramConfigStore(
|
|
|
379
492
|
});
|
|
380
493
|
},
|
|
381
494
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
495
|
+
persistedConfig = cloneTelegramConfig(config);
|
|
496
|
+
mutationVersion += 1;
|
|
385
497
|
},
|
|
386
|
-
persist:
|
|
387
|
-
const
|
|
498
|
+
persist: (nextConfig = getEffectiveConfig()) => {
|
|
499
|
+
const profileName = activeProfileName;
|
|
500
|
+
const desiredConfig = storeTelegramEffectiveConfig(
|
|
388
501
|
config,
|
|
389
|
-
nextConfig,
|
|
390
|
-
|
|
502
|
+
cloneTelegramConfig(nextConfig),
|
|
503
|
+
profileName,
|
|
391
504
|
);
|
|
392
|
-
|
|
393
|
-
|
|
505
|
+
const baseConfig = cloneTelegramConfig(persistedConfig);
|
|
506
|
+
const capturedMutationVersion = mutationVersion;
|
|
507
|
+
const persist = persistQueue.then(() => {
|
|
508
|
+
const mergedConfig = withTelegramFileTransaction(
|
|
509
|
+
`${configPath}.transaction`,
|
|
510
|
+
() => {
|
|
511
|
+
const latestConfig = readTelegramConfigForTransaction(configPath);
|
|
512
|
+
const merged = mergeTelegramConfigDelta(
|
|
513
|
+
baseConfig as Record<string, unknown>,
|
|
514
|
+
desiredConfig as Record<string, unknown>,
|
|
515
|
+
latestConfig as Record<string, unknown>,
|
|
516
|
+
) as TelegramConfig;
|
|
517
|
+
writeTelegramConfigInTransaction(agentDir, configPath, merged);
|
|
518
|
+
return merged;
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
persistedConfig = cloneTelegramConfig(mergedConfig);
|
|
522
|
+
if (mutationVersion === capturedMutationVersion) {
|
|
523
|
+
config = cloneTelegramConfig(mergedConfig);
|
|
524
|
+
} else {
|
|
525
|
+
config = mergeTelegramConfigDelta(
|
|
526
|
+
baseConfig as Record<string, unknown>,
|
|
527
|
+
config as Record<string, unknown>,
|
|
528
|
+
mergedConfig as Record<string, unknown>,
|
|
529
|
+
) as TelegramConfig;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
persistQueue = persist.catch(() => undefined);
|
|
533
|
+
return persist;
|
|
394
534
|
},
|
|
395
535
|
};
|
|
396
536
|
}
|
package/lib/delivery.ts
CHANGED
|
@@ -17,10 +17,20 @@ import {
|
|
|
17
17
|
getTelegramTargetThreadParams,
|
|
18
18
|
type TelegramTarget,
|
|
19
19
|
} from "./target.ts";
|
|
20
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
isTelegramApiCommitUnknownError,
|
|
22
|
+
type TelegramBridgeApiRuntime,
|
|
23
|
+
} from "./telegram-api.ts";
|
|
21
24
|
|
|
22
25
|
const TELEGRAM_DELIVERY_RUNTIME_KEY = "__piTelegramDeliveryRuntime__";
|
|
23
26
|
|
|
27
|
+
class TelegramDeliveryTransportGenerationError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("Telegram Delivery transport generation is no longer active.");
|
|
30
|
+
this.name = "TelegramDeliveryTransportGenerationError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export type TelegramDeliveryParseMode = "plain" | "html" | "markdown";
|
|
25
35
|
|
|
26
36
|
export interface TelegramDeliveryView {
|
|
@@ -49,6 +59,7 @@ export type TelegramDeliveryFailureReason =
|
|
|
49
59
|
| "target-unauthorized"
|
|
50
60
|
| "stale-handle"
|
|
51
61
|
| "invalid-view"
|
|
62
|
+
| "commit-unknown"
|
|
52
63
|
| "transport-failed";
|
|
53
64
|
|
|
54
65
|
export type TelegramDeliveryResult<T> =
|
|
@@ -67,10 +78,7 @@ export interface SendTelegramViewOptions {
|
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
export type TelegramDeliveryChatAction =
|
|
70
|
-
| "
|
|
71
|
-
| "upload_document"
|
|
72
|
-
| "upload_photo"
|
|
73
|
-
| "record_voice";
|
|
81
|
+
"typing" | "upload_document" | "upload_photo" | "record_voice";
|
|
74
82
|
|
|
75
83
|
/** @internal */
|
|
76
84
|
export interface TelegramDeliveryRuntime {
|
|
@@ -114,8 +122,7 @@ export interface TelegramDeliveryTransportOptions {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/** @internal */
|
|
117
|
-
export interface TelegramDeliveryRuntimeDeps
|
|
118
|
-
extends TelegramDeliveryTargetResolverDeps {
|
|
125
|
+
export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResolverDeps {
|
|
119
126
|
generation: string;
|
|
120
127
|
renderView: (
|
|
121
128
|
view: TelegramDeliveryView,
|
|
@@ -151,6 +158,7 @@ export interface TelegramBridgeDeliveryRuntimeDeps {
|
|
|
151
158
|
generation: string;
|
|
152
159
|
getTargetPolicyView: () => TelegramDeliveryTargetPolicyView;
|
|
153
160
|
getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
|
|
161
|
+
isTransportActive?: () => boolean;
|
|
154
162
|
api: Pick<
|
|
155
163
|
TelegramBridgeApiRuntime,
|
|
156
164
|
"sendMessage" | "editMessageText" | "deleteMessage" | "sendChatAction"
|
|
@@ -191,18 +199,28 @@ export function createTelegramDeliveryLifecycleHooks(
|
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
/** @internal */
|
|
194
|
-
export function createTelegramBridgeDeliveryLifecycleHooks(
|
|
195
|
-
deps: Omit<
|
|
202
|
+
export function createTelegramBridgeDeliveryLifecycleHooks<TTransportStamp>(
|
|
203
|
+
deps: Omit<
|
|
204
|
+
TelegramBridgeDeliveryRuntimeDeps,
|
|
205
|
+
"generation" | "isTransportActive"
|
|
206
|
+
> & {
|
|
196
207
|
generationSeed: string;
|
|
208
|
+
getTransportStamp?: () => TTransportStamp;
|
|
209
|
+
isTransportStampActive?: (stamp: TTransportStamp) => boolean;
|
|
197
210
|
},
|
|
198
211
|
): ReturnType<typeof createTelegramDeliveryLifecycleHooks> {
|
|
199
212
|
let generationSequence = 0;
|
|
200
|
-
return createTelegramDeliveryLifecycleHooks(() =>
|
|
201
|
-
|
|
213
|
+
return createTelegramDeliveryLifecycleHooks(() => {
|
|
214
|
+
const transportStamp = deps.getTransportStamp?.();
|
|
215
|
+
return createTelegramBridgeDeliveryRuntime({
|
|
202
216
|
...deps,
|
|
203
217
|
generation: `${deps.generationSeed}:${++generationSequence}`,
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
isTransportActive:
|
|
219
|
+
transportStamp !== undefined && deps.isTransportStampActive
|
|
220
|
+
? () => deps.isTransportStampActive?.(transportStamp) ?? false
|
|
221
|
+
: undefined,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
interface TelegramDeliveryRuntimeRegistry {
|
|
@@ -212,11 +230,7 @@ interface TelegramDeliveryRuntimeRegistry {
|
|
|
212
230
|
function getTelegramDeliveryRuntimeRegistry(): TelegramDeliveryRuntimeRegistry {
|
|
213
231
|
const globals = globalThis as Record<string, unknown>;
|
|
214
232
|
const existing = globals[TELEGRAM_DELIVERY_RUNTIME_KEY];
|
|
215
|
-
if (
|
|
216
|
-
existing &&
|
|
217
|
-
typeof existing === "object" &&
|
|
218
|
-
"runtime" in existing
|
|
219
|
-
) {
|
|
233
|
+
if (existing && typeof existing === "object" && "runtime" in existing) {
|
|
220
234
|
return existing as TelegramDeliveryRuntimeRegistry;
|
|
221
235
|
}
|
|
222
236
|
const registry: TelegramDeliveryRuntimeRegistry = {};
|
|
@@ -244,6 +258,43 @@ export interface TelegramDeliveryTargetPolicyView {
|
|
|
244
258
|
liveTargets?: readonly TelegramDeliveryTarget[];
|
|
245
259
|
}
|
|
246
260
|
|
|
261
|
+
/** @internal */
|
|
262
|
+
export interface TelegramDeliveryTargetPolicyRuntime {
|
|
263
|
+
getTargetPolicyView(): TelegramDeliveryTargetPolicyView;
|
|
264
|
+
getActiveTurnTarget(): TelegramDeliveryTarget | undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @internal */
|
|
268
|
+
export function createTelegramDeliveryTargetPolicyRuntime(deps: {
|
|
269
|
+
ownsDirect(): boolean;
|
|
270
|
+
isFollowerRegistered(): boolean;
|
|
271
|
+
getAllowedChatId(): number | undefined;
|
|
272
|
+
getFollowerTarget(): TelegramDeliveryTarget | undefined;
|
|
273
|
+
getLeaderTarget(): TelegramDeliveryTarget | undefined;
|
|
274
|
+
listThreadRecords(): readonly { target: TelegramDeliveryTarget }[];
|
|
275
|
+
getActiveTurnTarget(): TelegramDeliveryTarget | undefined;
|
|
276
|
+
getActiveGuestQueryId(): string | undefined;
|
|
277
|
+
}): TelegramDeliveryTargetPolicyRuntime {
|
|
278
|
+
return {
|
|
279
|
+
getTargetPolicyView() {
|
|
280
|
+
const ownsDirect = deps.ownsDirect();
|
|
281
|
+
return {
|
|
282
|
+
canDeliver: ownsDirect || deps.isFollowerRegistered(),
|
|
283
|
+
ownsDirect,
|
|
284
|
+
allowedChatId: deps.getAllowedChatId(),
|
|
285
|
+
followerTarget: deps.getFollowerTarget(),
|
|
286
|
+
leaderTarget: deps.getLeaderTarget(),
|
|
287
|
+
liveTargets: deps.listThreadRecords().map((record) => record.target),
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
getActiveTurnTarget() {
|
|
291
|
+
return deps.getActiveGuestQueryId()
|
|
292
|
+
? undefined
|
|
293
|
+
: deps.getActiveTurnTarget();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
247
298
|
/** @internal */
|
|
248
299
|
export function resolveTelegramDeliveryInstanceTarget(
|
|
249
300
|
view: TelegramDeliveryTargetPolicyView,
|
|
@@ -252,7 +303,9 @@ export function resolveTelegramDeliveryInstanceTarget(
|
|
|
252
303
|
return (
|
|
253
304
|
view.followerTarget ??
|
|
254
305
|
view.leaderTarget ??
|
|
255
|
-
(view.allowedChatId === undefined
|
|
306
|
+
(view.allowedChatId === undefined
|
|
307
|
+
? undefined
|
|
308
|
+
: { chatId: view.allowedChatId })
|
|
256
309
|
);
|
|
257
310
|
}
|
|
258
311
|
|
|
@@ -338,19 +391,6 @@ function resolveTelegramDeliveryTarget(
|
|
|
338
391
|
return { ok: true, value: cloneTarget(target) };
|
|
339
392
|
}
|
|
340
393
|
|
|
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
394
|
function createTelegramDeliveryTargetQueue() {
|
|
355
395
|
const queues = new Map<string, Promise<void>>();
|
|
356
396
|
return async function run<T>(
|
|
@@ -397,6 +437,10 @@ export function createTelegramDeliveryRuntime(
|
|
|
397
437
|
deps: TelegramDeliveryRuntimeDeps,
|
|
398
438
|
): TelegramDeliveryRuntime {
|
|
399
439
|
let active = true;
|
|
440
|
+
const handleBindings = new WeakMap<
|
|
441
|
+
TelegramDeliveryHandle,
|
|
442
|
+
{ target: TelegramDeliveryTarget; messageIds: readonly number[] }
|
|
443
|
+
>();
|
|
400
444
|
const runForTarget = createTelegramDeliveryTargetQueue();
|
|
401
445
|
const render = (
|
|
402
446
|
view: TelegramDeliveryView,
|
|
@@ -409,10 +453,18 @@ export function createTelegramDeliveryRuntime(
|
|
|
409
453
|
typeof chunk.text !== "string" || chunk.text.trim().length === 0,
|
|
410
454
|
)
|
|
411
455
|
) {
|
|
412
|
-
return failure(
|
|
456
|
+
return failure(
|
|
457
|
+
"invalid-view",
|
|
458
|
+
"Telegram delivery view rendered no content.",
|
|
459
|
+
);
|
|
413
460
|
}
|
|
414
461
|
return { ok: true, value: chunks };
|
|
415
462
|
};
|
|
463
|
+
const inactive = <T>(): TelegramDeliveryResult<T> =>
|
|
464
|
+
failure(
|
|
465
|
+
"runtime-unavailable",
|
|
466
|
+
"Telegram delivery runtime generation is inactive.",
|
|
467
|
+
);
|
|
416
468
|
const transportFailure = <T>(
|
|
417
469
|
operation: "send" | "edit" | "delete" | "chat-action",
|
|
418
470
|
error: unknown,
|
|
@@ -420,39 +472,58 @@ export function createTelegramDeliveryRuntime(
|
|
|
420
472
|
partial?: T,
|
|
421
473
|
): TelegramDeliveryResult<T> => {
|
|
422
474
|
deps.recordFailure?.(operation, error, target);
|
|
475
|
+
if (error instanceof TelegramDeliveryTransportGenerationError) {
|
|
476
|
+
return inactive();
|
|
477
|
+
}
|
|
423
478
|
return failure(
|
|
424
|
-
|
|
425
|
-
|
|
479
|
+
isTelegramApiCommitUnknownError(error)
|
|
480
|
+
? "commit-unknown"
|
|
481
|
+
: "transport-failed",
|
|
482
|
+
isTelegramApiCommitUnknownError(error)
|
|
483
|
+
? `Telegram delivery ${operation} may have committed before transport failed.`
|
|
484
|
+
: `Telegram delivery ${operation} failed.`,
|
|
426
485
|
partial,
|
|
427
486
|
);
|
|
428
487
|
};
|
|
429
|
-
const inactive = <T>(): TelegramDeliveryResult<T> =>
|
|
430
|
-
failure(
|
|
431
|
-
"runtime-unavailable",
|
|
432
|
-
"Telegram delivery runtime generation is inactive.",
|
|
433
|
-
);
|
|
434
488
|
const createHandle = (
|
|
435
489
|
target: TelegramDeliveryTarget,
|
|
436
490
|
messageIds: readonly number[],
|
|
437
|
-
): TelegramDeliveryHandle =>
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
491
|
+
): TelegramDeliveryHandle => {
|
|
492
|
+
const canonicalTarget = Object.freeze(cloneTarget(target));
|
|
493
|
+
const canonicalMessageIds = Object.freeze([...messageIds]);
|
|
494
|
+
const handle = Object.freeze({
|
|
495
|
+
target: canonicalTarget,
|
|
496
|
+
messageIds: canonicalMessageIds,
|
|
497
|
+
generation: deps.generation,
|
|
498
|
+
});
|
|
499
|
+
handleBindings.set(handle, {
|
|
500
|
+
target: canonicalTarget,
|
|
501
|
+
messageIds: canonicalMessageIds,
|
|
502
|
+
});
|
|
503
|
+
return handle;
|
|
504
|
+
};
|
|
505
|
+
const resolveHandle = (
|
|
443
506
|
handle: TelegramDeliveryHandle,
|
|
444
|
-
): TelegramDeliveryResult<
|
|
507
|
+
): TelegramDeliveryResult<{
|
|
508
|
+
target: TelegramDeliveryTarget;
|
|
509
|
+
messageIds: readonly number[];
|
|
510
|
+
}> => {
|
|
445
511
|
if (!active) return inactive();
|
|
446
|
-
|
|
512
|
+
const binding = handleBindings.get(handle);
|
|
513
|
+
if (!binding || handle.generation !== deps.generation) {
|
|
447
514
|
return failure(
|
|
448
515
|
"stale-handle",
|
|
449
516
|
"Telegram delivery handle belongs to an inactive runtime generation.",
|
|
450
517
|
);
|
|
451
518
|
}
|
|
452
|
-
|
|
453
|
-
{ kind: "target", target:
|
|
519
|
+
const authorized = resolveTelegramDeliveryTarget(
|
|
520
|
+
{ kind: "target", target: binding.target },
|
|
454
521
|
deps,
|
|
455
522
|
);
|
|
523
|
+
if (!authorized.ok) {
|
|
524
|
+
return failure(authorized.reason, authorized.message);
|
|
525
|
+
}
|
|
526
|
+
return { ok: true, value: binding };
|
|
456
527
|
};
|
|
457
528
|
return {
|
|
458
529
|
generation: deps.generation,
|
|
@@ -501,14 +572,14 @@ export function createTelegramDeliveryRuntime(
|
|
|
501
572
|
});
|
|
502
573
|
},
|
|
503
574
|
async editView(handle, view) {
|
|
504
|
-
const resolved =
|
|
575
|
+
const resolved = resolveHandle(handle);
|
|
505
576
|
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
506
577
|
const rendered = render(view);
|
|
507
578
|
if (!rendered.ok) return failure(rendered.reason, rendered.message);
|
|
508
|
-
const target = resolved.value;
|
|
579
|
+
const target = resolved.value.target;
|
|
509
580
|
return runForTarget(target, async () => {
|
|
510
581
|
if (!active) return inactive();
|
|
511
|
-
const visibleMessageIds = [...
|
|
582
|
+
const visibleMessageIds = [...resolved.value.messageIds];
|
|
512
583
|
try {
|
|
513
584
|
const sharedCount = Math.min(
|
|
514
585
|
visibleMessageIds.length,
|
|
@@ -571,20 +642,22 @@ export function createTelegramDeliveryRuntime(
|
|
|
571
642
|
});
|
|
572
643
|
},
|
|
573
644
|
async deleteView(handle) {
|
|
574
|
-
const resolved =
|
|
645
|
+
const resolved = resolveHandle(handle);
|
|
575
646
|
if (!resolved.ok) return failure(resolved.reason, resolved.message);
|
|
576
|
-
const target = resolved.value;
|
|
647
|
+
const target = resolved.value.target;
|
|
577
648
|
return runForTarget(target, async () => {
|
|
578
649
|
if (!active) return inactive();
|
|
579
650
|
try {
|
|
580
|
-
for (const messageId of
|
|
651
|
+
for (const messageId of resolved.value.messageIds) {
|
|
581
652
|
if (!active) return inactive();
|
|
582
653
|
await deps.deleteMessage(target, messageId);
|
|
583
654
|
if (!active) return inactive();
|
|
584
655
|
}
|
|
585
656
|
return { ok: true, value: undefined };
|
|
586
657
|
} catch (error) {
|
|
587
|
-
return active
|
|
658
|
+
return active
|
|
659
|
+
? transportFailure("delete", error, target)
|
|
660
|
+
: inactive();
|
|
588
661
|
}
|
|
589
662
|
});
|
|
590
663
|
},
|
|
@@ -614,6 +687,11 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
614
687
|
deps: TelegramBridgeDeliveryRuntimeDeps,
|
|
615
688
|
): TelegramDeliveryRuntime {
|
|
616
689
|
const getPolicyView = deps.getTargetPolicyView;
|
|
690
|
+
const assertTransportActive = (): void => {
|
|
691
|
+
if (deps.isTransportActive?.() === false) {
|
|
692
|
+
throw new TelegramDeliveryTransportGenerationError();
|
|
693
|
+
}
|
|
694
|
+
};
|
|
617
695
|
return createTelegramDeliveryRuntime({
|
|
618
696
|
generation: deps.generation,
|
|
619
697
|
getActiveTurnTarget: deps.getActiveTurnTarget,
|
|
@@ -624,7 +702,10 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
624
702
|
return resolveTelegramDeliveryAggregateTarget(getPolicyView());
|
|
625
703
|
},
|
|
626
704
|
isExplicitTargetAuthorized(target) {
|
|
627
|
-
return isTelegramDeliveryExplicitTargetAuthorized(
|
|
705
|
+
return isTelegramDeliveryExplicitTargetAuthorized(
|
|
706
|
+
target,
|
|
707
|
+
getPolicyView(),
|
|
708
|
+
);
|
|
628
709
|
},
|
|
629
710
|
renderView(view) {
|
|
630
711
|
assertTelegramInlineKeyboardCallbackData(view.replyMarkup);
|
|
@@ -638,6 +719,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
638
719
|
});
|
|
639
720
|
},
|
|
640
721
|
async sendChunk(target, chunk, options) {
|
|
722
|
+
assertTransportActive();
|
|
641
723
|
const replyParameters = buildTelegramReplyParameters(
|
|
642
724
|
target.chatId,
|
|
643
725
|
options.replyToMessageId,
|
|
@@ -656,6 +738,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
656
738
|
? markTelegramBusAggregateDelivery(body)
|
|
657
739
|
: body,
|
|
658
740
|
);
|
|
741
|
+
assertTransportActive();
|
|
659
742
|
deps.recordOwnership({
|
|
660
743
|
chatId: target.chatId,
|
|
661
744
|
messageId: sent.message_id,
|
|
@@ -664,6 +747,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
664
747
|
return sent.message_id;
|
|
665
748
|
},
|
|
666
749
|
async editChunk(target, messageId, chunk, options) {
|
|
750
|
+
assertTransportActive();
|
|
667
751
|
await deps.api.editMessageText({
|
|
668
752
|
chat_id: target.chatId,
|
|
669
753
|
message_id: messageId,
|
|
@@ -676,9 +760,11 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
676
760
|
});
|
|
677
761
|
},
|
|
678
762
|
deleteMessage(target, messageId) {
|
|
763
|
+
assertTransportActive();
|
|
679
764
|
return deps.api.deleteMessage(target.chatId, messageId);
|
|
680
765
|
},
|
|
681
766
|
async sendChatAction(target, action) {
|
|
767
|
+
assertTransportActive();
|
|
682
768
|
await deps.api.sendChatAction(target.chatId, action, {
|
|
683
769
|
message_thread_id: target.threadId,
|
|
684
770
|
});
|
|
@@ -688,8 +774,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
688
774
|
}
|
|
689
775
|
|
|
690
776
|
function getBoundTelegramDeliveryRuntime():
|
|
691
|
-
|
|
|
692
|
-
| TelegramDeliveryResult<never> {
|
|
777
|
+
TelegramDeliveryRuntime | TelegramDeliveryResult<never> {
|
|
693
778
|
const runtime = getTelegramDeliveryRuntimeRegistry().runtime;
|
|
694
779
|
return (
|
|
695
780
|
runtime ??
|
|
@@ -716,7 +801,9 @@ function validateView<T>(
|
|
|
716
801
|
}
|
|
717
802
|
|
|
718
803
|
async function runDeliveryOperation<T>(
|
|
719
|
-
operation: (
|
|
804
|
+
operation: (
|
|
805
|
+
runtime: TelegramDeliveryRuntime,
|
|
806
|
+
) => Promise<TelegramDeliveryResult<T>>,
|
|
720
807
|
): Promise<TelegramDeliveryResult<T>> {
|
|
721
808
|
const runtime = getBoundTelegramDeliveryRuntime();
|
|
722
809
|
if (isFailure(runtime)) return runtime;
|