@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.1

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 (295) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -2
  4. package/dist/adapters/clock.d.ts +3 -0
  5. package/dist/adapters/clock.js +4 -0
  6. package/dist/adapters/console-log.d.ts +13 -0
  7. package/dist/adapters/console-log.js +41 -0
  8. package/dist/adapters/d1-db.d.ts +5 -0
  9. package/dist/adapters/d1-db.js +37 -0
  10. package/dist/adapters/ids.d.ts +5 -0
  11. package/dist/adapters/ids.js +25 -0
  12. package/dist/adapters/index.d.ts +7 -0
  13. package/dist/adapters/keyring.d.ts +50 -0
  14. package/dist/adapters/keyring.js +174 -0
  15. package/dist/adapters/metrics.d.ts +28 -0
  16. package/dist/adapters/metrics.js +48 -0
  17. package/dist/adapters/r2-files.d.ts +20 -0
  18. package/dist/adapters/r2-files.js +53 -0
  19. package/dist/bin/cli.d.ts +1 -0
  20. package/dist/bin/cli.js +45 -0
  21. package/dist/client/broadcasts.d.ts +35 -0
  22. package/dist/client/commands.d.ts +23 -0
  23. package/dist/client/endpoint.d.ts +34 -0
  24. package/dist/client/events.d.ts +33 -0
  25. package/dist/client/index.d.ts +11 -0
  26. package/dist/client/index.js +3 -0
  27. package/dist/client/schedules.d.ts +42 -0
  28. package/dist/client/schema.d.ts +181 -0
  29. package/dist/client/send.d.ts +15 -0
  30. package/dist/client/templates.d.ts +47 -0
  31. package/dist/contracts/bots.d.ts +129 -0
  32. package/dist/contracts/bots.js +70 -0
  33. package/dist/contracts/broadcasts.d.ts +116 -0
  34. package/dist/contracts/broadcasts.js +63 -0
  35. package/dist/contracts/chats.d.ts +97 -0
  36. package/dist/contracts/chats.js +66 -0
  37. package/dist/contracts/commands.d.ts +52 -0
  38. package/dist/contracts/commands.js +53 -0
  39. package/dist/contracts/common.d.ts +38 -0
  40. package/dist/contracts/common.js +41 -0
  41. package/dist/contracts/deliveries.d.ts +103 -0
  42. package/dist/contracts/deliveries.js +74 -0
  43. package/dist/contracts/destinations.d.ts +24 -0
  44. package/dist/contracts/destinations.js +28 -0
  45. package/dist/contracts/events.d.ts +165 -0
  46. package/dist/contracts/events.js +98 -0
  47. package/dist/contracts/features.d.ts +20 -0
  48. package/dist/contracts/features.js +21 -0
  49. package/dist/contracts/health.d.ts +55 -0
  50. package/dist/contracts/health.js +58 -0
  51. package/dist/contracts/index.d.ts +22 -0
  52. package/dist/contracts/index.js +22 -0
  53. package/dist/contracts/labels.d.ts +38 -0
  54. package/dist/contracts/labels.js +52 -0
  55. package/dist/contracts/links.d.ts +51 -0
  56. package/dist/contracts/links.js +50 -0
  57. package/dist/contracts/meta.d.ts +132 -0
  58. package/dist/contracts/meta.js +85 -0
  59. package/dist/contracts/permissions.d.ts +24 -0
  60. package/dist/contracts/permissions.js +27 -0
  61. package/dist/contracts/problems.d.ts +106 -0
  62. package/dist/contracts/problems.js +101 -0
  63. package/dist/contracts/recipients.d.ts +36 -0
  64. package/dist/contracts/recipients.js +22 -0
  65. package/dist/contracts/schedules.d.ts +276 -0
  66. package/dist/contracts/schedules.js +143 -0
  67. package/dist/contracts/send.d.ts +126 -0
  68. package/dist/contracts/send.js +69 -0
  69. package/dist/contracts/subscriptions.d.ts +38 -0
  70. package/dist/contracts/subscriptions.js +31 -0
  71. package/dist/contracts/templates.d.ts +358 -0
  72. package/dist/contracts/templates.js +174 -0
  73. package/dist/contracts/users.d.ts +15 -0
  74. package/dist/contracts/users.js +19 -0
  75. package/dist/core/guards.d.ts +22 -0
  76. package/dist/core/guards.js +47 -0
  77. package/dist/core/html/escape.d.ts +2 -0
  78. package/dist/core/html/escape.js +36 -0
  79. package/dist/core/html/tokenize.js +65 -0
  80. package/dist/core/index.d.ts +18 -0
  81. package/dist/core/pacing.d.ts +1 -0
  82. package/dist/core/pacing.js +86 -0
  83. package/dist/core/random.d.ts +2 -0
  84. package/dist/core/random.js +38 -0
  85. package/dist/core/recipients.d.ts +1 -0
  86. package/dist/core/recipients.js +86 -0
  87. package/dist/core/schedule/describe.d.ts +3 -0
  88. package/dist/core/schedule/describe.js +74 -0
  89. package/dist/core/schedule/index.d.ts +4 -0
  90. package/dist/core/schedule/policy.d.ts +1 -0
  91. package/dist/core/schedule/policy.js +37 -0
  92. package/dist/core/schedule/rule.d.ts +25 -0
  93. package/dist/core/schedule/rule.js +142 -0
  94. package/dist/core/schedule/tz.d.ts +2 -0
  95. package/dist/core/schedule/tz.js +59 -0
  96. package/dist/core/split/plain.d.ts +2 -0
  97. package/dist/core/split/plain.js +23 -0
  98. package/dist/core/split/split.d.ts +15 -0
  99. package/dist/core/split/split.js +158 -0
  100. package/dist/core/template/ast.d.ts +58 -0
  101. package/dist/core/template/ast.js +5 -0
  102. package/dist/core/template/expr.js +108 -0
  103. package/dist/core/template/filters.d.ts +20 -0
  104. package/dist/core/template/filters.js +127 -0
  105. package/dist/core/template/format.d.ts +17 -0
  106. package/dist/core/template/format.js +88 -0
  107. package/dist/core/template/index.d.ts +20 -0
  108. package/dist/core/template/index.js +29 -0
  109. package/dist/core/template/parse.d.ts +8 -0
  110. package/dist/core/template/parse.js +369 -0
  111. package/dist/core/template/render.d.ts +17 -0
  112. package/dist/core/template/render.js +118 -0
  113. package/dist/core/template/shape.d.ts +28 -0
  114. package/dist/core/template/shape.js +151 -0
  115. package/dist/core/template/tags.js +105 -0
  116. package/dist/core/template/validate.d.ts +10 -0
  117. package/dist/core/template/validate.js +144 -0
  118. package/dist/core/template/variables.d.ts +15 -0
  119. package/dist/core/template/variables.js +41 -0
  120. package/dist/do/index.d.ts +3 -0
  121. package/dist/do/index.js +3 -0
  122. package/dist/do/telegram-bot-do.d.ts +45 -0
  123. package/dist/do/telegram-bot-do.js +219 -0
  124. package/dist/engine/actions/callback.js +256 -0
  125. package/dist/engine/actions/tokens.d.ts +2 -0
  126. package/dist/engine/actions/tokens.js +51 -0
  127. package/dist/engine/bot-api.js +20 -0
  128. package/dist/engine/bot-features.js +39 -0
  129. package/dist/engine/chat-state.js +66 -0
  130. package/dist/engine/content/audience.js +54 -0
  131. package/dist/engine/content/buttons.d.ts +1 -0
  132. package/dist/engine/content/buttons.js +113 -0
  133. package/dist/engine/content/fanout.js +284 -0
  134. package/dist/engine/content/intersect.d.ts +6 -0
  135. package/dist/engine/content/intersect.js +26 -0
  136. package/dist/engine/content/providers.d.ts +6 -0
  137. package/dist/engine/content/providers.js +142 -0
  138. package/dist/engine/content/recipients.js +128 -0
  139. package/dist/engine/content/render.js +138 -0
  140. package/dist/engine/content/reply.js +156 -0
  141. package/dist/engine/content/report-files.js +78 -0
  142. package/dist/engine/content/template-rows.js +74 -0
  143. package/dist/engine/dialogs/dialog.d.ts +2 -0
  144. package/dist/engine/dialogs/dialog.js +375 -0
  145. package/dist/engine/dispatch/builtins.js +198 -0
  146. package/dist/engine/dispatch/commands.js +194 -0
  147. package/dist/engine/dispatch/context.d.ts +2 -0
  148. package/dist/engine/dispatch/context.js +194 -0
  149. package/dist/engine/dispatch/dispatcher.js +66 -0
  150. package/dist/engine/dispatch/handlers.d.ts +1 -0
  151. package/dist/engine/dispatch/handlers.js +103 -0
  152. package/dist/engine/dispatch/lifecycle.js +85 -0
  153. package/dist/engine/dispatch/link-chat.js +31 -0
  154. package/dist/engine/dispatch/linking.js +273 -0
  155. package/dist/engine/errors.d.ts +60 -0
  156. package/dist/engine/errors.js +72 -0
  157. package/dist/engine/events/filters.d.ts +5 -0
  158. package/dist/engine/events/filters.js +82 -0
  159. package/dist/engine/events/kick.js +30 -0
  160. package/dist/engine/events/payload.d.ts +3 -0
  161. package/dist/engine/events/payload.js +16 -0
  162. package/dist/engine/events/tick.js +208 -0
  163. package/dist/engine/i18n.d.ts +12 -0
  164. package/dist/engine/i18n.js +55 -0
  165. package/dist/engine/inbound/files.js +121 -0
  166. package/dist/engine/inbound/handler.js +46 -0
  167. package/dist/engine/jobs.d.ts +32 -0
  168. package/dist/engine/jobs.js +85 -0
  169. package/dist/engine/link-codes.js +46 -0
  170. package/dist/engine/metrics.js +12 -0
  171. package/dist/engine/options.d.ts +369 -0
  172. package/dist/engine/options.js +81 -0
  173. package/dist/engine/outbox/dead-letter.js +78 -0
  174. package/dist/engine/outbox/do-registry.d.ts +10 -0
  175. package/dist/engine/outbox/do-registry.js +10 -0
  176. package/dist/engine/outbox/do-transport.d.ts +33 -0
  177. package/dist/engine/outbox/do-transport.js +73 -0
  178. package/dist/engine/outbox/drain.d.ts +13 -0
  179. package/dist/engine/outbox/drain.js +381 -0
  180. package/dist/engine/outbox/enqueue.d.ts +1 -0
  181. package/dist/engine/outbox/enqueue.js +63 -0
  182. package/dist/engine/outbox/select.js +24 -0
  183. package/dist/engine/outbox/send.js +362 -0
  184. package/dist/engine/outbox/transport.d.ts +35 -0
  185. package/dist/engine/outbox/transport.js +175 -0
  186. package/dist/engine/outbox/wake.js +28 -0
  187. package/dist/engine/registry.d.ts +502 -0
  188. package/dist/engine/registry.js +258 -0
  189. package/dist/engine/rows.d.ts +25 -0
  190. package/dist/engine/rows.js +131 -0
  191. package/dist/engine/runtime.d.ts +95 -0
  192. package/dist/engine/runtime.js +149 -0
  193. package/dist/engine/schedules/fire.js +208 -0
  194. package/dist/engine/schedules/resume.js +75 -0
  195. package/dist/engine/schedules/rows.js +69 -0
  196. package/dist/engine/schedules/tick.js +30 -0
  197. package/dist/engine/schedules/wake.js +50 -0
  198. package/dist/engine/state.d.ts +11 -0
  199. package/dist/engine/state.js +78 -0
  200. package/dist/locales/en.js +339 -0
  201. package/dist/locales/en.json +341 -0
  202. package/dist/locales/mr.js +339 -0
  203. package/dist/locales/mr.json +341 -0
  204. package/dist/ports/db.d.ts +22 -0
  205. package/dist/ports/index.d.ts +4 -0
  206. package/dist/ports/ports.d.ts +257 -0
  207. package/dist/ports/sql.d.ts +39 -0
  208. package/dist/ports/sql.js +76 -0
  209. package/dist/renderers/csv.d.ts +14 -0
  210. package/dist/renderers/csv.js +51 -0
  211. package/dist/renderers/html.d.ts +44 -0
  212. package/dist/renderers/html.js +81 -0
  213. package/dist/renderers/index.d.ts +5 -0
  214. package/dist/renderers/index.js +4 -0
  215. package/dist/renderers/table.d.ts +30 -0
  216. package/dist/renderers/table.js +114 -0
  217. package/dist/routes/admin.js +146 -0
  218. package/dist/routes/app.d.ts +3111 -0
  219. package/dist/routes/app.js +9 -0
  220. package/dist/routes/broadcasts.js +37 -0
  221. package/dist/routes/commands.js +35 -0
  222. package/dist/routes/content.js +12 -0
  223. package/dist/routes/events.js +36 -0
  224. package/dist/routes/http.d.ts +30 -0
  225. package/dist/routes/http.js +223 -0
  226. package/dist/routes/me.js +59 -0
  227. package/dist/routes/schedules.js +62 -0
  228. package/dist/routes/send.js +24 -0
  229. package/dist/routes/templates.js +70 -0
  230. package/dist/routes/webhook.js +147 -0
  231. package/dist/schema/columns.js +307 -0
  232. package/dist/schema/ddl.d.ts +7 -0
  233. package/dist/schema/ddl.js +45 -0
  234. package/dist/schema/index.d.ts +4255 -0
  235. package/dist/schema/index.js +44 -0
  236. package/dist/schema/reference.sql +359 -0
  237. package/dist/schema/tables.d.ts +4243 -0
  238. package/dist/schema/tables.js +327 -0
  239. package/dist/schema/version.d.ts +5 -0
  240. package/dist/schema/version.js +7 -0
  241. package/dist/server/index.d.ts +55 -0
  242. package/dist/server/index.js +46 -0
  243. package/dist/server/plugin.d.ts +81 -0
  244. package/dist/server/plugin.js +247 -0
  245. package/dist/server/version.d.ts +2 -0
  246. package/dist/server/version.js +4 -0
  247. package/dist/services/ai-tool.d.ts +26 -0
  248. package/dist/services/ai-tool.js +148 -0
  249. package/dist/services/bots.js +300 -0
  250. package/dist/services/broadcasts.js +390 -0
  251. package/dist/services/chats.js +83 -0
  252. package/dist/services/cleanup.js +83 -0
  253. package/dist/services/commands.js +140 -0
  254. package/dist/services/deliveries.js +53 -0
  255. package/dist/services/destinations.js +91 -0
  256. package/dist/services/event-rules.js +229 -0
  257. package/dist/services/events.js +20 -0
  258. package/dist/services/idempotency.js +64 -0
  259. package/dist/services/links.js +200 -0
  260. package/dist/services/menu.js +95 -0
  261. package/dist/services/ops.js +90 -0
  262. package/dist/services/rekey.js +63 -0
  263. package/dist/services/reports.js +87 -0
  264. package/dist/services/schedules.js +214 -0
  265. package/dist/services/send.d.ts +14 -0
  266. package/dist/services/send.js +421 -0
  267. package/dist/services/subscriptions.js +28 -0
  268. package/dist/services/templates.js +259 -0
  269. package/dist/telegram/classify.d.ts +30 -0
  270. package/dist/telegram/classify.js +164 -0
  271. package/dist/telegram/client.d.ts +73 -0
  272. package/dist/telegram/client.js +135 -0
  273. package/dist/telegram/errors.d.ts +37 -0
  274. package/dist/telegram/errors.js +62 -0
  275. package/dist/telegram/inbound.d.ts +40 -0
  276. package/dist/telegram/inbound.js +72 -0
  277. package/dist/telegram/index.d.ts +8 -0
  278. package/dist/telegram/multipart.d.ts +5 -0
  279. package/dist/telegram/multipart.js +47 -0
  280. package/dist/telegram/types.d.ts +271 -0
  281. package/dist/telegram/update.d.ts +833 -0
  282. package/dist/telegram/update.js +129 -0
  283. package/dist/testing/fake-html.d.ts +9 -0
  284. package/dist/testing/fake-html.js +83 -0
  285. package/dist/testing/fake-telegram.d.ts +206 -0
  286. package/dist/testing/fake-telegram.js +856 -0
  287. package/dist/testing/index.d.ts +7 -0
  288. package/dist/testing/index.js +7 -0
  289. package/dist/testing/reference-sql.d.ts +4 -0
  290. package/dist/testing/reference-sql.js +8 -0
  291. package/dist/testing/test-plugin.d.ts +126 -0
  292. package/dist/testing/test-plugin.js +182 -0
  293. package/dist/testing/updates.d.ts +100 -0
  294. package/dist/testing/updates.js +212 -0
  295. package/package.json +113 -3
@@ -0,0 +1,258 @@
1
+ import { Key, LocalizedText } from "../contracts/common.js";
2
+ import { BUILTIN_COMMANDS, CommandName } from "../contracts/commands.js";
3
+ import { TelegramConfigError } from "./errors.js";
4
+ import { z } from "zod";
5
+ // src/engine/registry.ts
6
+ /** `defaultRoles` / per-bot roles entry meaning "any role". */
7
+ const ANY_ROLE = "*";
8
+ /** Names the plugin answers itself; host commands may not reuse them. */
9
+ const RESERVED_COMMANDS = [
10
+ ...BUILTIN_COMMANDS,
11
+ "here",
12
+ "cancel",
13
+ "skip",
14
+ "report"
15
+ ];
16
+ function fail(what, msg) {
17
+ throw new TelegramConfigError(`${what}: ${msg}`);
18
+ }
19
+ function checkKey(what, key) {
20
+ if (!Key.safeParse(key).success) fail(what, `invalid key "${key}" (lowercase [a-z0-9._:-], ≤ 64)`);
21
+ }
22
+ function checkLabel(what, label) {
23
+ if (!LocalizedText.safeParse(label).success) fail(what, "label must be { en: '…', … } with at least one locale");
24
+ }
25
+ function checkPermission(what, permission) {
26
+ if (permission !== void 0 && (typeof permission !== "string" || permission.trim() === "")) fail(what, "permission must be a non-empty string");
27
+ }
28
+ function isZod(v) {
29
+ return v instanceof z.ZodType;
30
+ }
31
+ function objectKeys(schema) {
32
+ return schema instanceof z.ZodObject ? Object.keys(schema.shape) : null;
33
+ }
34
+ /** Validates and freezes a provider definition. Throws `TelegramConfigError` on an invalid key, label or schema. */
35
+ function defineProvider(def) {
36
+ const what = `provider ${def.key}`;
37
+ checkKey(what, def.key);
38
+ checkLabel(what, def.label);
39
+ checkPermission(what, def.permission);
40
+ if (!isZod(def.variables)) fail(what, "variables must be a Zod schema");
41
+ if (def.params !== void 0 && !isZod(def.params)) fail(what, "params must be a Zod schema");
42
+ if (typeof def.run !== "function") fail(what, "run() is required");
43
+ const { sample, run } = def;
44
+ return Object.freeze({
45
+ key: def.key,
46
+ label: def.label,
47
+ params: def.params,
48
+ permission: def.permission ?? null,
49
+ variables: def.variables,
50
+ cacheFor: def.cacheFor ?? "none",
51
+ ...sample ? { sample } : {},
52
+ run
53
+ });
54
+ }
55
+ /**
56
+ * Validates and freezes a command definition. Throws `TelegramConfigError` on a bad or reserved name or alias, or
57
+ * when not exactly one of `template` / `handle` is given.
58
+ */
59
+ function defineCommand(def) {
60
+ const what = `command /${def.command}`;
61
+ if (!CommandName.safeParse(def.command).success) fail(what, "name must match [a-z0-9_]{1,32}");
62
+ if (RESERVED_COMMANDS.includes(def.command)) fail(what, "is a built-in command");
63
+ checkLabel(what, def.description);
64
+ checkPermission(what, def.permission);
65
+ if (def.args !== void 0 && !isZod(def.args)) fail(what, "args must be a Zod schema");
66
+ if (def.template === void 0 === (def.handle === void 0)) fail(what, "give exactly one of template or handle");
67
+ if (def.template !== void 0) checkKey(what, def.template);
68
+ for (const a of def.aliases ?? []) if (!CommandName.safeParse(a).success || RESERVED_COMMANDS.includes(a)) fail(what, `invalid alias "${a}"`);
69
+ const scope = def.scope ?? [
70
+ "private",
71
+ "group",
72
+ "supergroup"
73
+ ];
74
+ if (scope.length === 0) fail(what, "scope is empty");
75
+ if (def.groupMembers !== void 0 && def.groupMembers !== "linked" && def.groupMembers !== "any") fail(what, "groupMembers must be \"linked\" or \"any\"");
76
+ const { parseArgs, handle } = def;
77
+ return Object.freeze({
78
+ command: def.command,
79
+ description: def.description,
80
+ permission: def.permission ?? null,
81
+ defaultRoles: Object.freeze([...def.defaultRoles ?? ["*"]]),
82
+ groupMembers: def.groupMembers,
83
+ args: def.args,
84
+ scope: Object.freeze(scope.includes("group") && !scope.includes("supergroup") ? [...scope, "supergroup"] : [...scope]),
85
+ template: def.template ?? null,
86
+ aliases: Object.freeze([...def.aliases ?? []]),
87
+ showInMenu: def.showInMenu ?? true,
88
+ sort: def.sort ?? 0,
89
+ ...parseArgs ? { parseArgs } : {},
90
+ ...handle ? { handle } : {}
91
+ });
92
+ }
93
+ /**
94
+ * Validates and freezes an event definition. Throws `TelegramConfigError` on a bad key or label, or a
95
+ * filterable / sensitive field missing from an object payload.
96
+ */
97
+ function defineEvent(def) {
98
+ const what = `event ${def.key}`;
99
+ checkKey(what, def.key);
100
+ checkLabel(what, def.label);
101
+ if (!isZod(def.payload)) fail(what, "payload must be a Zod schema");
102
+ if (def.provider !== void 0) checkKey(what, def.provider);
103
+ const keys = objectKeys(def.payload);
104
+ for (const f of [...def.filterable ?? [], ...def.sensitive ?? []]) if (keys && !keys.includes(f)) fail(what, `field "${f}" is not in the payload schema`);
105
+ return Object.freeze({
106
+ key: def.key,
107
+ label: def.label,
108
+ payload: def.payload,
109
+ provider: def.provider ?? null,
110
+ filterable: Object.freeze([...def.filterable ?? []]),
111
+ sensitive: Object.freeze([...def.sensitive ?? []]),
112
+ sensitivePermission: def.sensitivePermission ?? null
113
+ });
114
+ }
115
+ /** Validates and freezes an action definition. Throws `TelegramConfigError` if invalid. */
116
+ function defineAction(def) {
117
+ const what = `action ${def.key}`;
118
+ checkKey(what, def.key);
119
+ checkPermission(what, def.permission);
120
+ if (!isZod(def.params)) fail(what, "params must be a Zod schema");
121
+ if (def.confirm !== void 0) checkLabel(what, def.confirm);
122
+ if (def.result !== void 0) checkLabel(what, def.result);
123
+ if (typeof def.run !== "function") fail(what, "run() is required");
124
+ const ttl = def.ttlMinutes ?? 10080;
125
+ if (!Number.isInteger(ttl) || ttl < 1) fail(what, "ttlMinutes must be a positive integer");
126
+ const { run } = def;
127
+ return Object.freeze({
128
+ key: def.key,
129
+ permission: def.permission ?? null,
130
+ params: def.params,
131
+ confirm: def.confirm ?? null,
132
+ result: def.result ?? null,
133
+ singleUse: def.singleUse ?? true,
134
+ ttlMinutes: ttl,
135
+ run
136
+ });
137
+ }
138
+ /** Validates and freezes a dialog definition (unique step ids, positive timeout). Throws `TelegramConfigError`. */
139
+ function defineDialog(def) {
140
+ const what = `dialog ${def.key}`;
141
+ checkKey(what, def.key);
142
+ checkPermission(what, def.permission);
143
+ if (!Array.isArray(def.steps) || def.steps.length === 0) fail(what, "needs at least one step");
144
+ const ids = /* @__PURE__ */ new Set();
145
+ const steps = def.steps.map((s) => {
146
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,31}$/.test(s.id)) fail(what, `invalid step id "${s.id}"`);
147
+ if (ids.has(s.id)) fail(what, `duplicate step id "${s.id}"`);
148
+ ids.add(s.id);
149
+ checkLabel(`${what} step ${s.id}`, s.ask);
150
+ const input = s.input ?? "text";
151
+ if (typeof input !== "string" && !isZod(input)) fail(what, `step ${s.id}: input must be a Zod schema or a kind`);
152
+ const { choices } = s;
153
+ return Object.freeze({
154
+ id: s.id,
155
+ ask: s.ask,
156
+ input,
157
+ optional: s.optional ?? false,
158
+ ...choices ? { choices } : {}
159
+ });
160
+ });
161
+ const timeout = def.timeoutMinutes ?? 30;
162
+ if (!Number.isInteger(timeout) || timeout < 1) fail(what, "timeoutMinutes must be a positive integer");
163
+ const { finish } = def;
164
+ return Object.freeze({
165
+ key: def.key,
166
+ permission: def.permission ?? null,
167
+ steps: Object.freeze(steps),
168
+ timeoutMinutes: timeout,
169
+ finish
170
+ });
171
+ }
172
+ /** Validates and freezes a recipient-kind definition. Throws `TelegramConfigError` if invalid. */
173
+ function defineAudience(def) {
174
+ const what = `audience ${def.key}`;
175
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,31}$/.test(def.key)) fail(what, "key must match [a-zA-Z][a-zA-Z0-9_]{0,31}");
176
+ checkLabel(what, def.label);
177
+ if (typeof def.resolve !== "function") fail(what, "resolve() is required");
178
+ const { resolve, audienceFor } = def;
179
+ return Object.freeze({
180
+ key: def.key,
181
+ label: def.label,
182
+ resolve,
183
+ ...audienceFor ? { audienceFor } : {}
184
+ });
185
+ }
186
+ /** Validates and freezes a file-source definition. Throws `TelegramConfigError` if invalid. */
187
+ function defineFileSource(def) {
188
+ const what = `file source ${def.key}`;
189
+ checkKey(what, def.key);
190
+ if (def.label !== void 0) checkLabel(what, def.label);
191
+ if (typeof def.resolve !== "function") fail(what, "resolve() is required");
192
+ const { resolve } = def;
193
+ return Object.freeze({
194
+ key: def.key,
195
+ label: def.label ?? null,
196
+ resolve
197
+ });
198
+ }
199
+ /**
200
+ * All `define*` helpers typed for one host actor and (optionally) the Worker env type that every context's `env` has:
201
+ * `const d = definitions<HostActor, Env>(); d.command({…})`.
202
+ */
203
+ function definitions() {
204
+ return {
205
+ provider: (def) => defineProvider(def),
206
+ command: (def) => defineCommand(def),
207
+ event: (def) => defineEvent(def),
208
+ action: (def) => defineAction(def),
209
+ dialog: (def) => defineDialog(def),
210
+ audience: (def) => defineAudience(def),
211
+ fileSource: (def) => defineFileSource(def)
212
+ };
213
+ }
214
+ function index(kind, list, keyOf) {
215
+ const map = /* @__PURE__ */ new Map();
216
+ for (const item of list ?? []) {
217
+ if (typeof item !== "object" || item === null) throw new TelegramConfigError(`${kind}: entries must be define*()d`);
218
+ const key = keyOf(item);
219
+ if (map.has(key)) throw new TelegramConfigError(`${kind}: duplicate key "${key}"`);
220
+ map.set(key, item);
221
+ }
222
+ return map;
223
+ }
224
+ /** Indexes the host's definitions. Throws `TelegramConfigError` on duplicates or dangling references. */
225
+ function createRegistry(input) {
226
+ const providers = index("providers", input.providers, (p) => p.key);
227
+ const commandList = input.commands ?? [];
228
+ const commands = index("commands", commandList, (c) => c.command);
229
+ for (const c of commandList) for (const alias of c.aliases) {
230
+ if (commands.has(alias)) throw new TelegramConfigError(`commands: alias "${alias}" is taken`);
231
+ commands.set(alias, c);
232
+ }
233
+ const events = index("events", input.events, (e) => e.key);
234
+ for (const e of events.values()) if (e.provider !== null && !providers.has(e.provider)) throw new TelegramConfigError(`event ${e.key}: provider "${e.provider}" is not registered`);
235
+ return {
236
+ providers,
237
+ commands,
238
+ commandList,
239
+ events,
240
+ actions: index("actions", input.actions, (a) => a.key),
241
+ dialogs: index("dialogs", input.dialogs, (d) => d.key),
242
+ audiences: index("audiences", input.audiences, (a) => a.key),
243
+ fileSources: index("fileSources", input.fileSources, (f) => f.key)
244
+ };
245
+ }
246
+ /** JSON Schema of a Zod schema for meta (never throws; unrepresentable parts become `{}`). */
247
+ function jsonSchemaOf(schema) {
248
+ if (!schema) return {};
249
+ try {
250
+ return z.toJSONSchema(schema, {
251
+ unrepresentable: "any",
252
+ io: "input"
253
+ });
254
+ } catch {
255
+ return {};
256
+ }
257
+ }
258
+ export { ANY_ROLE, RESERVED_COMMANDS, createRegistry, defineAction, defineAudience, defineCommand, defineDialog, defineEvent, defineFileSource, defineProvider, definitions, jsonSchemaOf };
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export interface BotRow {
3
+ id: string;
4
+ tenant: string;
5
+ name: string;
6
+ tg_bot_id: string;
7
+ username: string;
8
+ token_enc: string;
9
+ webhook_secret_enc: string | null;
10
+ mode: string;
11
+ status: string;
12
+ status_detail: string | null;
13
+ checked_at: number | null;
14
+ default_locale: string;
15
+ features_json: string | null;
16
+ rate_per_sec: number;
17
+ paused_until: number | null;
18
+ drain_lease_until: number | null;
19
+ drain_token: string | null;
20
+ commands_synced_hash: string | null;
21
+ created_by: string | null;
22
+ created_at: number;
23
+ updated_at: number;
24
+ deleted_at: number | null;
25
+ }
@@ -0,0 +1,131 @@
1
+ import { BotDto, BotFeatures } from "../contracts/bots.js";
2
+ import { Audience, ChatDto, ChatTopics } from "../contracts/chats.js";
3
+ import { QuietHours } from "../contracts/subscriptions.js";
4
+ // src/engine/rows.ts
5
+ /** Parses a JSON column with a schema; null / invalid → `fallback`. */
6
+ function parseJson(text, schema, fallback) {
7
+ if (text === null || text === void 0 || text === "") return fallback;
8
+ try {
9
+ const r = schema.safeParse(JSON.parse(text));
10
+ return r.success ? r.data : fallback;
11
+ } catch {
12
+ return fallback;
13
+ }
14
+ }
15
+ /** A JSON object column, or null when absent / invalid. */
16
+ function parseObject(text) {
17
+ if (!text) return null;
18
+ try {
19
+ const v = JSON.parse(text);
20
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function botDto(r) {
26
+ return {
27
+ id: r.id,
28
+ name: r.name,
29
+ username: r.username,
30
+ tgBotId: r.tg_bot_id,
31
+ mode: r.mode === "polling" ? "polling" : "webhook",
32
+ status: BotDto.shape.status.safeParse(r.status).data ?? "active",
33
+ statusDetail: r.status_detail,
34
+ checkedAt: r.checked_at,
35
+ defaultLocale: r.default_locale,
36
+ features: parseJson(r.features_json, BotFeatures, {}),
37
+ ratePerSec: r.rate_per_sec,
38
+ pausedUntil: r.paused_until,
39
+ createdAt: r.created_at,
40
+ updatedAt: r.updated_at
41
+ };
42
+ }
43
+ function chatDto(r) {
44
+ return {
45
+ id: r.id,
46
+ botId: r.bot_id,
47
+ chatId: r.chat_id,
48
+ kind: ChatDto.shape.kind.safeParse(r.kind).data ?? "private",
49
+ title: r.title,
50
+ username: r.username,
51
+ userId: r.user_id,
52
+ audience: parseJson(r.audience_json, Audience.nullable(), null),
53
+ status: ChatDto.shape.status.safeParse(r.status).data ?? "pending",
54
+ locale: r.locale,
55
+ timezone: r.timezone,
56
+ topics: parseJson(r.topics_json, ChatTopics, {}),
57
+ linkedBy: r.linked_by,
58
+ linkedAt: r.linked_at,
59
+ updatedAt: r.updated_at
60
+ };
61
+ }
62
+ function destinationDto(r) {
63
+ return {
64
+ id: r.id,
65
+ name: r.name,
66
+ slug: r.slug,
67
+ botId: r.bot_id,
68
+ chatRef: r.chat_ref,
69
+ messageThreadId: r.message_thread_id,
70
+ aiAllowed: r.ai_allowed === 1,
71
+ createdAt: r.created_at
72
+ };
73
+ }
74
+ function subscriptionDto(r) {
75
+ return {
76
+ chatRef: r.chat_ref,
77
+ topic: r.topic,
78
+ muted: r.muted === 1,
79
+ quiet: parseJson(r.quiet_json, QuietHours.nullable(), null),
80
+ updatedAt: r.updated_at
81
+ };
82
+ }
83
+ const SOURCES = /* @__PURE__ */ new Set([
84
+ "schedule",
85
+ "event",
86
+ "broadcast",
87
+ "command",
88
+ "ai",
89
+ "api",
90
+ "system"
91
+ ]);
92
+ const KINDS = /* @__PURE__ */ new Set([
93
+ "text",
94
+ "document",
95
+ "photo",
96
+ "edit",
97
+ "delete"
98
+ ]);
99
+ const STATUSES = /* @__PURE__ */ new Set([
100
+ "queued",
101
+ "sending",
102
+ "sent",
103
+ "failed",
104
+ "dropped",
105
+ "cancelled"
106
+ ]);
107
+ /** `withBody`: only for callers with `telegram.ops.content`. */
108
+ function deliveryDto(r, withBody) {
109
+ return {
110
+ id: r.id,
111
+ botId: r.bot_id,
112
+ chatRef: r.chat_ref,
113
+ messageThreadId: r.message_thread_id,
114
+ source: SOURCES.has(r.source) ? r.source : "system",
115
+ sourceId: r.source_id,
116
+ kind: KINDS.has(r.kind) ? r.kind : "text",
117
+ bodyHtml: withBody ? r.body_html : null,
118
+ part: r.part,
119
+ parts: r.parts,
120
+ fileName: r.file_name,
121
+ status: STATUSES.has(r.status) ? r.status : "failed",
122
+ attempts: r.attempts,
123
+ nextAttemptAt: r.status === "queued" ? r.next_attempt_at : null,
124
+ tgMessageId: r.tg_message_id,
125
+ errorCode: r.error_code,
126
+ errorDetail: r.error_detail,
127
+ createdAt: r.created_at,
128
+ sentAt: r.sent_at
129
+ };
130
+ }
131
+ export { botDto, chatDto, deliveryDto, destinationDto, parseJson, parseObject, subscriptionDto };
@@ -0,0 +1,95 @@
1
+ import { PermissionName } from "../contracts/permissions.js";
2
+ import { RandomFill } from "../core/random.js";
3
+ import { FilterRegistry } from "../core/template/filters.js";
4
+ import { DbPort } from "../ports/db.js";
5
+ import { ActorInfo, ActorPort, AuditPort, AuthorizePort, ClockPort, ErrorsPort, FilesPort, IdsPort, KeyringPort, LogPort, MetricsPort, RendererPort, SettingsPort, TransportPort, UsersPort } from "../ports/ports.js";
6
+ import { TelegramClient } from "../telegram/client.js";
7
+ import { Translator } from "./i18n.js";
8
+ import { Registry } from "./registry.js";
9
+ import { FeatureFlags, ResolvedAiOptions, ResolvedGroupOptions, ResolvedInboundOptions, TelegramHooks, TelegramLimits, TelegramRetention } from "./options.js";
10
+ /**
11
+ * The resolved ports and options for one invocation (request, job run or queue batch). Services and the engine
12
+ * take a `Runtime`; hosts get one from `plugin.runtime(env)` to call exported services directly or in tests.
13
+ */
14
+ export interface Runtime<A> {
15
+ /** The raw Worker env of this invocation (what the `env` mapper received). */
16
+ readonly env: unknown;
17
+ /** Database port (`d1Db(env.db)` unless the mapper returned a `DbPort`). */
18
+ readonly db: DbPort;
19
+ /** Keyring for bot tokens and webhook secrets (built from `env.keyring`, cached per secret value). */
20
+ readonly keyring: KeyringPort;
21
+ /** Bot API client (`options.telegram`, default `fetchTelegram()`). */
22
+ readonly tg: TelegramClient;
23
+ /** Queue producer from `env.queue`, if any. */
24
+ readonly queue: Queue<unknown> | undefined;
25
+ /** Browser Rendering binding from `env.browser` (default pdf renderer), if any. */
26
+ readonly browser: unknown;
27
+ /** Without trailing slash, or null (polling mode). */
28
+ readonly publicBaseUrl: string | null;
29
+ /** `env.appEnv`, default `"production"` (disables the dev polling route). */
30
+ readonly appEnv: string;
31
+ /** Tenant column value (`options.tenant`, default `''`); every query is scoped to it. */
32
+ readonly tenant: string;
33
+ /** `ports.actor`. */
34
+ readonly actor: ActorPort<A>;
35
+ /** `ports.authorize`. */
36
+ readonly authorize: AuthorizePort<A>;
37
+ /** `ports.users`, if given. */
38
+ readonly users: UsersPort | undefined;
39
+ /** `ports.files` (or `r2Files(env.files)`), if available in this invocation. */
40
+ readonly files: FilesPort<A> | undefined;
41
+ /** `ports.audit`, default insert into `tg_audit`. */
42
+ readonly audit: AuditPort;
43
+ /** `ports.errors`, if given. */
44
+ readonly errors: ErrorsPort | undefined;
45
+ /** Resolved log port (`ports.log` or `consoleLog` at the given level). */
46
+ readonly log: LogPort;
47
+ /** `ports.clock`, default `systemClock`. */
48
+ readonly clock: ClockPort;
49
+ /** `ports.ids`, default `uuidv7Ids`. */
50
+ readonly ids: IdsPort;
51
+ /** Bot string translator (host `ports.i18n.t` first, then the plugin's `en`/`mr` strings). */
52
+ readonly i18n: Translator;
53
+ /** `ports.settings`, default `{ timezone: "UTC" }`. */
54
+ readonly settings: SettingsPort;
55
+ /** `ports.metrics`, if available in this invocation. */
56
+ readonly metrics: MetricsPort | undefined;
57
+ /** Host report renderers (`ports.renderers`); built-ins are added by `rendererFor`. */
58
+ readonly renderers: readonly RendererPort[];
59
+ /** Host template filters (`options.filters`). */
60
+ readonly filters: FilterRegistry;
61
+ /** Global feature flags (narrowed per bot inside bot-scoped work). */
62
+ readonly features: FeatureFlags;
63
+ /** `options.limits` over `DEFAULT_LIMITS`. */
64
+ readonly limits: TelegramLimits;
65
+ /** `options.retention` over `DEFAULT_RETENTION`. */
66
+ readonly retention: TelegramRetention;
67
+ /** The host definitions. */
68
+ readonly registry: Registry<A>;
69
+ /** `options.hooks` (call them through `hook()`). */
70
+ readonly hooks: TelegramHooks<A>;
71
+ /** `options.allowMultipleChatsPerUser`. */
72
+ readonly allowMultipleChatsPerUser: boolean;
73
+ /** AI bridge rules (options.ai, defaults applied). */
74
+ readonly ai: ResolvedAiOptions;
75
+ /** Inbound media limits (options.inbound, defaults applied). */
76
+ readonly inbound: ResolvedInboundOptions;
77
+ /** Group rules (options.groups, defaults applied). */
78
+ readonly groups: ResolvedGroupOptions;
79
+ /** Drain transport chosen by `options.transport`. */
80
+ readonly transport: TransportPort;
81
+ /** Fills a buffer with secure random bytes (`crypto.getRandomValues`). */
82
+ readonly rand: RandomFill;
83
+ /** Full permission name with the host prefix. */
84
+ perm(name: PermissionName): string;
85
+ /** `authorize` for a plugin permission. */
86
+ can(actor: A, name: PermissionName, resource?: unknown): Promise<boolean>;
87
+ /** `ports.actor.describe`. */
88
+ describe(actor: A): ActorInfo;
89
+ /** `clock.sleep`, else a real `setTimeout` wait. */
90
+ sleep(ms: number): Promise<void>;
91
+ /** Background work (ctx.waitUntil, or awaited-later promises when there is no execution context). */
92
+ waitUntil(p: Promise<unknown>): void;
93
+ /** Runs a hook; errors are logged and swallowed. */
94
+ hook<K extends keyof TelegramHooks<A>>(name: K, run: (fn: NonNullable<TelegramHooks<A>[K]>) => unknown): Promise<void>;
95
+ }
@@ -0,0 +1,149 @@
1
+ import { permissionName } from "../contracts/permissions.js";
2
+ import { systemClock } from "../adapters/clock.js";
3
+ import { consoleLog, silentLog } from "../adapters/console-log.js";
4
+ import { d1Db } from "../adapters/d1-db.js";
5
+ import { uuidv7Ids } from "../adapters/ids.js";
6
+ import { r2Files } from "../adapters/r2-files.js";
7
+ import { webCryptoKeyring } from "../adapters/keyring.js";
8
+ import { sql } from "../ports/sql.js";
9
+ import { DEFAULT_LIMITS, DEFAULT_RETENTION, resolveGroupOptions } from "./options.js";
10
+ import { fetchTelegram } from "../telegram/client.js";
11
+ import { createTranslator } from "./i18n.js";
12
+ // src/engine/runtime.ts
13
+ function isDbPort(db) {
14
+ return typeof db.all === "function" && typeof db.run === "function";
15
+ }
16
+ function isKeyringPort(k) {
17
+ return typeof k === "object" && k !== null && typeof k.encrypt === "function";
18
+ }
19
+ function isClient(t) {
20
+ return typeof t === "object" && t !== null && typeof t.bot === "function";
21
+ }
22
+ function defaultLimits(input) {
23
+ return {
24
+ ...DEFAULT_LIMITS,
25
+ ...input
26
+ };
27
+ }
28
+ function defaultRetention(input) {
29
+ return {
30
+ ...DEFAULT_RETENTION,
31
+ ...input
32
+ };
33
+ }
34
+ /** `ports.log` → a `LogPort` (default: `consoleLog` at `info`). */
35
+ function resolveLog(log) {
36
+ if (log === void 0) return consoleLog();
37
+ if (log === "silent") return silentLog;
38
+ if (typeof log === "string") return consoleLog({ level: log });
39
+ return log;
40
+ }
41
+ function fromFactory(p, env) {
42
+ return typeof p === "function" ? p(env) : p;
43
+ }
44
+ const realSleep = (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)));
45
+ function createRuntime(shared, env, exec) {
46
+ const { options } = shared;
47
+ const mapped = options.env(env);
48
+ const ports = options.ports;
49
+ const tenant = options.tenant ?? "";
50
+ const db = isDbPort(mapped.db) ? mapped.db : d1Db(mapped.db);
51
+ const log = resolveLog(ports.log);
52
+ const clock = ports.clock ?? systemClock;
53
+ const ids = ports.ids ?? uuidv7Ids(clock);
54
+ let keyring;
55
+ if (isKeyringPort(mapped.keyring)) keyring = mapped.keyring;
56
+ else {
57
+ const cacheKey = typeof mapped.keyring === "string" ? mapped.keyring : JSON.stringify(mapped.keyring);
58
+ const cached = shared.keyrings.get(cacheKey);
59
+ keyring = cached ?? webCryptoKeyring(mapped.keyring);
60
+ if (!cached) shared.keyrings.set(cacheKey, keyring);
61
+ }
62
+ const tgOpt = options.telegram;
63
+ const tg = isClient(tgOpt) ? tgOpt : fetchTelegram({
64
+ log,
65
+ ...tgOpt
66
+ });
67
+ const prefix = options.permissions?.prefix ?? "telegram";
68
+ const audit = ports.audit ?? ((e) => [sql`INSERT INTO tg_audit (id, tenant, actor, entity, entity_id, action, diff_json, at)
69
+ VALUES (${ids.uuid()}, ${tenant}, ${e.actor}, ${e.entity}, ${e.entityId}, ${e.action},
70
+ ${e.diff ? JSON.stringify(e.diff) : null}, ${e.at})`]);
71
+ const rand = (buf) => crypto.getRandomValues(buf);
72
+ const base = mapped.publicBaseUrl?.trim();
73
+ const files = fromFactory(ports.files, env) ?? (mapped.files ? r2Files(mapped.files) : void 0);
74
+ const rt = {
75
+ env,
76
+ db,
77
+ keyring,
78
+ tg,
79
+ queue: mapped.queue,
80
+ browser: mapped.browser,
81
+ publicBaseUrl: base ? base.replace(/\/+$/, "") : null,
82
+ appEnv: mapped.appEnv ?? "production",
83
+ tenant,
84
+ actor: ports.actor,
85
+ authorize: ports.authorize,
86
+ users: ports.users,
87
+ files,
88
+ audit,
89
+ errors: ports.errors,
90
+ log,
91
+ clock,
92
+ ids,
93
+ i18n: createTranslator(ports.i18n ?? {
94
+ locales: ["en", "mr"],
95
+ defaultLocale: "en"
96
+ }),
97
+ settings: ports.settings ?? { timezone: "UTC" },
98
+ metrics: fromFactory(ports.metrics, env),
99
+ renderers: ports.renderers ?? [],
100
+ filters: shared.filters,
101
+ features: shared.features,
102
+ limits: shared.limits,
103
+ retention: shared.retention,
104
+ registry: shared.registry,
105
+ hooks: options.hooks ?? {},
106
+ allowMultipleChatsPerUser: options.allowMultipleChatsPerUser ?? false,
107
+ ai: shared.ai,
108
+ inbound: shared.inbound,
109
+ groups: resolveGroupOptions(options.groups),
110
+ transport: {
111
+ name: "outbox",
112
+ kick: async () => {}
113
+ },
114
+ rand,
115
+ perm: (name) => permissionName(name, prefix),
116
+ async can(actor, name, resource) {
117
+ try {
118
+ return await ports.authorize(actor, permissionName(name, prefix), resource) === true;
119
+ } catch (e) {
120
+ log.error("tg.authorize_error", {
121
+ permission: name,
122
+ error: e instanceof Error ? e.name : "unknown"
123
+ });
124
+ return false;
125
+ }
126
+ },
127
+ describe: (actor) => ports.actor.describe(actor),
128
+ sleep: (ms) => clock.sleep ? clock.sleep(ms) : realSleep(ms),
129
+ waitUntil(p) {
130
+ const safe = p.catch((e) => log.error("tg.background_error", { error: e instanceof Error ? e.name : "unknown" }));
131
+ exec?.waitUntil(safe);
132
+ },
133
+ async hook(name, run) {
134
+ const fn = options.hooks?.[name];
135
+ if (!fn) return;
136
+ try {
137
+ await run(fn);
138
+ } catch (e) {
139
+ log.warn("tg.hook_error", {
140
+ hook: name,
141
+ error: e instanceof Error ? e.name : "unknown"
142
+ });
143
+ }
144
+ }
145
+ };
146
+ rt.transport = shared.makeTransport(rt, mapped);
147
+ return rt;
148
+ }
149
+ export { createRuntime, defaultLimits, defaultRetention, resolveLog };