stitchkit 0.100.2 → 0.100.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 +86 -0
- package/README.md +2 -1
- package/dist/entrypoints/agent-runtime/browser.js +119 -119
- package/dist/entrypoints/agent-runtime/coding-tools.js +15 -20
- package/dist/entrypoints/agent-runtime/harness.js +55 -63
- package/dist/entrypoints/agent-runtime/openrouter.js +6 -6
- package/dist/entrypoints/agent-runtime/sandbox.js +20 -25
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +11 -13
- package/dist/entrypoints/agent-runtime/sqlite/node.js +11 -13
- package/dist/entrypoints/agent-runtime/testing.js +32 -34
- package/dist/entrypoints/agent-runtime.js +395 -402
- package/dist/entrypoints/application/diagnostic-journal.js +26 -27
- package/dist/entrypoints/application/directory-inbox.js +16 -17
- package/dist/entrypoints/application/grammy.js +13 -13
- package/dist/entrypoints/application/opentelemetry.js +13 -13
- package/dist/entrypoints/application/schemas.js +24 -24
- package/dist/entrypoints/application.js +208 -210
- package/dist/entrypoints/bin/upgrade-cli.js +2 -2
- package/dist/entrypoints/cli.js +81 -84
- package/dist/entrypoints/contract.js +54 -57
- package/dist/entrypoints/declaration.js +22 -22
- package/dist/entrypoints/files.js +18 -21
- package/dist/entrypoints/geo.js +7 -8
- package/dist/entrypoints/google.js +2 -2
- package/dist/entrypoints/index.js +150 -154
- package/dist/entrypoints/live.js +45 -47
- package/dist/entrypoints/node.js +68 -74
- package/dist/entrypoints/oauth.js +6 -6
- package/dist/entrypoints/observability.d.ts +1 -0
- package/dist/entrypoints/observability.d.ts.map +1 -1
- package/dist/entrypoints/observability.js +115 -87
- package/dist/entrypoints/primitives.js +35 -35
- package/dist/entrypoints/react/keyboard.d.ts +4 -0
- package/dist/entrypoints/react/keyboard.d.ts.map +1 -0
- package/dist/entrypoints/react/keyboard.js +397 -0
- package/dist/entrypoints/react.js +10 -10
- package/dist/entrypoints/release.js +7 -7
- package/dist/entrypoints/remote.js +15 -17
- package/dist/entrypoints/server.js +198 -200
- package/dist/entrypoints/telegram/html.d.ts +5 -0
- package/dist/entrypoints/telegram/html.d.ts.map +1 -0
- package/dist/entrypoints/telegram/html.js +26 -0
- package/dist/entrypoints/telegram/screens.d.ts +14 -0
- package/dist/entrypoints/telegram/screens.d.ts.map +1 -0
- package/dist/entrypoints/telegram/screens.js +2028 -0
- package/dist/entrypoints/telegram.d.ts +5 -1
- package/dist/entrypoints/telegram.d.ts.map +1 -1
- package/dist/entrypoints/telegram.js +548 -156
- package/dist/entrypoints/testing.js +75 -79
- package/dist/entrypoints/tools/connections.js +30 -32
- package/dist/entrypoints/tools/contract.js +18 -20
- package/dist/entrypoints/tools/invoker.js +12 -17
- package/dist/entrypoints/tools.js +223 -229
- package/dist/entrypoints/tracking/server.js +6 -6
- package/dist/entrypoints/tracking.js +40 -42
- package/dist/entrypoints/voice.js +3 -3
- package/dist/{index-eaysmdtq.js → index-0ndk4555.js} +56 -56
- package/dist/{index-2bxbyab0.js → index-1bx1xv1k.js} +1 -1
- package/dist/{index-p5r7qpdd.js → index-1eeqpadm.js} +4 -4
- package/dist/{index-hadmp193.js → index-3ct7hhq6.js} +210 -210
- package/dist/{index-e01r054k.js → index-45tdvqa3.js} +26 -26
- package/dist/{index-dce5wxcq.js → index-4s0fep5q.js} +8 -8
- package/dist/index-50ejfz75.js +9 -0
- package/dist/{index-m9zn6cpq.js → index-5107q189.js} +12 -12
- package/dist/{index-sbdmyz75.js → index-5cc6r990.js} +3 -3
- package/dist/index-6k1svmjh.js +141 -0
- package/dist/{index-c6bxbsnc.js → index-7kxw1awn.js} +2 -2
- package/dist/{index-0r0fbk7z.js → index-7z9ym7t8.js} +28 -28
- package/dist/{index-p7hgdyhb.js → index-814xpr6d.js} +2 -2
- package/dist/{index-gtj3zc53.js → index-8kzct1d6.js} +99 -99
- package/dist/{index-1zbbzgfh.js → index-9pw0a51a.js} +4 -4
- package/dist/{index-79z7m7dt.js → index-a9pd417t.js} +35 -35
- package/dist/{index-ham5sz8m.js → index-aaj97r4y.js} +45 -45
- package/dist/{index-s812ycat.js → index-aan6hqb9.js} +3 -3
- package/dist/{index-10x2hbx0.js → index-adhgd4pe.js} +44 -44
- package/dist/{index-2gxzjvyr.js → index-afjvjyck.js} +23 -23
- package/dist/{index-qmy6zq75.js → index-ahvsksw5.js} +16 -16
- package/dist/{index-hr4tzbwq.js → index-ap3gqe56.js} +3 -3
- package/dist/{index-znfs20ss.js → index-b2bz0kdm.js} +16 -16
- package/dist/{index-r066jx76.js → index-c6aagrbd.js} +148 -120
- package/dist/{index-k30h66yd.js → index-ck71tbcf.js} +9 -9
- package/dist/{index-eqqvgdb3.js → index-ct75c3at.js} +2 -2
- package/dist/{index-gw8bmyaj.js → index-djjhhgjk.js} +41 -41
- package/dist/{index-8qt2m66q.js → index-dvwj1f43.js} +43 -43
- package/dist/{index-fhwqwxe0.js → index-ej331hah.js} +1 -1
- package/dist/{index-9t3dffv2.js → index-ewbaywk5.js} +5 -5
- package/dist/{index-7p1f0vp2.js → index-fyphpknh.js} +25 -25
- package/dist/{index-83bdg97s.js → index-g149e9pk.js} +1 -1
- package/dist/{index-ngfhvfpd.js → index-g6a3na27.js} +4 -4
- package/dist/{index-qwhynv6x.js → index-h124epcf.js} +20 -20
- package/dist/{index-h6eeys1v.js → index-h97tqqp1.js} +9 -9
- package/dist/{index-zcgf3gqf.js → index-hby4qhax.js} +10 -10
- package/dist/{index-5jrd0yng.js → index-j8aw2c0e.js} +4 -4
- package/dist/{index-d8m8zpd8.js → index-jzczzcsb.js} +14 -14
- package/dist/index-kqnd6a85.js +26 -0
- package/dist/{index-35mh5bhj.js → index-kw1ws0cs.js} +12 -12
- package/dist/{index-yw4fzmdz.js → index-kzz6rq0w.js} +28 -17
- package/dist/{index-4gcnakhx.js → index-m38ej7e5.js} +2 -2
- package/dist/{index-ndddxtyx.js → index-m79yr938.js} +19 -19
- package/dist/{index-5p2m7we1.js → index-m9fndta6.js} +11 -11
- package/dist/index-mnkk1ffz.js +150 -0
- package/dist/index-n15jny5b.js +4 -0
- package/dist/{index-2vtcmrj9.js → index-n53mswt3.js} +20 -20
- package/dist/{index-bt14acb5.js → index-n5yqxd3d.js} +21 -21
- package/dist/{index-bm5rx7n6.js → index-n9tyz8z3.js} +12 -12
- package/dist/index-nmaqxdx7.js +463 -0
- package/dist/{index-mqv3b5rh.js → index-nx6eazt0.js} +30 -30
- package/dist/{index-3z73fh2c.js → index-q0th8q0z.js} +9 -9
- package/dist/{index-v260df89.js → index-qjeggp8w.js} +20 -20
- package/dist/{index-zfrhfn82.js → index-r1z0rd9c.js} +20 -20
- package/dist/{index-wqgjfstn.js → index-r5c4fedv.js} +3 -3
- package/dist/{index-cyktc4bp.js → index-r5hsgkz5.js} +118 -118
- package/dist/{index-0b1ess5e.js → index-r7a7r7ha.js} +1 -1
- package/dist/{index-1j8pm5t0.js → index-rfkh44ss.js} +15 -15
- package/dist/index-rz3z13e9.js +47 -0
- package/dist/{index-k6gpggb9.js → index-s80awwsw.js} +35 -35
- package/dist/{index-vqr7f7v8.js → index-ssmd41kh.js} +124 -124
- package/dist/{index-ensd0r74.js → index-v14qqmys.js} +41 -41
- package/dist/{index-paz6sgdp.js → index-v4a2fjtk.js} +36 -36
- package/dist/{index-d9pk3td4.js → index-w0eqdhyd.js} +14 -14
- package/dist/{index-5qn1kx34.js → index-w575evhq.js} +29 -29
- package/dist/{index-3gye9wzb.js → index-w5s3xftz.js} +26 -26
- package/dist/{index-tm88avnj.js → index-wykebpn9.js} +3 -3
- package/dist/{index-sh3s86na.js → index-xkhk5d6g.js} +17 -17
- package/dist/{index-6k1937bx.js → index-xqhede6x.js} +4 -4
- package/dist/{index-f4fx2tza.js → index-zs4ms3th.js} +181 -181
- package/dist/{index-k5hsfc3j.js → index-zsvzhjtk.js} +1 -1
- package/dist/observability/bounded-logger.d.ts +2 -0
- package/dist/observability/bounded-logger.d.ts.map +1 -1
- package/dist/observability/sanitize.d.ts +6 -0
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/observability/secret-values.d.ts +25 -0
- package/dist/observability/secret-values.d.ts.map +1 -0
- package/dist/react/keyboard/hooks.d.ts +71 -0
- package/dist/react/keyboard/hooks.d.ts.map +1 -0
- package/dist/react/keyboard/layers.d.ts +98 -0
- package/dist/react/keyboard/layers.d.ts.map +1 -0
- package/dist/react/keyboard/zone.d.ts +139 -0
- package/dist/react/keyboard/zone.d.ts.map +1 -0
- package/dist/telegram/crypto.d.ts +16 -0
- package/dist/telegram/crypto.d.ts.map +1 -0
- package/dist/telegram/html/check.d.ts +26 -0
- package/dist/telegram/html/check.d.ts.map +1 -0
- package/dist/telegram/html/nodes.d.ts +70 -0
- package/dist/telegram/html/nodes.d.ts.map +1 -0
- package/dist/telegram/html/parse.d.ts +35 -0
- package/dist/telegram/html/parse.d.ts.map +1 -0
- package/dist/telegram/html/render.d.ts +39 -0
- package/dist/telegram/html/render.d.ts.map +1 -0
- package/dist/telegram/init-data.d.ts.map +1 -1
- package/dist/telegram/operator-channel.d.ts +36 -3
- package/dist/telegram/operator-channel.d.ts.map +1 -1
- package/dist/telegram/screens/callback-codec.d.ts +34 -0
- package/dist/telegram/screens/callback-codec.d.ts.map +1 -0
- package/dist/telegram/screens/dispatch.d.ts +22 -0
- package/dist/telegram/screens/dispatch.d.ts.map +1 -0
- package/dist/telegram/screens/display.d.ts +70 -0
- package/dist/telegram/screens/display.d.ts.map +1 -0
- package/dist/telegram/screens/execute.d.ts +57 -0
- package/dist/telegram/screens/execute.d.ts.map +1 -0
- package/dist/telegram/screens/html.d.ts +36 -0
- package/dist/telegram/screens/html.d.ts.map +1 -0
- package/dist/telegram/screens/outcome.d.ts +74 -0
- package/dist/telegram/screens/outcome.d.ts.map +1 -0
- package/dist/telegram/screens/path.d.ts +29 -0
- package/dist/telegram/screens/path.d.ts.map +1 -0
- package/dist/telegram/screens/reconcile.d.ts +67 -0
- package/dist/telegram/screens/reconcile.d.ts.map +1 -0
- package/dist/telegram/screens/registry.d.ts +23 -0
- package/dist/telegram/screens/registry.d.ts.map +1 -0
- package/dist/telegram/screens/render.d.ts +70 -0
- package/dist/telegram/screens/render.d.ts.map +1 -0
- package/dist/telegram/screens/runtime.d.ts +133 -0
- package/dist/telegram/screens/runtime.d.ts.map +1 -0
- package/dist/telegram/screens/screen-types.d.ts +130 -0
- package/dist/telegram/screens/screen-types.d.ts.map +1 -0
- package/dist/telegram/screens/screen.d.ts +138 -0
- package/dist/telegram/screens/screen.d.ts.map +1 -0
- package/dist/telegram/screens/state.d.ts +115 -0
- package/dist/telegram/screens/state.d.ts.map +1 -0
- package/dist/telegram/screens/telegram-screens.d.ts +18 -0
- package/dist/telegram/screens/telegram-screens.d.ts.map +1 -0
- package/dist/telegram/screens/test-chat-telegram.d.ts +47 -0
- package/dist/telegram/screens/test-chat-telegram.d.ts.map +1 -0
- package/dist/telegram/screens/test-chat.d.ts +66 -0
- package/dist/telegram/screens/test-chat.d.ts.map +1 -0
- package/dist/telegram/screens/view.d.ts +89 -0
- package/dist/telegram/screens/view.d.ts.map +1 -0
- package/dist/telegram/send-failure.d.ts +19 -0
- package/dist/telegram/send-failure.d.ts.map +1 -1
- package/dist/telegram/update-intake.d.ts +78 -0
- package/dist/telegram/update-intake.d.ts.map +1 -0
- package/dist/telegram/update-store-sqlite.d.ts +26 -0
- package/dist/telegram/update-store-sqlite.d.ts.map +1 -0
- package/dist/telegram/update-store.d.ts +67 -0
- package/dist/telegram/update-store.d.ts.map +1 -0
- package/dist/telegram/webhook.d.ts +105 -0
- package/dist/telegram/webhook.d.ts.map +1 -0
- package/dist/tools/cli/args-fields.d.ts +8 -0
- package/dist/tools/cli/args-fields.d.ts.map +1 -1
- package/dist/tools/cli/args-route.d.ts.map +1 -1
- package/dist/tools/cli/args.d.ts.map +1 -1
- package/dist/tools/cli/help.d.ts.map +1 -1
- package/llms/cli.txt +8 -2
- package/llms/observability.txt +2 -1
- package/llms/react-keyboard.txt +193 -0
- package/llms/stitchkit.txt +7 -1
- package/llms/telegram-html.txt +35 -0
- package/llms/telegram-screens.txt +365 -0
- package/llms/telegram.txt +160 -4
- package/llms-full.txt +736 -8
- package/llms.txt +10 -5
- package/package.json +17 -2
- package/dist/index-2k4yrqkc.js +0 -9
- package/dist/index-crykmqkr.js +0 -26
- package/dist/index-y64k3s0h.js +0 -150
- package/dist/index-y91ry9h2.js +0 -4
- /package/dist/{index-hffjpmq9.js → index-9jfdm5tg.js} +0 -0
- /package/dist/{index-zjjyynm3.js → index-d4pa2bgs.js} +0 -0
- /package/dist/{index-rxfy4cq7.js → index-dwfv9a85.js} +0 -0
- /package/dist/{index-5jm36zv1.js → index-gfk7nrht.js} +0 -0
- /package/dist/{index-b9aefrqd.js → index-n1dqr7gf.js} +0 -0
- /package/dist/{index-q3pyb9n0.js → index-sykb5ag7.js} +0 -0
- /package/dist/{index-48ffdxgk.js → index-xhzq2qvj.js} +0 -0
- /package/dist/{index-kzfs85xp.js → index-zsarb5xp.js} +0 -0
package/llms/telegram.txt
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
Contains: Guide: Telegram bots (docs/guide/telegram.md) · API reference: `stitchkit/telegram` (docs/api/reference.md)
|
|
7
7
|
|
|
8
|
+
Also read:
|
|
9
|
+
- see `llms/telegram-screens.txt` for the Telegram screens guide
|
|
10
|
+
|
|
8
11
|
==============================================================================
|
|
9
12
|
# Guide: Telegram bots (docs/guide/telegram.md)
|
|
10
13
|
==============================================================================
|
|
@@ -21,6 +24,8 @@ way: [ADR 0201](../decisions/0201-a-telegram-bot-is-assembled-from-primitives.md
|
|
|
21
24
|
| Job | Where |
|
|
22
25
|
|---|---|
|
|
23
26
|
| the bot as resources: command menu, then long polling with updates admitted by batch | `grammyBotResources` — [application kernel](application-kernel.md#optional-grammy-adapter) |
|
|
27
|
+
| a bot on a webhook: set only where it is this process's, updates recorded before Telegram is answered | `claimTelegramWebhook`, `createTelegramUpdateIntake`, `receiveTelegramWebhook` — [below](#a-bot-on-a-webhook) |
|
|
28
|
+
| text for Telegram's HTML: cleaned, cut to the limit, checked | `stitchkit/telegram/html` — [below](#message-markup) |
|
|
24
29
|
| the process journal | `createJsonLogger` from `stitchkit/observability` |
|
|
25
30
|
| the operator's chat: product events, apart from the journal | `createTelegramOperatorChannel` |
|
|
26
31
|
| a broadcast that survives crashes, deploys and Ctrl-C | `runTelegramBroadcast` |
|
|
@@ -28,6 +33,7 @@ way: [ADR 0201](../decisions/0201-a-telegram-bot-is-assembled-from-primitives.md
|
|
|
28
33
|
| why a send was refused | `classifyTelegramSendFailure` — [auth and errors](auth-and-errors.md#telegram-mini-apps) |
|
|
29
34
|
| Mini App `initData` | `verifyTelegramInitData` — [auth and errors](auth-and-errors.md#telegram-mini-apps) |
|
|
30
35
|
| one Bot API call without a bot library | `callTelegramBotApi` |
|
|
36
|
+
| menus: screens, buttons, actions, input and message editing | `stitchkit/telegram/screens` — [Telegram screens](telegram-screens.md) |
|
|
31
37
|
|
|
32
38
|
## The journal
|
|
33
39
|
|
|
@@ -51,6 +57,24 @@ export const logger = createJsonLogger({
|
|
|
51
57
|
logger.error('Application startup failed', { error })
|
|
52
58
|
```
|
|
53
59
|
|
|
60
|
+
A provider's key, a webhook secret or a database password has no shape a
|
|
61
|
+
pattern could guess, so the journal also masks exact values — and the
|
|
62
|
+
environment already names them. `secretValuesFromEnv` collects every variable
|
|
63
|
+
named as a token, secret, key, password or credential, the password and secret
|
|
64
|
+
query values inside any URL variable (`DATABASE_URL`), and each URL-encoded:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createJsonLogger, secretValuesFromEnv } from 'stitchkit/observability'
|
|
68
|
+
|
|
69
|
+
export const logger = createJsonLogger({
|
|
70
|
+
sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
|
|
71
|
+
sensitiveValues: secretValuesFromEnv(process.env),
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A key added to the environment is masked from its first line; a URL's user name
|
|
76
|
+
is left alone, since it names a role that appears in ordinary text.
|
|
77
|
+
|
|
54
78
|
The error line contract, for whatever recognises a failure from the journal: a
|
|
55
79
|
line whose `level` is `50` or more is an error. There is no separate `fatal`;
|
|
56
80
|
an unrecoverable end is an `error` line followed by a non-zero exit.
|
|
@@ -79,8 +103,32 @@ operators.post(`New user ${user.id}`, 'users')
|
|
|
79
103
|
|
|
80
104
|
Every message is masked before it leaves: a bot token always — bots post their
|
|
81
105
|
errors here, and an error about a local Bot API file carries the token in its
|
|
82
|
-
path — plus any `sensitivePatterns` the bot adds;
|
|
83
|
-
masks its own exact token
|
|
106
|
+
path — plus any `sensitivePatterns` and exact `sensitiveValues` the bot adds;
|
|
107
|
+
`telegramOperatorSender` also masks its own exact token, and with `parseMode:
|
|
108
|
+
'HTML'` repairs the markup — a text cut at Telegram's limit never leaves a tag
|
|
109
|
+
open.
|
|
110
|
+
|
|
111
|
+
A failure that repeats a hundred times, or a storm of a hundred different ones
|
|
112
|
+
when a database goes down, is a line with a count for the person reading, not a
|
|
113
|
+
hundred messages. `dedupe` sends one message per fingerprint per window — the
|
|
114
|
+
topic and the text with numbers, UUIDs and long identifiers blanked — and the
|
|
115
|
+
next one of that kind carries how many were held back; past `maxPerWindow`
|
|
116
|
+
messages in a window the rest are counted onto the next message sent:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const operators = createTelegramOperatorChannel<'errors'>({
|
|
120
|
+
chatId: env.OPERATOR_CHAT_ID,
|
|
121
|
+
send: telegramOperatorSender({ token: env.BOT_TOKEN }),
|
|
122
|
+
sensitiveValues: secretValuesFromEnv(process.env),
|
|
123
|
+
dedupe: {
|
|
124
|
+
windowMs: 10 * 60_000,
|
|
125
|
+
maxPerWindow: 20,
|
|
126
|
+
repeatedLine: (count) => `Same again: ${count}`,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Held-back messages reach `onDropped` as `repeated` or `over-budget`. What to post stays the bot's. On the way down, drain it within the grace period
|
|
84
132
|
and close it: `drain: (context) => operators.drain(context.signal)`,
|
|
85
133
|
`close: () => operators.close()` in a resource.
|
|
86
134
|
|
|
@@ -124,6 +172,102 @@ application (the application's signal, with admission held by the caller).
|
|
|
124
172
|
The recipient state contains Telegram ids: keep it in the state directory, out
|
|
125
173
|
of git.
|
|
126
174
|
|
|
175
|
+
## Message markup
|
|
176
|
+
|
|
177
|
+
`stitchkit/telegram/html` is Telegram's HTML parse mode as a tree, with no peer
|
|
178
|
+
and no DOM, so a server sending a message and a browser previewing it share one
|
|
179
|
+
parser. Text written by someone else — a model, an editor, an owner's greeting —
|
|
180
|
+
becomes markup Telegram accepts: synonyms renamed (`strong` → `b`), block tags
|
|
181
|
+
turned into line breaks, a link kept only with an `href` Telegram opens,
|
|
182
|
+
anything Telegram forbids (a tag it does not know, formatting inside `code`, a
|
|
183
|
+
quote inside a quote) left as its words.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { sanitizeTelegramHtml, splitTelegramHtml } from 'stitchkit/telegram/html'
|
|
187
|
+
|
|
188
|
+
for (const part of splitTelegramHtml(summary)) {
|
|
189
|
+
await ctx.reply(part, { parse_mode: 'HTML' })
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Telegram's limits — 4096 for a message, 1024 for a caption (`{ limit:
|
|
194
|
+
TELEGRAM_CAPTION_LIMIT }`) — count the text after the markup is parsed, so a
|
|
195
|
+
text is cut by what the reader sees, not by the length of its tags. A cut
|
|
196
|
+
prefers a paragraph, then a line, then a space; it never splits a character or
|
|
197
|
+
a custom emoji, and an element open across it is closed in one part and opened
|
|
198
|
+
again, attributes and all, in the next. `truncateTelegramHtml` keeps one part
|
|
199
|
+
with an ellipsis; `telegramHtmlText` is what the reader sees; `parseTelegramHtml`
|
|
200
|
+
gives the nodes to render a preview or an email from. `checkTelegramHtml` says,
|
|
201
|
+
without repairing anything, what Telegram would refuse — for an editor that
|
|
202
|
+
should warn a person rather than change their text.
|
|
203
|
+
|
|
204
|
+
## A bot on a webhook
|
|
205
|
+
|
|
206
|
+
A webhook is one place in all of Telegram, and `setWebhook` takes it without
|
|
207
|
+
asking whose it was: a staging host or a developer's machine started with the
|
|
208
|
+
production token takes the production bot with one call. `claimTelegramWebhook`
|
|
209
|
+
sets it only where it already points here — address *and* secret — or where the
|
|
210
|
+
bot names the current owner's host as the one it takes over from (`'none'` when
|
|
211
|
+
there is no webhook: a bot on long polling is somebody's bot too). Telegram never
|
|
212
|
+
returns the secret, so the address carries an `owner` tag derived from it; a
|
|
213
|
+
copy of the production address with another secret is refused as
|
|
214
|
+
`other-secret` instead of quietly installing its own.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import {
|
|
218
|
+
checkTelegramWebhook,
|
|
219
|
+
claimTelegramWebhook,
|
|
220
|
+
createTelegramUpdateIntake,
|
|
221
|
+
receiveTelegramWebhook,
|
|
222
|
+
sqliteTelegramUpdateStore,
|
|
223
|
+
} from 'stitchkit/telegram'
|
|
224
|
+
import { Database } from 'bun:sqlite'
|
|
225
|
+
import type { Update } from 'grammy/types'
|
|
226
|
+
|
|
227
|
+
const webhook = { token: env.BOT_TOKEN, url: env.WEBHOOK_URL, secret: env.WEBHOOK_SECRET }
|
|
228
|
+
|
|
229
|
+
const intake = createTelegramUpdateIntake<Update>({
|
|
230
|
+
store: sqliteTelegramUpdateStore({ database: new Database(env.UPDATES_DB) }),
|
|
231
|
+
handle: (update) => bot.handleUpdate(update),
|
|
232
|
+
onFailure: (failure) => logger.warn('Update failed', { ...failure }),
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// start: set the webhook, then handle what was recorded before the restart
|
|
236
|
+
await bot.init()
|
|
237
|
+
await claimTelegramWebhook({ ...webhook, takeoverFrom: env.WEBHOOK_TAKEOVER_FROM })
|
|
238
|
+
await intake.start()
|
|
239
|
+
|
|
240
|
+
// the route
|
|
241
|
+
const route = (request: Request) =>
|
|
242
|
+
receiveTelegramWebhook(request, { secret: webhook.secret, accept: intake.accept })
|
|
243
|
+
|
|
244
|
+
// stop admission: `await intake.close()` waits for the handlers in flight
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A refusal names the owner's host, never its path — a foreign webhook's path may
|
|
248
|
+
itself be a secret — and says what to pass: set the takeover for one start and
|
|
249
|
+
remove it. `checkTelegramWebhook` on a timer notices a webhook that moved
|
|
250
|
+
elsewhere, which this process cannot see any other way.
|
|
251
|
+
|
|
252
|
+
**The update is recorded before Telegram is answered.** A handler that runs
|
|
253
|
+
longer than Telegram waits — a large file from a local Bot API server — gets the
|
|
254
|
+
same update again while it still works, and a process that answers and then
|
|
255
|
+
dies loses the update for good. `receiveTelegramWebhook` answers 200 once the
|
|
256
|
+
intake recorded the body; the handler runs after, outside the request. Updates
|
|
257
|
+
of one chat run in order, other chats alongside (`maxConcurrent`, 32). An
|
|
258
|
+
attempt holds a lease it renews while the handler lives; a sweep takes over what
|
|
259
|
+
was left — `pending` whose process never got to it (`pendingGraceMs`), `failed`
|
|
260
|
+
whose retry is due, `processing` whose process died. A failure is retried with
|
|
261
|
+
backoff or after Telegram's `retry_after`; one Telegram said repeating cannot
|
|
262
|
+
fix (a refused message, a blocked user) is abandoned; `maxAttempts` (5) ends
|
|
263
|
+
the rest. Delivery is at least once: a handler that finished but could not be
|
|
264
|
+
recorded as finished runs again.
|
|
265
|
+
|
|
266
|
+
The store is an interface of six atomic steps (`TelegramUpdateStore`), so the
|
|
267
|
+
bot's own database can hold it; `sqliteTelegramUpdateStore` takes a
|
|
268
|
+
`bun:sqlite` or `node:sqlite` handle as it is, and `memoryTelegramUpdateStore`
|
|
269
|
+
is for tests.
|
|
270
|
+
|
|
127
271
|
## Files from a local Bot API server
|
|
128
272
|
|
|
129
273
|
A local `telegram-bot-api` keeps downloaded files under `<dir>/<bot token>/`,
|
|
@@ -179,11 +323,23 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
|
|
|
179
323
|
| `TelegramBroadcastConfig` / `TelegramBroadcastSend` / `TelegramBroadcastReport` / `TelegramBroadcastRunOutcome` / `TelegramBroadcastOutcome` / `TelegramBroadcastRecipient` | _type_ | `name`, `directory`, `recipients()`, `send({ recipient, attempt })`, `maxAttempts`, `signal`, `onProgress`; counts `delivered` / `unreachable` / `failed` / `uncertain` / `pending` and `finished` / `stopped` / `halted` / `dry-run` |
|
|
180
324
|
| `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
|
|
181
325
|
| `createTelegramOperatorChannel` | function | the operator chat, apart from the journal: `post(text, topic?)` returns at once and never throws; paced sends into forum topics, 429 honoured, oldest dropped on overflow, `drain(signal)` and `close()` for the way down |
|
|
182
|
-
| `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`, `sensitivePatterns` (a bot token is always masked)
|
|
183
|
-
| `
|
|
326
|
+
| `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`, `sensitivePatterns` and `sensitiveValues` (a bot token is always masked), `dedupe`; a drop is `overflow` / `refused` / `attempts` / `closed` / `repeated` / `over-budget` with its classification |
|
|
327
|
+
| `TelegramOperatorDedupe` | _type_ | `windowMs` (10 min): one message per `fingerprint` per window, the next one sent carries `repeatedLine(count)`; `maxPerWindow` (20) across fingerprints, the overflow counted onto the next one sent with `overBudgetLine(count)`; `now` |
|
|
328
|
+
| `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode`; with `'HTML'` the markup is cleaned and cut by what it shows, so the channel's cut never leaves a tag open |
|
|
184
329
|
| `createTelegramLocalFiles` | function | files the local Bot API server wrote under `<root>/<token>/`: `resolve(file_path)` and `remove(file_path)` only inside the bot's directory, judged on real paths so a link is judged by where it leads; `check()` for readiness. Recommended root variable: `BOT_API_FILES_ROOT` |
|
|
185
330
|
| `TelegramLocalFilesConfig` / `TelegramLocalFiles` / `TelegramLocalFilesCheck` / `TelegramLocalFileError` / `TelegramLocalFileRefusal` | _type_ / class | `root`, `token`; refusals `root-not-absolute` / `bot-directory-unavailable` / `outside-bot-directory` / `missing` / `not-a-file`, never carrying the path or the token |
|
|
186
331
|
| `callTelegramBotApi` / `TelegramBotApiCall` / `TelegramBotApiError` | function / _type_ / class | one Bot API method over `fetch`; a refusal keeps `error_code`, `description` and `parameters` for the classifier, and no error names the URL |
|
|
187
332
|
| `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
|
|
333
|
+
| `claimTelegramWebhook` | function | set the webhook only where it already points here, or by naming the host it is taken from; confirm Telegram holds it; pending updates kept unless `dropPendingUpdates` |
|
|
334
|
+
| `ClaimTelegramWebhookConfig` / `TelegramWebhookConfig` / `TelegramWebhookClaim` | _type_ | `token`, `url`, `secret`, `apiRoot`, `fetch`, `signal`; `takeoverFrom` (a host, or `'none'`), `allowedUpdates`, `maxConnections`, `dropPendingUpdates`; the claim's tagged `url`, `takenFrom`, `pendingUpdates` |
|
|
335
|
+
| `TelegramWebhookClaimError` / `TelegramWebhookRefusal` / `TELEGRAM_WEBHOOK_NONE` | class / _type_ / constant | `owned-elsewhere` / `other-secret` / `not-confirmed` with `ownerHost` — a host, never a path; `'none'` names no webhook |
|
|
336
|
+
| `telegramWebhookUrl` | function | the address with an `owner` tag derived from the secret: equal for one secret, different for another, the path untouched |
|
|
337
|
+
| `checkTelegramWebhook` / `TelegramWebhookState` | function / _type_ | whether the webhook still points here: `owned`, `ownerHost`, `pendingUpdates`, Telegram's `lastError` |
|
|
338
|
+
| `receiveTelegramWebhook` / `ReceiveTelegramWebhookOptions` / `TelegramUpdateAcceptance` | function / _type_ | one webhook `Request`: 403 without the secret (compared in constant time), 405 for another method, 400 for no update, 200 once `accept` recorded it |
|
|
339
|
+
| `createTelegramUpdateIntake` | function | webhook updates recorded before Telegram is answered and handled after: chat order kept, other chats alongside up to `maxConcurrent`, a renewed lease per attempt, a sweep that takes over what a restart or a dead process left, retries, `maxAttempts`, finished updates remembered `retainMs` to refuse repeats; at least once |
|
|
340
|
+
| `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle`, `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
|
|
341
|
+
| `TelegramUpdateStore` / `StoredTelegramUpdate` / `TelegramUpdateState` / `TelegramUpdateSettlement` / `TelegramUpdateClaimOptions` / `TelegramUpdateDueQuery` | _type_ | the store an intake runs on: `add`, `claim`, `renew`, `settle`, `due`, `prune`, each atomic on one record — a claim takes `pending`, a due `failed` or a lapsed `processing`; renew and settle only by the attempt holding it |
|
|
342
|
+
| `memoryTelegramUpdateStore` | function | the store in this process's memory — tests, or a bot that accepts losing unhandled updates to a restart |
|
|
343
|
+
| `sqliteTelegramUpdateStore` / `SqliteTelegramUpdateStoreConfig` / `TelegramSqliteDatabase` | function / _type_ | the store in SQLite over the handle the application opened — `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync`; `table` (default `telegram_updates`) created when missing |
|
|
188
344
|
|
|
189
345
|
---
|