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.
Files changed (225) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +2 -1
  3. package/dist/entrypoints/agent-runtime/browser.js +119 -119
  4. package/dist/entrypoints/agent-runtime/coding-tools.js +15 -20
  5. package/dist/entrypoints/agent-runtime/harness.js +55 -63
  6. package/dist/entrypoints/agent-runtime/openrouter.js +6 -6
  7. package/dist/entrypoints/agent-runtime/sandbox.js +20 -25
  8. package/dist/entrypoints/agent-runtime/sqlite/bun.js +11 -13
  9. package/dist/entrypoints/agent-runtime/sqlite/node.js +11 -13
  10. package/dist/entrypoints/agent-runtime/testing.js +32 -34
  11. package/dist/entrypoints/agent-runtime.js +395 -402
  12. package/dist/entrypoints/application/diagnostic-journal.js +26 -27
  13. package/dist/entrypoints/application/directory-inbox.js +16 -17
  14. package/dist/entrypoints/application/grammy.js +13 -13
  15. package/dist/entrypoints/application/opentelemetry.js +13 -13
  16. package/dist/entrypoints/application/schemas.js +24 -24
  17. package/dist/entrypoints/application.js +208 -210
  18. package/dist/entrypoints/bin/upgrade-cli.js +2 -2
  19. package/dist/entrypoints/cli.js +81 -84
  20. package/dist/entrypoints/contract.js +54 -57
  21. package/dist/entrypoints/declaration.js +22 -22
  22. package/dist/entrypoints/files.js +18 -21
  23. package/dist/entrypoints/geo.js +7 -8
  24. package/dist/entrypoints/google.js +2 -2
  25. package/dist/entrypoints/index.js +150 -154
  26. package/dist/entrypoints/live.js +45 -47
  27. package/dist/entrypoints/node.js +68 -74
  28. package/dist/entrypoints/oauth.js +6 -6
  29. package/dist/entrypoints/observability.d.ts +1 -0
  30. package/dist/entrypoints/observability.d.ts.map +1 -1
  31. package/dist/entrypoints/observability.js +115 -87
  32. package/dist/entrypoints/primitives.js +35 -35
  33. package/dist/entrypoints/react/keyboard.d.ts +4 -0
  34. package/dist/entrypoints/react/keyboard.d.ts.map +1 -0
  35. package/dist/entrypoints/react/keyboard.js +397 -0
  36. package/dist/entrypoints/react.js +10 -10
  37. package/dist/entrypoints/release.js +7 -7
  38. package/dist/entrypoints/remote.js +15 -17
  39. package/dist/entrypoints/server.js +198 -200
  40. package/dist/entrypoints/telegram/html.d.ts +5 -0
  41. package/dist/entrypoints/telegram/html.d.ts.map +1 -0
  42. package/dist/entrypoints/telegram/html.js +26 -0
  43. package/dist/entrypoints/telegram/screens.d.ts +14 -0
  44. package/dist/entrypoints/telegram/screens.d.ts.map +1 -0
  45. package/dist/entrypoints/telegram/screens.js +2028 -0
  46. package/dist/entrypoints/telegram.d.ts +5 -1
  47. package/dist/entrypoints/telegram.d.ts.map +1 -1
  48. package/dist/entrypoints/telegram.js +548 -156
  49. package/dist/entrypoints/testing.js +75 -79
  50. package/dist/entrypoints/tools/connections.js +30 -32
  51. package/dist/entrypoints/tools/contract.js +18 -20
  52. package/dist/entrypoints/tools/invoker.js +12 -17
  53. package/dist/entrypoints/tools.js +223 -229
  54. package/dist/entrypoints/tracking/server.js +6 -6
  55. package/dist/entrypoints/tracking.js +40 -42
  56. package/dist/entrypoints/voice.js +3 -3
  57. package/dist/{index-eaysmdtq.js → index-0ndk4555.js} +56 -56
  58. package/dist/{index-2bxbyab0.js → index-1bx1xv1k.js} +1 -1
  59. package/dist/{index-p5r7qpdd.js → index-1eeqpadm.js} +4 -4
  60. package/dist/{index-hadmp193.js → index-3ct7hhq6.js} +210 -210
  61. package/dist/{index-e01r054k.js → index-45tdvqa3.js} +26 -26
  62. package/dist/{index-dce5wxcq.js → index-4s0fep5q.js} +8 -8
  63. package/dist/index-50ejfz75.js +9 -0
  64. package/dist/{index-m9zn6cpq.js → index-5107q189.js} +12 -12
  65. package/dist/{index-sbdmyz75.js → index-5cc6r990.js} +3 -3
  66. package/dist/index-6k1svmjh.js +141 -0
  67. package/dist/{index-c6bxbsnc.js → index-7kxw1awn.js} +2 -2
  68. package/dist/{index-0r0fbk7z.js → index-7z9ym7t8.js} +28 -28
  69. package/dist/{index-p7hgdyhb.js → index-814xpr6d.js} +2 -2
  70. package/dist/{index-gtj3zc53.js → index-8kzct1d6.js} +99 -99
  71. package/dist/{index-1zbbzgfh.js → index-9pw0a51a.js} +4 -4
  72. package/dist/{index-79z7m7dt.js → index-a9pd417t.js} +35 -35
  73. package/dist/{index-ham5sz8m.js → index-aaj97r4y.js} +45 -45
  74. package/dist/{index-s812ycat.js → index-aan6hqb9.js} +3 -3
  75. package/dist/{index-10x2hbx0.js → index-adhgd4pe.js} +44 -44
  76. package/dist/{index-2gxzjvyr.js → index-afjvjyck.js} +23 -23
  77. package/dist/{index-qmy6zq75.js → index-ahvsksw5.js} +16 -16
  78. package/dist/{index-hr4tzbwq.js → index-ap3gqe56.js} +3 -3
  79. package/dist/{index-znfs20ss.js → index-b2bz0kdm.js} +16 -16
  80. package/dist/{index-r066jx76.js → index-c6aagrbd.js} +148 -120
  81. package/dist/{index-k30h66yd.js → index-ck71tbcf.js} +9 -9
  82. package/dist/{index-eqqvgdb3.js → index-ct75c3at.js} +2 -2
  83. package/dist/{index-gw8bmyaj.js → index-djjhhgjk.js} +41 -41
  84. package/dist/{index-8qt2m66q.js → index-dvwj1f43.js} +43 -43
  85. package/dist/{index-fhwqwxe0.js → index-ej331hah.js} +1 -1
  86. package/dist/{index-9t3dffv2.js → index-ewbaywk5.js} +5 -5
  87. package/dist/{index-7p1f0vp2.js → index-fyphpknh.js} +25 -25
  88. package/dist/{index-83bdg97s.js → index-g149e9pk.js} +1 -1
  89. package/dist/{index-ngfhvfpd.js → index-g6a3na27.js} +4 -4
  90. package/dist/{index-qwhynv6x.js → index-h124epcf.js} +20 -20
  91. package/dist/{index-h6eeys1v.js → index-h97tqqp1.js} +9 -9
  92. package/dist/{index-zcgf3gqf.js → index-hby4qhax.js} +10 -10
  93. package/dist/{index-5jrd0yng.js → index-j8aw2c0e.js} +4 -4
  94. package/dist/{index-d8m8zpd8.js → index-jzczzcsb.js} +14 -14
  95. package/dist/index-kqnd6a85.js +26 -0
  96. package/dist/{index-35mh5bhj.js → index-kw1ws0cs.js} +12 -12
  97. package/dist/{index-yw4fzmdz.js → index-kzz6rq0w.js} +28 -17
  98. package/dist/{index-4gcnakhx.js → index-m38ej7e5.js} +2 -2
  99. package/dist/{index-ndddxtyx.js → index-m79yr938.js} +19 -19
  100. package/dist/{index-5p2m7we1.js → index-m9fndta6.js} +11 -11
  101. package/dist/index-mnkk1ffz.js +150 -0
  102. package/dist/index-n15jny5b.js +4 -0
  103. package/dist/{index-2vtcmrj9.js → index-n53mswt3.js} +20 -20
  104. package/dist/{index-bt14acb5.js → index-n5yqxd3d.js} +21 -21
  105. package/dist/{index-bm5rx7n6.js → index-n9tyz8z3.js} +12 -12
  106. package/dist/index-nmaqxdx7.js +463 -0
  107. package/dist/{index-mqv3b5rh.js → index-nx6eazt0.js} +30 -30
  108. package/dist/{index-3z73fh2c.js → index-q0th8q0z.js} +9 -9
  109. package/dist/{index-v260df89.js → index-qjeggp8w.js} +20 -20
  110. package/dist/{index-zfrhfn82.js → index-r1z0rd9c.js} +20 -20
  111. package/dist/{index-wqgjfstn.js → index-r5c4fedv.js} +3 -3
  112. package/dist/{index-cyktc4bp.js → index-r5hsgkz5.js} +118 -118
  113. package/dist/{index-0b1ess5e.js → index-r7a7r7ha.js} +1 -1
  114. package/dist/{index-1j8pm5t0.js → index-rfkh44ss.js} +15 -15
  115. package/dist/index-rz3z13e9.js +47 -0
  116. package/dist/{index-k6gpggb9.js → index-s80awwsw.js} +35 -35
  117. package/dist/{index-vqr7f7v8.js → index-ssmd41kh.js} +124 -124
  118. package/dist/{index-ensd0r74.js → index-v14qqmys.js} +41 -41
  119. package/dist/{index-paz6sgdp.js → index-v4a2fjtk.js} +36 -36
  120. package/dist/{index-d9pk3td4.js → index-w0eqdhyd.js} +14 -14
  121. package/dist/{index-5qn1kx34.js → index-w575evhq.js} +29 -29
  122. package/dist/{index-3gye9wzb.js → index-w5s3xftz.js} +26 -26
  123. package/dist/{index-tm88avnj.js → index-wykebpn9.js} +3 -3
  124. package/dist/{index-sh3s86na.js → index-xkhk5d6g.js} +17 -17
  125. package/dist/{index-6k1937bx.js → index-xqhede6x.js} +4 -4
  126. package/dist/{index-f4fx2tza.js → index-zs4ms3th.js} +181 -181
  127. package/dist/{index-k5hsfc3j.js → index-zsvzhjtk.js} +1 -1
  128. package/dist/observability/bounded-logger.d.ts +2 -0
  129. package/dist/observability/bounded-logger.d.ts.map +1 -1
  130. package/dist/observability/sanitize.d.ts +6 -0
  131. package/dist/observability/sanitize.d.ts.map +1 -1
  132. package/dist/observability/secret-values.d.ts +25 -0
  133. package/dist/observability/secret-values.d.ts.map +1 -0
  134. package/dist/react/keyboard/hooks.d.ts +71 -0
  135. package/dist/react/keyboard/hooks.d.ts.map +1 -0
  136. package/dist/react/keyboard/layers.d.ts +98 -0
  137. package/dist/react/keyboard/layers.d.ts.map +1 -0
  138. package/dist/react/keyboard/zone.d.ts +139 -0
  139. package/dist/react/keyboard/zone.d.ts.map +1 -0
  140. package/dist/telegram/crypto.d.ts +16 -0
  141. package/dist/telegram/crypto.d.ts.map +1 -0
  142. package/dist/telegram/html/check.d.ts +26 -0
  143. package/dist/telegram/html/check.d.ts.map +1 -0
  144. package/dist/telegram/html/nodes.d.ts +70 -0
  145. package/dist/telegram/html/nodes.d.ts.map +1 -0
  146. package/dist/telegram/html/parse.d.ts +35 -0
  147. package/dist/telegram/html/parse.d.ts.map +1 -0
  148. package/dist/telegram/html/render.d.ts +39 -0
  149. package/dist/telegram/html/render.d.ts.map +1 -0
  150. package/dist/telegram/init-data.d.ts.map +1 -1
  151. package/dist/telegram/operator-channel.d.ts +36 -3
  152. package/dist/telegram/operator-channel.d.ts.map +1 -1
  153. package/dist/telegram/screens/callback-codec.d.ts +34 -0
  154. package/dist/telegram/screens/callback-codec.d.ts.map +1 -0
  155. package/dist/telegram/screens/dispatch.d.ts +22 -0
  156. package/dist/telegram/screens/dispatch.d.ts.map +1 -0
  157. package/dist/telegram/screens/display.d.ts +70 -0
  158. package/dist/telegram/screens/display.d.ts.map +1 -0
  159. package/dist/telegram/screens/execute.d.ts +57 -0
  160. package/dist/telegram/screens/execute.d.ts.map +1 -0
  161. package/dist/telegram/screens/html.d.ts +36 -0
  162. package/dist/telegram/screens/html.d.ts.map +1 -0
  163. package/dist/telegram/screens/outcome.d.ts +74 -0
  164. package/dist/telegram/screens/outcome.d.ts.map +1 -0
  165. package/dist/telegram/screens/path.d.ts +29 -0
  166. package/dist/telegram/screens/path.d.ts.map +1 -0
  167. package/dist/telegram/screens/reconcile.d.ts +67 -0
  168. package/dist/telegram/screens/reconcile.d.ts.map +1 -0
  169. package/dist/telegram/screens/registry.d.ts +23 -0
  170. package/dist/telegram/screens/registry.d.ts.map +1 -0
  171. package/dist/telegram/screens/render.d.ts +70 -0
  172. package/dist/telegram/screens/render.d.ts.map +1 -0
  173. package/dist/telegram/screens/runtime.d.ts +133 -0
  174. package/dist/telegram/screens/runtime.d.ts.map +1 -0
  175. package/dist/telegram/screens/screen-types.d.ts +130 -0
  176. package/dist/telegram/screens/screen-types.d.ts.map +1 -0
  177. package/dist/telegram/screens/screen.d.ts +138 -0
  178. package/dist/telegram/screens/screen.d.ts.map +1 -0
  179. package/dist/telegram/screens/state.d.ts +115 -0
  180. package/dist/telegram/screens/state.d.ts.map +1 -0
  181. package/dist/telegram/screens/telegram-screens.d.ts +18 -0
  182. package/dist/telegram/screens/telegram-screens.d.ts.map +1 -0
  183. package/dist/telegram/screens/test-chat-telegram.d.ts +47 -0
  184. package/dist/telegram/screens/test-chat-telegram.d.ts.map +1 -0
  185. package/dist/telegram/screens/test-chat.d.ts +66 -0
  186. package/dist/telegram/screens/test-chat.d.ts.map +1 -0
  187. package/dist/telegram/screens/view.d.ts +89 -0
  188. package/dist/telegram/screens/view.d.ts.map +1 -0
  189. package/dist/telegram/send-failure.d.ts +19 -0
  190. package/dist/telegram/send-failure.d.ts.map +1 -1
  191. package/dist/telegram/update-intake.d.ts +78 -0
  192. package/dist/telegram/update-intake.d.ts.map +1 -0
  193. package/dist/telegram/update-store-sqlite.d.ts +26 -0
  194. package/dist/telegram/update-store-sqlite.d.ts.map +1 -0
  195. package/dist/telegram/update-store.d.ts +67 -0
  196. package/dist/telegram/update-store.d.ts.map +1 -0
  197. package/dist/telegram/webhook.d.ts +105 -0
  198. package/dist/telegram/webhook.d.ts.map +1 -0
  199. package/dist/tools/cli/args-fields.d.ts +8 -0
  200. package/dist/tools/cli/args-fields.d.ts.map +1 -1
  201. package/dist/tools/cli/args-route.d.ts.map +1 -1
  202. package/dist/tools/cli/args.d.ts.map +1 -1
  203. package/dist/tools/cli/help.d.ts.map +1 -1
  204. package/llms/cli.txt +8 -2
  205. package/llms/observability.txt +2 -1
  206. package/llms/react-keyboard.txt +193 -0
  207. package/llms/stitchkit.txt +7 -1
  208. package/llms/telegram-html.txt +35 -0
  209. package/llms/telegram-screens.txt +365 -0
  210. package/llms/telegram.txt +160 -4
  211. package/llms-full.txt +736 -8
  212. package/llms.txt +10 -5
  213. package/package.json +17 -2
  214. package/dist/index-2k4yrqkc.js +0 -9
  215. package/dist/index-crykmqkr.js +0 -26
  216. package/dist/index-y64k3s0h.js +0 -150
  217. package/dist/index-y91ry9h2.js +0 -4
  218. /package/dist/{index-hffjpmq9.js → index-9jfdm5tg.js} +0 -0
  219. /package/dist/{index-zjjyynm3.js → index-d4pa2bgs.js} +0 -0
  220. /package/dist/{index-rxfy4cq7.js → index-dwfv9a85.js} +0 -0
  221. /package/dist/{index-5jm36zv1.js → index-gfk7nrht.js} +0 -0
  222. /package/dist/{index-b9aefrqd.js → index-n1dqr7gf.js} +0 -0
  223. /package/dist/{index-q3pyb9n0.js → index-sykb5ag7.js} +0 -0
  224. /package/dist/{index-48ffdxgk.js → index-xhzq2qvj.js} +0 -0
  225. /package/dist/{index-kzfs85xp.js → index-zsarb5xp.js} +0 -0
@@ -0,0 +1,2028 @@
1
+ import {
2
+ TELEGRAM_TEXT_LIMIT2,
3
+ TELEGRAM_CAPTION_LIMIT2,
4
+ escapeTelegramHtml2
5
+ } from "../../index-nmaqxdx7.js";
6
+ import {
7
+ joinRoutePath,
8
+ parseRoutePattern
9
+ } from "../../index-m38ej7e5.js";
10
+ import {
11
+ classifyTelegramEditRefusal
12
+ } from "../../index-6k1svmjh.js";
13
+ import {
14
+ checkTelegramHtml2
15
+ } from "../../index-rz3z13e9.js";
16
+ import {
17
+ createMutationQueue
18
+ } from "../../index-sykb5ag7.js";
19
+ import {
20
+ bytesToBase64Url,
21
+ base64UrlToBytes
22
+ } from "../../index-dwfv9a85.js";
23
+ import {
24
+ argumentsDigest
25
+ } from "../../index-r7a7r7ha.js";
26
+ import {
27
+ compareCodeUnits
28
+ } from "../../index-zsvzhjtk.js";
29
+ import {
30
+ isUnsafeKey
31
+ } from "../../index-zsarb5xp.js";
32
+ import {
33
+ isRecord,
34
+ transportResult
35
+ } from "../../index-9jfdm5tg.js";
36
+
37
+ // src/telegram/screens/html.ts
38
+ var MINT = Symbol("TelegramHtml");
39
+
40
+ class TelegramHtml {
41
+ #markup;
42
+ constructor(mint, markup) {
43
+ if (mint !== MINT) {
44
+ throw new TypeError("[stitchkit] telegram screens: markup is made by html`…`, or html.raw for trusted text.");
45
+ }
46
+ this.#markup = markup;
47
+ }
48
+ toString() {
49
+ return this.#markup;
50
+ }
51
+ }
52
+ function markupOf(value) {
53
+ if (value instanceof TelegramHtml)
54
+ return value.toString();
55
+ if (value === false || value === null || value === undefined)
56
+ return "";
57
+ return escapeTelegramHtml2(String(value));
58
+ }
59
+ function tag(literals, ...values) {
60
+ let markup = literals[0] ?? "";
61
+ for (const [index, value] of values.entries()) {
62
+ markup += markupOf(value) + (literals[index + 1] ?? "");
63
+ }
64
+ return new TelegramHtml(MINT, markup);
65
+ }
66
+ var html = Object.assign(tag, {
67
+ raw: (markup) => new TelegramHtml(MINT, markup),
68
+ join: (parts, separator = "") => new TelegramHtml(MINT, parts.map(markupOf).filter((part) => part !== "").join(markupOf(separator)))
69
+ });
70
+ // src/telegram/screens/outcome.ts
71
+ var TOAST_LIMIT = 200;
72
+ var MAX_EXPIRY_MS = 2147483647;
73
+
74
+ class ScreenOutcome {
75
+ #outcome = true;
76
+ navigation;
77
+ static is(value) {
78
+ return typeof value === "object" && value !== null && #outcome in value;
79
+ }
80
+ toastSpec;
81
+ notices;
82
+ constructor(navigation = { type: "stay" }, toast = undefined, notices = []) {
83
+ this.navigation = navigation;
84
+ this.toastSpec = toast;
85
+ this.notices = notices;
86
+ }
87
+ stay() {
88
+ return new ScreenOutcome({ type: "stay" }, this.toastSpec, this.notices);
89
+ }
90
+ go(screen, ...params) {
91
+ const [values] = params;
92
+ return new ScreenOutcome({ type: "go", screen, params: values ?? {} }, this.toastSpec, this.notices);
93
+ }
94
+ back() {
95
+ return new ScreenOutcome({ type: "back" }, this.toastSpec, this.notices);
96
+ }
97
+ toast(text, options = {}) {
98
+ const length = [...text].length;
99
+ if (length > TOAST_LIMIT) {
100
+ throw new RangeError(`[stitchkit] telegram screens: a toast is at most ${TOAST_LIMIT} characters (got ${length}).`);
101
+ }
102
+ return new ScreenOutcome(this.navigation, { text, alert: options.alert ?? false }, this.notices);
103
+ }
104
+ notice(content, options = {}) {
105
+ const { expiresInMs } = options;
106
+ if (expiresInMs !== undefined && (!Number.isInteger(expiresInMs) || expiresInMs <= 0 || expiresInMs > MAX_EXPIRY_MS)) {
107
+ throw new RangeError(`[stitchkit] telegram screens: expiresInMs must be a positive integer up to ${MAX_EXPIRY_MS}.`);
108
+ }
109
+ const notice = expiresInMs === undefined ? { content } : { content, expiresInMs };
110
+ return new ScreenOutcome(this.navigation, this.toastSpec, [...this.notices, notice]);
111
+ }
112
+ }
113
+ function outcomeBuilders() {
114
+ const start = new ScreenOutcome;
115
+ return {
116
+ stay: () => start.stay(),
117
+ go: (screen, ...params) => start.go(screen, ...params),
118
+ back: () => start.back(),
119
+ toast: (text, options) => start.toast(text, options),
120
+ notice: (content, options) => start.notice(content, options)
121
+ };
122
+ }
123
+ // src/telegram/screens/screen.ts
124
+ import { z } from "zod";
125
+
126
+ // src/telegram/screens/callback-codec.ts
127
+ var CALLBACK_DATA_LIMIT = 64;
128
+ var NAME = /^[A-Za-z0-9_-]+$/;
129
+ var INPUT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
130
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
131
+ var COMPACT_UUID = /^~u[A-Za-z0-9_-]{22}$/;
132
+ function utf8Length(text) {
133
+ return new TextEncoder().encode(text).length;
134
+ }
135
+ function assertCallbackName(kind, name) {
136
+ if (!NAME.test(name)) {
137
+ throw new Error(`[stitchkit] telegram screens: ${kind} "${name}" may contain only letters, digits, "_" and "-".`);
138
+ }
139
+ }
140
+ function escapeText(value) {
141
+ const escaped = value.replaceAll("%", "%25").replaceAll(":", "%3A");
142
+ return escaped.startsWith("~") ? `%7E${escaped.slice(1)}` : escaped;
143
+ }
144
+ function unescapeText(value) {
145
+ try {
146
+ return decodeURIComponent(value);
147
+ } catch {
148
+ return;
149
+ }
150
+ }
151
+ function compactUuid(uuid) {
152
+ const hex = uuid.replaceAll("-", "");
153
+ const bytes = new Uint8Array(16);
154
+ for (let index = 0;index < 16; index += 1) {
155
+ bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
156
+ }
157
+ return `~u${bytesToBase64Url(bytes)}`;
158
+ }
159
+ function expandUuid(segment) {
160
+ let bytes;
161
+ try {
162
+ bytes = base64UrlToBytes(segment.slice(2));
163
+ } catch {
164
+ return;
165
+ }
166
+ if (bytes.length !== 16)
167
+ return;
168
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
169
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
170
+ }
171
+ function encodeValue(value) {
172
+ if (typeof value === "string")
173
+ return UUID.test(value) ? compactUuid(value) : escapeText(value);
174
+ if (value === null)
175
+ return "~n";
176
+ if (typeof value === "boolean")
177
+ return value ? "~t" : "~f";
178
+ if (!Number.isFinite(value)) {
179
+ throw new Error("[stitchkit] telegram screens: a number in a button must be finite.");
180
+ }
181
+ return `~${value}`;
182
+ }
183
+ function decodeValue(segment) {
184
+ if (!segment.startsWith("~"))
185
+ return unescapeText(segment);
186
+ if (segment === "~n")
187
+ return null;
188
+ if (segment === "~t")
189
+ return true;
190
+ if (segment === "~f")
191
+ return false;
192
+ if (COMPACT_UUID.test(segment))
193
+ return expandUuid(segment);
194
+ const number = Number(segment.slice(1));
195
+ return segment.length > 1 && Number.isFinite(number) ? number : undefined;
196
+ }
197
+ function encodeCallback(address) {
198
+ assertCallbackName("screen id", address.screen);
199
+ if (address.action !== undefined)
200
+ assertCallbackName("action", address.action);
201
+ let body = address.detached ? `!${address.screen}` : address.screen;
202
+ if (address.action !== undefined)
203
+ body += `.${address.action}`;
204
+ for (const param of address.params)
205
+ body += `:${encodeValue(param)}`;
206
+ const entries = Object.entries(address.input ?? {}).filter((entry) => entry[1] !== undefined).sort(([left], [right]) => compareCodeUnits(left, right));
207
+ for (const [key, value] of entries) {
208
+ if (!INPUT_KEY.test(key) || isUnsafeKey(key)) {
209
+ throw new Error(`[stitchkit] telegram screens: button input key "${key}" is not allowed.`);
210
+ }
211
+ body += `:${key}=${encodeValue(value)}`;
212
+ }
213
+ return body;
214
+ }
215
+ function encodeToken(token) {
216
+ return `#${token}`;
217
+ }
218
+ function parseCallback(body) {
219
+ if (body.startsWith("#")) {
220
+ const token = body.slice(1);
221
+ return NAME.test(token) ? { kind: "token", token } : null;
222
+ }
223
+ const detached = body.startsWith("!");
224
+ const [head = "", ...segments] = (detached ? body.slice(1) : body).split(":");
225
+ const dot = head.indexOf(".");
226
+ const screen = dot === -1 ? head : head.slice(0, dot);
227
+ const action = dot === -1 ? undefined : head.slice(dot + 1);
228
+ if (!NAME.test(screen) || action !== undefined && !NAME.test(action))
229
+ return null;
230
+ return { kind: "address", screen, action, detached, segments };
231
+ }
232
+ function decodeParams(segments) {
233
+ const params = [];
234
+ for (const segment of segments) {
235
+ const value = decodeValue(segment);
236
+ if (value === undefined)
237
+ return null;
238
+ params.push(value);
239
+ }
240
+ return params;
241
+ }
242
+ function decodeInput(segments) {
243
+ const input = {};
244
+ for (const segment of segments) {
245
+ const equals = segment.indexOf("=");
246
+ if (equals === -1)
247
+ return null;
248
+ const key = segment.slice(0, equals);
249
+ if (!INPUT_KEY.test(key) || isUnsafeKey(key) || Object.hasOwn(input, key))
250
+ return null;
251
+ const value = decodeValue(segment.slice(equals + 1));
252
+ if (value === undefined)
253
+ return null;
254
+ input[key] = value;
255
+ }
256
+ return input;
257
+ }
258
+
259
+ // src/telegram/screens/path.ts
260
+ function parseScreenPath(path) {
261
+ if (!path.startsWith("/")) {
262
+ throw new Error(`[stitchkit] telegram screens: path "${path}" must start with "/".`);
263
+ }
264
+ const parameters = parseRoutePattern(path);
265
+ const wildcard = parameters.find((parameter) => parameter.kind === "wildcard");
266
+ if (wildcard) {
267
+ throw new Error(`[stitchkit] telegram screens: path "${path}" may not end in a wildcard ("*${wildcard.name}").`);
268
+ }
269
+ return {
270
+ path,
271
+ segments: path.split("/").filter(Boolean),
272
+ params: parameters.map((parameter) => parameter.name)
273
+ };
274
+ }
275
+ function isAncestorPath(parent, child) {
276
+ if (parent.segments.length >= child.segments.length)
277
+ return false;
278
+ return parent.segments.every((segment, index) => child.segments[index] === segment);
279
+ }
280
+ function derivedScreenId(path) {
281
+ return argumentsDigest({ screen: path }).slice(0, 6);
282
+ }
283
+
284
+ // src/telegram/screens/screen-types.ts
285
+ class TelegramScreen {
286
+ definition;
287
+ constructor(definition) {
288
+ this.definition = definition;
289
+ }
290
+ get path() {
291
+ return this.definition.path.path;
292
+ }
293
+ }
294
+
295
+ // src/telegram/screens/view.ts
296
+ var SCREEN_MEDIA_KINDS = [
297
+ "photo",
298
+ "video",
299
+ "animation",
300
+ "document",
301
+ "audio"
302
+ ];
303
+
304
+ class ScreenButton {
305
+ label;
306
+ target;
307
+ constructor(label, target) {
308
+ this.label = label;
309
+ this.target = target;
310
+ }
311
+ }
312
+ function link(label, screen, ...params) {
313
+ const [values] = params;
314
+ return new ScreenButton(label, { kind: "link", screen, params: values ?? {} });
315
+ }
316
+ function back(label) {
317
+ return new ScreenButton(label, { kind: "back" });
318
+ }
319
+
320
+ // src/telegram/screens/screen.ts
321
+ function outcomeOf(result) {
322
+ return ScreenOutcome.is(result) ? result : undefined;
323
+ }
324
+ function isActionInput(value) {
325
+ return isRecord(value) && Object.values(value).every((field) => field === undefined || field === null || typeof field === "string" || typeof field === "boolean" || typeof field === "number" && Number.isFinite(field));
326
+ }
327
+ function excludeOutcome(value) {
328
+ return transportResult(value);
329
+ }
330
+ function chainLoad(load, fetch) {
331
+ return async (ctx, params) => {
332
+ const data = await load(ctx, params);
333
+ if (ScreenOutcome.is(data))
334
+ return data;
335
+ const value = await fetch({ ctx, params, data, ...outcomeBuilders() });
336
+ return ScreenOutcome.is(value) ? value : excludeOutcome(value);
337
+ };
338
+ }
339
+ function ownParamsParser(path, inherited, schema) {
340
+ const names = parseScreenPath(path).params.filter((name) => !inherited.includes(name));
341
+ if (schema) {
342
+ const shape = z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }).properties ?? {};
343
+ for (const name of names) {
344
+ if (!(name in shape)) {
345
+ throw new Error(`[stitchkit] telegram screens: the params schema of "${path}" is missing "${name}".`);
346
+ }
347
+ }
348
+ }
349
+ const effective = schema ?? transportResult(z.object(Object.fromEntries(names.map((name) => [name, z.string()]))).strict());
350
+ return {
351
+ names,
352
+ parse: (raw) => {
353
+ const own = {};
354
+ for (const name of names) {
355
+ const value = raw[name];
356
+ if (value !== undefined)
357
+ own[name] = value;
358
+ }
359
+ const parsed = effective.safeParse(own);
360
+ return parsed.success ? parsed.data : undefined;
361
+ }
362
+ };
363
+ }
364
+ function scopeFor(scope, sub, schema) {
365
+ const prefix = joinRoutePath(scope.prefix, sub);
366
+ const own = ownParamsParser(prefix, scope.paramNames, schema);
367
+ return {
368
+ prefix,
369
+ paramNames: [...scope.paramNames, ...own.names],
370
+ parseParams: (raw) => {
371
+ const inherited = scope.parseParams(raw);
372
+ const added = own.parse(raw);
373
+ return inherited && added ? { ...inherited, ...added } : undefined;
374
+ },
375
+ load: scope.load,
376
+ guarded: scope.guarded && own.names.length === 0
377
+ };
378
+ }
379
+
380
+ class ScreenBodyBuilder {
381
+ core;
382
+ id;
383
+ actions;
384
+ inputs;
385
+ constructor(core, id, actions, inputs) {
386
+ this.core = core;
387
+ this.id = id;
388
+ this.actions = actions;
389
+ this.inputs = inputs;
390
+ }
391
+ action(name, ...args) {
392
+ assertCallbackName("action", name);
393
+ if (this.actions.has(name)) {
394
+ throw new Error(`[stitchkit] telegram screens: "${this.core.prefix}" declares action "${name}" twice.`);
395
+ }
396
+ let entry;
397
+ if (args.length === 1) {
398
+ const [handler] = args;
399
+ entry = async (base) => outcomeOf(await handler({ ...base, ...outcomeBuilders(), input: undefined }));
400
+ } else {
401
+ const [schema, handler] = args;
402
+ entry = async (base, raw) => {
403
+ const parsed = schema.safeParse(raw ?? {});
404
+ if (!parsed.success)
405
+ return "invalid-input";
406
+ return outcomeOf(await handler({ ...base, ...outcomeBuilders(), input: parsed.data }));
407
+ };
408
+ }
409
+ return new ScreenBodyBuilder(this.core, this.id, new Map([...this.actions, [name, entry]]), this.inputs);
410
+ }
411
+ on(kinds, handler, options = {}) {
412
+ const list = typeof kinds === "string" ? [kinds] : kinds;
413
+ if (list.length === 0) {
414
+ throw new Error(`[stitchkit] telegram screens: "${this.core.prefix}" takes input of no kind.`);
415
+ }
416
+ const inputs = new Map(this.inputs);
417
+ for (const kind of list) {
418
+ if (inputs.has(kind)) {
419
+ throw new Error(`[stitchkit] telegram screens: "${this.core.prefix}" takes "${kind}" input twice.`);
420
+ }
421
+ inputs.set(kind, { keepMessage: options.keepMessage ?? false, handler });
422
+ }
423
+ return new ScreenBodyBuilder(this.core, this.id, this.actions, inputs);
424
+ }
425
+ view(render) {
426
+ const { core, id, actions, inputs } = this;
427
+ const screen = new TelegramScreen({
428
+ path: parseScreenPath(core.prefix),
429
+ id: id.value,
430
+ explicitId: id.explicit,
431
+ guarded: core.guarded,
432
+ actions: [...actions.keys()],
433
+ inputKinds: [...inputs.keys()],
434
+ keepsInput: (kind) => inputs.get(kind)?.keepMessage ?? false,
435
+ acceptsParams: (raw) => core.parseParams(raw) !== undefined,
436
+ resolve: async (ctx, raw) => {
437
+ const params = core.parseParams(raw);
438
+ if (params === undefined)
439
+ return "invalid-params";
440
+ const data = await core.load(ctx, params);
441
+ if (ScreenOutcome.is(data))
442
+ return data;
443
+ return resolvedScreen(screen, { ctx, params, data }, render, actions, inputs);
444
+ }
445
+ });
446
+ return screen;
447
+ }
448
+ }
449
+ function resolvedScreen(screen, base, render, actions, inputs) {
450
+ const buttons = {};
451
+ for (const action of actions.keys()) {
452
+ buttons[action] = (label, input) => {
453
+ if (input !== undefined && !isActionInput(input)) {
454
+ throw new Error(`[stitchkit] telegram screens: the input of action "${action}" must be a flat object of strings, finite numbers, booleans and null.`);
455
+ }
456
+ return new ScreenButton(label, {
457
+ kind: "action",
458
+ screen,
459
+ params: base.params,
460
+ action,
461
+ input
462
+ });
463
+ };
464
+ }
465
+ const act = transportResult(buttons);
466
+ return {
467
+ params: base.params,
468
+ render: () => render({ ...base, act }),
469
+ runAction: async (action, raw) => {
470
+ const entry = actions.get(action);
471
+ return entry ? entry(base, raw) : "unknown-action";
472
+ },
473
+ input: (kind) => {
474
+ const entry = inputs.get(kind);
475
+ if (!entry)
476
+ return;
477
+ return {
478
+ keepMessage: entry.keepMessage,
479
+ handle: async (message, pending) => {
480
+ const { toast: _toast, ...builders } = outcomeBuilders();
481
+ const text = message.text ?? message.caption ?? "";
482
+ return outcomeOf(await entry.handler({ ...base, ...builders, message, text, pending }));
483
+ }
484
+ };
485
+ }
486
+ };
487
+ }
488
+
489
+ class ScreenBuilder extends ScreenBodyBuilder {
490
+ load(fetch) {
491
+ return new ScreenBodyBuilder({ ...this.core, load: chainLoad(this.core.load, fetch), guarded: true }, this.id, new Map, new Map);
492
+ }
493
+ }
494
+
495
+ class ScreenScope {
496
+ core;
497
+ constructor(core) {
498
+ this.core = core;
499
+ }
500
+ screen(path, options = {}) {
501
+ const core = scopeFor(this.core, path, options.params);
502
+ const id = options.id ?? derivedScreenId(core.prefix);
503
+ assertCallbackName("screen id", id);
504
+ return new ScreenBuilder(core, { value: id, explicit: options.id !== undefined }, new Map, new Map);
505
+ }
506
+ group(path, options = {}) {
507
+ return new ScreenGroupBuilder(scopeFor(this.core, path, options.params));
508
+ }
509
+ }
510
+
511
+ class ScreenGroupBuilder extends ScreenScope {
512
+ load(fetch) {
513
+ return new ScreenScope({
514
+ ...this.core,
515
+ load: chainLoad(this.core.load, fetch),
516
+ guarded: true
517
+ });
518
+ }
519
+ }
520
+ function rootScopeCore() {
521
+ return {
522
+ prefix: "/",
523
+ paramNames: [],
524
+ parseParams: () => ({}),
525
+ load: async () => {
526
+ return;
527
+ },
528
+ guarded: false
529
+ };
530
+ }
531
+ // src/telegram/screens/state.ts
532
+ import { z as z2 } from "zod";
533
+ var RECORD_LIMITS = {
534
+ messages: 100,
535
+ key: 64,
536
+ tokens: 100,
537
+ tokenBody: 4096,
538
+ remarks: 100
539
+ };
540
+ var MessageFingerprintSchema = z2.object({ content: z2.string(), media: z2.string(), markup: z2.string() }).strict();
541
+ var ShownMessageSchema = z2.object({
542
+ key: z2.string().min(1).max(RECORD_LIMITS.key),
543
+ id: z2.number().int().positive(),
544
+ kind: z2.enum(["text", "rich", "photo", "video", "animation", "document", "audio"]),
545
+ fingerprint: MessageFingerprintSchema.nullable()
546
+ }).strict();
547
+ var ScreenViewStateSchema = z2.object({
548
+ screen: z2.string().min(1).max(64),
549
+ params: z2.record(z2.string(), z2.union([z2.string(), z2.number()])),
550
+ messages: z2.array(ShownMessageSchema).min(1).max(RECORD_LIMITS.messages),
551
+ tokens: z2.record(z2.string(), z2.string().max(RECORD_LIMITS.tokenBody)).refine((tokens) => Object.keys(tokens).length <= 2 * RECORD_LIMITS.tokens)
552
+ }).strict();
553
+ var ExpiringNoticeSchema = z2.object({ id: z2.number().int().positive(), at: z2.number().int().nonnegative() }).strict();
554
+ var ScreenChatStateSchema = z2.object({
555
+ version: z2.literal(1),
556
+ view: ScreenViewStateSchema.nullable(),
557
+ expiring: z2.array(ExpiringNoticeSchema).max(RECORD_LIMITS.remarks)
558
+ }).strict();
559
+ var emptyChatState = () => ({
560
+ version: 1,
561
+ view: null,
562
+ expiring: []
563
+ });
564
+ async function readChatState(storage, key) {
565
+ const stored = await storage.read(key);
566
+ if (stored === undefined)
567
+ return { state: emptyChatState(), discarded: false };
568
+ const parsed = ScreenChatStateSchema.safeParse(stored);
569
+ return parsed.success ? { state: parsed.data, discarded: false } : { state: emptyChatState(), discarded: true };
570
+ }
571
+ async function writeChatState(storage, key, state) {
572
+ if (state.view === null && state.expiring.length === 0) {
573
+ await storage.delete(key);
574
+ return;
575
+ }
576
+ await storage.write(key, ScreenChatStateSchema.parse(state));
577
+ }
578
+ function createChatQueue(maxPending) {
579
+ const queues = new Map;
580
+ return async (key, task) => {
581
+ let queue = queues.get(key);
582
+ if (!queue) {
583
+ queue = { run: createMutationQueue(maxPending), pending: 0 };
584
+ queues.set(key, queue);
585
+ }
586
+ const entry = queue;
587
+ entry.pending += 1;
588
+ try {
589
+ return await entry.run(task);
590
+ } finally {
591
+ entry.pending -= 1;
592
+ if (entry.pending === 0 && queues.get(key) === entry)
593
+ queues.delete(key);
594
+ }
595
+ };
596
+ }
597
+ // src/telegram/screens/execute.ts
598
+ class PlanInterrupted extends Error {
599
+ shown;
600
+ cause;
601
+ constructor(shown, cause) {
602
+ super("[stitchkit] telegram screens: a view was left half-drawn by a Bot API failure.", {
603
+ cause
604
+ });
605
+ this.shown = shown;
606
+ this.cause = cause;
607
+ this.name = "PlanInterrupted";
608
+ }
609
+ }
610
+ function linkPreview(message) {
611
+ return message.linkPreview === false ? { is_disabled: true } : undefined;
612
+ }
613
+ function markupOption(markup) {
614
+ return markup ? { reply_markup: markup } : {};
615
+ }
616
+ var HTML = "HTML";
617
+ function captionOption(message) {
618
+ return message.html === undefined ? {} : { caption: message.html, parse_mode: HTML };
619
+ }
620
+ async function sendOutgoing(api, chatId, message) {
621
+ const extra = { ...captionOption(message), ...markupOption(message.markup) };
622
+ const source = message.media ?? "";
623
+ switch (message.kind) {
624
+ case "text":
625
+ return (await api.sendMessage(chatId, message.html ?? "", {
626
+ parse_mode: HTML,
627
+ link_preview_options: linkPreview(message),
628
+ ...markupOption(message.markup)
629
+ })).message_id;
630
+ case "rich":
631
+ return (await api.sendRichMessage(chatId, message.rich ?? {}, markupOption(message.markup))).message_id;
632
+ case "photo":
633
+ return (await api.sendPhoto(chatId, source, extra)).message_id;
634
+ case "video":
635
+ return (await api.sendVideo(chatId, source, extra)).message_id;
636
+ case "animation":
637
+ return (await api.sendAnimation(chatId, source, extra)).message_id;
638
+ case "document":
639
+ return (await api.sendDocument(chatId, source, extra)).message_id;
640
+ case "audio":
641
+ return (await api.sendAudio(chatId, source, extra)).message_id;
642
+ }
643
+ }
644
+ async function editOutgoing(api, chatId, step) {
645
+ const { message, parts, id } = step;
646
+ const markup = markupOption(message.markup);
647
+ if (message.kind === "text" || message.kind === "rich") {
648
+ if (!parts.content) {
649
+ await api.editMessageReplyMarkup(chatId, id, markup);
650
+ } else if (message.kind === "rich") {
651
+ await api.editMessageText(chatId, id, message.rich ?? {}, markup);
652
+ } else {
653
+ await api.editMessageText(chatId, id, message.html ?? "", {
654
+ parse_mode: HTML,
655
+ link_preview_options: linkPreview(message),
656
+ ...markup
657
+ });
658
+ }
659
+ return;
660
+ }
661
+ if (parts.media) {
662
+ await api.editMessageMedia(chatId, id, { type: message.kind, media: message.media ?? "", ...captionOption(message) }, markup);
663
+ } else if (parts.content) {
664
+ await api.editMessageCaption(chatId, id, { ...captionOption(message), ...markup });
665
+ } else {
666
+ await api.editMessageReplyMarkup(chatId, id, markup);
667
+ }
668
+ }
669
+ function shownFrom(id, message) {
670
+ return { key: message.key, id, kind: message.kind, fingerprint: message.fingerprint };
671
+ }
672
+ async function deleteMessages(api, chatId, ids) {
673
+ const undeleted = [];
674
+ for (const id of ids) {
675
+ try {
676
+ await api.deleteMessage(chatId, id);
677
+ } catch (error) {
678
+ if (classifyTelegramEditRefusal(error) === "message-gone")
679
+ continue;
680
+ undeleted.push(id);
681
+ await api.editMessageReplyMarkup(chatId, id).catch(() => {
682
+ return;
683
+ });
684
+ }
685
+ }
686
+ return undeleted;
687
+ }
688
+ function interruptedView(remaining, deletions, done) {
689
+ const untouched = [];
690
+ for (const step of remaining) {
691
+ if (step.op !== "send")
692
+ untouched.push({ ...step.from, fingerprint: null });
693
+ }
694
+ const present = new Set([...done, ...untouched].map((message) => message.id));
695
+ const leftovers = deletions.filter((id) => !present.has(id)).map((id) => ({ key: `left-${id}`, id, kind: "text", fingerprint: null }));
696
+ return [...done, ...untouched, ...leftovers].sort((left, right) => left.id - right.id);
697
+ }
698
+ async function executePlan(api, chatId, plan) {
699
+ const shown = [];
700
+ const deletions = [...plan.deletions];
701
+ let resending = false;
702
+ let sent = 0;
703
+ let edited = 0;
704
+ let kept = 0;
705
+ for (const [index, step] of plan.steps.entries()) {
706
+ try {
707
+ if (step.op !== "send" && resending)
708
+ deletions.push(step.id);
709
+ if (step.op === "send" || resending) {
710
+ shown.push(shownFrom(await sendOutgoing(api, chatId, step.message), step.message));
711
+ sent += 1;
712
+ continue;
713
+ }
714
+ if (step.op === "keep") {
715
+ shown.push(shownFrom(step.id, step.message));
716
+ kept += 1;
717
+ continue;
718
+ }
719
+ try {
720
+ await editOutgoing(api, chatId, step);
721
+ edited += 1;
722
+ } catch (error) {
723
+ const refusal = classifyTelegramEditRefusal(error);
724
+ if (refusal === "message-gone" || refusal === "message-locked") {
725
+ resending = true;
726
+ deletions.push(step.id);
727
+ shown.push(shownFrom(await sendOutgoing(api, chatId, step.message), step.message));
728
+ sent += 1;
729
+ continue;
730
+ }
731
+ if (refusal !== "not-modified")
732
+ throw error;
733
+ kept += 1;
734
+ }
735
+ shown.push(shownFrom(step.id, step.message));
736
+ } catch (error) {
737
+ throw new PlanInterrupted(interruptedView(plan.steps.slice(index), deletions, shown), error);
738
+ }
739
+ }
740
+ const undeleted = await deleteMessages(api, chatId, deletions);
741
+ return {
742
+ shown,
743
+ report: { sent, edited, kept, deleted: deletions.length - undeleted.length, undeleted }
744
+ };
745
+ }
746
+
747
+ // src/telegram/screens/reconcile.ts
748
+ var isMedia = (kind) => kind !== "text" && kind !== "rich";
749
+ function isEditable(from, to) {
750
+ return isMedia(to) || from === to;
751
+ }
752
+ function stepFor(shown, message) {
753
+ const before = shown.fingerprint;
754
+ const mediaChanged = shown.kind !== message.kind || before?.media !== message.fingerprint.media;
755
+ const parts = {
756
+ content: before?.content !== message.fingerprint.content,
757
+ media: isMedia(message.kind) && mediaChanged,
758
+ markup: before?.markup !== message.fingerprint.markup
759
+ };
760
+ return parts.content || parts.media || parts.markup ? { op: "edit", id: shown.id, from: shown, message, parts } : { op: "keep", id: shown.id, from: shown, message };
761
+ }
762
+ function planView(shown, next, options = {}) {
763
+ if (options.fresh) {
764
+ const deletions = [...shown.map((message) => message.id)];
765
+ if (options.adopt)
766
+ deletions.push(options.adopt.id);
767
+ return { steps: next.map((message) => ({ op: "send", message })), deletions };
768
+ }
769
+ const kept = new Set;
770
+ const steps = [];
771
+ let cursor = 0;
772
+ let sending = false;
773
+ for (const [index, message] of next.entries()) {
774
+ if (!sending) {
775
+ const position = options.adopt ? -1 : shown.findIndex((entry, at) => at >= cursor && entry.key === message.key);
776
+ const candidate = options.adopt ? index === 0 ? options.adopt : undefined : shown[position];
777
+ if (candidate && isEditable(candidate.kind, message.kind)) {
778
+ cursor = position + 1;
779
+ kept.add(candidate.id);
780
+ steps.push(stepFor(candidate, message));
781
+ continue;
782
+ }
783
+ sending = true;
784
+ }
785
+ steps.push({ op: "send", message });
786
+ }
787
+ const previous = options.adopt ? [options.adopt] : shown;
788
+ const deletions = previous.filter((entry) => !kept.has(entry.id)).map((entry) => entry.id);
789
+ return { steps, deletions };
790
+ }
791
+
792
+ // src/telegram/screens/render.ts
793
+ function htmlOf(text) {
794
+ return text instanceof TelegramHtml ? text.toString() : escapeTelegramHtml2(text);
795
+ }
796
+ function mediaOf(message) {
797
+ for (const kind of SCREEN_MEDIA_KINDS) {
798
+ const source = message[kind];
799
+ if (typeof source === "string")
800
+ return { kind, source };
801
+ }
802
+ return;
803
+ }
804
+ function paramValues(names, params, where) {
805
+ return names.map((name) => {
806
+ const value = params[name];
807
+ if (value === undefined) {
808
+ throw new Error(`[stitchkit] telegram screens: a button to "${where}" is missing param "${name}".`);
809
+ }
810
+ return value;
811
+ });
812
+ }
813
+ function labelled(label, callbackData) {
814
+ return typeof label === "string" ? { text: label, callback_data: callbackData } : { ...label, callback_data: callbackData };
815
+ }
816
+
817
+ class TokenBook {
818
+ prefix;
819
+ where;
820
+ tokens = {};
821
+ constructor(prefix, where) {
822
+ this.prefix = prefix;
823
+ this.where = where;
824
+ }
825
+ data(body) {
826
+ const direct = this.prefix + body;
827
+ if (utf8Length(direct) <= CALLBACK_DATA_LIMIT)
828
+ return direct;
829
+ if (body.length > RECORD_LIMITS.tokenBody) {
830
+ throw new Error(`[stitchkit] telegram screens: a button of "${this.where}" carries ${body.length} characters; a kept address holds ${RECORD_LIMITS.tokenBody}.`);
831
+ }
832
+ const token = argumentsDigest({ body }).slice(0, 12);
833
+ const known = this.tokens[token];
834
+ if (known === undefined && Object.keys(this.tokens).length >= RECORD_LIMITS.tokens) {
835
+ throw new Error(`[stitchkit] telegram screens: "${this.where}" renders more than ${RECORD_LIMITS.tokens} buttons too long to carry their address.`);
836
+ }
837
+ if (known !== undefined && known !== body) {
838
+ throw new Error(`[stitchkit] telegram screens: two buttons of "${this.where}" share a token; shorten one of their addresses.`);
839
+ }
840
+ this.tokens[token] = body;
841
+ const spilled = this.prefix + encodeToken(token);
842
+ if (utf8Length(spilled) > CALLBACK_DATA_LIMIT) {
843
+ throw new Error(`[stitchkit] telegram screens: callback prefix "${this.prefix}" leaves no room for a button.`);
844
+ }
845
+ return spilled;
846
+ }
847
+ }
848
+ function buttonData(button, context, book) {
849
+ const { target } = button;
850
+ const { resolver } = context;
851
+ if (target.kind === "back") {
852
+ const parent = resolver.parent;
853
+ if (!parent) {
854
+ throw new Error(`[stitchkit] telegram screens: "${resolver.path}" has no parent to go back to.`);
855
+ }
856
+ return book.data(encodeCallback({
857
+ screen: parent.id,
858
+ params: paramValues(parent.params, context.params, resolver.path)
859
+ }));
860
+ }
861
+ const address = resolver.addressOf(target.screen);
862
+ const params = paramValues(address.params, target.params, target.screen.path);
863
+ return book.data(target.kind === "link" ? encodeCallback({ screen: address.id, params }) : encodeCallback({
864
+ screen: address.id,
865
+ action: target.action,
866
+ params,
867
+ input: target.input
868
+ }));
869
+ }
870
+ function renderKeyboard(keyboard, context, book) {
871
+ if (!keyboard)
872
+ return;
873
+ const rows = [];
874
+ for (const row of keyboard) {
875
+ if (!row)
876
+ continue;
877
+ const buttons = [];
878
+ for (const button of row) {
879
+ if (!button)
880
+ continue;
881
+ buttons.push(button instanceof ScreenButton ? labelled(button.label, buttonData(button, context, book)) : button);
882
+ }
883
+ if (buttons.length > 0)
884
+ rows.push(buttons);
885
+ }
886
+ return rows.length > 0 ? { inline_keyboard: rows } : undefined;
887
+ }
888
+ function renderMessage(message, key, context, book) {
889
+ const markup = renderKeyboard(message.keyboard, context, book);
890
+ const media = mediaOf(message);
891
+ const rich = message.rich;
892
+ const kind = media ? media.kind : rich ? "rich" : "text";
893
+ const bodyText = media ? message.caption : message.text;
894
+ const html = bodyText === undefined ? undefined : htmlOf(bodyText);
895
+ const linkPreview = message.linkPreview;
896
+ return {
897
+ key,
898
+ kind,
899
+ html,
900
+ rich,
901
+ linkPreview,
902
+ media: media?.source,
903
+ markup,
904
+ fingerprint: {
905
+ content: argumentsDigest({ html, rich, linkPreview }),
906
+ media: argumentsDigest({ kind, source: media?.source }),
907
+ markup: argumentsDigest({ markup })
908
+ }
909
+ };
910
+ }
911
+ function renderView(view, context) {
912
+ const messages = Array.isArray(view) ? view : [view];
913
+ if (messages.length === 0) {
914
+ throw new Error(`[stitchkit] telegram screens: "${context.resolver.path}" rendered no messages.`);
915
+ }
916
+ if (messages.length > RECORD_LIMITS.messages) {
917
+ throw new Error(`[stitchkit] telegram screens: "${context.resolver.path}" renders ${messages.length} messages; a view holds ${RECORD_LIMITS.messages}.`);
918
+ }
919
+ const book = new TokenBook(context.prefix, context.resolver.path);
920
+ const keys = new Set;
921
+ const rendered = messages.map((message, index) => {
922
+ const key = message.key ?? (index === messages.length - 1 ? "main" : `s${index}`);
923
+ if (key.length === 0 || key.length > RECORD_LIMITS.key) {
924
+ throw new Error(`[stitchkit] telegram screens: "${context.resolver.path}" renders a message key of ${key.length} characters; a key has 1 to ${RECORD_LIMITS.key}.`);
925
+ }
926
+ if (keys.has(key)) {
927
+ throw new Error(`[stitchkit] telegram screens: "${context.resolver.path}" renders two messages with key "${key}".`);
928
+ }
929
+ keys.add(key);
930
+ return renderMessage(message, key, context, book);
931
+ });
932
+ return { messages: rendered, tokens: book.tokens };
933
+ }
934
+ function renderDetachedButton(button, prefix, addressOf) {
935
+ const { target } = button;
936
+ if (target.kind !== "link") {
937
+ throw new Error("[stitchkit] telegram screens: only a link can be used outside a screen.");
938
+ }
939
+ const address = addressOf(target.screen);
940
+ const data = prefix + encodeCallback({
941
+ screen: address.id,
942
+ params: paramValues(address.params, target.params, target.screen.path),
943
+ detached: true
944
+ });
945
+ if (utf8Length(data) > CALLBACK_DATA_LIMIT) {
946
+ throw new Error(`[stitchkit] telegram screens: a link to "${target.screen.path}" outside a screen needs ${utf8Length(data)} bytes; Telegram allows ${CALLBACK_DATA_LIMIT}.`);
947
+ }
948
+ return labelled(button.label, data);
949
+ }
950
+
951
+ // src/telegram/screens/display.ts
952
+ var MAX_REDIRECTS = 4;
953
+ function paramsFor(entry, params) {
954
+ const values = {};
955
+ for (const name of entry.definition.path.params) {
956
+ const value = params[name];
957
+ if (value !== undefined)
958
+ values[name] = value;
959
+ }
960
+ return values;
961
+ }
962
+ async function settleNavigation(runtime, ctx, origin, outcome) {
963
+ let toast = outcome.toastSpec;
964
+ const notices = [...outcome.notices];
965
+ let from = origin;
966
+ let navigation = outcome.navigation;
967
+ for (let hop = 0;hop <= MAX_REDIRECTS; hop += 1) {
968
+ let next;
969
+ if (navigation.type === "stay") {
970
+ if (!from)
971
+ return { target: undefined, toast, notices };
972
+ next = from;
973
+ } else if (navigation.type === "back") {
974
+ const parent = from?.entry.parent;
975
+ if (!from || !parent) {
976
+ throw new Error("[stitchkit] telegram screens: back() from a screen with no parent.");
977
+ }
978
+ next = { entry: parent, params: paramsFor(parent, from.params) };
979
+ } else {
980
+ const entry = runtime.registry.entryOf(navigation.screen);
981
+ next = { entry, params: navigation.params };
982
+ }
983
+ const resolved = await next.entry.definition.resolve(ctx, next.params);
984
+ if (resolved === "invalid-params") {
985
+ throw new Error(`[stitchkit] telegram screens: "${next.entry.definition.path.path}" refused the params it was sent with.`);
986
+ }
987
+ if ("navigation" in resolved) {
988
+ if (resolved.navigation.type === "stay") {
989
+ throw new Error(`[stitchkit] telegram screens: a load of "${next.entry.definition.path.path}" answered stay(); return data instead.`);
990
+ }
991
+ toast = resolved.toastSpec ?? toast;
992
+ notices.push(...resolved.notices);
993
+ from = next;
994
+ navigation = resolved.navigation;
995
+ continue;
996
+ }
997
+ return { target: { entry: next.entry, resolved }, toast, notices };
998
+ }
999
+ throw new Error(`[stitchkit] telegram screens: loads redirected more than ${MAX_REDIRECTS} times in a row.`);
1000
+ }
1001
+ async function sendText(api, chatId, content) {
1002
+ return (await api.sendMessage(chatId, htmlOf(content), { parse_mode: "HTML" })).message_id;
1003
+ }
1004
+ function planFor(base, state, next, moves) {
1005
+ const current = state.view?.messages ?? [];
1006
+ if (base.mode === "new")
1007
+ return planView([], next);
1008
+ if (base.mode === "fresh" || moves) {
1009
+ const shown = base.mode === "foreign" ? [...current, base.message] : current;
1010
+ return planView(shown, next, { fresh: true });
1011
+ }
1012
+ if (base.mode === "state")
1013
+ return planView(current, next);
1014
+ const others = current.map((message) => message.id).filter((id) => id !== base.message.id);
1015
+ if (next.length === 1) {
1016
+ const adopted = planView([], next, { adopt: base.message });
1017
+ return { steps: adopted.steps, deletions: [...adopted.deletions, ...others] };
1018
+ }
1019
+ return planView([...current, base.message], next, { fresh: true });
1020
+ }
1021
+ function templateOf(runtime, view) {
1022
+ return view ? runtime.registry.byId(view.screen)?.definition.path.path : undefined;
1023
+ }
1024
+ async function sweepDue(runtime, api, chat, chatId) {
1025
+ const state = await runtime.readState(chat);
1026
+ const now = runtime.clock();
1027
+ const due = state.expiring.filter((remark) => remark.at <= now);
1028
+ if (due.length === 0)
1029
+ return state;
1030
+ const kept = await deleteMessages(api, chatId, due.map((remark) => remark.id));
1031
+ if (kept.length > 0)
1032
+ runtime.emit({ type: "message-kept", chat, messageIds: kept });
1033
+ const swept = {
1034
+ ...state,
1035
+ expiring: state.expiring.filter((remark) => remark.at > now)
1036
+ };
1037
+ await writeChatState(runtime.config.storage, chat, swept);
1038
+ return swept;
1039
+ }
1040
+ var nothingDone = {
1041
+ sent: 0,
1042
+ edited: 0,
1043
+ kept: 0,
1044
+ deleted: 0,
1045
+ undeleted: []
1046
+ };
1047
+ function interruptedRecord(previous, base, drawn, shown) {
1048
+ const untouched = base.mode === "new" ? previous?.messages ?? [] : [];
1049
+ const messages = [...untouched, ...shown];
1050
+ if (messages.length === 0)
1051
+ return previous;
1052
+ const identity = previous ?? drawn;
1053
+ return {
1054
+ screen: identity.screen,
1055
+ params: identity.params,
1056
+ messages,
1057
+ tokens: { ...previous?.tokens, ...drawn.tokens }
1058
+ };
1059
+ }
1060
+ async function evictOldestRemark(runtime, api, chat, chatId, remarks) {
1061
+ const oldest = remarks.shift();
1062
+ if (!oldest)
1063
+ return;
1064
+ const kept = await deleteMessages(api, chatId, [oldest.id]);
1065
+ if (kept.length > 0)
1066
+ runtime.emit({ type: "message-kept", chat, messageIds: kept });
1067
+ }
1068
+ async function displayScreen(runtime, request) {
1069
+ const { ctx, chat, chatId, state, settled, base } = request;
1070
+ const api = ctx.api;
1071
+ const persistent = settled.notices.filter((notice) => notice.expiresInMs === undefined);
1072
+ const expiring = settled.notices.filter((notice) => notice.expiresInMs !== undefined);
1073
+ const target = settled.target;
1074
+ const rendered = target ? renderView(target.resolved.render(), {
1075
+ prefix: runtime.prefix,
1076
+ resolver: runtime.registry.resolverFor(target.entry),
1077
+ params: target.resolved.params
1078
+ }) : undefined;
1079
+ let view = state.view;
1080
+ const remarks = [...state.expiring];
1081
+ let report = nothingDone;
1082
+ let failure;
1083
+ try {
1084
+ if (target && rendered) {
1085
+ const plan = planFor(base, state, rendered.messages, persistent.length > 0);
1086
+ const leaving = target.entry.definition.id !== state.view?.screen;
1087
+ if (remarks.length > 0 && (leaving || plan.steps.some((step) => step.op === "send"))) {
1088
+ const kept = await deleteMessages(api, chatId, remarks.map((remark) => remark.id));
1089
+ if (kept.length > 0)
1090
+ runtime.emit({ type: "message-kept", chat, messageIds: kept });
1091
+ remarks.length = 0;
1092
+ }
1093
+ const drawn = (messages) => ({
1094
+ screen: target.entry.definition.id,
1095
+ params: { ...target.resolved.params },
1096
+ messages: [...messages],
1097
+ tokens: { ...rendered.tokens }
1098
+ });
1099
+ for (const notice of persistent)
1100
+ await sendText(api, chatId, notice.content);
1101
+ try {
1102
+ const executed = await executePlan(api, chatId, plan);
1103
+ report = executed.report;
1104
+ view = drawn(executed.shown);
1105
+ } catch (error) {
1106
+ if (!(error instanceof PlanInterrupted))
1107
+ throw error;
1108
+ view = interruptedRecord(state.view, base, drawn([]), error.shown);
1109
+ throw error.cause;
1110
+ }
1111
+ if (report.undeleted.length > 0) {
1112
+ runtime.emit({ type: "message-kept", chat, messageIds: report.undeleted });
1113
+ }
1114
+ } else {
1115
+ for (const notice of persistent)
1116
+ await sendText(api, chatId, notice.content);
1117
+ }
1118
+ const now = runtime.clock();
1119
+ for (const notice of expiring) {
1120
+ if (remarks.length >= RECORD_LIMITS.remarks) {
1121
+ await evictOldestRemark(runtime, api, chat, chatId, remarks);
1122
+ }
1123
+ const delayMs = notice.expiresInMs ?? 0;
1124
+ remarks.push({ id: await sendText(api, chatId, notice.content), at: now + delayMs });
1125
+ runtime.scheduleSweep(api, chat, chatId, delayMs);
1126
+ }
1127
+ } catch (error) {
1128
+ failure = { error };
1129
+ }
1130
+ await writeChatState(runtime.config.storage, chat, { version: 1, view, expiring: remarks });
1131
+ if (failure)
1132
+ throw failure.error;
1133
+ if (target) {
1134
+ runtime.emit({
1135
+ type: "transition",
1136
+ trigger: request.trigger,
1137
+ chat,
1138
+ from: templateOf(runtime, state.view),
1139
+ to: target.entry.definition.path.path,
1140
+ durationMs: Math.max(0, runtime.clock() - request.started),
1141
+ report
1142
+ });
1143
+ }
1144
+ }
1145
+
1146
+ // src/telegram/screens/dispatch.ts
1147
+ function onKeyboard(message, data) {
1148
+ if (message.date === 0 || !("reply_markup" in message))
1149
+ return false;
1150
+ return (message.reply_markup?.inline_keyboard ?? []).some((row) => row.some((button) => ("callback_data" in button) && button.callback_data === data));
1151
+ }
1152
+ function kindOfMessage(message) {
1153
+ if (message.date === 0)
1154
+ return "text";
1155
+ if ("photo" in message && message.photo)
1156
+ return "photo";
1157
+ if ("video" in message && message.video)
1158
+ return "video";
1159
+ if ("animation" in message && message.animation)
1160
+ return "animation";
1161
+ if ("audio" in message && message.audio)
1162
+ return "audio";
1163
+ if ("document" in message && message.document)
1164
+ return "document";
1165
+ if ("rich_message" in message && message.rich_message)
1166
+ return "rich";
1167
+ return "text";
1168
+ }
1169
+ function inputKindOf(message) {
1170
+ if (message.text !== undefined)
1171
+ return "text";
1172
+ if (message.rich_message)
1173
+ return "rich";
1174
+ if (message.photo)
1175
+ return "photo";
1176
+ if (message.animation)
1177
+ return "animation";
1178
+ if (message.video)
1179
+ return "video";
1180
+ if (message.video_note)
1181
+ return "video_note";
1182
+ if (message.voice)
1183
+ return "voice";
1184
+ if (message.audio)
1185
+ return "audio";
1186
+ if (message.document)
1187
+ return "document";
1188
+ if (message.sticker)
1189
+ return "sticker";
1190
+ if (message.location)
1191
+ return "location";
1192
+ if (message.contact)
1193
+ return "contact";
1194
+ return;
1195
+ }
1196
+ function isCommand(message) {
1197
+ const first = message.entities?.[0];
1198
+ return first?.type === "bot_command" && first.offset === 0;
1199
+ }
1200
+ function originOf(runtime, state) {
1201
+ const view = state.view;
1202
+ const entry = view ? runtime.registry.byId(view.screen) : undefined;
1203
+ return view && entry ? { entry, params: view.params } : undefined;
1204
+ }
1205
+ function decodePress(runtime, press, state) {
1206
+ if (press.message.date === 0)
1207
+ return "inaccessible";
1208
+ if (!onKeyboard(press.message, press.data))
1209
+ return "forged";
1210
+ let parsed = parseCallback(press.data.slice(runtime.prefix.length));
1211
+ if (parsed?.kind === "token") {
1212
+ const body = state.view?.tokens[parsed.token];
1213
+ if (body === undefined)
1214
+ return "unknown-token";
1215
+ parsed = parseCallback(body);
1216
+ if (parsed?.kind === "token")
1217
+ return "malformed";
1218
+ }
1219
+ if (!parsed)
1220
+ return "malformed";
1221
+ const entry = runtime.registry.byId(parsed.screen);
1222
+ if (!entry)
1223
+ return "unknown-screen";
1224
+ const names = entry.definition.path.params;
1225
+ const values = decodeParams(parsed.segments.slice(0, names.length));
1226
+ if (!values || values.length !== names.length)
1227
+ return "invalid-params";
1228
+ const rest = parsed.segments.slice(names.length);
1229
+ const input = parsed.action === undefined ? undefined : decodeInput(rest);
1230
+ if (parsed.action === undefined ? rest.length > 0 : input === null)
1231
+ return "invalid-input";
1232
+ const params = {};
1233
+ for (const [index, name] of names.entries())
1234
+ params[name] = values[index] ?? null;
1235
+ return {
1236
+ entry,
1237
+ params,
1238
+ action: parsed.action,
1239
+ input: input ?? undefined,
1240
+ detached: parsed.detached
1241
+ };
1242
+ }
1243
+ async function staleOutcome(runtime, ctx, reason) {
1244
+ const result = await runtime.config.onStale?.({ ctx, reason, ...outcomeBuilders() });
1245
+ return ScreenOutcome.is(result) ? result : undefined;
1246
+ }
1247
+ async function settlePress(runtime, ctx, press, state) {
1248
+ const origin = originOf(runtime, state);
1249
+ const decoded = decodePress(runtime, press, state);
1250
+ const stale = async (reason) => {
1251
+ const outcome = await staleOutcome(runtime, ctx, reason);
1252
+ return {
1253
+ settled: outcome ? await settleNavigation(runtime, ctx, origin, outcome) : { target: undefined, toast: undefined, notices: [] },
1254
+ detached: false,
1255
+ navigated: false
1256
+ };
1257
+ };
1258
+ if (typeof decoded === "string")
1259
+ return stale(decoded);
1260
+ const { entry, params, action, input, detached } = decoded;
1261
+ const resolved = await entry.definition.resolve(ctx, params);
1262
+ if (resolved === "invalid-params")
1263
+ return stale("invalid-params");
1264
+ const here = { entry, params };
1265
+ if (ScreenOutcome.is(resolved)) {
1266
+ return {
1267
+ settled: await settleNavigation(runtime, ctx, here, resolved),
1268
+ detached,
1269
+ navigated: true
1270
+ };
1271
+ }
1272
+ if (action === undefined) {
1273
+ return {
1274
+ settled: { target: { entry, resolved }, toast: undefined, notices: [] },
1275
+ detached,
1276
+ navigated: true
1277
+ };
1278
+ }
1279
+ const result = await resolved.runAction(action, input);
1280
+ if (typeof result === "string")
1281
+ return stale(result);
1282
+ const settled = await settleNavigation(runtime, ctx, { entry, params: resolved.params }, result ?? new ScreenOutcome);
1283
+ return { settled, detached, navigated: navigates(result) };
1284
+ }
1285
+ function navigates(outcome) {
1286
+ return outcome !== undefined && outcome.navigation.type !== "stay";
1287
+ }
1288
+ function assertOpenStands(navigated, settled) {
1289
+ if (navigated || settled.notices.length > 0) {
1290
+ throw new Error("[stitchkit] telegram screens: a handler that calls open() shows that screen; return nothing or a toast.");
1291
+ }
1292
+ }
1293
+ async function guarded(runtime, ctx, trigger, chat, chatId, operation, answer) {
1294
+ try {
1295
+ await operation();
1296
+ } catch (error) {
1297
+ const onError = runtime.config.onError;
1298
+ if (!onError)
1299
+ throw error;
1300
+ const result = await onError(error, { ctx, trigger, ...outcomeBuilders() });
1301
+ if (!ScreenOutcome.is(result))
1302
+ return;
1303
+ const state = await runtime.readState(chat);
1304
+ const settled = await settleNavigation(runtime, ctx, originOf(runtime, state), result);
1305
+ await answer?.(settled);
1306
+ await runtime.display({
1307
+ ctx,
1308
+ chat,
1309
+ chatId,
1310
+ state,
1311
+ settled,
1312
+ base: { mode: "state" },
1313
+ trigger,
1314
+ started: runtime.clock()
1315
+ });
1316
+ }
1317
+ }
1318
+ async function dispatchPress(runtime, ctx) {
1319
+ const query = ctx.callbackQuery;
1320
+ const message = query?.message;
1321
+ if (!query?.data || !message)
1322
+ return;
1323
+ const chat = runtime.chatKey(ctx);
1324
+ if (chat === undefined) {
1325
+ await ctx.api.answerCallbackQuery(query.id);
1326
+ return;
1327
+ }
1328
+ const press = { id: query.id, data: query.data, message };
1329
+ const chatId = message.chat.id;
1330
+ let answered = false;
1331
+ const answer = async (settled) => {
1332
+ if (answered)
1333
+ return;
1334
+ answered = true;
1335
+ const toast = settled?.toast;
1336
+ try {
1337
+ await ctx.api.answerCallbackQuery(press.id, toast ? { text: toast.text, show_alert: toast.alert } : undefined);
1338
+ } catch (error) {
1339
+ if (classifyTelegramEditRefusal(error) !== "query-expired")
1340
+ throw error;
1341
+ }
1342
+ };
1343
+ try {
1344
+ await runtime.exclusive(ctx, chat, () => guarded(runtime, ctx, "press", chat, chatId, async () => {
1345
+ const started = runtime.clock();
1346
+ const state = await sweepDue(runtime, ctx.api, chat, chatId);
1347
+ const { settled, detached, navigated } = await settlePress(runtime, ctx, press, state);
1348
+ if (runtime.openedDuring(ctx)) {
1349
+ assertOpenStands(navigated, settled);
1350
+ await answer(settled);
1351
+ return;
1352
+ }
1353
+ await answer(settled);
1354
+ const known = state.view?.messages.some((shown) => shown.id === message.message_id) ?? false;
1355
+ const pressed = {
1356
+ key: `pressed-${message.message_id}`,
1357
+ id: message.message_id,
1358
+ kind: kindOfMessage(message),
1359
+ fingerprint: null
1360
+ };
1361
+ const base = detached ? { mode: "new" } : known ? { mode: "state" } : { mode: "foreign", message: pressed };
1362
+ await runtime.display({
1363
+ ctx,
1364
+ chat,
1365
+ chatId,
1366
+ state,
1367
+ settled,
1368
+ base,
1369
+ trigger: "press",
1370
+ started
1371
+ });
1372
+ }, answer));
1373
+ } finally {
1374
+ await answer(undefined);
1375
+ }
1376
+ }
1377
+ async function dispatchInput(runtime, ctx, message) {
1378
+ const chat = runtime.chatKey(ctx);
1379
+ const kind = inputKindOf(message);
1380
+ if (chat === undefined || kind === undefined || isCommand(message))
1381
+ return false;
1382
+ const chatId = message.chat.id;
1383
+ return runtime.exclusive(ctx, chat, async () => {
1384
+ let taken = true;
1385
+ await guarded(runtime, ctx, "input", chat, chatId, async () => {
1386
+ const started = runtime.clock();
1387
+ const state = await sweepDue(runtime, ctx.api, chat, chatId);
1388
+ const origin = originOf(runtime, state);
1389
+ const definition = origin?.entry.definition;
1390
+ if (!origin || !definition?.inputKinds.includes(kind) || !definition.acceptsParams(origin.params)) {
1391
+ taken = false;
1392
+ return;
1393
+ }
1394
+ if (!definition.keepsInput(kind)) {
1395
+ const kept = await deleteMessages(ctx.api, chatId, [message.message_id]);
1396
+ if (kept.length > 0)
1397
+ runtime.emit({ type: "message-kept", chat, messageIds: kept });
1398
+ }
1399
+ const resolved = await definition.resolve(ctx, origin.params);
1400
+ if (resolved === "invalid-params") {
1401
+ throw new Error(`[stitchkit] telegram screens: "${definition.path.path}" refused params it accepted a moment ago.`);
1402
+ }
1403
+ const display = (settled, base) => runtime.display({
1404
+ ctx,
1405
+ chat,
1406
+ chatId,
1407
+ state,
1408
+ settled,
1409
+ base,
1410
+ trigger: "input",
1411
+ started
1412
+ });
1413
+ if (ScreenOutcome.is(resolved)) {
1414
+ await display(await settleNavigation(runtime, ctx, origin, resolved), {
1415
+ mode: "state"
1416
+ });
1417
+ return;
1418
+ }
1419
+ const input = resolved.input(kind);
1420
+ if (!input) {
1421
+ throw new Error(`[stitchkit] telegram screens: "${definition.path.path}" declared "${kind}" input it does not take.`);
1422
+ }
1423
+ const pending = [];
1424
+ let outcome;
1425
+ try {
1426
+ outcome = await input.handle(message, async (content) => {
1427
+ pending.push(await sendText(ctx.api, chatId, content));
1428
+ });
1429
+ } finally {
1430
+ if (pending.length > 0)
1431
+ await deleteMessages(ctx.api, chatId, pending);
1432
+ }
1433
+ const settled = await settleNavigation(runtime, ctx, { entry: origin.entry, params: resolved.params }, outcome ?? new ScreenOutcome);
1434
+ if (runtime.openedDuring(ctx)) {
1435
+ assertOpenStands(navigates(outcome), settled);
1436
+ return;
1437
+ }
1438
+ await display({ ...settled, toast: undefined }, input.keepMessage ? { mode: "fresh" } : { mode: "state" });
1439
+ });
1440
+ return taken;
1441
+ });
1442
+ }
1443
+ async function openScreen(runtime, ctx, origin, previous) {
1444
+ const chat = runtime.chatKey(ctx);
1445
+ const chatId = ctx.chat?.id;
1446
+ if (chat === undefined || chatId === undefined) {
1447
+ throw new Error("[stitchkit] telegram screens: open() needs an update from a chat.");
1448
+ }
1449
+ await runtime.exclusive(ctx, chat, () => guarded(runtime, ctx, "open", chat, chatId, async () => {
1450
+ const started = runtime.clock();
1451
+ const state = await sweepDue(runtime, ctx.api, chat, chatId);
1452
+ const outcome = new ScreenOutcome({
1453
+ type: "go",
1454
+ screen: origin.entry.screen,
1455
+ params: origin.params
1456
+ });
1457
+ const settled = await settleNavigation(runtime, ctx, undefined, outcome);
1458
+ const base = previous === "delete" ? { mode: "fresh" } : { mode: "new" };
1459
+ await runtime.display({
1460
+ ctx,
1461
+ chat,
1462
+ chatId,
1463
+ state,
1464
+ settled: { ...settled, toast: undefined },
1465
+ base,
1466
+ trigger: "open",
1467
+ started
1468
+ });
1469
+ runtime.markOpened(ctx);
1470
+ }));
1471
+ }
1472
+
1473
+ // src/telegram/screens/registry.ts
1474
+ function createScreenRegistry(screens) {
1475
+ if (screens.length === 0) {
1476
+ throw new Error("[stitchkit] telegram screens: declare at least one screen.");
1477
+ }
1478
+ const byPath = new Map;
1479
+ const byId = new Map;
1480
+ for (const screen of screens) {
1481
+ const { path, id } = screen.definition;
1482
+ const samePath = byPath.get(path.path);
1483
+ if (samePath) {
1484
+ throw new Error(`[stitchkit] telegram screens: two screens are declared at "${path.path}".`);
1485
+ }
1486
+ const sameId = byId.get(id);
1487
+ if (sameId) {
1488
+ throw new Error(`[stitchkit] telegram screens: "${path.path}" and "${sameId.definition.path.path}" share the id "${id}"; give one of them an explicit id.`);
1489
+ }
1490
+ if (path.params.length > 0 && !screen.definition.guarded) {
1491
+ throw new Error(`[stitchkit] telegram screens: "${path.path}" takes params but nothing loads before it. Params arrive from a button and are user input: give the screen or its group a load that checks them.`);
1492
+ }
1493
+ byPath.set(path.path, screen);
1494
+ byId.set(id, screen);
1495
+ }
1496
+ const entries = new Map;
1497
+ const register = (screen) => {
1498
+ const known = entries.get(screen);
1499
+ if (known)
1500
+ return known;
1501
+ const own = screen.definition.path;
1502
+ let parentScreen;
1503
+ for (const candidate of screens) {
1504
+ const path = candidate.definition.path;
1505
+ if (!isAncestorPath(path, own))
1506
+ continue;
1507
+ if (!parentScreen || path.segments.length > parentScreen.definition.path.segments.length) {
1508
+ parentScreen = candidate;
1509
+ }
1510
+ }
1511
+ const entry = {
1512
+ screen,
1513
+ definition: screen.definition,
1514
+ parent: parentScreen ? register(parentScreen) : undefined
1515
+ };
1516
+ entries.set(screen, entry);
1517
+ return entry;
1518
+ };
1519
+ for (const screen of screens)
1520
+ register(screen);
1521
+ const entryOf = (screen) => {
1522
+ const entry = entries.get(screen);
1523
+ if (!entry) {
1524
+ throw new Error(`[stitchkit] telegram screens: "${screen.path}" is linked to but not among the declared screens.`);
1525
+ }
1526
+ return entry;
1527
+ };
1528
+ return {
1529
+ byId: (id) => {
1530
+ const screen = byId.get(id);
1531
+ return screen ? entries.get(screen) : undefined;
1532
+ },
1533
+ entryOf,
1534
+ resolverFor: (entry) => ({
1535
+ path: entry.definition.path.path,
1536
+ parent: entry.parent ? { id: entry.parent.definition.id, params: entry.parent.definition.path.params } : undefined,
1537
+ addressOf: (screen) => {
1538
+ const target = entryOf(screen).definition;
1539
+ return { id: target.id, params: target.path.params };
1540
+ }
1541
+ })
1542
+ };
1543
+ }
1544
+
1545
+ // src/telegram/screens/runtime.ts
1546
+ var MAX_PENDING_PER_CHAT = 16;
1547
+ function createScreensRuntime(config) {
1548
+ const prefix = config.callbackPrefix ?? "~";
1549
+ if (prefix.length === 0 || /[#!]/.test(prefix)) {
1550
+ throw new Error('[stitchkit] telegram screens: callbackPrefix must be non-empty and free of "#" and "!".');
1551
+ }
1552
+ const queue = createChatQueue(MAX_PENDING_PER_CHAT);
1553
+ const inside = new WeakSet;
1554
+ const timers = new Set;
1555
+ const opened = new WeakSet;
1556
+ const runtime = {
1557
+ registry: createScreenRegistry(config.screens),
1558
+ config,
1559
+ prefix,
1560
+ clock: config.clock ?? Date.now,
1561
+ timers,
1562
+ chatKey: (ctx) => config.chatKey ? config.chatKey(ctx) : ctx.chat ? `screens:${ctx.chat.id}` : undefined,
1563
+ answers: (ctx) => (config.chats ?? "private") === "all" || ctx.chat?.type === "private",
1564
+ exclusive: async (ctx, chat, task) => {
1565
+ if (inside.has(ctx))
1566
+ return task();
1567
+ return queue(chat, async () => {
1568
+ inside.add(ctx);
1569
+ try {
1570
+ return await task();
1571
+ } finally {
1572
+ inside.delete(ctx);
1573
+ }
1574
+ });
1575
+ },
1576
+ readState: async (chat) => {
1577
+ const read = await readChatState(config.storage, chat);
1578
+ if (read.discarded)
1579
+ runtime.emit({ type: "state-discarded", chat });
1580
+ return read.state;
1581
+ },
1582
+ display: (request) => displayScreen(runtime, request),
1583
+ markOpened: (ctx) => {
1584
+ opened.add(ctx);
1585
+ },
1586
+ openedDuring: (ctx) => opened.has(ctx),
1587
+ scheduleSweep: (api, chat, chatId, delayMs) => {
1588
+ const timer = setTimeout(() => {
1589
+ timers.delete(timer);
1590
+ queue(chat, () => sweepDue(runtime, api, chat, chatId)).catch((error) => runtime.emit({ type: "sweep-failed", chat, error }));
1591
+ }, delayMs);
1592
+ timers.add(timer);
1593
+ if (typeof timer === "object" && "unref" in timer)
1594
+ timer.unref();
1595
+ },
1596
+ emit: (event) => config.onEvent?.(event)
1597
+ };
1598
+ return runtime;
1599
+ }
1600
+
1601
+ // src/telegram/screens/telegram-screens.ts
1602
+ class TelegramScreensRoot extends ScreenScope {
1603
+ create(config) {
1604
+ return createTelegramScreens(config);
1605
+ }
1606
+ }
1607
+ function telegramScreens() {
1608
+ return new TelegramScreensRoot(rootScopeCore());
1609
+ }
1610
+ function linkParams(value) {
1611
+ const params = {};
1612
+ if (value === undefined)
1613
+ return params;
1614
+ if (!isRecord(value))
1615
+ throw new TypeError("[stitchkit] telegram screens: params must be an object.");
1616
+ for (const [name, field] of Object.entries(value)) {
1617
+ if (typeof field !== "string" && typeof field !== "number") {
1618
+ throw new TypeError(`[stitchkit] telegram screens: param "${name}" must be a string or a number.`);
1619
+ }
1620
+ params[name] = field;
1621
+ }
1622
+ return params;
1623
+ }
1624
+ function createTelegramScreens(config) {
1625
+ const runtime = createScreensRuntime(config);
1626
+ const middleware = () => async (ctx, next) => {
1627
+ if (!runtime.answers(ctx))
1628
+ return next();
1629
+ const data = ctx.callbackQuery?.data;
1630
+ if (data !== undefined) {
1631
+ if (!data.startsWith(runtime.prefix) || !ctx.callbackQuery?.message)
1632
+ return next();
1633
+ return dispatchPress(runtime, ctx);
1634
+ }
1635
+ const message = ctx.message;
1636
+ if (message && await dispatchInput(runtime, ctx, message))
1637
+ return;
1638
+ return next();
1639
+ };
1640
+ return {
1641
+ middleware,
1642
+ open: async (ctx, screen, ...args) => {
1643
+ const [params, options] = args;
1644
+ const entry = runtime.registry.entryOf(screen);
1645
+ await openScreen(runtime, ctx, { entry, params: linkParams(params) }, options?.previous ?? "keep");
1646
+ },
1647
+ button: (link) => renderDetachedButton(link, runtime.prefix, (screen) => {
1648
+ const target = runtime.registry.entryOf(screen).definition;
1649
+ return { id: target.id, params: target.path.params };
1650
+ }),
1651
+ close: () => {
1652
+ for (const timer of runtime.timers)
1653
+ clearTimeout(timer);
1654
+ runtime.timers.clear();
1655
+ }
1656
+ };
1657
+ }
1658
+ // src/telegram/screens/test-chat-telegram.ts
1659
+ var MEDIA = ["photo", "video", "animation", "document", "audio"];
1660
+ var SENT_KINDS = {
1661
+ sendPhoto: "photo",
1662
+ sendVideo: "video",
1663
+ sendAnimation: "animation",
1664
+ sendDocument: "document",
1665
+ sendAudio: "audio"
1666
+ };
1667
+ var CALLBACK_DATA_LIMIT2 = 64;
1668
+ var PARSE_REFUSALS = {
1669
+ "bare-character": "unexpected character",
1670
+ "unsupported-tag": "Unsupported start tag",
1671
+ "unmatched-end-tag": "Can't find end tag corresponding to start tag",
1672
+ "unclosed-tag": "Can't find end of the entity"
1673
+ };
1674
+ function htmlRefusal(markup, limit) {
1675
+ const { problem, text } = checkTelegramHtml2(markup);
1676
+ if (problem) {
1677
+ const detail = problem.kind === "unsupported-tag" ? ` "${problem.tag}"` : "";
1678
+ return `Bad Request: can't parse entities: ${PARSE_REFUSALS[problem.kind]}${detail}`;
1679
+ }
1680
+ if ([...text].length > limit) {
1681
+ return limit === TELEGRAM_CAPTION_LIMIT2 ? "Bad Request: message caption is too long" : "Bad Request: message is too long";
1682
+ }
1683
+ return;
1684
+ }
1685
+ function payloadRefusal(payload, field) {
1686
+ for (const row of markupAt(payload)?.inline_keyboard ?? []) {
1687
+ for (const button of row) {
1688
+ if ("callback_data" in button && new TextEncoder().encode(button.callback_data).length > CALLBACK_DATA_LIMIT2) {
1689
+ return "Bad Request: BUTTON_DATA_INVALID";
1690
+ }
1691
+ }
1692
+ }
1693
+ const text = stringAt(payload, field);
1694
+ if (text === undefined || payload.parse_mode !== "HTML")
1695
+ return;
1696
+ return htmlRefusal(text, field === "caption" ? TELEGRAM_CAPTION_LIMIT2 : TELEGRAM_TEXT_LIMIT2);
1697
+ }
1698
+ var NOT_MODIFIED = "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message";
1699
+ function stringAt(payload, key) {
1700
+ const value = payload[key];
1701
+ return typeof value === "string" ? value : undefined;
1702
+ }
1703
+ function markupAt(payload) {
1704
+ const markup = payload.reply_markup;
1705
+ if (!isRecord(markup) || !Array.isArray(markup.inline_keyboard))
1706
+ return;
1707
+ return transportResult(markup);
1708
+ }
1709
+ function sameMarkup(left, right) {
1710
+ return JSON.stringify(left?.inline_keyboard ?? []) === JSON.stringify(right?.inline_keyboard ?? []);
1711
+ }
1712
+ function editContent(message, method, payload) {
1713
+ if (method === "editMessageText") {
1714
+ if (message.kind !== "text" && message.kind !== "rich") {
1715
+ return { refused: "Bad Request: there is no text in the message to edit" };
1716
+ }
1717
+ const text = stringAt(payload, "text") ?? "";
1718
+ const rich = payload.rich_message;
1719
+ const changed = text !== message.text || JSON.stringify(rich) !== JSON.stringify(message.rich);
1720
+ message.text = text;
1721
+ message.rich = rich;
1722
+ return { changed };
1723
+ }
1724
+ if (method === "editMessageCaption") {
1725
+ if (!MEDIA.includes(message.kind)) {
1726
+ return { refused: "Bad Request: there is no caption in the message to edit" };
1727
+ }
1728
+ const caption = stringAt(payload, "caption") ?? "";
1729
+ const changed = caption !== message.text;
1730
+ message.text = caption;
1731
+ return { changed };
1732
+ }
1733
+ if (method === "editMessageMedia") {
1734
+ const media = isRecord(payload.media) ? payload.media : {};
1735
+ const type = stringAt(media, "type");
1736
+ const kind = MEDIA.find((candidate) => candidate === type);
1737
+ if (!kind)
1738
+ return { refused: "Bad Request: wrong media type" };
1739
+ const source = stringAt(media, "media");
1740
+ const caption = stringAt(media, "caption") ?? "";
1741
+ const changed = kind !== message.kind || source !== message.media || caption !== message.text;
1742
+ Object.assign(message, { kind, media: source, text: caption, rich: undefined });
1743
+ return { changed };
1744
+ }
1745
+ return { changed: false };
1746
+ }
1747
+ function createFakeTelegram({
1748
+ chat,
1749
+ botUser,
1750
+ user
1751
+ }) {
1752
+ const stored = [];
1753
+ const answers = [];
1754
+ let lastMessageId = 0;
1755
+ const refuse = (description, errorCode = 400) => ({
1756
+ ok: false,
1757
+ error_code: errorCode,
1758
+ description
1759
+ });
1760
+ const find = (id) => stored.find((message) => message.id === id && !message.deleted);
1761
+ const asTelegram = (message) => {
1762
+ const base = {
1763
+ message_id: message.id,
1764
+ date: 1700000000,
1765
+ chat,
1766
+ from: message.from === "bot" ? botUser : user
1767
+ };
1768
+ const markup = message.markup ? { reply_markup: message.markup } : {};
1769
+ if (message.kind === "text")
1770
+ return transportResult({ ...base, text: message.text, ...markup });
1771
+ if (message.kind === "rich")
1772
+ return transportResult({ ...base, rich_message: message.rich, ...markup });
1773
+ const file = { file_id: message.media ?? "", file_unique_id: message.media ?? "" };
1774
+ const media = message.kind === "photo" ? { photo: [{ ...file, width: 1, height: 1 }] } : { [message.kind]: file };
1775
+ return transportResult({
1776
+ ...base,
1777
+ ...media,
1778
+ ...message.text ? { caption: message.text } : {},
1779
+ ...markup
1780
+ });
1781
+ };
1782
+ const send = (kind, payload) => {
1783
+ const refusal = payloadRefusal(payload, kind === "text" ? "text" : "caption");
1784
+ if (refusal)
1785
+ return refuse(refusal);
1786
+ const message = {
1787
+ id: ++lastMessageId,
1788
+ from: "bot",
1789
+ kind,
1790
+ text: stringAt(payload, kind === "text" ? "text" : "caption") ?? "",
1791
+ media: kind === "text" || kind === "rich" ? undefined : stringAt(payload, kind),
1792
+ rich: payload.rich_message,
1793
+ markup: markupAt(payload),
1794
+ deleted: false
1795
+ };
1796
+ stored.push(message);
1797
+ return { ok: true, result: asTelegram(message) };
1798
+ };
1799
+ const edit = (method, payload) => {
1800
+ const message = find(payload.message_id);
1801
+ if (!message)
1802
+ return refuse("Bad Request: message to edit not found");
1803
+ const media = isRecord(payload.media) ? payload.media : undefined;
1804
+ const refusal = method === "editMessageMedia" && media ? payloadRefusal({ ...media, reply_markup: payload.reply_markup }, "caption") : payloadRefusal(payload, method === "editMessageCaption" ? "caption" : "text");
1805
+ if (refusal)
1806
+ return refuse(refusal);
1807
+ const markup = markupAt(payload);
1808
+ const content = editContent(message, method, payload);
1809
+ if ("refused" in content)
1810
+ return refuse(content.refused);
1811
+ if (!content.changed && sameMarkup(message.markup, markup))
1812
+ return refuse(NOT_MODIFIED);
1813
+ message.markup = markup;
1814
+ return { ok: true, result: asTelegram(message) };
1815
+ };
1816
+ const answer = (method, payload) => {
1817
+ switch (method) {
1818
+ case "sendMessage":
1819
+ return send("text", payload);
1820
+ case "sendRichMessage":
1821
+ return send("rich", payload);
1822
+ case "sendPhoto":
1823
+ case "sendVideo":
1824
+ case "sendAnimation":
1825
+ case "sendDocument":
1826
+ case "sendAudio":
1827
+ return send(SENT_KINDS[method] ?? "document", payload);
1828
+ case "editMessageText":
1829
+ case "editMessageCaption":
1830
+ case "editMessageMedia":
1831
+ case "editMessageReplyMarkup":
1832
+ return edit(method, payload);
1833
+ case "deleteMessage": {
1834
+ const message = find(payload.message_id);
1835
+ if (!message)
1836
+ return refuse("Bad Request: message to delete not found");
1837
+ message.deleted = true;
1838
+ return { ok: true, result: true };
1839
+ }
1840
+ case "answerCallbackQuery":
1841
+ answers.push(stringAt(payload, "text") ?? "");
1842
+ return { ok: true, result: true };
1843
+ default:
1844
+ return { ok: true, result: true };
1845
+ }
1846
+ };
1847
+ return {
1848
+ stored,
1849
+ answers,
1850
+ find,
1851
+ asTelegram,
1852
+ answer,
1853
+ refuse,
1854
+ nextMessageId: () => ++lastMessageId
1855
+ };
1856
+ }
1857
+
1858
+ // src/telegram/screens/test-chat.ts
1859
+ function createScreenTestChat(first, options = {}) {
1860
+ const chatId = options.chatId ?? 1000;
1861
+ const userId = options.userId ?? chatId;
1862
+ const chat = options.chatType === "group" ? { id: chatId, type: "group", title: "Test" } : options.chatType === "supergroup" ? { id: chatId, type: "supergroup", title: "Test" } : { id: chatId, type: "private", first_name: "Test" };
1863
+ const user = { id: userId, is_bot: false, first_name: "Test" };
1864
+ const botUser = {
1865
+ id: 42,
1866
+ is_bot: true,
1867
+ first_name: "Screens",
1868
+ username: "screens_test_bot"
1869
+ };
1870
+ const botInfo = {
1871
+ ...botUser,
1872
+ is_bot: true,
1873
+ can_join_groups: true,
1874
+ can_read_all_group_messages: false,
1875
+ supports_inline_queries: false,
1876
+ can_connect_to_business: false,
1877
+ has_main_web_app: false,
1878
+ has_topics_enabled: false,
1879
+ allows_users_to_create_topics: false,
1880
+ can_manage_bots: false,
1881
+ supports_join_request_queries: false
1882
+ };
1883
+ let bot = first;
1884
+ const calls = [];
1885
+ const failures = new Map;
1886
+ let nextUpdateId = 1;
1887
+ const telegram = createFakeTelegram({ chat, botUser, user });
1888
+ const { stored, find, asTelegram } = telegram;
1889
+ const connect = (next) => {
1890
+ if (!next.isInited())
1891
+ next.botInfo = botInfo;
1892
+ next.api.config.use(async (_previous, method, payload) => {
1893
+ const body = isRecord(payload) ? payload : {};
1894
+ calls.push({ method, payload: body });
1895
+ const failure = failures.get(method);
1896
+ if (failure) {
1897
+ failures.delete(method);
1898
+ return transportResult(telegram.refuse(failure.description, failure.errorCode));
1899
+ }
1900
+ return transportResult(telegram.answer(method, body));
1901
+ });
1902
+ bot = next;
1903
+ };
1904
+ connect(first);
1905
+ const deliver = async (update) => {
1906
+ try {
1907
+ await bot.handleUpdate(transportResult({ update_id: nextUpdateId++, ...update }));
1908
+ } catch (error) {
1909
+ if (!(isRecord(error) && error.name === "BotError" && ("ctx" in error)))
1910
+ throw error;
1911
+ await bot.errorHandler(transportResult(error));
1912
+ }
1913
+ };
1914
+ const userMessage = (fields) => {
1915
+ const message = {
1916
+ id: telegram.nextMessageId(),
1917
+ from: "user",
1918
+ kind: "photo" in fields ? "photo" : "text",
1919
+ text: typeof fields.text === "string" ? fields.text : typeof fields.caption === "string" ? fields.caption : "",
1920
+ media: undefined,
1921
+ rich: undefined,
1922
+ markup: undefined,
1923
+ deleted: false
1924
+ };
1925
+ stored.push(message);
1926
+ return {
1927
+ ...message,
1928
+ telegram: transportResult({
1929
+ message_id: message.id,
1930
+ date: 1700000000,
1931
+ chat,
1932
+ from: user,
1933
+ ...fields
1934
+ })
1935
+ };
1936
+ };
1937
+ const pressOn = async (message, data) => {
1938
+ await deliver({
1939
+ callback_query: {
1940
+ id: `press-${nextUpdateId}`,
1941
+ from: user,
1942
+ chat_instance: "test",
1943
+ data,
1944
+ message: asTelegram(message)
1945
+ }
1946
+ });
1947
+ };
1948
+ return {
1949
+ get messages() {
1950
+ return stored.filter((message) => !message.deleted).map((message) => ({
1951
+ id: message.id,
1952
+ from: message.from,
1953
+ kind: message.kind,
1954
+ text: message.text,
1955
+ media: message.media,
1956
+ rich: message.rich,
1957
+ buttons: (message.markup?.inline_keyboard ?? []).map((row) => row.map((button) => button.text)),
1958
+ markup: message.markup
1959
+ }));
1960
+ },
1961
+ calls,
1962
+ answers: telegram.answers,
1963
+ send: async (text) => {
1964
+ const command = /^\/\w+/.exec(text);
1965
+ const { telegram } = userMessage({
1966
+ text,
1967
+ ...command ? { entities: [{ type: "bot_command", offset: 0, length: command[0].length }] } : {}
1968
+ });
1969
+ await deliver({ message: telegram });
1970
+ },
1971
+ sendPhoto: async (fileId, caption) => {
1972
+ const { telegram } = userMessage({
1973
+ photo: [{ file_id: fileId, file_unique_id: fileId, width: 1, height: 1 }],
1974
+ ...caption === undefined ? {} : { caption }
1975
+ });
1976
+ await deliver({ message: telegram });
1977
+ },
1978
+ press: async (label) => {
1979
+ for (const message of [...stored].reverse()) {
1980
+ if (message.deleted)
1981
+ continue;
1982
+ for (const row of message.markup?.inline_keyboard ?? []) {
1983
+ for (const button of row) {
1984
+ if (button.text === label && "callback_data" in button)
1985
+ return pressOn(message, button.callback_data);
1986
+ }
1987
+ }
1988
+ }
1989
+ throw new Error(`No button "${label}" in the chat.`);
1990
+ },
1991
+ pressData: async (messageId, data) => {
1992
+ const message = find(messageId);
1993
+ if (!message)
1994
+ throw new Error(`No message ${messageId} in the chat.`);
1995
+ await pressOn(message, data);
1996
+ },
1997
+ failNext: (method, description, errorCode = 400) => {
1998
+ failures.set(method, { description, errorCode });
1999
+ },
2000
+ deleteByUser: (messageId) => {
2001
+ const message = find(messageId);
2002
+ if (message)
2003
+ message.deleted = true;
2004
+ },
2005
+ restart: (next) => connect(next),
2006
+ clearCalls: () => {
2007
+ calls.length = 0;
2008
+ telegram.answers.length = 0;
2009
+ }
2010
+ };
2011
+ }
2012
+ export {
2013
+ ScreenBodyBuilder,
2014
+ ScreenBuilder,
2015
+ ScreenButton,
2016
+ ScreenChatStateSchema,
2017
+ ScreenGroupBuilder,
2018
+ ScreenOutcome,
2019
+ ScreenScope,
2020
+ TelegramHtml,
2021
+ TelegramScreen,
2022
+ TelegramScreensRoot,
2023
+ back,
2024
+ createScreenTestChat,
2025
+ html,
2026
+ link,
2027
+ telegramScreens
2028
+ };