@llblab/pi-telegram 0.9.2 → 0.9.3
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.3: External Handlers Rename
|
|
4
|
+
|
|
5
|
+
- `[External Handlers]` Renamed the external update handlers domain to `external-handlers` across source, tests, and docs. Impact: the interop domain now has a cleaner name aligned with inbound/outbound handler naming.
|
|
6
|
+
- `[Breaking]` Removed the old `external-update-handlers` module/doc path and old exported update/interceptor aliases. Impact: layered extensions should import from `@llblab/pi-telegram/lib/external-handlers.ts` and use the `TelegramExternalHandler*` names.
|
|
7
|
+
- `[Package]` Bumped package metadata to `0.9.3` and kept the lockfile in sync.
|
|
8
|
+
|
|
3
9
|
## 0.9.2: External Update Interceptors
|
|
4
10
|
|
|
5
11
|
- `[External Update Interceptors]` Added a versioned `globalThis` registry that lets layered pi extensions observe and optionally consume Telegram updates before pi-telegram's default routing. Impact: approval gates and other same-process extensions can react synchronously to Telegram callbacks without owning a second bot poller.
|
package/README.md
CHANGED
|
@@ -224,7 +224,7 @@ List the main risks first.
|
|
|
224
224
|
<!-- telegram_button: OK -->
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
-
Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Unknown inline-button callbacks that do not belong to pi-telegram are forwarded to π as `[callback] <data>` so other extensions can namespace and handle their own Telegram buttons without polling the bot themselves; see the [Callback Namespace Standard](./docs/callback-namespaces.md). Layered extensions that need to react to Telegram updates synchronously inside their own runtime (for example, to resolve a blocking-tool approval Promise the moment a callback arrives) can register a runtime interceptor on the shared update registry; see [External
|
|
227
|
+
Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Unknown inline-button callbacks that do not belong to pi-telegram are forwarded to π as `[callback] <data>` so other extensions can namespace and handle their own Telegram buttons without polling the bot themselves; see the [Callback Namespace Standard](./docs/callback-namespaces.md). Layered extensions that need to react to Telegram updates synchronously inside their own runtime (for example, to resolve a blocking-tool approval Promise the moment a callback arrives) can register a runtime interceptor on the shared update registry; see [External Handlers](./docs/external-handlers.md). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
|
|
228
228
|
|
|
229
229
|
## Streaming
|
|
230
230
|
|
package/docs/README.md
CHANGED
|
@@ -10,4 +10,4 @@ Living index of project documentation in `/docs`.
|
|
|
10
10
|
- [outbound-handlers.md](./outbound-handlers.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, artifact outputs, and callback routing
|
|
11
11
|
- [locks.md](./locks.md) — Shared `locks.json` standard for singleton extension ownership
|
|
12
12
|
- [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
|
|
13
|
-
- [external-
|
|
13
|
+
- [external-handlers.md](./external-handlers.md) — Runtime interceptor registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# External
|
|
1
|
+
# External Handlers
|
|
2
2
|
|
|
3
3
|
`pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other
|
|
4
4
|
pi extensions cannot open a competing poller against the same bot — the
|
|
@@ -12,7 +12,7 @@ fires.
|
|
|
12
12
|
|
|
13
13
|
It is the runtime counterpart to
|
|
14
14
|
[Callback Namespaces](./callback-namespaces.md): callback namespaces define
|
|
15
|
-
how to share `callback_data` cleanly; external
|
|
15
|
+
how to share `callback_data` cleanly; external handlers define how to
|
|
16
16
|
observe and optionally short-circuit the dispatch of those updates.
|
|
17
17
|
|
|
18
18
|
## When to use it
|
|
@@ -63,9 +63,9 @@ Two equivalent paths.
|
|
|
63
63
|
### Typed import (recommended when you can depend on `@llblab/pi-telegram`)
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
|
-
import {
|
|
66
|
+
import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
|
|
67
67
|
|
|
68
|
-
const off =
|
|
68
|
+
const off = onTelegramExternalUpdate(async (update) => {
|
|
69
69
|
const cb = (update as { callback_query?: { id?: string; data?: string } })
|
|
70
70
|
.callback_query;
|
|
71
71
|
if (!cb?.data?.startsWith("myext:")) return "pass";
|
|
@@ -83,7 +83,7 @@ When the layered extension prefers no `import` from `@llblab/pi-telegram` (so
|
|
|
83
83
|
load order between the two extensions does not matter, and either can be
|
|
84
84
|
installed first), it must implement the **full v1 registry contract**, not
|
|
85
85
|
just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on
|
|
86
|
-
whatever object it finds at `globalThis.
|
|
86
|
+
whatever object it finds at `globalThis.__piTelegramExternalHandlerRegistry__`,
|
|
87
87
|
so a partial object would silently break the first update.
|
|
88
88
|
|
|
89
89
|
pi-telegram defensively re-creates the registry if the object on `globalThis`
|
|
@@ -100,19 +100,19 @@ type PiTelegramVerdict =
|
|
|
100
100
|
| Promise<"consume" | "pass" | void>;
|
|
101
101
|
type PiTelegramInterceptor = (update: unknown) => PiTelegramVerdict;
|
|
102
102
|
|
|
103
|
-
interface
|
|
103
|
+
interface PiTelegramExternalHandlerRegistry {
|
|
104
104
|
readonly version: 1;
|
|
105
105
|
add: (handler: PiTelegramInterceptor) => () => void;
|
|
106
106
|
// Required: pi-telegram's polling loop calls this on every update.
|
|
107
107
|
dispatch: (update: unknown) => Promise<"consume" | "pass">;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
const REGISTRY_KEY = "
|
|
110
|
+
const REGISTRY_KEY = "__piTelegramExternalHandlerRegistry__";
|
|
111
111
|
|
|
112
|
-
function getOrCreateRegistry():
|
|
112
|
+
function getOrCreateRegistry(): PiTelegramExternalHandlerRegistry {
|
|
113
113
|
const g = globalThis as Record<string, unknown>;
|
|
114
114
|
const existing = g[REGISTRY_KEY] as
|
|
115
|
-
|
|
|
115
|
+
| PiTelegramExternalHandlerRegistry
|
|
116
116
|
| undefined;
|
|
117
117
|
if (
|
|
118
118
|
existing &&
|
|
@@ -123,7 +123,7 @@ function getOrCreateRegistry(): PiTelegramExternalUpdateRegistry {
|
|
|
123
123
|
return existing;
|
|
124
124
|
}
|
|
125
125
|
const handlers = new Set<PiTelegramInterceptor>();
|
|
126
|
-
const registry:
|
|
126
|
+
const registry: PiTelegramExternalHandlerRegistry = {
|
|
127
127
|
version: 1,
|
|
128
128
|
add(handler) {
|
|
129
129
|
handlers.add(handler);
|
|
@@ -151,7 +151,7 @@ const off = getOrCreateRegistry().add((update) => {
|
|
|
151
151
|
});
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
The registry object on `globalThis.
|
|
154
|
+
The registry object on `globalThis.__piTelegramExternalHandlerRegistry__` is
|
|
155
155
|
versioned (`version: 1`) and stable across pi-telegram releases; future
|
|
156
156
|
breaking changes will use a new schema version and a new key.
|
|
157
157
|
|
package/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as Api from "./lib/api.ts";
|
|
|
8
8
|
import * as CommandTemplates from "./lib/command-templates.ts";
|
|
9
9
|
import * as Commands from "./lib/commands.ts";
|
|
10
10
|
import * as Config from "./lib/config.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { createTelegramExternalHandleUpdate } from "./lib/external-handlers.ts";
|
|
12
12
|
import * as InboundHandlers from "./lib/inbound-handlers.ts";
|
|
13
13
|
import * as Keyboard from "./lib/keyboard.ts";
|
|
14
14
|
import * as Lifecycle from "./lib/lifecycle.ts";
|
|
@@ -359,7 +359,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
359
359
|
deleteWebhook,
|
|
360
360
|
getUpdates,
|
|
361
361
|
persistConfig: configStore.persist,
|
|
362
|
-
handleUpdate:
|
|
362
|
+
handleUpdate: createTelegramExternalHandleUpdate({
|
|
363
363
|
defaultHandle: inboundRouteRuntime.handleUpdate,
|
|
364
364
|
}),
|
|
365
365
|
stopTypingLoop: typing.stop,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* External Telegram
|
|
2
|
+
* External Telegram handler registry
|
|
3
3
|
* Zones: telegram transport, layered extension interop
|
|
4
4
|
* Lets other pi extensions hook into the polling loop without owning their own getUpdates connection
|
|
5
5
|
*/
|
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
* - `"consume"` — the interceptor handled this update; pi-telegram skips default routing.
|
|
11
11
|
* - `"pass"` (or `void`/`undefined`) — pi-telegram routes the update normally.
|
|
12
12
|
*/
|
|
13
|
-
export type
|
|
13
|
+
export type TelegramExternalHandlerVerdict = "consume" | "pass";
|
|
14
14
|
|
|
15
|
-
export type
|
|
15
|
+
export type TelegramExternalHandler = (
|
|
16
16
|
update: unknown,
|
|
17
17
|
) =>
|
|
18
|
-
|
|
|
18
|
+
| TelegramExternalHandlerVerdict
|
|
19
19
|
| void
|
|
20
|
-
| Promise<
|
|
20
|
+
| Promise<TelegramExternalHandlerVerdict | void>;
|
|
21
21
|
|
|
22
|
-
export interface
|
|
22
|
+
export interface TelegramExternalHandlerRegistry {
|
|
23
23
|
/** Schema version of this registry shape. */
|
|
24
24
|
readonly version: 1;
|
|
25
25
|
/**
|
|
@@ -29,17 +29,17 @@ export interface TelegramExternalUpdateRegistry {
|
|
|
29
29
|
* before pi-telegram's own routing. The first interceptor that returns
|
|
30
30
|
* `"consume"` wins and stops the chain for that update.
|
|
31
31
|
*/
|
|
32
|
-
add: (handler:
|
|
32
|
+
add: (handler: TelegramExternalHandler) => () => void;
|
|
33
33
|
/**
|
|
34
34
|
* Run all registered interceptors against an update.
|
|
35
35
|
*
|
|
36
36
|
* Used by pi-telegram's polling runtime; layered extensions should call
|
|
37
|
-
* {@link
|
|
37
|
+
* {@link onTelegramExternalUpdate} or `add` instead of dispatching directly.
|
|
38
38
|
*/
|
|
39
|
-
dispatch: (update: unknown) => Promise<
|
|
39
|
+
dispatch: (update: unknown) => Promise<TelegramExternalHandlerVerdict>;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const REGISTRY_KEY = "
|
|
42
|
+
const REGISTRY_KEY = "__piTelegramExternalHandlerRegistry__";
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Validate that a value on `globalThis` matches the full v1 registry contract.
|
|
@@ -55,9 +55,9 @@ const REGISTRY_KEY = "__piTelegramExternalUpdateRegistry__";
|
|
|
55
55
|
*/
|
|
56
56
|
function isValidV1Registry(
|
|
57
57
|
candidate: unknown,
|
|
58
|
-
): candidate is
|
|
58
|
+
): candidate is TelegramExternalHandlerRegistry {
|
|
59
59
|
if (!candidate || typeof candidate !== "object") return false;
|
|
60
|
-
const r = candidate as Partial<
|
|
60
|
+
const r = candidate as Partial<TelegramExternalHandlerRegistry>;
|
|
61
61
|
return (
|
|
62
62
|
r.version === 1 &&
|
|
63
63
|
typeof r.add === "function" &&
|
|
@@ -65,12 +65,12 @@ function isValidV1Registry(
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function getOrCreateRegistry():
|
|
68
|
+
function getOrCreateRegistry(): TelegramExternalHandlerRegistry {
|
|
69
69
|
const g = globalThis as Record<string, unknown>;
|
|
70
70
|
const existing = g[REGISTRY_KEY];
|
|
71
71
|
if (isValidV1Registry(existing)) return existing;
|
|
72
|
-
const handlers = new Set<
|
|
73
|
-
const registry:
|
|
72
|
+
const handlers = new Set<TelegramExternalHandler>();
|
|
73
|
+
const registry: TelegramExternalHandlerRegistry = {
|
|
74
74
|
version: 1,
|
|
75
75
|
add(handler) {
|
|
76
76
|
handlers.add(handler);
|
|
@@ -95,16 +95,18 @@ function getOrCreateRegistry(): TelegramExternalUpdateRegistry {
|
|
|
95
95
|
/**
|
|
96
96
|
* Called by pi-telegram's own runtime to obtain the registry it dispatches
|
|
97
97
|
* through. Layered extensions should not call this; use
|
|
98
|
-
* {@link
|
|
98
|
+
* {@link onTelegramExternalUpdate} instead.
|
|
99
99
|
*/
|
|
100
|
-
export function
|
|
100
|
+
export function getTelegramExternalHandlerRegistry(): TelegramExternalHandlerRegistry {
|
|
101
101
|
return getOrCreateRegistry();
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export interface
|
|
104
|
+
export interface TelegramExternalHandlerWrapDeps<TUpdate, TContext> {
|
|
105
105
|
defaultHandle: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
106
|
-
registry?:
|
|
106
|
+
registry?: TelegramExternalHandlerRegistry;
|
|
107
107
|
}
|
|
108
|
+
export type TelegramExternalInterceptorWrapDeps<TUpdate, TContext> =
|
|
109
|
+
TelegramExternalHandlerWrapDeps<TUpdate, TContext>;
|
|
108
110
|
|
|
109
111
|
/**
|
|
110
112
|
* Wrap a default polling `handleUpdate` with the external interceptor registry.
|
|
@@ -115,8 +117,8 @@ export interface TelegramExternalInterceptorWrapDeps<TUpdate, TContext> {
|
|
|
115
117
|
* Composition-root callers (pi-telegram's `index.ts`) should use this builder
|
|
116
118
|
* instead of writing the lifting logic inline.
|
|
117
119
|
*/
|
|
118
|
-
export function
|
|
119
|
-
deps:
|
|
120
|
+
export function createTelegramExternalHandleUpdate<TUpdate, TContext>(
|
|
121
|
+
deps: TelegramExternalHandlerWrapDeps<TUpdate, TContext>,
|
|
120
122
|
): (update: TUpdate, ctx: TContext) => Promise<void> {
|
|
121
123
|
const registry = deps.registry ?? getOrCreateRegistry();
|
|
122
124
|
const { defaultHandle } = deps;
|
|
@@ -140,9 +142,9 @@ export function createTelegramInterceptedHandleUpdate<TUpdate, TContext>(
|
|
|
140
142
|
*
|
|
141
143
|
* @example
|
|
142
144
|
* ```ts
|
|
143
|
-
* import {
|
|
145
|
+
* import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
|
|
144
146
|
*
|
|
145
|
-
* const off =
|
|
147
|
+
* const off = onTelegramExternalUpdate(async (update) => {
|
|
146
148
|
* const cb = (update as { callback_query?: { data?: string } }).callback_query;
|
|
147
149
|
* if (!cb?.data?.startsWith("myext:")) return "pass";
|
|
148
150
|
* await handleMyCallback(cb);
|
|
@@ -154,12 +156,12 @@ export function createTelegramInterceptedHandleUpdate<TUpdate, TContext>(
|
|
|
154
156
|
* ```
|
|
155
157
|
*
|
|
156
158
|
* Extensions that prefer zero coupling can also reach the registry directly
|
|
157
|
-
* via `globalThis.
|
|
158
|
-
* see {@link
|
|
159
|
+
* via `globalThis.__piTelegramExternalHandlerRegistry__` (versioned object,
|
|
160
|
+
* see {@link TelegramExternalHandlerRegistry}). This avoids importing
|
|
159
161
|
* `@llblab/pi-telegram` and tolerates either install order.
|
|
160
162
|
*/
|
|
161
|
-
export function
|
|
162
|
-
handler:
|
|
163
|
+
export function onTelegramExternalUpdate(
|
|
164
|
+
handler: TelegramExternalHandler,
|
|
163
165
|
): () => void {
|
|
164
166
|
return getOrCreateRegistry().add(handler);
|
|
165
167
|
}
|