@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,174 @@
1
+ import { EpochMs, Id, JsonObject, Key, Locale, LocalizedText } from "./common.js";
2
+ import { TemplateError } from "./problems.js";
3
+ import { z } from "zod";
4
+ // src/contracts/templates.ts
5
+ /** `message` renders chat text; `report` renders a file (PDF/CSV) with a caption. */
6
+ const TEMPLATE_KINDS = ["message", "report"];
7
+ /** Zod enum of {@link TEMPLATE_KINDS}. */
8
+ const TemplateKind = z.enum(TEMPLATE_KINDS);
9
+ /** Body syntax: Telegram HTML subset, or plain text (escaped on send). */
10
+ const TEMPLATE_FORMATS = ["html", "text"];
11
+ /** Zod enum of {@link TEMPLATE_FORMATS}. */
12
+ const TemplateFormat = z.enum(TEMPLATE_FORMATS);
13
+ /** Report file formats with built-in renderers. */
14
+ const REPORT_FORMATS = ["pdf", "csv"];
15
+ /** Zod enum of {@link REPORT_FORMATS}. */
16
+ const ReportFormat = z.enum(REPORT_FORMATS);
17
+ /** Optional inline button styles. */
18
+ const BUTTON_STYLES = [
19
+ "primary",
20
+ "success",
21
+ "danger"
22
+ ];
23
+ /** Zod enum of {@link BUTTON_STYLES}. */
24
+ const ButtonStyle = z.enum(BUTTON_STYLES);
25
+ /** One inline button in a template. Exactly one of `action` or `url`. Params may hold `{{variables}}`. */
26
+ const ButtonSpec = z.object({
27
+ text: LocalizedText,
28
+ /** Registered action key (`defineAction`) run on tap. */
29
+ action: Key.optional(),
30
+ params: JsonObject.optional(),
31
+ url: z.string().max(2048).optional(),
32
+ style: ButtonStyle.optional(),
33
+ /** Shown but not tappable. */
34
+ disabled: z.boolean().optional()
35
+ }).refine((b) => b.action === void 0 !== (b.url === void 0), "exactly one of action or url");
36
+ /** Rows of buttons (Telegram: ≤ 8 per row, ≤ 100 total). */
37
+ const ButtonRows = z.array(z.array(ButtonSpec).min(1).max(8)).max(12);
38
+ /** How a `report` template renders its file. */
39
+ const ReportSpec = z.object({
40
+ /** Default file format. */
41
+ renderer: ReportFormat,
42
+ /** File name template, for example `daily-{{date|date}}.pdf`. */
43
+ fileName: z.string().min(1).max(128),
44
+ /** Renderer-specific layout; the built-ins read {@link ReportLayout}. */
45
+ layout: JsonObject.optional()
46
+ });
47
+ /** Bot API send options: `silent` = `disable_notification`, `protectContent`, `linkPreview` (default off). */
48
+ const TemplateOptions = z.object({
49
+ silent: z.boolean().optional(),
50
+ protectContent: z.boolean().optional(),
51
+ linkPreview: z.boolean().optional()
52
+ });
53
+ /** A saved template (`/admin/templates`, permission `telegram.templates.manage`). */
54
+ const TemplateDto = z.object({
55
+ id: Id,
56
+ key: Key,
57
+ name: z.string(),
58
+ kind: TemplateKind,
59
+ /** Data provider key that supplies the template variables, or null. */
60
+ provider: Key.nullable(),
61
+ /** Fixed params passed to the provider, or null. */
62
+ providerParams: JsonObject.nullable(),
63
+ /** Template source per locale. */
64
+ body: LocalizedText,
65
+ format: TemplateFormat,
66
+ report: ReportSpec.nullable(),
67
+ buttons: ButtonRows.nullable(),
68
+ options: TemplateOptions,
69
+ /** Increments on every save; older versions are kept. */
70
+ version: z.number().int().min(1),
71
+ /** Host user id of the last editor, or null. */
72
+ updatedBy: z.string().nullable(),
73
+ updatedAt: EpochMs
74
+ });
75
+ /** `POST`/`PUT /admin/templates` body (defaults: `message`, `html`, no provider, no buttons). Invalid source fails with `template_invalid`. */
76
+ const TemplateSave = z.object({
77
+ key: Key,
78
+ name: z.string().trim().min(1).max(120),
79
+ kind: TemplateKind.default("message"),
80
+ provider: Key.nullable().default(null),
81
+ providerParams: JsonObject.nullable().default(null),
82
+ body: LocalizedText,
83
+ format: TemplateFormat.default("html"),
84
+ report: ReportSpec.nullable().default(null),
85
+ buttons: ButtonRows.nullable().default(null),
86
+ options: TemplateOptions.default({})
87
+ });
88
+ /** One saved version (`GET /admin/templates/:id/versions`; restore with `POST …/restore/:version`). */
89
+ const TemplateVersionDto = z.object({
90
+ templateId: Id,
91
+ version: z.number().int().min(1),
92
+ body: LocalizedText,
93
+ report: ReportSpec.nullable(),
94
+ buttons: ButtonRows.nullable(),
95
+ savedBy: z.string().nullable(),
96
+ savedAt: EpochMs
97
+ });
98
+ /** Preview as a role or a user, so the admin sees exactly what that audience gets. */
99
+ const PreviewAs = z.union([z.strictObject({ role: z.string().min(1) }), z.strictObject({ userId: z.string().min(1) })]);
100
+ /** `POST /admin/templates/preview`: render a saved template (`templateId`) or unsaved `source`. */
101
+ const TemplatePreviewRequest = z.object({
102
+ templateId: Id.optional(),
103
+ /** Unsaved template source for `locale`. */
104
+ source: z.string().max(2e4).optional(),
105
+ buttons: ButtonRows.optional(),
106
+ format: TemplateFormat.optional(),
107
+ provider: Key.optional(),
108
+ params: JsonObject.optional(),
109
+ as: PreviewAs,
110
+ locale: Locale,
111
+ /** Use the provider's `sample()` instead of live data. */
112
+ sample: z.boolean().optional()
113
+ }).refine((r) => r.templateId === void 0 !== (r.source === void 0), "exactly one of templateId or source");
114
+ /** A rendered inline button (callback data is not exposed in previews). */
115
+ const PreviewButton = z.object({
116
+ text: z.string(),
117
+ kind: z.enum(["action", "url"]),
118
+ url: z.string().nullable(),
119
+ style: ButtonStyle.nullable(),
120
+ disabled: z.boolean()
121
+ });
122
+ /** A file a preview would attach (`size` in bytes, null if unknown). */
123
+ const PreviewFile = z.object({
124
+ name: z.string(),
125
+ mime: z.string(),
126
+ size: z.number().int().nullable()
127
+ });
128
+ /** Rendered preview; `warnings` are non-fatal template issues. */
129
+ const TemplatePreviewResponse = z.object({
130
+ /** Telegram HTML, one entry per message part (≤ 4096 chars each). */
131
+ parts: z.array(z.string()),
132
+ buttons: z.array(z.array(PreviewButton)).nullable(),
133
+ file: PreviewFile.nullable(),
134
+ warnings: z.array(TemplateError)
135
+ });
136
+ /** Cell formats. `money` reads integer minor units; `date`/`datetime` read epoch ms (or ISO) in the report's zone. */
137
+ const REPORT_COLUMN_FORMATS = [
138
+ "text",
139
+ "number",
140
+ "money",
141
+ "date",
142
+ "datetime",
143
+ "yesno"
144
+ ];
145
+ /** Zod enum of {@link REPORT_COLUMN_FORMATS}. */
146
+ const ReportColumnFormat = z.enum(REPORT_COLUMN_FORMATS);
147
+ /** One report column: a field of each row (dotted path), its header and how the value is written. */
148
+ const ReportColumn = z.object({
149
+ key: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/, "path"),
150
+ label: z.union([LocalizedText, z.string().min(1).max(128)]),
151
+ format: ReportColumnFormat.default("text")
152
+ });
153
+ /**
154
+ * `report_json.layout` for the built-in renderers: `rows` names the provider variable holding the row array
155
+ * (default: the first array variable); `columns` picks and labels fields (default: every field of the first row).
156
+ */
157
+ const ReportLayout = z.object({
158
+ rows: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/, "path").optional(),
159
+ columns: z.array(ReportColumn).min(1).max(100).optional(),
160
+ /** Report heading, per locale. */
161
+ title: LocalizedText.optional()
162
+ });
163
+ /** Filters for `GET /admin/templates`. */
164
+ const TemplateQuery = z.object({ kind: TemplateKind.optional() });
165
+ /** `POST /admin/reports/render`: the same file the bot would send, as a download. */
166
+ const ReportRenderRequest = z.object({
167
+ /** Report template key. */
168
+ template: Key,
169
+ params: JsonObject.optional(),
170
+ as: PreviewAs,
171
+ format: ReportFormat.optional(),
172
+ locale: Locale.optional()
173
+ });
174
+ export { BUTTON_STYLES, ButtonRows, ButtonSpec, ButtonStyle, PreviewAs, PreviewButton, PreviewFile, REPORT_COLUMN_FORMATS, REPORT_FORMATS, ReportColumn, ReportColumnFormat, ReportFormat, ReportLayout, ReportRenderRequest, ReportSpec, TEMPLATE_FORMATS, TEMPLATE_KINDS, TemplateDto, TemplateFormat, TemplateKind, TemplateOptions, TemplatePreviewRequest, TemplatePreviewResponse, TemplateQuery, TemplateSave, TemplateVersionDto };
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ /** `GET /admin/users?q=`: search the host's users for linking screens (needs `ports.users.search`). */
3
+ export declare const UserQuery: z.ZodObject<{
4
+ q: z.ZodString;
5
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
6
+ }, z.core.$strip>;
7
+ export type UserQuery = z.infer<typeof UserQuery>;
8
+ /** One app user as the linking screen shows it (from `UsersPort`). */
9
+ export declare const UserDto: z.ZodObject<{
10
+ id: z.ZodString;
11
+ name: z.ZodString;
12
+ role: z.ZodNullable<z.ZodString>;
13
+ locale: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ export type UserDto = z.infer<typeof UserDto>;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ // src/contracts/users.ts
3
+ /** `GET /admin/users?q=`: search the host's users for linking screens (needs `ports.users.search`). */
4
+ const UserQuery = z.object({
5
+ /** Name, phone, e-mail or id fragment, as the host's `UsersPort.search` understands it. */
6
+ q: z.string().trim().min(1).max(100),
7
+ /** Max results. Default 20, at most 50. */
8
+ limit: z.coerce.number().int().min(1).max(50).default(20)
9
+ });
10
+ /** One app user as the linking screen shows it (from `UsersPort`). */
11
+ const UserDto = z.object({
12
+ id: z.string().min(1),
13
+ name: z.string(),
14
+ /** The user's host role, or null. */
15
+ role: z.string().nullable(),
16
+ /** Preferred locale, if the host knows it. */
17
+ locale: z.string().optional()
18
+ });
19
+ export { UserDto, UserQuery };
@@ -0,0 +1,22 @@
1
+ /** Why {@link checkAiText} rejected a text (reported in `send_text_rejected` problems as `reasons`). */
2
+ export type AiTextReason = "empty" | "too_long" | "money" | "url" | "phone" | "email";
3
+ /** Options for {@link checkAiText}. */
4
+ export interface AiTextOptions {
5
+ /** May the audience see money? When false, money-like text is rejected. */
6
+ audienceSeesMoney: boolean;
7
+ /** Allow phone numbers and emails (host setting). */
8
+ allowContacts?: boolean;
9
+ /** The app's own domains (`app.example.com`); links to them (and subdomains) are allowed. */
10
+ appDomains?: readonly string[];
11
+ /** Max length (default {@link AI_TEXT_MAX}). */
12
+ maxLength?: number;
13
+ }
14
+ /** Result of {@link checkAiText}. */
15
+ export type AiTextCheck = {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ reasons: AiTextReason[];
20
+ };
21
+ /** Conservative content check for AI-written text: empty/too long, foreign links, contacts, and money unless the audience may see it. */
22
+ export declare function checkAiText(text: string, opts: AiTextOptions): AiTextCheck;
@@ -0,0 +1,47 @@
1
+ // src/core/guards.ts
2
+ /** Default max length (UTF-16 units) of AI-written text. */
3
+ const AI_TEXT_MAX = 1e3;
4
+ const D = "[0-9०-९]";
5
+ const MONEY_PATTERNS = [
6
+ /[₹$€£¥]/u,
7
+ /(?<![A-Za-z])(?:rs|inr|usd|eur|gbp)\.?(?![A-Za-z])/iu,
8
+ new RegExp(`${D}\\s*/-`, "u"),
9
+ /(?<![A-Za-z])(?:lakh|lakhs|lac|lacs|crore|crores)(?![A-Za-z])/iu,
10
+ /लाख|कोटी|करोड|रुपये|रुपया|रु\./u,
11
+ new RegExp(`(?<!${D})${D}{1,3}(?:,${D}{2,3})+(?:\\.${D}+)?(?!${D})`, "u"),
12
+ new RegExp(`${D}+(?:\\.${D}+)?\\s*(?:k|K|L|cr|Cr|CR)(?![A-Za-z])`, "u")
13
+ ];
14
+ const URL_RE = /\b(?:https?:\/\/|www\.)[^\s<>"]+|\b(?:[a-z0-9-]+\.)+(?:com|in|net|org|io|app|dev|co|me|ly|xyz|info|biz|link|site|online|page|ai)\b(?:\/[^\s<>"]*)?|\bt\.me\/[^\s]+/giu;
15
+ const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/u;
16
+ const PHONE_RE = new RegExp(`(?<![0-9०-९])\\+?(?:${D}[\\s\\-.()]?){9,12}${D}(?![0-9०-९])`, "u");
17
+ function hostOf(raw) {
18
+ return (raw.replace(/^https?:\/\//i, "").replace(/^www\./i, "").split(/[/?#:]/)[0] ?? "").toLowerCase();
19
+ }
20
+ function allowedHost(host, domains) {
21
+ return domains.some((d) => {
22
+ const dom = d.toLowerCase().replace(/^\.+/, "");
23
+ return host === dom || host.endsWith(`.${dom}`);
24
+ });
25
+ }
26
+ /** Conservative content check for AI-written text: empty/too long, foreign links, contacts, and money unless the audience may see it. */
27
+ function checkAiText(text, opts) {
28
+ const reasons = [];
29
+ const t = text.normalize("NFC");
30
+ if (t.trim().length === 0) reasons.push("empty");
31
+ if (t.length > (opts.maxLength ?? 1e3)) reasons.push("too_long");
32
+ const withoutEmails = t.replace(new RegExp(EMAIL_RE.source, "gu"), " ");
33
+ for (const m of withoutEmails.matchAll(URL_RE)) if (!allowedHost(hostOf(m[0]), opts.appDomains ?? [])) {
34
+ reasons.push("url");
35
+ break;
36
+ }
37
+ if (!opts.allowContacts) {
38
+ if (EMAIL_RE.test(t)) reasons.push("email");
39
+ if (PHONE_RE.test(withoutEmails)) reasons.push("phone");
40
+ }
41
+ if (!opts.audienceSeesMoney && MONEY_PATTERNS.some((re) => re.test(withoutEmails))) reasons.push("money");
42
+ return reasons.length === 0 ? { ok: true } : {
43
+ ok: false,
44
+ reasons
45
+ };
46
+ }
47
+ export { AI_TEXT_MAX, checkAiText };
@@ -0,0 +1,2 @@
1
+ /** Escapes `& < > "` so a value is safe in Telegram HTML text and in double-quoted attributes. */
2
+ export declare function escapeHtml(value: string): string;
@@ -0,0 +1,36 @@
1
+ // src/core/html/escape.ts
2
+ const ESCAPES = {
3
+ "&": "&amp;",
4
+ "<": "&lt;",
5
+ ">": "&gt;",
6
+ "\"": "&quot;"
7
+ };
8
+ /** Escapes `& < > "` so a value is safe in Telegram HTML text and in double-quoted attributes. */
9
+ function escapeHtml(value) {
10
+ return value.replace(/[&<>"]/g, (c) => ESCAPES[c] ?? c);
11
+ }
12
+ const NAMED = {
13
+ lt: "<",
14
+ gt: ">",
15
+ amp: "&",
16
+ quot: "\""
17
+ };
18
+ /** Matches one entity Telegram accepts. */
19
+ const ENTITY_RE = /&(?:lt|gt|amp|quot|#\d{1,7}|#[xX][0-9a-fA-F]{1,6});/y;
20
+ /** Decodes one entity (`&amp;`, `&#128077;`, `&#x1F44D;`). Unknown input is returned unchanged. */
21
+ function decodeEntity(entity) {
22
+ const body = entity.slice(1, -1);
23
+ const named = NAMED[body];
24
+ if (named !== void 0) return named;
25
+ if (body.startsWith("#")) {
26
+ const hex = body[1] === "x" || body[1] === "X";
27
+ const code = Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);
28
+ if (Number.isFinite(code) && code >= 0 && code <= 1114111) return String.fromCodePoint(code);
29
+ }
30
+ return entity;
31
+ }
32
+ /** Decodes every entity in a text run. */
33
+ function decodeEntities(text) {
34
+ return text.replace(/&(?:lt|gt|amp|quot|#\d{1,7}|#[xX][0-9a-fA-F]{1,6});/g, decodeEntity);
35
+ }
36
+ export { ENTITY_RE, decodeEntities, decodeEntity, escapeHtml };
@@ -0,0 +1,65 @@
1
+ import { ENTITY_RE, decodeEntity } from "./escape.js";
2
+ // src/core/html/tokenize.ts
3
+ const TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[a-zA-Z-]+(?:\s*=\s*(?:"[^"]*"|'[^']*'))?)*)\s*>/y;
4
+ const ATTR_RE = /([a-zA-Z-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
5
+ function parseAttrs(src) {
6
+ const out = {};
7
+ for (const m of src.matchAll(ATTR_RE)) {
8
+ const name = m[1];
9
+ if (name !== void 0) out[name.toLowerCase()] = m[2] ?? m[3] ?? "";
10
+ }
11
+ return out;
12
+ }
13
+ function tokenizeHtml(html) {
14
+ const out = [];
15
+ let text = "";
16
+ const flush = () => {
17
+ if (text) out.push({
18
+ type: "text",
19
+ raw: text
20
+ });
21
+ text = "";
22
+ };
23
+ let i = 0;
24
+ while (i < html.length) {
25
+ const c = html[i];
26
+ if (c === "<") {
27
+ TAG_RE.lastIndex = i;
28
+ const m = TAG_RE.exec(html);
29
+ if (m) {
30
+ flush();
31
+ const name = (m[2] ?? "").toLowerCase();
32
+ out.push(m[1] === "/" ? {
33
+ type: "close",
34
+ raw: m[0],
35
+ name
36
+ } : {
37
+ type: "open",
38
+ raw: m[0],
39
+ name,
40
+ attrs: parseAttrs(m[3] ?? "")
41
+ });
42
+ i += m[0].length;
43
+ continue;
44
+ }
45
+ } else if (c === "&") {
46
+ ENTITY_RE.lastIndex = i;
47
+ const m = ENTITY_RE.exec(html);
48
+ if (m) {
49
+ flush();
50
+ out.push({
51
+ type: "entity",
52
+ raw: m[0],
53
+ text: decodeEntity(m[0])
54
+ });
55
+ i += m[0].length;
56
+ continue;
57
+ }
58
+ }
59
+ text += c;
60
+ i++;
61
+ }
62
+ flush();
63
+ return out;
64
+ }
65
+ export { tokenizeHtml };
@@ -0,0 +1,18 @@
1
+ import { RandomFill } from "./random.js";
2
+ import { Attr, AttrPart, CoreTemplateError, ElementNode, FilterArg, FilterCall, Node, Pos, SectionNode, TemplateAst, TemplateErrorCode, TextNode, VarNode } from "./template/ast.js";
3
+ import { FormatContext, Numerals } from "./template/format.js";
4
+ import { BUILTIN_FILTERS, FilterContext, FilterDef, FilterFn, FilterRegistry, defineFilter } from "./template/filters.js";
5
+ import { AiTextCheck, AiTextOptions, AiTextReason, checkAiText } from "./guards.js";
6
+ import { escapeHtml } from "./html/escape.js";
7
+ import { describeRule } from "./schedule/describe.js";
8
+ import { RuleCheck, ScheduleErrorCode, nextRuns, validateRule } from "./schedule/rule.js";
9
+ import { isValidTimeZone } from "./schedule/tz.js";
10
+ import { toPlainText } from "./split/plain.js";
11
+ import { CAPTION_LIMIT, SplitOptions, TEXT_LIMIT, splitMessage, visibleLength } from "./split/split.js";
12
+ import { ValidateOptions, validateTemplate } from "./template/validate.js";
13
+ import { ParseResult, parseTemplate } from "./template/parse.js";
14
+ import { RenderContext, RenderResult, renderTemplate } from "./template/render.js";
15
+ import { Shape, ShapeVariable, shapeVariables, zodToShape } from "./template/shape.js";
16
+ import { VariableRef, listVariables } from "./template/variables.js";
17
+ import { CompileResult, compileTemplate } from "./template/index.js";
18
+ export { AiTextCheck, AiTextOptions, AiTextReason, Attr, AttrPart, BUILTIN_FILTERS, CAPTION_LIMIT, CompileResult, CoreTemplateError, ElementNode, FilterArg, FilterCall, FilterContext, FilterDef, FilterFn, FilterRegistry, FormatContext, Node, Numerals, ParseResult, Pos, RandomFill, RenderContext, RenderResult, RuleCheck, ScheduleErrorCode, SectionNode, Shape, ShapeVariable, SplitOptions, TEXT_LIMIT, TemplateAst, TemplateErrorCode, TextNode, ValidateOptions, VarNode, VariableRef, checkAiText, compileTemplate, defineFilter, describeRule, escapeHtml, isValidTimeZone, listVariables, nextRuns, parseTemplate, renderTemplate, shapeVariables, splitMessage, toPlainText, validateRule, validateTemplate, visibleLength, zodToShape };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ // src/core/pacing.ts
2
+ const BACKOFF_BASE_MS = 5e3;
3
+ const BACKOFF_CAP_MS = 36e5;
4
+ /**
5
+ * Delay before retry number `attempt` (0-based): 5 s · 2^attempt, ±20 % jitter, capped at 1 h.
6
+ * `rand` returns [0, 1) (inject `Math.random` or a seeded source in tests).
7
+ */
8
+ function nextBackoff(attempt, rand) {
9
+ const exp = BACKOFF_BASE_MS * 2 ** Math.max(0, Math.min(30, Math.floor(attempt)));
10
+ const jitter = 1 + (Math.min(1, Math.max(0, rand())) * 2 - 1) * .2;
11
+ return Math.min(BACKOFF_CAP_MS, Math.round(exp * jitter));
12
+ }
13
+ const PACING_DEFAULTS = {
14
+ /** Per bot (Telegram allows ~30/s). */
15
+ perBotPerSec: 25,
16
+ /** Per chat: one message every 1.1 s. */
17
+ perChatIntervalMs: 1100,
18
+ /** Groups, supergroups and channels: 18 per minute (Telegram allows 20). */
19
+ perGroupPerMin: 18
20
+ };
21
+ /** Token bucket: `ratePerSec` refill, `burst` capacity (default = rate). A null state starts full. */
22
+ function takeToken(state, now, cfg) {
23
+ const burst = cfg.burst ?? cfg.ratePerSec;
24
+ const prev = state ?? {
25
+ tokens: burst,
26
+ at: now
27
+ };
28
+ const tokens = Math.min(burst, prev.tokens + Math.max(0, now - prev.at) * cfg.ratePerSec / 1e3);
29
+ if (tokens >= 1) return {
30
+ ok: true,
31
+ state: {
32
+ tokens: tokens - 1,
33
+ at: now
34
+ }
35
+ };
36
+ return {
37
+ ok: false,
38
+ state: {
39
+ tokens,
40
+ at: now
41
+ },
42
+ waitMs: Math.ceil((1 - tokens) * 1e3 / cfg.ratePerSec)
43
+ };
44
+ }
45
+ /** Can this chat get a message now? Private chats: interval only. Groups/channels: interval + per-minute window. */
46
+ function takeChatSlot(state, now, kind, limits = {}) {
47
+ const interval = limits.perChatIntervalMs ?? PACING_DEFAULTS.perChatIntervalMs;
48
+ const perMin = limits.perGroupPerMin ?? PACING_DEFAULTS.perGroupPerMin;
49
+ if (state.nextAllowedAt !== null && state.nextAllowedAt > now) return {
50
+ ok: false,
51
+ state,
52
+ waitMs: state.nextAllowedAt - now
53
+ };
54
+ let windowStart = state.windowStart;
55
+ let windowCount = state.windowCount;
56
+ if (kind !== "private") {
57
+ if (windowStart === null || now - windowStart >= 6e4) {
58
+ windowStart = now;
59
+ windowCount = 0;
60
+ }
61
+ if (windowCount >= perMin) return {
62
+ ok: false,
63
+ state,
64
+ waitMs: windowStart + 6e4 - now
65
+ };
66
+ windowCount++;
67
+ }
68
+ return {
69
+ ok: true,
70
+ state: {
71
+ nextAllowedAt: now + interval,
72
+ windowStart,
73
+ windowCount
74
+ }
75
+ };
76
+ }
77
+ /** Throttle key for an event rule: `thr:<ruleId>:<key value or *>`. */
78
+ function throttleKey(ruleId, throttle, payload) {
79
+ let v = "*";
80
+ if (throttle.key && payload !== null && typeof payload === "object" && Object.hasOwn(payload, throttle.key)) {
81
+ const raw = payload[throttle.key];
82
+ if (raw !== void 0 && raw !== null) v = String(raw);
83
+ }
84
+ return `thr:${ruleId}:${v}`;
85
+ }
86
+ export { PACING_DEFAULTS, nextBackoff, takeChatSlot, takeToken, throttleKey };
@@ -0,0 +1,2 @@
1
+ /** Fills the buffer with cryptographically random bytes (in place). */
2
+ export type RandomFill = (buf: Uint8Array) => unknown;
@@ -0,0 +1,38 @@
1
+ // src/core/random.ts
2
+ const B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
3
+ const B62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
4
+ /** `length` chars of `[A-Za-z0-9_-]` (6 random bits each). Default 32: link codes (`/start <code>`, ≤ 64). */
5
+ function randomCode(rand, length = 32) {
6
+ const bytes = new Uint8Array(length);
7
+ rand(bytes);
8
+ let out = "";
9
+ for (const b of bytes) out += B64URL[b & 63];
10
+ return out;
11
+ }
12
+ /** `length` chars of base62 with rejection sampling (no modulo bias). Default 10: action ids (`a:<id>`). */
13
+ function shortId(rand, length = 10) {
14
+ let out = "";
15
+ const bytes = new Uint8Array(Math.max(16, length * 2));
16
+ while (out.length < length) {
17
+ rand(bytes);
18
+ for (const b of bytes) {
19
+ if (b >= 248) continue;
20
+ out += B62[b % 62];
21
+ if (out.length === length) break;
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ /** Webhook secret: 64 chars `[A-Za-z0-9_-]` (Telegram allows 1–256). */
27
+ function webhookSecret(rand) {
28
+ return randomCode(rand, 64);
29
+ }
30
+ /** Lowercase hex SHA-256 of a UTF-8 string or bytes (link codes are stored hashed). */
31
+ async function sha256Hex(subtle, input) {
32
+ const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
33
+ const digest = new Uint8Array(await subtle.digest("SHA-256", data));
34
+ let hex = "";
35
+ for (const b of digest) hex += b.toString(16).padStart(2, "0");
36
+ return hex;
37
+ }
38
+ export { randomCode, sha256Hex, shortId, webhookSecret };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { quietUntil } from "./schedule/policy.js";
2
+ // src/core/recipients.ts
3
+ function planRecipients(specs) {
4
+ const plan = {
5
+ destinations: [],
6
+ chats: [],
7
+ users: [],
8
+ roles: [],
9
+ allLinked: false,
10
+ audiences: {}
11
+ };
12
+ const add = (list, v) => {
13
+ if (!list.includes(v)) list.push(v);
14
+ };
15
+ for (const s of specs) if ("destination" in s) add(plan.destinations, s.destination);
16
+ else if ("chat" in s) add(plan.chats, s.chat);
17
+ else if ("user" in s) add(plan.users, s.user);
18
+ else if ("role" in s) add(plan.roles, s.role);
19
+ else if ("allLinked" in s) plan.allLinked = true;
20
+ else {
21
+ const refs = plan.audiences[s.audience] ?? [];
22
+ add(refs, s.ref);
23
+ plan.audiences[s.audience] = refs;
24
+ }
25
+ return plan;
26
+ }
27
+ /** One entry per (chat, topic thread), keeping the first and merging `via`. */
28
+ function dedupeChats(chats) {
29
+ const byKey = /* @__PURE__ */ new Map();
30
+ for (const c of chats) {
31
+ const key = `${c.chatRef}#${c.messageThreadId ?? ""}`;
32
+ const prev = byKey.get(key);
33
+ if (!prev) byKey.set(key, {
34
+ ...c,
35
+ via: [...c.via]
36
+ });
37
+ else for (const v of c.via) if (!prev.via.includes(v)) prev.via.push(v);
38
+ }
39
+ return [...byKey.values()];
40
+ }
41
+ /** `*` matches every topic. */
42
+ function topicMatches(pattern, topic) {
43
+ return pattern === "*" || pattern === topic;
44
+ }
45
+ /** The subscription that applies to a chat for a topic: an exact topic wins over `*`. */
46
+ function subscriptionFor(subs, chatRef, topic) {
47
+ let wildcard;
48
+ for (const s of subs) {
49
+ if (s.chatRef !== chatRef || !topicMatches(s.topic, topic)) continue;
50
+ if (s.topic === topic) return s;
51
+ wildcard ??= s;
52
+ }
53
+ return wildcard;
54
+ }
55
+ /**
56
+ * Applies mute and quiet hours for a topic. `tzOf` gives each chat's zone (chat → user → bot/host default); a
57
+ * quiet window with its own `tz` uses that instead.
58
+ */
59
+ function applySubscriptions(chats, subs, opts) {
60
+ const out = {
61
+ send: [],
62
+ held: [],
63
+ muted: []
64
+ };
65
+ for (const chat of chats) {
66
+ const sub = subscriptionFor(subs, chat.chatRef, opts.topic);
67
+ if (sub?.muted) {
68
+ out.muted.push(chat);
69
+ continue;
70
+ }
71
+ const until = quietUntil(sub?.quiet, chat.timezone ?? opts.defaultTz, opts.now);
72
+ if (until !== null) out.held.push({
73
+ chat,
74
+ until
75
+ });
76
+ else out.send.push(chat);
77
+ }
78
+ return out;
79
+ }
80
+ /** Subscription topics, as stored in `tg_subscription.topic`. */
81
+ const topics = {
82
+ schedule: (scheduleId) => `schedule:${scheduleId}`,
83
+ event: (eventKey) => `event:${eventKey}`,
84
+ broadcast: () => "broadcast"
85
+ };
86
+ export { applySubscriptions, dedupeChats, planRecipients, subscriptionFor, topicMatches, topics };
@@ -0,0 +1,3 @@
1
+ import { ScheduleRule } from "../../contracts/schedules.js";
2
+ /** Human sentence for a schedule rule in `en` or `mr` (other locales fall back to `en`), for example "Every day at 09:00". */
3
+ export declare function describeRule(rule: ScheduleRule, locale?: string): string;