@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.0

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 +87 -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,74 @@
1
+ // src/core/schedule/describe.ts
2
+ const EN = {
3
+ daily: (at) => `Every day at ${at}`,
4
+ weekdays: (at) => `Every weekday (Mon–Fri) at ${at}`,
5
+ weekends: (at) => `Every weekend (Sat, Sun) at ${at}`,
6
+ weekly: (days, at) => `Every ${days} at ${at}`,
7
+ monthly: (day, at) => `Every month on day ${day} at ${at}${day > 28 ? " (or the last day)" : ""}`,
8
+ monthlyLast: (at) => `Every month on the last day at ${at}`,
9
+ once: (date, at) => `Once on ${date} at ${at}`,
10
+ cron: (expr) => `Custom schedule (cron ${expr})`,
11
+ days: {
12
+ mon: "Mon",
13
+ tue: "Tue",
14
+ wed: "Wed",
15
+ thu: "Thu",
16
+ fri: "Fri",
17
+ sat: "Sat",
18
+ sun: "Sun"
19
+ },
20
+ list: (items) => items.join(", ")
21
+ };
22
+ const WORDS = {
23
+ en: EN,
24
+ mr: {
25
+ daily: (at) => `दररोज ${at} वाजता`,
26
+ weekdays: (at) => `दर सोमवार ते शुक्रवार ${at} वाजता`,
27
+ weekends: (at) => `दर शनिवार व रविवार ${at} वाजता`,
28
+ weekly: (days, at) => `दर ${days} ${at} वाजता`,
29
+ monthly: (day, at) => `दर महिन्याच्या ${day} तारखेला ${at} वाजता${day > 28 ? " (किंवा शेवटच्या दिवशी)" : ""}`,
30
+ monthlyLast: (at) => `दर महिन्याच्या शेवटच्या दिवशी ${at} वाजता`,
31
+ once: (date, at) => `एकदा, ${date} रोजी ${at} वाजता`,
32
+ cron: (expr) => `सानुकूल वेळापत्रक (cron ${expr})`,
33
+ days: {
34
+ mon: "सोम",
35
+ tue: "मंगळ",
36
+ wed: "बुध",
37
+ thu: "गुरु",
38
+ fri: "शुक्र",
39
+ sat: "शनि",
40
+ sun: "रवि"
41
+ },
42
+ list: (items) => items.join(", ")
43
+ }
44
+ };
45
+ const ORDER = [
46
+ "mon",
47
+ "tue",
48
+ "wed",
49
+ "thu",
50
+ "fri",
51
+ "sat",
52
+ "sun"
53
+ ];
54
+ /** Human sentence for a schedule rule in `en` or `mr` (other locales fall back to `en`), for example "Every day at 09:00". */
55
+ function describeRule(rule, locale = "en") {
56
+ const w = WORDS[locale.split("-")[0] ?? "en"] ?? EN;
57
+ switch (rule.kind) {
58
+ case "daily": return w.daily(rule.at);
59
+ case "weekly": {
60
+ const days = ORDER.filter((d) => rule.days.includes(d));
61
+ if (days.length === 7) return w.daily(rule.at);
62
+ if (days.join() === "mon,tue,wed,thu,fri") return w.weekdays(rule.at);
63
+ if (days.join() === "sat,sun") return w.weekends(rule.at);
64
+ return w.weekly(w.list(days.map((d) => w.days[d])), rule.at);
65
+ }
66
+ case "monthly": return rule.day === "last" ? w.monthlyLast(rule.at) : w.monthly(rule.day, rule.at);
67
+ case "once": {
68
+ const [date = "", at = ""] = rule.at.split("T");
69
+ return w.once(date, at);
70
+ }
71
+ case "cron": return w.cron(rule.expr.trim());
72
+ }
73
+ }
74
+ export { describeRule };
@@ -0,0 +1,4 @@
1
+ import { describeRule } from "./describe.js";
2
+ import { RuleCheck, ScheduleErrorCode, nextRuns, validateRule } from "./rule.js";
3
+ import { isValidTimeZone } from "./tz.js";
4
+ export { RuleCheck, ScheduleErrorCode, describeRule, isValidTimeZone, nextRuns, validateRule };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { localParts } from "./tz.js";
2
+ import { Cron } from "croner";
3
+ // src/core/schedule/policy.ts
4
+ /**
5
+ * What to do with a claimed slot. Within `graceMin` of the planned time it always runs; later, `skip` writes a
6
+ * skipped run and `run_once` runs it once (catch-up runs only the latest slot; the caller advances past the rest).
7
+ */
8
+ function decideMissed(input) {
9
+ if (input.now - input.nextRunAt <= Math.max(0, input.graceMin) * 6e4) return "run";
10
+ return input.policy === "skip" ? "skip" : "run";
11
+ }
12
+ function minutes(at) {
13
+ const [h, m] = at.split(":").map(Number);
14
+ return (h ?? 0) * 60 + (m ?? 0);
15
+ }
16
+ /** True when the local wall time of `now` is inside the window (`from` ≤ t < `to`, wrapping past midnight). */
17
+ function inQuietHours(quiet, tz, now) {
18
+ const from = minutes(quiet.from);
19
+ const to = minutes(quiet.to);
20
+ if (from === to) return false;
21
+ const p = localParts(now, quiet.tz ?? tz);
22
+ const t = p.hour * 60 + p.minute;
23
+ return from < to ? t >= from && t < to : t >= from || t < to;
24
+ }
25
+ /**
26
+ * When a held message may be sent: the next local `to` time (epoch ms) if `now` is inside quiet hours, else null.
27
+ * `quiet.tz` wins over `tz` (the chat's zone). DST follows croner (a `to` inside a gap is shifted forward).
28
+ */
29
+ function quietUntil(quiet, tz, now) {
30
+ if (!quiet || !inQuietHours(quiet, tz, now)) return null;
31
+ const [h, m] = quiet.to.split(":").map(Number);
32
+ return new Cron(`${m ?? 0} ${h ?? 0} * * *`, {
33
+ timezone: quiet.tz ?? tz,
34
+ paused: true
35
+ }).nextRun(new Date(now))?.getTime() ?? null;
36
+ }
37
+ export { decideMissed, inQuietHours, quietUntil };
@@ -0,0 +1,25 @@
1
+ import { ScheduleRule } from "../../contracts/schedules.js";
2
+ /** Problem codes a schedule rule check can fail with. */
3
+ export type ScheduleErrorCode = "schedule_rule_invalid" | "timezone_invalid";
4
+ /** Result of {@link validateRule}; `next` is the first run (epoch ms UTC), or null if none. */
5
+ export type RuleCheck = {
6
+ ok: true;
7
+ rule: ScheduleRule;
8
+ next: number | null;
9
+ } | {
10
+ ok: false;
11
+ code: ScheduleErrorCode;
12
+ message: string;
13
+ };
14
+ /**
15
+ * Validates a rule for saving. With `now`, a `once` rule in the past and a cron that never fires are errors.
16
+ * Returns the next run so callers can store `next_run_at`.
17
+ */
18
+ export declare function validateRule(input: unknown, tz: string, opts?: {
19
+ now?: number;
20
+ }): RuleCheck;
21
+ /**
22
+ * The next `count` run instants (epoch ms, ascending) strictly after `after`, for a rule in `tz`.
23
+ * Monthly days 29–31 run on the last day of shorter months. Throws `ScheduleError` on an invalid rule or zone.
24
+ */
25
+ export declare function nextRuns(input: ScheduleRule | unknown, tz: string, after: number, count?: number): number[];
@@ -0,0 +1,142 @@
1
+ import { ScheduleRule } from "../../contracts/schedules.js";
2
+ import { daysInMonth, isValidTimeZone, localParts } from "./tz.js";
3
+ import { Cron } from "croner";
4
+ // src/core/schedule/rule.ts
5
+ /** Thrown for an invalid rule or IANA zone; `code` is a plugin problem code. */
6
+ var ScheduleError = class extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "ScheduleError";
11
+ this.code = code;
12
+ }
13
+ };
14
+ const DOW = {
15
+ sun: 0,
16
+ mon: 1,
17
+ tue: 2,
18
+ wed: 3,
19
+ thu: 4,
20
+ fri: 5,
21
+ sat: 6
22
+ };
23
+ function clock(at) {
24
+ const [h, m] = at.split(":").map(Number);
25
+ return {
26
+ h: h ?? 0,
27
+ m: m ?? 0
28
+ };
29
+ }
30
+ /** The croner pattern(s) for a rule. Monthly days 29–31 also need the last-day pattern (short months). */
31
+ function rulePatterns(rule) {
32
+ switch (rule.kind) {
33
+ case "daily": {
34
+ const { h, m } = clock(rule.at);
35
+ return [`${m} ${h} * * *`];
36
+ }
37
+ case "weekly": {
38
+ const { h, m } = clock(rule.at);
39
+ return [`${m} ${h} * * ${[...new Set(rule.days.map((d) => DOW[d] ?? 0))].sort((a, b) => a - b).join(",")}`];
40
+ }
41
+ case "monthly": {
42
+ const { h, m } = clock(rule.at);
43
+ if (rule.day === "last") return [`${m} ${h} L * *`];
44
+ return rule.day > 28 ? [`${m} ${h} ${rule.day} * *`, `${m} ${h} L * *`] : [`${m} ${h} ${rule.day} * *`];
45
+ }
46
+ case "cron": return [rule.expr.trim().split(/\s+/).join(" ")];
47
+ case "once": return [`${rule.at}:00`];
48
+ }
49
+ }
50
+ function cron(pattern, tz) {
51
+ try {
52
+ return new Cron(pattern, {
53
+ timezone: tz,
54
+ paused: true
55
+ });
56
+ } catch (e) {
57
+ throw new ScheduleError("schedule_rule_invalid", e instanceof Error ? e.message.replace(/^CronPattern:\s*/, "") : "invalid pattern");
58
+ }
59
+ }
60
+ /** Parses and checks a rule + zone. Throws `ScheduleError`. */
61
+ function parseRule(input, tz) {
62
+ if (!isValidTimeZone(tz)) throw new ScheduleError("timezone_invalid", `unknown time zone "${String(tz)}"`);
63
+ const r = ScheduleRule.safeParse(input);
64
+ if (!r.success) {
65
+ const issue = r.error.issues[0];
66
+ throw new ScheduleError("schedule_rule_invalid", `${issue?.path.length ? `${issue.path.join(".")}: ` : ""}${issue?.message ?? "invalid rule"}`);
67
+ }
68
+ for (const p of rulePatterns(r.data)) cron(p, tz);
69
+ return r.data;
70
+ }
71
+ /**
72
+ * Validates a rule for saving. With `now`, a `once` rule in the past and a cron that never fires are errors.
73
+ * Returns the next run so callers can store `next_run_at`.
74
+ */
75
+ function validateRule(input, tz, opts = {}) {
76
+ try {
77
+ const rule = parseRule(input, tz);
78
+ const now = opts.now;
79
+ if (now === void 0) return {
80
+ ok: true,
81
+ rule,
82
+ next: null
83
+ };
84
+ const next = nextRuns(rule, tz, now, 1)[0] ?? null;
85
+ if (next === null) return {
86
+ ok: false,
87
+ code: "schedule_rule_invalid",
88
+ message: rule.kind === "once" ? "the time is in the past" : "the rule never runs"
89
+ };
90
+ return {
91
+ ok: true,
92
+ rule,
93
+ next
94
+ };
95
+ } catch (e) {
96
+ if (e instanceof ScheduleError) return {
97
+ ok: false,
98
+ code: e.code,
99
+ message: e.message
100
+ };
101
+ throw e;
102
+ }
103
+ }
104
+ /**
105
+ * The next `count` run instants (epoch ms, ascending) strictly after `after`, for a rule in `tz`.
106
+ * Monthly days 29–31 run on the last day of shorter months. Throws `ScheduleError` on an invalid rule or zone.
107
+ */
108
+ function nextRuns(input, tz, after, count = 1) {
109
+ const rule = parseRule(input, tz);
110
+ const n = Math.max(0, Math.min(1e3, Math.floor(count)));
111
+ const [main, last] = rulePatterns(rule).map((p) => cron(p, tz));
112
+ if (!main) return [];
113
+ const out = [];
114
+ const wanted = rule.kind === "monthly" && typeof rule.day === "number" ? rule.day : null;
115
+ const nextLast = (from) => {
116
+ if (!last || wanted === null) return null;
117
+ let cursor = from;
118
+ for (let k = 0; k < 13; k++) {
119
+ const d = last.nextRun(new Date(cursor));
120
+ if (!d) return null;
121
+ const p = localParts(d.getTime(), tz);
122
+ if (daysInMonth(p.year, p.month) < wanted) return d.getTime();
123
+ cursor = d.getTime();
124
+ }
125
+ return null;
126
+ };
127
+ let cursor = after;
128
+ while (out.length < n) {
129
+ const a = main.nextRun(new Date(cursor))?.getTime() ?? null;
130
+ const b = nextLast(cursor);
131
+ const next = a === null ? b : b === null ? a : Math.min(a, b);
132
+ if (next === null || next <= cursor) break;
133
+ out.push(next);
134
+ cursor = next;
135
+ }
136
+ return out;
137
+ }
138
+ /** Convenience: the next run after `after`, or null when the rule is finished. */
139
+ function nextRun(rule, tz, after) {
140
+ return nextRuns(rule, tz, after, 1)[0] ?? null;
141
+ }
142
+ export { ScheduleError, nextRun, nextRuns, parseRule, rulePatterns, validateRule };
@@ -0,0 +1,2 @@
1
+ /** True for IANA names Intl knows (`Asia/Kolkata`, `UTC`). Offsets like `+05:30` are rejected. */
2
+ export declare function isValidTimeZone(tz: unknown): tz is string;
@@ -0,0 +1,59 @@
1
+ // src/core/schedule/tz.ts
2
+ const valid = /* @__PURE__ */ new Map();
3
+ /** True for IANA names Intl knows (`Asia/Kolkata`, `UTC`). Offsets like `+05:30` are rejected. */
4
+ function isValidTimeZone(tz) {
5
+ if (typeof tz !== "string" || tz.length === 0 || tz.length > 64 || /^[+-]/.test(tz)) return false;
6
+ const hit = valid.get(tz);
7
+ if (hit !== void 0) return hit;
8
+ let ok;
9
+ try {
10
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
11
+ ok = true;
12
+ } catch {
13
+ ok = false;
14
+ }
15
+ valid.set(tz, ok);
16
+ return ok;
17
+ }
18
+ const formatters = /* @__PURE__ */ new Map();
19
+ const WEEKDAY = {
20
+ Sun: 0,
21
+ Mon: 1,
22
+ Tue: 2,
23
+ Wed: 3,
24
+ Thu: 4,
25
+ Fri: 5,
26
+ Sat: 6
27
+ };
28
+ /** Wall-clock parts of an instant in a zone. */
29
+ function localParts(epochMs, tz) {
30
+ let f = formatters.get(tz);
31
+ if (!f) {
32
+ f = new Intl.DateTimeFormat("en-US", {
33
+ timeZone: tz,
34
+ hourCycle: "h23",
35
+ year: "numeric",
36
+ month: "numeric",
37
+ day: "numeric",
38
+ hour: "numeric",
39
+ minute: "numeric",
40
+ weekday: "short"
41
+ });
42
+ formatters.set(tz, f);
43
+ }
44
+ const out = {
45
+ year: 0,
46
+ month: 0,
47
+ day: 0,
48
+ hour: 0,
49
+ minute: 0,
50
+ weekday: 0
51
+ };
52
+ for (const p of f.formatToParts(epochMs)) if (p.type === "weekday") out.weekday = WEEKDAY[p.value] ?? 0;
53
+ else if (p.type === "year" || p.type === "month" || p.type === "day" || p.type === "hour" || p.type === "minute") out[p.type] = Number(p.value);
54
+ return out;
55
+ }
56
+ function daysInMonth(year, month) {
57
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
58
+ }
59
+ export { daysInMonth, isValidTimeZone, localParts };
@@ -0,0 +1,2 @@
1
+ /** Strips tags and decodes entities. Links keep their URL after the label: `Order (https://…)`. */
2
+ export declare function toPlainText(html: string): string;
@@ -0,0 +1,23 @@
1
+ import { decodeEntities } from "../html/escape.js";
2
+ import { tokenizeHtml } from "../html/tokenize.js";
3
+ // src/core/split/plain.ts
4
+ /** Strips tags and decodes entities. Links keep their URL after the label: `Order (https://…)`. */
5
+ function toPlainText(html) {
6
+ let out = "";
7
+ const links = [];
8
+ for (const tok of tokenizeHtml(html)) if (tok.type === "text") out += tok.raw;
9
+ else if (tok.type === "entity") out += tok.text;
10
+ else if (tok.type === "open" && tok.name === "a") links.push({
11
+ href: tok.attrs.href ?? "",
12
+ from: out.length
13
+ });
14
+ else if (tok.type === "close" && tok.name === "a") {
15
+ const link = links.pop();
16
+ if (!link) continue;
17
+ const href = decodeEntities(link.href);
18
+ const label = out.slice(link.from).trim();
19
+ if (href && href !== label && /^https?:\/\//i.test(href)) out += ` (${href})`;
20
+ }
21
+ return out;
22
+ }
23
+ export { toPlainText };
@@ -0,0 +1,15 @@
1
+ /** `sendMessage` / `editMessageText` text limit. */
2
+ export declare const TEXT_LIMIT = 4096;
3
+ /** Caption limit (`sendDocument`, `sendPhoto`, …). */
4
+ export declare const CAPTION_LIMIT = 1024;
5
+ /** Visible length of Telegram HTML as Telegram counts it (UTF-16 code units of the parsed text). */
6
+ export declare function visibleLength(html: string): number;
7
+ /** Options for {@link splitMessage}. */
8
+ export interface SplitOptions {
9
+ /** Max visible length per part (default 4096). */
10
+ limit?: number;
11
+ /** Limit for the first part only (for example 1024 when it becomes a caption). */
12
+ firstLimit?: number;
13
+ }
14
+ /** Splits Telegram HTML into parts of at most `limit` visible chars, keeping tags balanced in every part (prefers paragraph, line, then word breaks). */
15
+ export declare function splitMessage(html: string, opts?: SplitOptions): string[];
@@ -0,0 +1,158 @@
1
+ import { tokenizeHtml } from "../html/tokenize.js";
2
+ // src/core/split/split.ts
3
+ /** `sendMessage` / `editMessageText` text limit. */
4
+ const TEXT_LIMIT = 4096;
5
+ /** Caption limit (`sendDocument`, `sendPhoto`, …). */
6
+ const CAPTION_LIMIT = 1024;
7
+ const ATOMIC = /* @__PURE__ */ new Set(["tg-emoji", "tg-time"]);
8
+ let segmenter;
9
+ function atomize(html) {
10
+ segmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
11
+ const atoms = [];
12
+ for (const tok of tokenizeHtml(html)) if (tok.type === "open") atoms.push({
13
+ t: "open",
14
+ raw: tok.raw,
15
+ name: tok.name
16
+ });
17
+ else if (tok.type === "close") atoms.push({
18
+ t: "close",
19
+ raw: tok.raw,
20
+ name: tok.name
21
+ });
22
+ else if (tok.type === "entity") atoms.push({
23
+ t: "vis",
24
+ raw: tok.raw,
25
+ ch: tok.text,
26
+ w: tok.text.length
27
+ });
28
+ else for (const { segment } of segmenter.segment(tok.raw)) atoms.push({
29
+ t: "vis",
30
+ raw: segment,
31
+ ch: segment,
32
+ w: segment.length
33
+ });
34
+ return atoms;
35
+ }
36
+ function stacks(atoms) {
37
+ const out = new Array(atoms.length + 1);
38
+ let s = null;
39
+ for (let i = 0; i < atoms.length; i++) {
40
+ out[i] = s;
41
+ const a = atoms[i];
42
+ if (a?.t === "open") s = {
43
+ atom: a,
44
+ parent: s
45
+ };
46
+ else if (a?.t === "close") {
47
+ let p = s;
48
+ while (p && p.atom.name !== a.name) p = p.parent;
49
+ if (p) s = p.parent;
50
+ }
51
+ }
52
+ out[atoms.length] = s;
53
+ return out;
54
+ }
55
+ const isSpace = (a) => a?.t === "vis" && /^\s+$/.test(a.ch);
56
+ const isNewline = (a) => a?.t === "vis" && /[\n\r]/.test(a.ch);
57
+ function inAtomic(s) {
58
+ for (let p = s ?? null; p; p = p.parent) if (ATOMIC.has(p.atom.name)) return true;
59
+ return false;
60
+ }
61
+ /** Visible length of Telegram HTML as Telegram counts it (UTF-16 code units of the parsed text). */
62
+ function visibleLength(html) {
63
+ let n = 0;
64
+ for (const a of atomize(html)) if (a.t === "vis") n += a.w;
65
+ return n;
66
+ }
67
+ /** Splits Telegram HTML into parts of at most `limit` visible chars, keeping tags balanced in every part (prefers paragraph, line, then word breaks). */
68
+ function splitMessage(html, opts = {}) {
69
+ const limit = Math.max(16, opts.limit ?? 4096);
70
+ const atoms = atomize(html);
71
+ const stackAt = stacks(atoms);
72
+ const parts = [];
73
+ let start = 0;
74
+ while (start < atoms.length) {
75
+ const max = parts.length === 0 ? Math.max(16, opts.firstLimit ?? limit) : limit;
76
+ let s0 = start;
77
+ while (s0 < atoms.length && (atoms[s0]?.t !== "vis" || isSpace(atoms[s0]))) s0++;
78
+ let width = 0;
79
+ let end = s0;
80
+ const widthAt = /* @__PURE__ */ new Map();
81
+ while (end < atoms.length) {
82
+ const a = atoms[end];
83
+ const w = a?.t === "vis" ? a.w : 0;
84
+ if (width + w > max) break;
85
+ widthAt.set(end, width);
86
+ width += w;
87
+ end++;
88
+ }
89
+ widthAt.set(end, width);
90
+ if (end >= atoms.length) {
91
+ parts.push(emit(atoms, stackAt, start, atoms.length));
92
+ break;
93
+ }
94
+ const cut = chooseCut(atoms, stackAt, s0, end, max, widthAt);
95
+ parts.push(emit(atoms, stackAt, start, cut));
96
+ start = cut;
97
+ }
98
+ return parts.filter((p) => visibleLength(p) > 0);
99
+ }
100
+ function chooseCut(atoms, stackAt, s0, end, max, widthAt) {
101
+ const fill = (i) => widthAt.get(i) ?? 0;
102
+ const prevVisible = (i) => {
103
+ for (let k = i - 1; k >= s0; k--) {
104
+ const a = atoms[k];
105
+ if (a?.t === "vis") return a;
106
+ }
107
+ };
108
+ const scan = (ok, minFill) => {
109
+ for (let i = end; i > s0; i--) {
110
+ if (fill(i) < minFill) return null;
111
+ if (!inAtomic(stackAt[i]) && ok(i)) return i;
112
+ }
113
+ return null;
114
+ };
115
+ const para = scan((i) => isNewline(atoms[i]) && isNewline(prevVisible(i)), max / 2);
116
+ if (para !== null) return para;
117
+ const line = scan((i) => isNewline(atoms[i]), max / 2);
118
+ if (line !== null) return line;
119
+ const word = scan((i) => isSpace(atoms[i]), 1);
120
+ if (word !== null) return word;
121
+ for (let i = end; i > s0; i--) if (!inAtomic(stackAt[i])) return i;
122
+ return Math.max(end, s0 + 1);
123
+ }
124
+ function emit(atoms, stackAt, from, to) {
125
+ const open = [];
126
+ for (let p = stackAt[from] ?? null; p; p = p.parent) open.unshift(p.atom.raw);
127
+ const drop = /* @__PURE__ */ new Set();
128
+ for (let i = from; i < to && (atoms[i]?.t !== "vis" || isSpace(atoms[i])); i++) if (isSpace(atoms[i])) drop.add(i);
129
+ for (let i = to - 1; i >= from && (atoms[i]?.t !== "vis" || isSpace(atoms[i])); i--) if (isSpace(atoms[i])) drop.add(i);
130
+ let body = "";
131
+ for (let i = from; i < to; i++) if (!drop.has(i)) body += atoms[i]?.raw ?? "";
132
+ let close = "";
133
+ for (let p = stackAt[to] ?? null; p; p = p.parent) close += `</${p.atom.name}>`;
134
+ return dropEmptyElements(open.join("") + body + close);
135
+ }
136
+ const EMPTY_RE = /<([a-z][a-z0-9-]*)(?:\s[^>]*)?>(\s*)<\/\1>/g;
137
+ /** Removes elements left empty by a split (`<b></b>`), keeping any whitespace they held. */
138
+ function dropEmptyElements(html) {
139
+ let prev = "";
140
+ let out = html;
141
+ while (out !== prev) {
142
+ prev = out;
143
+ out = out.replace(EMPTY_RE, "$2");
144
+ }
145
+ return out;
146
+ }
147
+ /** First part as a caption (≤ 1024), the rest as follow-up messages (≤ 4096). */
148
+ function fitCaption(html, opts = {}) {
149
+ const parts = splitMessage(html, {
150
+ firstLimit: opts.captionLimit ?? 1024,
151
+ limit: opts.textLimit ?? 4096
152
+ });
153
+ return {
154
+ caption: parts[0] ?? "",
155
+ rest: parts.slice(1)
156
+ };
157
+ }
158
+ export { CAPTION_LIMIT, TEXT_LIMIT, fitCaption, splitMessage, visibleLength };
@@ -0,0 +1,58 @@
1
+ /** 1-based source position. */
2
+ export interface Pos {
3
+ line: number;
4
+ column: number;
5
+ }
6
+ /** A literal filter argument. */
7
+ export type FilterArg = string | number | boolean;
8
+ /** One `| name: args` filter application. */
9
+ export interface FilterCall extends Pos {
10
+ name: string;
11
+ args: FilterArg[];
12
+ }
13
+ /** `{{a.b | filter:arg}}`. `path` is empty for `{{.}}` (the current item). */
14
+ export interface VarNode extends Pos {
15
+ type: "var";
16
+ path: string[];
17
+ filters: FilterCall[];
18
+ }
19
+ /** Literal text, already safe Telegram HTML (stray `& < >` were escaped by the parser). */
20
+ export interface TextNode extends Pos {
21
+ type: "text";
22
+ value: string;
23
+ }
24
+ /** `{{#path}}…{{/path}}` or `{{^path}}…{{/path}}`. */
25
+ export interface SectionNode extends Pos {
26
+ type: "section" | "inverted";
27
+ path: string[];
28
+ children: Node[];
29
+ }
30
+ export type AttrPart = {
31
+ type: "text";
32
+ value: string;
33
+ } | VarNode;
34
+ export interface Attr {
35
+ name: string;
36
+ /** null for a boolean attribute (`<blockquote expandable>`). */
37
+ value: AttrPart[] | null;
38
+ }
39
+ /** One allowed Telegram HTML element written in the template. */
40
+ export interface ElementNode extends Pos {
41
+ type: "element";
42
+ name: string;
43
+ attrs: Attr[];
44
+ children: Node[];
45
+ }
46
+ export type Node = TextNode | VarNode | SectionNode | ElementNode;
47
+ /** Root of a parsed template (from {@link parseTemplate}). */
48
+ export interface TemplateAst {
49
+ type: "root";
50
+ children: Node[];
51
+ }
52
+ /** Machine code of a template error. */
53
+ export type TemplateErrorCode = "syntax" | "raw_not_allowed" | "tag_not_allowed" | "attr_invalid" | "url_invalid" | "nesting_invalid" | "unclosed" | "variable_unknown" | "section_invalid" | "variable_type" | "filter_unknown" | "filter_args" | "render";
54
+ /** Compatible with the `TemplateError` contract, plus a machine `code`. */
55
+ export interface CoreTemplateError extends Pos {
56
+ code: TemplateErrorCode;
57
+ message: string;
58
+ }
@@ -0,0 +1,5 @@
1
+ // src/core/template/ast.ts
2
+ function pathText(path) {
3
+ return path.length === 0 ? "." : path.join(".");
4
+ }
5
+ export { pathText };