@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,259 @@
1
+ import { ReportLayout } from "../contracts/templates.js";
2
+ import { SAFE_URL_RE } from "../core/template/tags.js";
3
+ import { sql } from "../ports/sql.js";
4
+ import { TelegramPluginError, featureDisabled } from "../engine/errors.js";
5
+ import { checkBody, parseSource, renderBody, templateProblem } from "../engine/content/render.js";
6
+ import { hostCan, parseProviderParams, requireProvider, sampleData } from "../engine/content/providers.js";
7
+ import { buildButtons } from "../engine/content/buttons.js";
8
+ import { renderReportFile, rendererFor } from "../engine/content/report-files.js";
9
+ import { viewerData } from "./reports.js";
10
+ import { audienceForPreview } from "../engine/content/audience.js";
11
+ import { requireTemplate, templateDto, templateModel, templateVersionDto } from "../engine/content/template-rows.js";
12
+ import { actorId } from "./bots.js";
13
+ // src/services/templates.ts
14
+ function issue(path, message) {
15
+ return new TelegramPluginError("validation_failed", message, { extensions: { issues: [{
16
+ path,
17
+ message
18
+ }] } });
19
+ }
20
+ /** Save-time checks: provider + params, every locale's syntax and variables, report spec, buttons. */
21
+ function validateTemplateSave(rt, save) {
22
+ const def = save.provider ? requireProvider(rt, save.provider) : null;
23
+ if (def && save.providerParams) parseProviderParams(def, save.providerParams);
24
+ const errors = checkBody(save.body, save.format, {
25
+ filters: rt.filters,
26
+ ...def ? { variables: def.variables } : {}
27
+ });
28
+ if (save.kind === "report") {
29
+ if (!rt.features.reports) throw featureDisabled("reports");
30
+ if (!save.report) throw issue("body.report", "report templates need a report spec");
31
+ rendererFor(rt, save.report.renderer);
32
+ const name = parseSource(save.report.fileName, "text");
33
+ for (const e of name.errors) errors.push({
34
+ ...e,
35
+ locale: "fileName"
36
+ });
37
+ if (save.report.layout !== void 0 && !ReportLayout.safeParse(save.report.layout).success) throw issue("body.report.layout", "layout is not valid ({ rows?, columns?: [{ key, label, format }], title? })");
38
+ }
39
+ for (const [r, row] of (save.buttons ?? []).entries()) for (const [c, b] of row.entries()) {
40
+ const path = `body.buttons.${r}.${c}`;
41
+ if (b.action !== void 0 && !rt.registry.actions.has(b.action)) throw issue(path, `action "${b.action}" is not registered`);
42
+ if (b.url !== void 0) {
43
+ const parsed = parseSource(b.url, "text");
44
+ for (const e of parsed.errors) errors.push({
45
+ ...e,
46
+ locale: path
47
+ });
48
+ const at = b.url.indexOf("{{");
49
+ const prefix = at === -1 ? b.url : b.url.slice(0, at);
50
+ if (parsed.errors.length === 0 && prefix !== "" && !SAFE_URL_RE.test(prefix)) throw issue(path, "url must start with http(s):// or tg://");
51
+ }
52
+ }
53
+ if (errors.length > 0) throw templateProblem(errors);
54
+ }
55
+ async function listTemplates(rt, q, page) {
56
+ return rt.db.all(sql`SELECT * FROM tg_template WHERE tenant = ${rt.tenant} AND deleted_at IS NULL
57
+ ${q.kind ? sql`AND kind = ${q.kind}` : sql.empty}
58
+ ${page.cursor ? sql`AND id < ${page.cursor}` : sql.empty} ORDER BY id DESC LIMIT ${page.limit + 1}`);
59
+ }
60
+ function versionInsert(rt, id, by, now) {
61
+ return sql`INSERT INTO tg_template_version (tenant, template_id, version, body_json, report_json, buttons_json, saved_by,
62
+ saved_at)
63
+ SELECT tenant, id, version, body_json, report_json, buttons_json, ${by}, ${now}
64
+ FROM tg_template WHERE tenant = ${rt.tenant} AND id = ${id}`;
65
+ }
66
+ const json = (v) => v === null || v === void 0 ? null : JSON.stringify(v);
67
+ /**
68
+ * Creates a template (version 1) with its version row and audit in one batch; a soft-deleted template with the same
69
+ * key is renamed out of the way. Throws `validation_failed` (key taken, invalid body / provider / buttons). No
70
+ * permission check: the admin routes require `telegram.templates.manage`.
71
+ */
72
+ async function createTemplate(rt, actor, save) {
73
+ validateTemplateSave(rt, save);
74
+ const taken = await rt.db.first(sql`SELECT id, deleted_at FROM tg_template WHERE tenant = ${rt.tenant} AND key = ${save.key}`);
75
+ if (taken && taken.deleted_at === null) throw issue("body.key", "key is already used by another template");
76
+ const now = rt.clock.now();
77
+ const by = actorId(rt, actor);
78
+ const id = rt.ids.uuid();
79
+ await rt.db.batch([
80
+ ...taken ? [sql`UPDATE tg_template SET key = ${`${save.key}~${taken.id}`} WHERE id = ${taken.id}`] : [],
81
+ sql`INSERT INTO tg_template (id, tenant, key, name, kind, provider, provider_params_json, body_json, format, report_json,
82
+ buttons_json, options_json, version, created_by, created_at, updated_by, updated_at)
83
+ VALUES (${id}, ${rt.tenant}, ${save.key}, ${save.name}, ${save.kind}, ${save.provider}, ${json(save.providerParams)},
84
+ ${JSON.stringify(save.body)}, ${save.format}, ${json(save.report)}, ${json(save.buttons)}, ${json(save.options)},
85
+ 1, ${by}, ${now}, ${by}, ${now})`,
86
+ versionInsert(rt, id, by, now),
87
+ ...rt.audit({
88
+ actor: by,
89
+ entity: "telegram_template",
90
+ entityId: id,
91
+ action: "create",
92
+ diff: { key: save.key },
93
+ at: now
94
+ })
95
+ ]);
96
+ return templateDto(await requireTemplate(rt, id));
97
+ }
98
+ /**
99
+ * Replaces a template and appends a new version (+ audit) in one batch. Throws `not_found` or `validation_failed`.
100
+ * No permission check (the routes do it).
101
+ */
102
+ async function updateTemplate(rt, actor, id, save) {
103
+ const old = await requireTemplate(rt, id);
104
+ validateTemplateSave(rt, save);
105
+ if (save.key !== old.key) {
106
+ if (await rt.db.first(sql`SELECT id FROM tg_template WHERE tenant = ${rt.tenant} AND key = ${save.key} AND id != ${id}`)) throw issue("body.key", "key is already used by another template");
107
+ }
108
+ const now = rt.clock.now();
109
+ const by = actorId(rt, actor);
110
+ await rt.db.batch([
111
+ sql`UPDATE tg_template SET key = ${save.key}, name = ${save.name}, kind = ${save.kind}, provider = ${save.provider},
112
+ provider_params_json = ${json(save.providerParams)}, body_json = ${JSON.stringify(save.body)},
113
+ format = ${save.format}, report_json = ${json(save.report)}, buttons_json = ${json(save.buttons)},
114
+ options_json = ${json(save.options)}, version = version + 1, updated_by = ${by}, updated_at = ${now}
115
+ WHERE tenant = ${rt.tenant} AND id = ${id} AND deleted_at IS NULL`,
116
+ versionInsert(rt, id, by, now),
117
+ ...rt.audit({
118
+ actor: by,
119
+ entity: "telegram_template",
120
+ entityId: id,
121
+ action: "update",
122
+ diff: { version: old.version + 1 },
123
+ at: now
124
+ })
125
+ ]);
126
+ return templateDto(await requireTemplate(rt, id));
127
+ }
128
+ async function deleteTemplate(rt, actor, id) {
129
+ const old = await requireTemplate(rt, id);
130
+ const now = rt.clock.now();
131
+ const by = actorId(rt, actor);
132
+ await rt.db.batch([sql`UPDATE tg_template SET deleted_at = ${now}, updated_by = ${by}, updated_at = ${now}
133
+ WHERE tenant = ${rt.tenant} AND id = ${id}`, ...rt.audit({
134
+ actor: by,
135
+ entity: "telegram_template",
136
+ entityId: id,
137
+ action: "delete",
138
+ diff: { key: old.key },
139
+ at: now
140
+ })]);
141
+ }
142
+ async function listVersions(rt, id) {
143
+ await requireTemplate(rt, id);
144
+ return (await rt.db.all(sql`SELECT * FROM tg_template_version WHERE tenant = ${rt.tenant} AND template_id = ${id} ORDER BY version DESC LIMIT 200`)).map(templateVersionDto);
145
+ }
146
+ /** Restores a saved version as a new version (history stays append-only). */
147
+ async function restoreVersion(rt, actor, id, version) {
148
+ const current = await requireTemplate(rt, id);
149
+ const v = await rt.db.first(sql`SELECT * FROM tg_template_version WHERE tenant = ${rt.tenant} AND template_id = ${id} AND version = ${version}`);
150
+ if (!v) throw new TelegramPluginError("not_found", "version not found");
151
+ const dto = templateDto({
152
+ ...current,
153
+ body_json: v.body_json,
154
+ report_json: v.report_json,
155
+ buttons_json: v.buttons_json
156
+ });
157
+ return updateTemplate(rt, actor, id, {
158
+ key: dto.key,
159
+ name: dto.name,
160
+ kind: dto.kind,
161
+ provider: dto.provider,
162
+ providerParams: dto.providerParams,
163
+ body: dto.body,
164
+ format: dto.format,
165
+ report: dto.report,
166
+ buttons: dto.buttons,
167
+ options: dto.options
168
+ });
169
+ }
170
+ /** Unsaved source or a saved template, as one model. */
171
+ async function previewModel(rt, req) {
172
+ if (req.templateId) return templateModel(await requireTemplate(rt, req.templateId));
173
+ return {
174
+ id: "preview",
175
+ key: "preview",
176
+ name: "preview",
177
+ kind: "message",
178
+ provider: req.provider ?? null,
179
+ providerParams: null,
180
+ body: { [req.locale]: req.source ?? "" },
181
+ format: req.format ?? "html",
182
+ report: null,
183
+ layout: null,
184
+ buttons: req.buttons ?? null,
185
+ options: {},
186
+ version: 1
187
+ };
188
+ }
189
+ /** What the audience would get: rendered parts, buttons, file, render warnings. Writes nothing. */
190
+ async function previewTemplate(rt, caller, req) {
191
+ const t = await previewModel(rt, req);
192
+ const def = t.provider ? requireProvider(rt, t.provider) : null;
193
+ const errors = checkBody(t.body, t.format, {
194
+ filters: rt.filters,
195
+ ...def ? { variables: def.variables } : {}
196
+ });
197
+ if (errors.length > 0) throw templateProblem(errors);
198
+ const aud = await audienceForPreview(rt, req.as);
199
+ const tz = rt.settings.timezone;
200
+ const now = rt.clock.now();
201
+ let vars = {};
202
+ if (def) {
203
+ if (req.sample) vars = sampleData(rt, def);
204
+ else {
205
+ if (!await hostCan(rt, aud, def.permission)) throw new TelegramPluginError("provider_forbidden", `this audience may not read "${def.key}"`);
206
+ const params = parseProviderParams(def, {
207
+ ...t.providerParams,
208
+ ...req.params
209
+ });
210
+ vars = await viewerData(rt, {
211
+ def,
212
+ actor: aud,
213
+ viewer: caller,
214
+ params,
215
+ now,
216
+ tz,
217
+ locale: req.locale
218
+ });
219
+ }
220
+ }
221
+ const body = renderBody(rt, {
222
+ body: t.body,
223
+ format: t.format,
224
+ vars,
225
+ locales: [req.locale],
226
+ tz
227
+ });
228
+ const buttons = await buildButtons(rt, t.buttons, {
229
+ vars,
230
+ locale: body.locale,
231
+ tz,
232
+ botId: "",
233
+ chatRef: null,
234
+ userId: null,
235
+ now
236
+ });
237
+ let file = null;
238
+ if (t.kind === "report") {
239
+ const out = await renderReportFile(rt, {
240
+ template: t,
241
+ vars,
242
+ locale: body.locale,
243
+ tz,
244
+ html: body.html
245
+ });
246
+ file = {
247
+ name: out.name,
248
+ mime: out.mime,
249
+ size: out.size ?? null
250
+ };
251
+ }
252
+ return {
253
+ parts: body.parts,
254
+ buttons: buttons.preview,
255
+ file,
256
+ warnings: body.warnings
257
+ };
258
+ }
259
+ export { createTemplate, deleteTemplate, listTemplates, listVersions, previewTemplate, restoreVersion, updateTemplate, validateTemplateSave };
@@ -0,0 +1,30 @@
1
+ /** Coarse result of a Bot API call: what the sender does next (retry, mark chat blocked/left/migrated, fail). */
2
+ export type TelegramOutcomeKind = "ok" | "retry_after" | "migrated" | "parse_error" | "bad_request" | "blocked" | "left" | "not_found_chat" | "unauthorized" | "server_error" | "network" | "timeout";
3
+ /** Finer reason inside a kind (stable strings, safe to store in `error_code`). */
4
+ export type TelegramOutcomeReason = "not_modified" | "message_gone" | "too_long" | "caption_too_long" | "empty_text" | "query_too_old" | "button_data_invalid" | "file_invalid" | "no_rights" | "thread_not_found" | "too_large" | "conflict" | "user_blocked" | "user_deactivated" | "cant_initiate" | "forbidden" | "kicked" | "not_member" | "bad_token" | "aborted" | "other";
5
+ /** Classified Bot API result (see {@link classifyTelegramError}). */
6
+ export interface TelegramOutcome {
7
+ kind: TelegramOutcomeKind;
8
+ reason?: TelegramOutcomeReason;
9
+ /** Worth retrying later (429, 5xx, network, timeout). */
10
+ retryable: boolean;
11
+ /** Milliseconds to wait (429). */
12
+ retryAfterMs?: number;
13
+ /** New supergroup id (400 migrate), as a string like `tg_chat.chat_id`. */
14
+ migrateToChatId?: string;
15
+ /** HTTP status, when Telegram answered. */
16
+ status?: number;
17
+ /** Telegram's description, safe to store (contains no token). */
18
+ description?: string;
19
+ }
20
+ /** Outcome of a failed call. Anything that is not a Telegram error is treated as a network failure. */
21
+ export declare function classifyTelegramError(err: unknown): TelegramOutcome;
22
+ /** Runs a call and returns its outcome next to the value (never throws for Telegram/network failures). */
23
+ export declare function settle<T>(call: Promise<T>): Promise<{
24
+ outcome: TelegramOutcome;
25
+ value: T;
26
+ } | {
27
+ outcome: TelegramOutcome;
28
+ value?: undefined;
29
+ error: unknown;
30
+ }>;
@@ -0,0 +1,164 @@
1
+ import { TelegramApiError, TelegramNetworkError } from "./errors.js";
2
+ // src/telegram/classify.ts
3
+ const OK = {
4
+ kind: "ok",
5
+ retryable: false
6
+ };
7
+ function has(desc, ...needles) {
8
+ const d = desc.toLowerCase();
9
+ return needles.some((n) => d.includes(n));
10
+ }
11
+ function bad(e, reason) {
12
+ return {
13
+ kind: "bad_request",
14
+ reason,
15
+ retryable: false,
16
+ status: e.status,
17
+ description: e.description
18
+ };
19
+ }
20
+ function classify400(e) {
21
+ const d = e.description;
22
+ const base = {
23
+ retryable: false,
24
+ status: e.status,
25
+ description: d
26
+ };
27
+ if (e.migrateToChatId !== void 0) return {
28
+ kind: "migrated",
29
+ ...base,
30
+ migrateToChatId: String(e.migrateToChatId)
31
+ };
32
+ if (has(d, "can't parse entities", "can't find end of the entity", "unsupported start tag")) return {
33
+ kind: "parse_error",
34
+ ...base
35
+ };
36
+ if (has(d, "chat not found", "peer_id_invalid", "chat_id is empty")) return {
37
+ kind: "not_found_chat",
38
+ ...base
39
+ };
40
+ if (has(d, "message is not modified")) return {
41
+ ...OK,
42
+ reason: "not_modified",
43
+ description: d
44
+ };
45
+ if (has(d, "message to edit not found", "message to delete not found", "message can't be edited", "message can't be deleted", "message_id_invalid")) return bad(e, "message_gone");
46
+ if (has(d, "message caption is too long")) return bad(e, "caption_too_long");
47
+ if (has(d, "message is too long")) return bad(e, "too_long");
48
+ if (has(d, "message text is empty")) return bad(e, "empty_text");
49
+ if (has(d, "query is too old", "query id is invalid")) return bad(e, "query_too_old");
50
+ if (has(d, "button_data_invalid")) return bad(e, "button_data_invalid");
51
+ if (has(d, "wrong file identifier", "failed to get http url content", "wrong type of the web page content")) return bad(e, "file_invalid");
52
+ if (has(d, "not enough rights", "have no rights", "chat_write_forbidden", "chat_restricted")) return bad(e, "no_rights");
53
+ if (has(d, "message thread not found", "topic_closed", "topic_deleted")) return bad(e, "thread_not_found");
54
+ return bad(e, "other");
55
+ }
56
+ function classify403(e) {
57
+ const d = e.description;
58
+ const base = {
59
+ retryable: false,
60
+ status: e.status,
61
+ description: d
62
+ };
63
+ if (has(d, "bot was kicked")) return {
64
+ kind: "left",
65
+ reason: "kicked",
66
+ ...base
67
+ };
68
+ if (has(d, "bot is not a member", "chat_write_forbidden")) return {
69
+ kind: "left",
70
+ reason: "not_member",
71
+ ...base
72
+ };
73
+ if (has(d, "bot was blocked by the user")) return {
74
+ kind: "blocked",
75
+ reason: "user_blocked",
76
+ ...base
77
+ };
78
+ if (has(d, "user is deactivated")) return {
79
+ kind: "blocked",
80
+ reason: "user_deactivated",
81
+ ...base
82
+ };
83
+ if (has(d, "can't initiate conversation")) return {
84
+ kind: "blocked",
85
+ reason: "cant_initiate",
86
+ ...base
87
+ };
88
+ return {
89
+ kind: "blocked",
90
+ reason: "forbidden",
91
+ ...base
92
+ };
93
+ }
94
+ /** Outcome of a failed call. Anything that is not a Telegram error is treated as a network failure. */
95
+ function classifyTelegramError(err) {
96
+ if (err instanceof TelegramNetworkError) {
97
+ if (err.reason === "timeout") return {
98
+ kind: "timeout",
99
+ retryable: true
100
+ };
101
+ if (err.reason === "aborted") return {
102
+ kind: "network",
103
+ reason: "aborted",
104
+ retryable: false
105
+ };
106
+ return {
107
+ kind: "network",
108
+ retryable: true
109
+ };
110
+ }
111
+ if (!(err instanceof TelegramApiError)) return {
112
+ kind: "network",
113
+ retryable: true
114
+ };
115
+ const e = err;
116
+ const code = e.errorCode || e.status;
117
+ if (code === 429 || e.retryAfter !== void 0) return {
118
+ kind: "retry_after",
119
+ retryable: true,
120
+ retryAfterMs: Math.max(1, e.retryAfter ?? 1) * 1e3,
121
+ status: e.status,
122
+ description: e.description
123
+ };
124
+ if (code === 400) return classify400(e);
125
+ if (code === 401) return {
126
+ kind: "unauthorized",
127
+ retryable: false,
128
+ status: e.status,
129
+ description: e.description
130
+ };
131
+ if (code === 404) return {
132
+ kind: "unauthorized",
133
+ reason: "bad_token",
134
+ retryable: false,
135
+ status: e.status,
136
+ description: e.description
137
+ };
138
+ if (code === 403) return classify403(e);
139
+ if (code === 409) return bad(e, "conflict");
140
+ if (code === 413) return bad(e, "too_large");
141
+ if (code >= 500) return {
142
+ kind: "server_error",
143
+ retryable: true,
144
+ status: e.status,
145
+ description: e.description
146
+ };
147
+ return bad(e, "other");
148
+ }
149
+ /** Runs a call and returns its outcome next to the value (never throws for Telegram/network failures). */
150
+ async function settle(call) {
151
+ try {
152
+ return {
153
+ outcome: OK,
154
+ value: await call
155
+ };
156
+ } catch (error) {
157
+ if (!(error instanceof TelegramApiError) && !(error instanceof TelegramNetworkError)) throw error;
158
+ return {
159
+ outcome: classifyTelegramError(error),
160
+ error
161
+ };
162
+ }
163
+ }
164
+ export { classifyTelegramError, settle };
@@ -0,0 +1,73 @@
1
+ import { LogPort } from "../ports/ports.js";
2
+ import { AnswerCallbackQueryParams, BotCommand, ChatId, CommandScopeParams, DeleteMessageParams, DeleteWebhookParams, EditMessageReplyMarkupParams, EditMessageTextParams, GetUpdatesParams, MethodResults, SendChatActionParams, SendDocumentParams, SendMessageParams, SendPhotoParams, SetMyCommandsParams, SetWebhookParams } from "./types.js";
3
+ /** Default Bot API origin. */
4
+ export declare const DEFAULT_TELEGRAM_BASE_URL = "https://api.telegram.org";
5
+ /** Default per-request timeout (ms). */
6
+ export declare const DEFAULT_TELEGRAM_TIMEOUT_MS = 15000;
7
+ /** Any fetch-compatible function (global fetch, a service binding, or `FakeTelegram#fetch`). */
8
+ export type FetchLike = (input: Request | string | URL, init?: RequestInit) => Promise<Response>;
9
+ /** Returns the plaintext token for one request (for example by decrypting it with the keyring). */
10
+ export type TokenGetter = () => string | Promise<string>;
11
+ /** Options for {@link fetchTelegram}. */
12
+ export interface TelegramClientOptions {
13
+ /** Fetch to use. Default: global `fetch`. */
14
+ fetch?: FetchLike;
15
+ /** Default `https://api.telegram.org` (a local Bot API server or a proxy also works). */
16
+ baseUrl?: string;
17
+ /** Per-request timeout. Default 15 s. `getUpdates` adds its long-poll `timeout` on top. */
18
+ timeoutMs?: number;
19
+ /** Gets `tg.api` debug lines with method, duration and error code only. */
20
+ log?: LogPort;
21
+ }
22
+ /** Per-call options: abort signal and a timeout override (ms). */
23
+ export interface CallOptions {
24
+ signal?: AbortSignal;
25
+ timeoutMs?: number;
26
+ }
27
+ /** Typed methods for one bot. Cheap to create; holds only the token getter. */
28
+ export interface BotApi {
29
+ /** Any method by name (for methods without a typed wrapper). */
30
+ call<T = unknown>(method: string, params?: object, opts?: CallOptions): Promise<T>;
31
+ getMe(opts?: CallOptions): Promise<MethodResults["getMe"]>;
32
+ setWebhook(p: SetWebhookParams, opts?: CallOptions): Promise<true>;
33
+ deleteWebhook(p?: DeleteWebhookParams, opts?: CallOptions): Promise<true>;
34
+ getWebhookInfo(opts?: CallOptions): Promise<MethodResults["getWebhookInfo"]>;
35
+ /** Raw updates: validate each with `parseUpdate`. */
36
+ getUpdates(p?: GetUpdatesParams, opts?: CallOptions): Promise<unknown[]>;
37
+ setMyCommands(p: SetMyCommandsParams, opts?: CallOptions): Promise<true>;
38
+ deleteMyCommands(p?: CommandScopeParams, opts?: CallOptions): Promise<true>;
39
+ getMyCommands(p?: CommandScopeParams, opts?: CallOptions): Promise<BotCommand[]>;
40
+ sendMessage(p: SendMessageParams, opts?: CallOptions): Promise<MethodResults["sendMessage"]>;
41
+ sendDocument(p: SendDocumentParams, opts?: CallOptions): Promise<MethodResults["sendDocument"]>;
42
+ sendPhoto(p: SendPhotoParams, opts?: CallOptions): Promise<MethodResults["sendPhoto"]>;
43
+ sendChatAction(p: SendChatActionParams, opts?: CallOptions): Promise<true>;
44
+ editMessageText(p: EditMessageTextParams, opts?: CallOptions): Promise<MethodResults["editMessageText"]>;
45
+ editMessageReplyMarkup(p: EditMessageReplyMarkupParams, opts?: CallOptions): Promise<MethodResults["editMessageReplyMarkup"]>;
46
+ deleteMessage(p: DeleteMessageParams, opts?: CallOptions): Promise<true>;
47
+ answerCallbackQuery(p: AnswerCallbackQueryParams, opts?: CallOptions): Promise<true>;
48
+ getFile(p: {
49
+ file_id: string;
50
+ }, opts?: CallOptions): Promise<MethodResults["getFile"]>;
51
+ /** Download URL for a `getFile` path. It contains the token: use it at once, never log or store it. */
52
+ fileUrl(filePath: string): Promise<string>;
53
+ /** Downloads a file by its `getFile` path (≤ 20 MB on the cloud Bot API). */
54
+ downloadFile(filePath: string, opts?: CallOptions): Promise<Response>;
55
+ getChat(p: {
56
+ chat_id: ChatId;
57
+ }, opts?: CallOptions): Promise<MethodResults["getChat"]>;
58
+ getChatMember(p: {
59
+ chat_id: ChatId;
60
+ user_id: number;
61
+ }, opts?: CallOptions): Promise<MethodResults["getChatMember"]>;
62
+ leaveChat(p: {
63
+ chat_id: ChatId;
64
+ }, opts?: CallOptions): Promise<true>;
65
+ }
66
+ /** Creates `BotApi` handles per bot. This is the default `fetchTelegram` adapter. */
67
+ export interface TelegramClient {
68
+ bot(getToken: TokenGetter): BotApi;
69
+ /** Runs `fn` with a handle for one bot; the handle should not outlive the call. */
70
+ withToken<T>(getToken: TokenGetter, fn: (api: BotApi) => Promise<T>): Promise<T>;
71
+ }
72
+ /** Bot API client on `fetch`. Errors are {@link TelegramApiError} or {@link TelegramNetworkError}, never raw fetch errors. */
73
+ export declare function fetchTelegram(options?: TelegramClientOptions): TelegramClient;
@@ -0,0 +1,135 @@
1
+ import { TelegramApiError, TelegramNetworkError } from "./errors.js";
2
+ import { encodeBody } from "./multipart.js";
3
+ // src/telegram/client.ts
4
+ /** Default Bot API origin. */
5
+ const DEFAULT_TELEGRAM_BASE_URL = "https://api.telegram.org";
6
+ /** Default per-request timeout (ms). */
7
+ const DEFAULT_TELEGRAM_TIMEOUT_MS = 15e3;
8
+ function signalFor(timeoutMs, outer) {
9
+ const timeout = AbortSignal.timeout(timeoutMs);
10
+ return {
11
+ signal: outer ? AbortSignal.any([outer, timeout]) : timeout,
12
+ timeout
13
+ };
14
+ }
15
+ async function readBody(res) {
16
+ try {
17
+ const body = await res.json();
18
+ return typeof body === "object" && body !== null ? body : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** Bot API client on `fetch`. Errors are {@link TelegramApiError} or {@link TelegramNetworkError}, never raw fetch errors. */
24
+ function fetchTelegram(options = {}) {
25
+ const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
26
+ const baseUrl = (options.baseUrl ?? "https://api.telegram.org").replace(/\/+$/, "");
27
+ const defaultTimeout = options.timeoutMs ?? 15e3;
28
+ const log = options.log;
29
+ async function send(method, getToken, build, opts, extraMs = 0) {
30
+ const { signal, timeout } = signalFor((opts.timeoutMs ?? defaultTimeout) + extraMs, opts.signal);
31
+ const { url, init } = build(await getToken());
32
+ try {
33
+ return await doFetch(url, {
34
+ ...init,
35
+ signal
36
+ });
37
+ } catch {
38
+ throw new TelegramNetworkError(method, timeout.aborted ? "timeout" : signal.aborted ? "aborted" : "network");
39
+ }
40
+ }
41
+ function bot(getToken) {
42
+ async function call(method, params = {}, opts = {}) {
43
+ const started = Date.now();
44
+ const { body, contentType } = await encodeBody(params);
45
+ const extraMs = method === "getUpdates" ? Math.max(0, Number(params.timeout ?? 0)) * 1e3 : 0;
46
+ let res;
47
+ try {
48
+ res = await send(method, getToken, (token) => ({
49
+ url: `${baseUrl}/bot${token}/${method}`,
50
+ init: {
51
+ method: "POST",
52
+ body,
53
+ headers: contentType ? { "content-type": contentType } : {}
54
+ }
55
+ }), opts, extraMs);
56
+ } catch (e) {
57
+ log?.debug("tg.api", {
58
+ method,
59
+ ok: false,
60
+ error: e.reason,
61
+ ms: Date.now() - started
62
+ });
63
+ throw e;
64
+ }
65
+ const parsed = await readBody(res);
66
+ if (res.ok && parsed?.ok === true) {
67
+ log?.debug("tg.api", {
68
+ method,
69
+ ok: true,
70
+ ms: Date.now() - started
71
+ });
72
+ return parsed.result;
73
+ }
74
+ const err = new TelegramApiError({
75
+ method,
76
+ status: res.status,
77
+ errorCode: parsed?.error_code ?? res.status,
78
+ description: parsed?.description ?? `HTTP ${res.status}`,
79
+ retryAfter: parsed?.parameters?.retry_after,
80
+ migrateToChatId: parsed?.parameters?.migrate_to_chat_id
81
+ });
82
+ log?.debug("tg.api", {
83
+ method,
84
+ ok: false,
85
+ errorCode: err.errorCode,
86
+ ms: Date.now() - started
87
+ });
88
+ throw err;
89
+ }
90
+ const filePath = (path) => path.replace(/^\/+/, "");
91
+ return {
92
+ call,
93
+ getMe: (o) => call("getMe", {}, o),
94
+ setWebhook: (p, o) => call("setWebhook", p, o),
95
+ deleteWebhook: (p, o) => call("deleteWebhook", p ?? {}, o),
96
+ getWebhookInfo: (o) => call("getWebhookInfo", {}, o),
97
+ getUpdates: (p, o) => call("getUpdates", p ?? {}, o),
98
+ setMyCommands: (p, o) => call("setMyCommands", p, o),
99
+ deleteMyCommands: (p, o) => call("deleteMyCommands", p ?? {}, o),
100
+ getMyCommands: (p, o) => call("getMyCommands", p ?? {}, o),
101
+ sendMessage: (p, o) => call("sendMessage", p, o),
102
+ sendDocument: (p, o) => call("sendDocument", p, o),
103
+ sendPhoto: (p, o) => call("sendPhoto", p, o),
104
+ sendChatAction: (p, o) => call("sendChatAction", p, o),
105
+ editMessageText: (p, o) => call("editMessageText", p, o),
106
+ editMessageReplyMarkup: (p, o) => call("editMessageReplyMarkup", p, o),
107
+ deleteMessage: (p, o) => call("deleteMessage", p, o),
108
+ answerCallbackQuery: (p, o) => call("answerCallbackQuery", p, o),
109
+ getFile: (p, o) => call("getFile", p, o),
110
+ fileUrl: async (path) => `${baseUrl}/file/bot${await getToken()}/${filePath(path)}`,
111
+ async downloadFile(path, o = {}) {
112
+ const res = await send("downloadFile", getToken, (token) => ({
113
+ url: `${baseUrl}/file/bot${token}/${filePath(path)}`,
114
+ init: { method: "GET" }
115
+ }), o);
116
+ if (res.ok) return res;
117
+ const parsed = await readBody(res);
118
+ throw new TelegramApiError({
119
+ method: "downloadFile",
120
+ status: res.status,
121
+ errorCode: parsed?.error_code ?? res.status,
122
+ description: parsed?.description ?? `HTTP ${res.status}`
123
+ });
124
+ },
125
+ getChat: (p, o) => call("getChat", p, o),
126
+ getChatMember: (p, o) => call("getChatMember", p, o),
127
+ leaveChat: (p, o) => call("leaveChat", p, o)
128
+ };
129
+ }
130
+ return {
131
+ bot,
132
+ withToken: (getToken, fn) => fn(bot(getToken))
133
+ };
134
+ }
135
+ export { DEFAULT_TELEGRAM_BASE_URL, DEFAULT_TELEGRAM_TIMEOUT_MS, fetchTelegram };