@llblab/pi-telegram 0.9.0 β 0.9.2
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 +17 -0
- package/README.md +4 -4
- package/docs/README.md +1 -0
- package/docs/architecture.md +25 -25
- package/docs/external-update-handlers.md +193 -0
- package/index.ts +4 -4
- package/lib/config.ts +0 -25
- package/lib/external-update-handlers.ts +165 -0
- package/lib/menu-model.ts +14 -2
- package/lib/menu-queue.ts +3 -5
- package/lib/queue.ts +1 -10
- package/lib/updates.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.2: External Update Interceptors
|
|
4
|
+
|
|
5
|
+
- `[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.
|
|
6
|
+
- `[External Update Interceptors]` Validated the full v1 registry shape (`version`, `add`, and `dispatch`) before reusing a pre-existing global registry and documented the zero-coupling bootstrap contract. Impact: install-order interop stays safe even when another extension initializes the registry first.
|
|
7
|
+
- `[Queue Menu]` Non-empty queue lists now keep the `π Refresh` row below queued items, matching the empty-queue surface. Impact: users can manually refresh the queue screen while waiting for changes without navigating away.
|
|
8
|
+
- `[Security]` Refreshed the lockfile to resolve the transitive `basic-ftp` audit advisory. Impact: release validation returns to a clean npm audit state.
|
|
9
|
+
- `[Package]` Bumped package metadata to `0.9.2` and kept the lockfile in sync.
|
|
10
|
+
|
|
11
|
+
## 0.9.1: Model Detail Hotfix
|
|
12
|
+
|
|
13
|
+
- `[Model Menu]` Detail-mode activation now preserves scoped `thinkingLevel` by resolving the selected scoped entry before falling back to the unscoped model list. Impact: scoped model shortcuts opened through the detail submenu keep their reasoning/thinking level.
|
|
14
|
+
- `[Model Menu]` Activating an already active model from the detail submenu now still runs the refresh path that applies scoped thinking changes while returning to the model list. Impact: tapping Active can still correct the thinking level instead of becoming a no-op.
|
|
15
|
+
- `[Proactive Push]` Removed the unused proactive reply-target store and always sends proactive local-result pushes without `reply_to_message_id`. Impact: the runtime no longer carries dead state for a target-capture behavior that does not exist yet.
|
|
16
|
+
- `[Queue Reactions]` Added `π₯` as a priority reaction and `π` as a queue-removal reaction. Impact: the intuitive fire/removal gestures now work alongside the existing reaction controls.
|
|
17
|
+
- `[Docs]` Updated the status-bar example to match the compact active/queued display.
|
|
18
|
+
- `[Package]` Bumped package metadata to `0.9.1` and kept the lockfile in sync.
|
|
19
|
+
|
|
3
20
|
## 0.9.0: Hidden Settings And Proactive Push
|
|
4
21
|
|
|
5
22
|
- `[Settings Menu]` Added hidden Telegram `/settings` with a proactive push checkbox detail submenu plus `/telegram-settings` in the terminal. Impact: operators can see green/black binary flag state, use green/black/yellow On/Off checkbox controls from Telegram, and toggle the same proactive push flag locally without adding a visible bot-command entry.
|
package/README.md
CHANGED
|
@@ -110,8 +110,8 @@ Run these inside Ο, not Telegram:
|
|
|
110
110
|
|
|
111
111
|
- If you send more Telegram messages while Ο is busy, they enter the default prompt queue and are processed in order.
|
|
112
112
|
- Very long text messages that Telegram appears to split automatically are coalesced through a short conservative debounce and forwarded to Ο as one prompt when the first chunk is near Telegram's text limit, currently using a 3600-character threshold. Commands, bot messages, media groups, and normal short follow-ups are not coalesced.
|
|
113
|
-
- `π`, `β‘οΈ`, `β€οΈ`, and
|
|
114
|
-
- `π`, `π»`, `π`, and
|
|
113
|
+
- `π`, `β‘οΈ`, `β€οΈ`, `π`, and `π₯` move a waiting prompt into the priority prompt queue, behind control actions but ahead of default prompts. Removing the last priority reaction sends it back to its normal queue position, and adding a priority reaction again gives it a fresh priority position.
|
|
114
|
+
- `π`, `π»`, `π`, `π©`, and `π` remove a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to removal reactions.
|
|
115
115
|
- Reactions apply to any waiting Telegram turn, including text, voice, files, images, and media groups. For media groups, a reaction on any message in the group applies to the whole queued turn.
|
|
116
116
|
- If you edit a Telegram message while it is still waiting in the queue, the queued turn is updated instead of creating a duplicate prompt. Edits after a turn has already started may not affect the active run.
|
|
117
117
|
- Telegram replies to earlier text or caption messages are forwarded as `[reply]` context for normal prompts, while slash commands still parse from the new message text only.
|
|
@@ -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). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
|
|
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 Update Handlers](./docs/external-update-handlers.md). Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
|
|
228
228
|
|
|
229
229
|
## Streaming
|
|
230
230
|
|
|
@@ -237,7 +237,7 @@ Rich previews are sent through editable messages because Telegram drafts are tex
|
|
|
237
237
|
The Ο status bar shows the current bridge state plus queued Telegram turns as compact previews. Busy labels distinguish states such as `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`. Telegram prompt guidance asks agents to keep tables, dense list items, and compact text blocks within about 37 visible cells when possible so mobile replies stay readable.
|
|
238
238
|
|
|
239
239
|
```text
|
|
240
|
-
telegram
|
|
240
|
+
telegram active +3
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
## Notes
|
package/docs/README.md
CHANGED
|
@@ -10,3 +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-update-handlers.md](./external-update-handlers.md) β Runtime interceptor registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
|
package/docs/architecture.md
CHANGED
|
@@ -23,24 +23,24 @@ Naming rule: because the repository already scopes this codebase to Telegram, ex
|
|
|
23
23
|
|
|
24
24
|
Current runtime areas use these ownership boundaries:
|
|
25
25
|
|
|
26
|
-
| Domain
|
|
27
|
-
|
|
|
28
|
-
| `index.ts`
|
|
29
|
-
| `api`
|
|
30
|
-
| `config` / `setup`
|
|
31
|
-
| `locks` / `polling`
|
|
32
|
-
| `updates` / `routing`
|
|
33
|
-
| `media` / `text-groups` / `turns` / `inbound-handlers`
|
|
34
|
-
| `queue`
|
|
35
|
-
| `runtime`
|
|
26
|
+
| Domain | Owns |
|
|
27
|
+
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `index.ts` | Single composition root for live Ο/Telegram ports, session state, API-bound transport adapters, and status updates |
|
|
29
|
+
| `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
|
|
30
|
+
| `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
|
|
31
|
+
| `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
|
|
32
|
+
| `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
|
|
33
|
+
| `media` / `text-groups` / `turns` / `inbound-handlers` | Text/media extraction, media-group debounce, long-text split coalescing, inbound downloads, inbound text/media handler execution, turn building/editing, image reads, legacy `attachmentHandlers` compatibility |
|
|
34
|
+
| `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
|
|
35
|
+
| `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
|
|
36
36
|
| `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, model-menu UI, thinking-menu UI, status-menu UI, inline application callback composition, queue-menu UI, hidden settings UI, slash commands, bot command registration |
|
|
37
|
-
| `keyboard`
|
|
38
|
-
| `preview` / `replies` / `rendering`
|
|
39
|
-
| `outbound-handlers`
|
|
40
|
-
| `outbound-attachments`
|
|
41
|
-
| `status`
|
|
42
|
-
| `lifecycle` / `prompts` / `prompt-templates` / `pi`
|
|
43
|
-
| `command-templates`
|
|
37
|
+
| `keyboard` | Shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction |
|
|
38
|
+
| `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
|
|
39
|
+
| `outbound-handlers` | Outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery |
|
|
40
|
+
| `outbound-attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
|
|
41
|
+
| `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped Ο diagnostics |
|
|
42
|
+
| `lifecycle` / `prompts` / `prompt-templates` / `pi` | Ο hook registration, Telegram-specific before-agent prompt injection, Ο prompt-template discovery/expansion, centralized direct pi SDK imports and context adapters |
|
|
43
|
+
| `command-templates` | Portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution |
|
|
44
44
|
|
|
45
45
|
Boundary invariants:
|
|
46
46
|
|
|
@@ -95,13 +95,13 @@ Queued items now use two explicit dimensions:
|
|
|
95
95
|
|
|
96
96
|
Admission contract:
|
|
97
97
|
|
|
98
|
-
| Admission | Examples | Queue shape
|
|
99
|
-
| --------------------- | ------------------------------------------------------------ |
|
|
100
|
-
| Immediate execution | `/compact`, `/queue`, `/stop`, `/help`, `/start`
|
|
101
|
-
| Queued prompt command | `/continue`, `/template_name args`
|
|
102
|
-
| Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts
|
|
103
|
-
| Priority prompt queue | A waiting prompt promoted by `π`, `β‘οΈ`, `β€οΈ`, or
|
|
104
|
-
| Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default`
|
|
98
|
+
| Admission | Examples | Queue shape | Dispatch rank |
|
|
99
|
+
| --------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
100
|
+
| Immediate execution | `/compact`, `/queue`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue; `/help` opens the same menu as `/start`; `/stop` also clears queued items | N/A |
|
|
101
|
+
| Queued prompt command | `/continue`, `/template_name args` | `/continue` enqueues a Telegram-owned `continue` prompt; prompt-template commands expand the matching Ο template before entering the normal prompt queue | priority for `/continue`, otherwise default |
|
|
102
|
+
| Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts | 0 |
|
|
103
|
+
| Priority prompt queue | A waiting prompt promoted by `π`, `β‘οΈ`, `β€οΈ`, `π`, or `π₯` | `kind: prompt`, `queueLane: priority` | 1 |
|
|
104
|
+
| Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
|
|
105
105
|
|
|
106
106
|
The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the Ο status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts and priority control items are marked with `β‘`. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
|
|
107
107
|
|
|
@@ -181,7 +181,7 @@ Current operator controls include:
|
|
|
181
181
|
- `/stop` for aborting the active Telegram-owned run and clearing waiting Telegram queue items
|
|
182
182
|
- `/telegram-status` for Ο-side diagnostics as grouped line-by-line sections separated by blank lines: connection, polling, execution, queue, and the recent redacted runtime/API event ring. These sections include polling state, last update id, active turn source ids, pending dispatch, compaction state, active tool count, pending model-switch state, total queue depth, and queue-lane counts. The event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures; benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures
|
|
183
183
|
- `/telegram-settings` for Ο-side bridge settings; it currently exposes proactive push as a local toggle backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu
|
|
184
|
-
- Queue reactions apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids: `π`, `β‘οΈ`, `β€οΈ`, and
|
|
184
|
+
- Queue reactions apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids: `π`, `β‘οΈ`, `β€οΈ`, `π`, and `π₯` promote waiting prompts, while `π`, `π»`, `π`, `π©`, and `π` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
|
|
185
185
|
|
|
186
186
|
## In-Flight Model Switching
|
|
187
187
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# External Update Handlers
|
|
2
|
+
|
|
3
|
+
`pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other
|
|
4
|
+
pi extensions cannot open a competing poller against the same bot β the
|
|
5
|
+
Telegram Bot API uses a per-bot `offset` cursor, and two pollers race each
|
|
6
|
+
other and lose updates.
|
|
7
|
+
|
|
8
|
+
This document describes the registry that lets layered pi extensions
|
|
9
|
+
(running in the same pi process) hook into `pi-telegram`'s polling loop and
|
|
10
|
+
react to inbound Telegram updates **before** `pi-telegram`'s default routing
|
|
11
|
+
fires.
|
|
12
|
+
|
|
13
|
+
It is the runtime counterpart to
|
|
14
|
+
[Callback Namespaces](./callback-namespaces.md): callback namespaces define
|
|
15
|
+
how to share `callback_data` cleanly; external update handlers define how to
|
|
16
|
+
observe and optionally short-circuit the dispatch of those updates.
|
|
17
|
+
|
|
18
|
+
## When to use it
|
|
19
|
+
|
|
20
|
+
Use it when a layered extension needs to:
|
|
21
|
+
|
|
22
|
+
- Resolve out-of-band state (for example, a `tool_call` approval Promise)
|
|
23
|
+
the moment a Telegram callback arrives, rather than waiting for the next
|
|
24
|
+
agent turn.
|
|
25
|
+
- Suppress `pi-telegram`'s default routing for callbacks owned by the
|
|
26
|
+
layered extension (so `pi-telegram` does not also forward them as
|
|
27
|
+
`[callback] <data>` text).
|
|
28
|
+
- Observe arbitrary update types (messages, edits, channel posts, reactions)
|
|
29
|
+
without owning the polling connection.
|
|
30
|
+
|
|
31
|
+
If the layered extension only needs to read assistant-visible callbacks, the
|
|
32
|
+
existing `[callback] <data>` fallback documented in
|
|
33
|
+
[Callback Namespaces](./callback-namespaces.md) is enough.
|
|
34
|
+
|
|
35
|
+
## Constraints
|
|
36
|
+
|
|
37
|
+
- One bot, one pi process, one `getUpdates` poller. This registry does **not**
|
|
38
|
+
enable running multiple pi instances against the same bot.
|
|
39
|
+
- Interceptors run in the polling loop. They must return quickly; long
|
|
40
|
+
awaits delay subsequent updates.
|
|
41
|
+
- Interceptor errors are caught and logged silently so polling never breaks.
|
|
42
|
+
If you need durable error reporting, do it inside your interceptor.
|
|
43
|
+
- The registry lives on `globalThis`. Module instance identity is not
|
|
44
|
+
required, so layered extensions can reach it without importing
|
|
45
|
+
`@llblab/pi-telegram`.
|
|
46
|
+
|
|
47
|
+
## Verdicts
|
|
48
|
+
|
|
49
|
+
Each interceptor returns one of:
|
|
50
|
+
|
|
51
|
+
- `"consume"` β `pi-telegram` skips its default routing for this update.
|
|
52
|
+
- `"pass"` (or `void` / `undefined`) β `pi-telegram` routes the update
|
|
53
|
+
normally. Other interceptors registered after this one still run for the
|
|
54
|
+
same update.
|
|
55
|
+
|
|
56
|
+
The first interceptor that returns `"consume"` wins; later interceptors are
|
|
57
|
+
not called for that update.
|
|
58
|
+
|
|
59
|
+
## Registering an interceptor
|
|
60
|
+
|
|
61
|
+
Two equivalent paths.
|
|
62
|
+
|
|
63
|
+
### Typed import (recommended when you can depend on `@llblab/pi-telegram`)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { onTelegramUpdate } from "@llblab/pi-telegram/lib/external-update-handlers.ts";
|
|
67
|
+
|
|
68
|
+
const off = onTelegramUpdate(async (update) => {
|
|
69
|
+
const cb = (update as { callback_query?: { id?: string; data?: string } })
|
|
70
|
+
.callback_query;
|
|
71
|
+
if (!cb?.data?.startsWith("myext:")) return "pass";
|
|
72
|
+
await resolveMyApproval(cb);
|
|
73
|
+
return "consume";
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Later, when your extension shuts down:
|
|
77
|
+
off();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Zero-coupling globalThis lookup
|
|
81
|
+
|
|
82
|
+
When the layered extension prefers no `import` from `@llblab/pi-telegram` (so
|
|
83
|
+
load order between the two extensions does not matter, and either can be
|
|
84
|
+
installed first), it must implement the **full v1 registry contract**, not
|
|
85
|
+
just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on
|
|
86
|
+
whatever object it finds at `globalThis.__piTelegramExternalUpdateRegistry__`,
|
|
87
|
+
so a partial object would silently break the first update.
|
|
88
|
+
|
|
89
|
+
pi-telegram defensively re-creates the registry if the object on `globalThis`
|
|
90
|
+
is missing `add` or `dispatch` (validated as `version === 1`,
|
|
91
|
+
`typeof add === "function"`, `typeof dispatch === "function"`). Handlers
|
|
92
|
+
registered against a malformed object are dropped β make sure your bootstrap
|
|
93
|
+
implements all three fields.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
type PiTelegramVerdict =
|
|
97
|
+
| "consume"
|
|
98
|
+
| "pass"
|
|
99
|
+
| void
|
|
100
|
+
| Promise<"consume" | "pass" | void>;
|
|
101
|
+
type PiTelegramInterceptor = (update: unknown) => PiTelegramVerdict;
|
|
102
|
+
|
|
103
|
+
interface PiTelegramExternalUpdateRegistry {
|
|
104
|
+
readonly version: 1;
|
|
105
|
+
add: (handler: PiTelegramInterceptor) => () => void;
|
|
106
|
+
// Required: pi-telegram's polling loop calls this on every update.
|
|
107
|
+
dispatch: (update: unknown) => Promise<"consume" | "pass">;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const REGISTRY_KEY = "__piTelegramExternalUpdateRegistry__";
|
|
111
|
+
|
|
112
|
+
function getOrCreateRegistry(): PiTelegramExternalUpdateRegistry {
|
|
113
|
+
const g = globalThis as Record<string, unknown>;
|
|
114
|
+
const existing = g[REGISTRY_KEY] as
|
|
115
|
+
| PiTelegramExternalUpdateRegistry
|
|
116
|
+
| undefined;
|
|
117
|
+
if (
|
|
118
|
+
existing &&
|
|
119
|
+
existing.version === 1 &&
|
|
120
|
+
typeof existing.add === "function" &&
|
|
121
|
+
typeof existing.dispatch === "function"
|
|
122
|
+
) {
|
|
123
|
+
return existing;
|
|
124
|
+
}
|
|
125
|
+
const handlers = new Set<PiTelegramInterceptor>();
|
|
126
|
+
const registry: PiTelegramExternalUpdateRegistry = {
|
|
127
|
+
version: 1,
|
|
128
|
+
add(handler) {
|
|
129
|
+
handlers.add(handler);
|
|
130
|
+
return () => handlers.delete(handler);
|
|
131
|
+
},
|
|
132
|
+
async dispatch(update) {
|
|
133
|
+
for (const handler of handlers) {
|
|
134
|
+
try {
|
|
135
|
+
const result = await handler(update);
|
|
136
|
+
if (result === "consume") return "consume";
|
|
137
|
+
} catch {
|
|
138
|
+
// Never break polling because of an interceptor error.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return "pass";
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
g[REGISTRY_KEY] = registry;
|
|
145
|
+
return registry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const off = getOrCreateRegistry().add((update) => {
|
|
149
|
+
/* β¦ */
|
|
150
|
+
return "pass";
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The registry object on `globalThis.__piTelegramExternalUpdateRegistry__` is
|
|
155
|
+
versioned (`version: 1`) and stable across pi-telegram releases; future
|
|
156
|
+
breaking changes will use a new schema version and a new key.
|
|
157
|
+
|
|
158
|
+
## Interaction with built-in routing
|
|
159
|
+
|
|
160
|
+
`pi-telegram` invokes registered interceptors first, then routes the update
|
|
161
|
+
through its own handlers (commands, app menu, queue menu, model menu,
|
|
162
|
+
default prompt routing, callback namespace fallback). If any interceptor
|
|
163
|
+
returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
|
|
164
|
+
|
|
165
|
+
This means:
|
|
166
|
+
|
|
167
|
+
- Extensions can claim callback namespaces that `pi-telegram` would
|
|
168
|
+
otherwise forward as `[callback] <data>` text.
|
|
169
|
+
- Extensions can observe (but not consume) updates by always returning
|
|
170
|
+
`"pass"`.
|
|
171
|
+
- Extensions must not consume updates that belong to `pi-telegram`'s own
|
|
172
|
+
prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`)
|
|
173
|
+
unless they are deliberately replacing that behavior.
|
|
174
|
+
|
|
175
|
+
## Ownership semantics
|
|
176
|
+
|
|
177
|
+
The interceptor registry is ownership-agnostic and does not interact with
|
|
178
|
+
the `locks.json` singleton lock documented in [Locks](./locks.md). When the
|
|
179
|
+
locked polling runtime stops `pi-telegram`'s poller (for example, after
|
|
180
|
+
ownership is moved to another pi process), interceptors stop receiving
|
|
181
|
+
updates because no updates are being fetched. They are not unregistered.
|
|
182
|
+
|
|
183
|
+
If a layered extension needs to react to ownership changes, it should
|
|
184
|
+
observe `pi-telegram` lifecycle events through the standard pi extension
|
|
185
|
+
hooks rather than through the interceptor registry.
|
|
186
|
+
|
|
187
|
+
## Not a multiplexer
|
|
188
|
+
|
|
189
|
+
This registry does not multiplex one bot across multiple pi processes, and
|
|
190
|
+
it does not bypass Telegram's single-poller-per-bot constraint. To run
|
|
191
|
+
multiple pi instances on Telegram, give each instance its own bot and its
|
|
192
|
+
own `~/.pi/agent` directory; the registry is for layered extensions inside
|
|
193
|
+
**one** pi process.
|
package/index.ts
CHANGED
|
@@ -8,6 +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 { createTelegramInterceptedHandleUpdate } from "./lib/external-update-handlers.ts";
|
|
11
12
|
import * as InboundHandlers from "./lib/inbound-handlers.ts";
|
|
12
13
|
import * as Keyboard from "./lib/keyboard.ts";
|
|
13
14
|
import * as Lifecycle from "./lib/lifecycle.ts";
|
|
@@ -53,8 +54,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
53
54
|
Config.createTelegramProactivePushChecker(configStore);
|
|
54
55
|
const setProactivePushEnabled =
|
|
55
56
|
Config.createTelegramProactivePushSetter(configStore);
|
|
56
|
-
const proactivePromptTargetStore =
|
|
57
|
-
Config.createTelegramProactivePromptTargetStore();
|
|
58
57
|
const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
|
|
59
58
|
const lockOwnershipGuard =
|
|
60
59
|
Locks.createTelegramLockOwnershipGuard(lockRuntime);
|
|
@@ -360,7 +359,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
360
359
|
deleteWebhook,
|
|
361
360
|
getUpdates,
|
|
362
361
|
persistConfig: configStore.persist,
|
|
363
|
-
handleUpdate:
|
|
362
|
+
handleUpdate: createTelegramInterceptedHandleUpdate({
|
|
363
|
+
defaultHandle: inboundRouteRuntime.handleUpdate,
|
|
364
|
+
}),
|
|
364
365
|
stopTypingLoop: typing.stop,
|
|
365
366
|
updateStatus,
|
|
366
367
|
recordRuntimeEvent,
|
|
@@ -496,7 +497,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
496
497
|
sendOutboundReplyArtifacts: outboundReplyArtifactSender,
|
|
497
498
|
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
|
498
499
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
499
|
-
consumeProactiveReplyToMessageId: proactivePromptTargetStore.consumeForChat,
|
|
500
500
|
isProactivePushEnabled,
|
|
501
501
|
recordRuntimeEvent,
|
|
502
502
|
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
package/lib/config.ts
CHANGED
|
@@ -148,31 +148,6 @@ export function createTelegramProactivePushChatIdGetter(deps: {
|
|
|
148
148
|
return () => deps.getActiveTurnChatId() ?? deps.getAllowedUserId();
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
export interface TelegramProactivePromptTarget {
|
|
152
|
-
chatId: number;
|
|
153
|
-
messageId: number;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface TelegramProactivePromptTargetStore {
|
|
157
|
-
set: (target: TelegramProactivePromptTarget) => void;
|
|
158
|
-
consumeForChat: (chatId: number) => number | undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function createTelegramProactivePromptTargetStore(): TelegramProactivePromptTargetStore {
|
|
162
|
-
let target: TelegramProactivePromptTarget | undefined;
|
|
163
|
-
return {
|
|
164
|
-
set: (nextTarget) => {
|
|
165
|
-
target = nextTarget;
|
|
166
|
-
},
|
|
167
|
-
consumeForChat: (chatId) => {
|
|
168
|
-
if (!target || target.chatId !== chatId) return undefined;
|
|
169
|
-
const messageId = target.messageId;
|
|
170
|
-
target = undefined;
|
|
171
|
-
return messageId;
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
151
|
export type TelegramAuthorizationState =
|
|
177
152
|
| { kind: "pair"; userId: number }
|
|
178
153
|
| { kind: "allow" }
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External Telegram update interceptor registry
|
|
3
|
+
* Zones: telegram transport, layered extension interop
|
|
4
|
+
* Lets other pi extensions hook into the polling loop without owning their own getUpdates connection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Verdict returned by an interceptor.
|
|
9
|
+
*
|
|
10
|
+
* - `"consume"` β the interceptor handled this update; pi-telegram skips default routing.
|
|
11
|
+
* - `"pass"` (or `void`/`undefined`) β pi-telegram routes the update normally.
|
|
12
|
+
*/
|
|
13
|
+
export type TelegramExternalUpdateVerdict = "consume" | "pass";
|
|
14
|
+
|
|
15
|
+
export type TelegramExternalUpdateInterceptor = (
|
|
16
|
+
update: unknown,
|
|
17
|
+
) =>
|
|
18
|
+
| TelegramExternalUpdateVerdict
|
|
19
|
+
| void
|
|
20
|
+
| Promise<TelegramExternalUpdateVerdict | void>;
|
|
21
|
+
|
|
22
|
+
export interface TelegramExternalUpdateRegistry {
|
|
23
|
+
/** Schema version of this registry shape. */
|
|
24
|
+
readonly version: 1;
|
|
25
|
+
/**
|
|
26
|
+
* Register an interceptor. Returns a disposer that removes it.
|
|
27
|
+
*
|
|
28
|
+
* Interceptors are invoked in registration order on every Telegram update,
|
|
29
|
+
* before pi-telegram's own routing. The first interceptor that returns
|
|
30
|
+
* `"consume"` wins and stops the chain for that update.
|
|
31
|
+
*/
|
|
32
|
+
add: (handler: TelegramExternalUpdateInterceptor) => () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Run all registered interceptors against an update.
|
|
35
|
+
*
|
|
36
|
+
* Used by pi-telegram's polling runtime; layered extensions should call
|
|
37
|
+
* {@link onTelegramUpdate} or `add` instead of dispatching directly.
|
|
38
|
+
*/
|
|
39
|
+
dispatch: (update: unknown) => Promise<TelegramExternalUpdateVerdict>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const REGISTRY_KEY = "__piTelegramExternalUpdateRegistry__";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate that a value on `globalThis` matches the full v1 registry contract.
|
|
46
|
+
*
|
|
47
|
+
* pi-telegram's polling runtime invokes `dispatch`, so a partial object that
|
|
48
|
+
* only carries `version` and `add` (which an early draft of the zero-coupling
|
|
49
|
+
* docs showed) would silently break the first update. We treat any object
|
|
50
|
+
* tagged `version === 1` but missing required methods as malformed and
|
|
51
|
+
* replace it with a fresh, fully-formed registry. Layered extensions that
|
|
52
|
+
* follow the full documented shape are unaffected; ones that don't lose any
|
|
53
|
+
* handlers they registered against the malformed object, which is the
|
|
54
|
+
* desired fail-loud-during-development behavior.
|
|
55
|
+
*/
|
|
56
|
+
function isValidV1Registry(
|
|
57
|
+
candidate: unknown,
|
|
58
|
+
): candidate is TelegramExternalUpdateRegistry {
|
|
59
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
60
|
+
const r = candidate as Partial<TelegramExternalUpdateRegistry>;
|
|
61
|
+
return (
|
|
62
|
+
r.version === 1 &&
|
|
63
|
+
typeof r.add === "function" &&
|
|
64
|
+
typeof r.dispatch === "function"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getOrCreateRegistry(): TelegramExternalUpdateRegistry {
|
|
69
|
+
const g = globalThis as Record<string, unknown>;
|
|
70
|
+
const existing = g[REGISTRY_KEY];
|
|
71
|
+
if (isValidV1Registry(existing)) return existing;
|
|
72
|
+
const handlers = new Set<TelegramExternalUpdateInterceptor>();
|
|
73
|
+
const registry: TelegramExternalUpdateRegistry = {
|
|
74
|
+
version: 1,
|
|
75
|
+
add(handler) {
|
|
76
|
+
handlers.add(handler);
|
|
77
|
+
return () => handlers.delete(handler);
|
|
78
|
+
},
|
|
79
|
+
async dispatch(update) {
|
|
80
|
+
for (const handler of handlers) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await handler(update);
|
|
83
|
+
if (result === "consume") return "consume";
|
|
84
|
+
} catch {
|
|
85
|
+
// External handler errors must not break polling.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return "pass";
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
g[REGISTRY_KEY] = registry;
|
|
92
|
+
return registry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Called by pi-telegram's own runtime to obtain the registry it dispatches
|
|
97
|
+
* through. Layered extensions should not call this; use
|
|
98
|
+
* {@link onTelegramUpdate} instead.
|
|
99
|
+
*/
|
|
100
|
+
export function getTelegramExternalUpdateRegistry(): TelegramExternalUpdateRegistry {
|
|
101
|
+
return getOrCreateRegistry();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TelegramExternalInterceptorWrapDeps<TUpdate, TContext> {
|
|
105
|
+
defaultHandle: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
106
|
+
registry?: TelegramExternalUpdateRegistry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wrap a default polling `handleUpdate` with the external interceptor registry.
|
|
111
|
+
*
|
|
112
|
+
* Returned function dispatches `update` through registered interceptors first;
|
|
113
|
+
* if any returns `"consume"`, default routing is skipped for that update.
|
|
114
|
+
*
|
|
115
|
+
* Composition-root callers (pi-telegram's `index.ts`) should use this builder
|
|
116
|
+
* instead of writing the lifting logic inline.
|
|
117
|
+
*/
|
|
118
|
+
export function createTelegramInterceptedHandleUpdate<TUpdate, TContext>(
|
|
119
|
+
deps: TelegramExternalInterceptorWrapDeps<TUpdate, TContext>,
|
|
120
|
+
): (update: TUpdate, ctx: TContext) => Promise<void> {
|
|
121
|
+
const registry = deps.registry ?? getOrCreateRegistry();
|
|
122
|
+
const { defaultHandle } = deps;
|
|
123
|
+
return async function handleInterceptedUpdate(update, ctx) {
|
|
124
|
+
const verdict = await registry.dispatch(update);
|
|
125
|
+
if (verdict === "consume") return;
|
|
126
|
+
await defaultHandle(update, ctx);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Register an interceptor that runs before pi-telegram routes a Telegram
|
|
132
|
+
* update through its built-in handlers (commands, app menu, queue menu,
|
|
133
|
+
* model menu, default prompt routing).
|
|
134
|
+
*
|
|
135
|
+
* This is the recommended public surface for layered extensions that share
|
|
136
|
+
* the same bot and pi process with pi-telegram (single bot β single
|
|
137
|
+
* `getUpdates` poller).
|
|
138
|
+
*
|
|
139
|
+
* Returns a disposer that removes the interceptor.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { onTelegramUpdate } from "@llblab/pi-telegram/lib/external-update-handlers.ts";
|
|
144
|
+
*
|
|
145
|
+
* const off = onTelegramUpdate(async (update) => {
|
|
146
|
+
* const cb = (update as { callback_query?: { data?: string } }).callback_query;
|
|
147
|
+
* if (!cb?.data?.startsWith("myext:")) return "pass";
|
|
148
|
+
* await handleMyCallback(cb);
|
|
149
|
+
* return "consume"; // skip pi-telegram's default routing for this update
|
|
150
|
+
* });
|
|
151
|
+
*
|
|
152
|
+
* // later, e.g. on session shutdown:
|
|
153
|
+
* off();
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* Extensions that prefer zero coupling can also reach the registry directly
|
|
157
|
+
* via `globalThis.__piTelegramExternalUpdateRegistry__` (versioned object,
|
|
158
|
+
* see {@link TelegramExternalUpdateRegistry}). This avoids importing
|
|
159
|
+
* `@llblab/pi-telegram` and tolerates either install order.
|
|
160
|
+
*/
|
|
161
|
+
export function onTelegramUpdate(
|
|
162
|
+
handler: TelegramExternalUpdateInterceptor,
|
|
163
|
+
): () => void {
|
|
164
|
+
return getOrCreateRegistry().add(handler);
|
|
165
|
+
}
|
package/lib/menu-model.ts
CHANGED
|
@@ -620,6 +620,19 @@ export function applyTelegramModelDetailSelection(
|
|
|
620
620
|
export function getTelegramSelectedDetailModel<
|
|
621
621
|
TModel extends MenuModel = MenuModel,
|
|
622
622
|
>(state: TelegramModelMenuState<TModel>): TelegramMenuSelectionResult<TModel> {
|
|
623
|
+
const indexedSelection = getTelegramModelSelection(
|
|
624
|
+
state,
|
|
625
|
+
state.selectedModelIndex?.toString(),
|
|
626
|
+
);
|
|
627
|
+
if (indexedSelection.kind === "selected") {
|
|
628
|
+
if (!state.selectedModelKey) return indexedSelection;
|
|
629
|
+
const indexedKey = getCanonicalModelId(
|
|
630
|
+
indexedSelection.selection.model,
|
|
631
|
+
).toLowerCase();
|
|
632
|
+
if (indexedKey === state.selectedModelKey.toLowerCase()) {
|
|
633
|
+
return indexedSelection;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
623
636
|
if (state.selectedModelKey) {
|
|
624
637
|
const lowerKey = state.selectedModelKey.toLowerCase();
|
|
625
638
|
const selection = state.allModels.find(
|
|
@@ -627,7 +640,7 @@ export function getTelegramSelectedDetailModel<
|
|
|
627
640
|
);
|
|
628
641
|
if (selection) return { kind: "selected", selection };
|
|
629
642
|
}
|
|
630
|
-
return
|
|
643
|
+
return indexedSelection;
|
|
631
644
|
}
|
|
632
645
|
|
|
633
646
|
export function isTelegramModelScoped(
|
|
@@ -817,7 +830,6 @@ export function buildTelegramModelCallbackPlan<
|
|
|
817
830
|
if (modelsMatch(selection.model, params.activeModel)) {
|
|
818
831
|
if (action.action === "pick-selected") {
|
|
819
832
|
focusTelegramModelListPage(params.state, selection.model);
|
|
820
|
-
return { kind: "update-menu", text: `Model: ${selection.model.id}` };
|
|
821
833
|
}
|
|
822
834
|
return {
|
|
823
835
|
kind: "refresh-status",
|
package/lib/menu-queue.ts
CHANGED
|
@@ -57,10 +57,8 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
57
57
|
items: readonly TelegramQueueMenuItem[],
|
|
58
58
|
): TelegramQueueMenuReplyMarkup {
|
|
59
59
|
const backRow = [{ text: "β¬οΈ Main menu", callback_data: "menu:back" }];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return { inline_keyboard: [backRow, refreshRow] };
|
|
63
|
-
}
|
|
60
|
+
const refreshRow = [{ text: "π Refresh", callback_data: "queue:refresh" }];
|
|
61
|
+
if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
|
|
64
62
|
const rows = items.map(function buildTelegramQueueMenuRow(item, index) {
|
|
65
63
|
const prefix = item.isPriority
|
|
66
64
|
? `${item.priorityEmoji ?? "β‘"} `
|
|
@@ -75,7 +73,7 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
75
73
|
},
|
|
76
74
|
];
|
|
77
75
|
});
|
|
78
|
-
return { inline_keyboard: [backRow, ...rows] };
|
|
76
|
+
return { inline_keyboard: [backRow, ...rows, refreshRow] };
|
|
79
77
|
}
|
|
80
78
|
function findTelegramQueueItem<Context>(
|
|
81
79
|
items: readonly Queue.TelegramQueueItem<Context>[],
|
package/lib/queue.ts
CHANGED
|
@@ -794,7 +794,6 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
794
794
|
options?: { replyToPrompt?: boolean },
|
|
795
795
|
) => Promise<void>;
|
|
796
796
|
getDefaultChatId?: () => number | undefined;
|
|
797
|
-
consumeProactiveReplyToMessageId?: (chatId: number) => number | undefined;
|
|
798
797
|
isProactivePushEnabled?: () => boolean;
|
|
799
798
|
recordRuntimeEvent?: (
|
|
800
799
|
category: string,
|
|
@@ -839,7 +838,6 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
839
838
|
>["planOutboundReply"];
|
|
840
839
|
sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
|
|
841
840
|
getDefaultChatId?: TelegramAgentEndRuntimeDeps<TTurn>["getDefaultChatId"];
|
|
842
|
-
consumeProactiveReplyToMessageId?: TelegramAgentEndRuntimeDeps<TTurn>["consumeProactiveReplyToMessageId"];
|
|
843
841
|
isProactivePushEnabled?: TelegramAgentEndRuntimeDeps<TTurn>["isProactivePushEnabled"];
|
|
844
842
|
recordRuntimeEvent?: TelegramAgentEndRuntimeDeps<TTurn>["recordRuntimeEvent"];
|
|
845
843
|
}
|
|
@@ -957,7 +955,6 @@ export function createTelegramAgentEndHook<
|
|
|
957
955
|
planOutboundReply: deps.planOutboundReply,
|
|
958
956
|
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
959
957
|
getDefaultChatId: deps.getDefaultChatId,
|
|
960
|
-
consumeProactiveReplyToMessageId: deps.consumeProactiveReplyToMessageId,
|
|
961
958
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
962
959
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
963
960
|
});
|
|
@@ -998,14 +995,8 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
998
995
|
) {
|
|
999
996
|
const defaultChatId = deps.getDefaultChatId?.();
|
|
1000
997
|
if (defaultChatId !== undefined) {
|
|
1001
|
-
const replyToMessageId =
|
|
1002
|
-
deps.consumeProactiveReplyToMessageId?.(defaultChatId);
|
|
1003
998
|
try {
|
|
1004
|
-
await deps.sendMarkdownReply(
|
|
1005
|
-
defaultChatId,
|
|
1006
|
-
replyToMessageId,
|
|
1007
|
-
finalText,
|
|
1008
|
-
);
|
|
999
|
+
await deps.sendMarkdownReply(defaultChatId, undefined, finalText);
|
|
1009
1000
|
} catch (error) {
|
|
1010
1001
|
deps.recordRuntimeEvent?.("proactive-push", error, {
|
|
1011
1002
|
chatId: defaultChatId,
|
package/lib/updates.ts
CHANGED
|
@@ -31,12 +31,14 @@ export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
|
31
31
|
{ id: 11, name: "lightning", emoji: "β‘" },
|
|
32
32
|
{ id: 12, name: "heart", emoji: "β€" },
|
|
33
33
|
{ id: 13, name: "dove", emoji: "π" },
|
|
34
|
+
{ id: 14, name: "fire", emoji: "π₯" },
|
|
34
35
|
] as const;
|
|
35
36
|
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
36
37
|
{ id: 20, name: "dislike", emoji: "π" },
|
|
37
38
|
{ id: 21, name: "ghost", emoji: "π»" },
|
|
38
39
|
{ id: 22, name: "broken-heart", emoji: "π" },
|
|
39
40
|
{ id: 23, name: "poop", emoji: "π©" },
|
|
41
|
+
{ id: 24, name: "wastebasket", emoji: "π" },
|
|
40
42
|
] as const;
|
|
41
43
|
export const TELEGRAM_PRIORITY_REACTION_EMOJIS =
|
|
42
44
|
TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|