@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,273 @@
1
+ import { Audience } from "../../contracts/chats.js";
2
+ import { sql } from "../../ports/sql.js";
3
+ import { chatDto, parseJson } from "../rows.js";
4
+ import { parseStartPayload } from "../../telegram/inbound.js";
5
+ import { deliveryFragments } from "../outbox/enqueue.js";
6
+ import { setStateSql } from "../state.js";
7
+ import { helpText } from "./builtins.js";
8
+ import { PENDING_LINK_PREFIX, PENDING_LINK_TTL_MS, hashLinkCode, pendingLinkDto, pendingLinkKey } from "../link-codes.js";
9
+ import { displayName, releaseOtherChatsSql, upsertChatSql } from "./link-chat.js";
10
+ // src/engine/dispatch/linking.ts
11
+ function chatKindOf(type) {
12
+ return type === "group" || type === "supergroup" || type === "channel" ? type : "private";
13
+ }
14
+ async function consume(ctx, code, chatRefHint) {
15
+ return await ctx.rt.db.first(sql`UPDATE tg_link_code SET used_at = ${ctx.now}, used_by_chat = ${chatRefHint}
16
+ WHERE code_hash = ${code.code_hash} AND tenant = ${ctx.rt.tenant} AND used_at IS NULL AND expires_at > ${ctx.now}
17
+ RETURNING code_hash`) !== null;
18
+ }
19
+ async function loadChat(ctx) {
20
+ return ctx.rt.db.first(sql`SELECT * FROM tg_chat WHERE tenant = ${ctx.rt.tenant} AND bot_id = ${ctx.bot.id}
21
+ AND chat_id = ${String(ctx.tgChat?.id ?? "")}`);
22
+ }
23
+ /**
24
+ * An admin invite for someone else's account: the code is used up, but the chat is only parked until the target
25
+ * user confirms in the app (so an admin cannot take on another user's identity in the bot).
26
+ */
27
+ async function parkPending(ctx, code, userId, name) {
28
+ const { rt, bot, now, from, tgChat } = ctx;
29
+ if (!from || !tgChat) return;
30
+ const chatRef = ctx.chat?.id ?? rt.ids.uuid();
31
+ if (!await consume(ctx, code, chatRef)) {
32
+ await ctx.reply(await ctx.t(code.used_at ? "bot.link.used" : "bot.link.expired"));
33
+ return;
34
+ }
35
+ const chatId = String(tgChat.id);
36
+ const state = {
37
+ id: rt.ids.uuid(),
38
+ botId: bot.id,
39
+ chatId,
40
+ chatRef,
41
+ userId,
42
+ createdBy: code.created_by,
43
+ tgName: displayName(from.first_name, from.last_name),
44
+ tgUsername: from.username ?? null,
45
+ locale: rt.i18n.match(from.language_code) ?? null,
46
+ createdAt: now,
47
+ expiresAt: now + PENDING_LINK_TTL_MS
48
+ };
49
+ const notify = await rt.db.first(sql`SELECT * FROM tg_chat WHERE tenant = ${rt.tenant} AND bot_id = ${bot.id} AND user_id = ${userId}
50
+ AND kind = 'private' AND status = 'active' AND chat_id <> ${chatId} ORDER BY linked_at DESC LIMIT 1`);
51
+ const notice = notify ? await pendingNotice(ctx, notify, code.created_by) : null;
52
+ await rt.db.batch([
53
+ sql`DELETE FROM tg_state WHERE tenant = ${rt.tenant} AND key LIKE ${`${PENDING_LINK_PREFIX}%`}
54
+ AND json_extract(value, '$.botId') = ${bot.id} AND json_extract(value, '$.chatId') = ${chatId}`,
55
+ setStateSql(rt, pendingLinkKey(state.id), JSON.stringify(state), now),
56
+ ...rt.audit({
57
+ actor: code.created_by,
58
+ entity: "telegram_link",
59
+ entityId: state.id,
60
+ action: "pending",
61
+ diff: {
62
+ userId,
63
+ botId: bot.id
64
+ },
65
+ at: now
66
+ })
67
+ ]);
68
+ rt.log.info("tg.link_pending", { botId: bot.id });
69
+ await ctx.reply(await ctx.t("bot.link.pending_confirm", { name }));
70
+ if (notice && notify) await rt.db.batch(deliveryFragments(rt, {
71
+ botId: bot.id,
72
+ chatRef: notify.id,
73
+ source: "system",
74
+ kind: "text",
75
+ html: notice,
76
+ dedupeKey: `link-pending:${state.id}`
77
+ }, now));
78
+ if (notice) await rt.transport.kick([bot.id]).catch(() => {});
79
+ const dto = pendingLinkDto(state);
80
+ await rt.hook("onPendingLink", (fn) => fn(dto, userId));
81
+ }
82
+ /** "You have a pending Telegram link from <admin>" in the invited user's linked chat's language. */
83
+ async function pendingNotice(ctx, chat, createdBy) {
84
+ const { rt, bot } = ctx;
85
+ const locale = chat.locale ?? bot.default_locale;
86
+ const admin = createdBy === null ? null : await rt.actor.forUser(createdBy);
87
+ const adminName = (admin === null ? null : rt.describe(admin).name) ?? rt.i18n.t(locale, "bot.link.an_admin");
88
+ return rt.i18n.t(locale, "bot.link.pending_notice", { admin: adminName });
89
+ }
90
+ async function linkPrivate(ctx, code) {
91
+ const { rt, bot, now, from, tgChat } = ctx;
92
+ if (!from || !tgChat || code.purpose !== "user" || !code.user_id) {
93
+ await ctx.reply(await ctx.t("bot.link.expired"));
94
+ return "handled";
95
+ }
96
+ const existing = ctx.chat;
97
+ if (existing?.status === "active" && existing.user_id === code.user_id) {
98
+ const info = await ctx.actorInfo();
99
+ await ctx.reply(await ctx.t("bot.start.already_linked", { name: info?.name ?? "" }));
100
+ return "handled";
101
+ }
102
+ if (existing?.status === "active" && existing.user_id !== null && existing.user_id !== code.user_id) {
103
+ await ctx.reply(await ctx.t("bot.link.other_account"));
104
+ return "handled";
105
+ }
106
+ const actor = await rt.actor.forUser(code.user_id);
107
+ if (actor === null) {
108
+ await ctx.reply(await ctx.t("bot.error.forbidden"));
109
+ return "handled";
110
+ }
111
+ const info = rt.describe(actor);
112
+ if (code.created_by !== code.user_id) {
113
+ await parkPending(ctx, code, code.user_id, info.name ?? "");
114
+ return "handled";
115
+ }
116
+ const id = existing?.id ?? rt.ids.uuid();
117
+ if (!await consume(ctx, code, id)) {
118
+ await ctx.reply(await ctx.t(code.used_at ? "bot.link.used" : "bot.link.expired"));
119
+ return "handled";
120
+ }
121
+ const locale = rt.i18n.match(from.language_code);
122
+ await rt.db.batch([
123
+ ...await releaseOtherChatsSql(rt, bot.id, code.user_id, id, now),
124
+ upsertChatSql(rt, id, {
125
+ botId: bot.id,
126
+ chatId: String(tgChat.id),
127
+ kind: "private",
128
+ title: displayName(from.first_name, from.last_name),
129
+ username: from.username ?? null,
130
+ userId: code.user_id,
131
+ audienceJson: null,
132
+ locale,
133
+ linkedBy: code.user_id
134
+ }, now),
135
+ ...rt.audit({
136
+ actor: code.user_id,
137
+ entity: "telegram_chat",
138
+ entityId: id,
139
+ action: "link",
140
+ diff: {
141
+ kind: "private",
142
+ userId: code.user_id,
143
+ botId: bot.id
144
+ },
145
+ at: now
146
+ })
147
+ ]);
148
+ ctx.chat = await loadChat(ctx);
149
+ rt.log.info("tg.linked", {
150
+ botId: bot.id,
151
+ chatRef: id,
152
+ kind: "private"
153
+ });
154
+ const role = info.role ?? "";
155
+ await ctx.reply(`${await ctx.t("bot.start.linked", {
156
+ name: info.name ?? "",
157
+ role
158
+ })}\n\n${await helpText(ctx)}`);
159
+ if (ctx.chat) {
160
+ const dto = chatDto(ctx.chat);
161
+ await rt.hook("onLinked", (fn) => fn(dto, actor));
162
+ }
163
+ return "handled";
164
+ }
165
+ async function linkGroup(ctx, code) {
166
+ const { rt, bot, now, tgChat } = ctx;
167
+ if (!tgChat || code.purpose !== "group") {
168
+ await ctx.reply(await ctx.t("bot.link.expired"));
169
+ return "handled";
170
+ }
171
+ const sender = await ctx.senderActor();
172
+ if (!sender || sender.userId !== code.created_by) {
173
+ await ctx.reply(await ctx.t("bot.link.not_creator"));
174
+ return "handled";
175
+ }
176
+ const audience = parseJson(code.audience_json, Audience.nullable(), null);
177
+ if (!audience) {
178
+ await ctx.reply(await ctx.t("bot.link.expired"));
179
+ return "handled";
180
+ }
181
+ const id = ctx.chat?.id ?? rt.ids.uuid();
182
+ if (!await consume(ctx, code, id)) {
183
+ await ctx.reply(await ctx.t(code.used_at ? "bot.link.used" : "bot.link.expired"));
184
+ return "handled";
185
+ }
186
+ await rt.db.batch([upsertChatSql(rt, id, {
187
+ botId: bot.id,
188
+ chatId: String(tgChat.id),
189
+ kind: chatKindOf(tgChat.type),
190
+ title: tgChat.title?.slice(0, 128) ?? null,
191
+ username: tgChat.username ?? null,
192
+ userId: null,
193
+ audienceJson: JSON.stringify(audience),
194
+ locale: null,
195
+ linkedBy: sender.userId
196
+ }, now), ...rt.audit({
197
+ actor: sender.userId,
198
+ entity: "telegram_chat",
199
+ entityId: id,
200
+ action: "link",
201
+ diff: {
202
+ kind: chatKindOf(tgChat.type),
203
+ audience,
204
+ botId: bot.id
205
+ },
206
+ at: now
207
+ })]);
208
+ ctx.chat = await loadChat(ctx);
209
+ rt.log.info("tg.linked", {
210
+ botId: bot.id,
211
+ chatRef: id,
212
+ kind: "group"
213
+ });
214
+ const locale = await ctx.locale();
215
+ const who = "role" in audience ? audience.role : rt.i18n.plain(locale, "bot.start.audience_users", { count: audience.users.length });
216
+ await ctx.reply(rt.i18n.t(locale, "bot.start.group_linked", { audience: who }));
217
+ const groupActor = await rt.actor.forAudience(audience);
218
+ if (ctx.chat && groupActor !== null) {
219
+ const dto = chatDto(ctx.chat);
220
+ await rt.hook("onLinked", (fn) => fn(dto, groupActor));
221
+ }
222
+ return "handled";
223
+ }
224
+ async function groupMuted(ctx, chatRef) {
225
+ return (await ctx.rt.db.first(sql`SELECT muted FROM tg_subscription WHERE tenant = ${ctx.rt.tenant} AND chat_ref = ${chatRef} AND topic = '*'`))?.muted === 1;
226
+ }
227
+ async function startHandler(ctx) {
228
+ const cmd = ctx.command;
229
+ if (ctx.cls !== "command" || cmd?.command !== "start" || cmd.mentionedOther) return "next";
230
+ const payload = parseStartPayload(cmd);
231
+ const isPrivate = ctx.tgChat?.type === "private";
232
+ if (payload === null) {
233
+ if (cmd.args.length > 0) {
234
+ await ctx.reply(await ctx.t("bot.link.expired"));
235
+ return "handled";
236
+ }
237
+ if (ctx.chat?.status === "active") {
238
+ if (!isPrivate && await groupMuted(ctx, ctx.chat.id)) {
239
+ const sender = await ctx.senderActor();
240
+ if (!sender || !await ctx.rt.can(sender.actor, "telegram.chats.manage")) {
241
+ await ctx.reply(await ctx.t("bot.error.forbidden"));
242
+ return "handled";
243
+ }
244
+ }
245
+ if ((await ctx.rt.db.run(sql`DELETE FROM tg_subscription WHERE tenant = ${ctx.rt.tenant} AND chat_ref = ${ctx.chat.id}
246
+ AND topic = '*' AND muted = 1`)).changes > 0) {
247
+ await ctx.reply(await ctx.t("bot.start.unmuted"));
248
+ return "handled";
249
+ }
250
+ const info = await ctx.actorInfo();
251
+ if (isPrivate && info) {
252
+ await ctx.reply(`${await ctx.t("bot.start.already_linked", { name: info.name ?? "" })}\n\n${await helpText(ctx)}`);
253
+ return "handled";
254
+ }
255
+ await ctx.reply(await helpText(ctx));
256
+ return "handled";
257
+ }
258
+ if (isPrivate) await ctx.reply(await ctx.t("bot.start.unlinked"));
259
+ return isPrivate ? "handled" : "ignored";
260
+ }
261
+ const code = await ctx.rt.db.first(sql`SELECT * FROM tg_link_code WHERE tenant = ${ctx.rt.tenant} AND code_hash = ${await hashLinkCode(payload)}
262
+ AND bot_id = ${ctx.bot.id}`);
263
+ if (!code || code.expires_at <= ctx.now) {
264
+ await ctx.reply(await ctx.t("bot.link.expired"));
265
+ return "handled";
266
+ }
267
+ if (code.used_at !== null) {
268
+ await ctx.reply(await ctx.t("bot.link.used"));
269
+ return "handled";
270
+ }
271
+ return isPrivate ? linkPrivate(ctx, code) : linkGroup(ctx, code);
272
+ }
273
+ export { startHandler };
@@ -0,0 +1,60 @@
1
+ import { TelegramProblem, TelegramProblemCode } from "../contracts/problems.js";
2
+ /** RFC 9457 extension members a problem may carry (each only for the codes named). */
3
+ export interface TelegramProblemExtensions {
4
+ /** `validation_failed`: field issues (`path` is dotted, for example `body.features.ai`). */
5
+ issues?: {
6
+ path: string;
7
+ message: string;
8
+ }[];
9
+ /** `template_invalid` / `template_variable_unknown`: template errors (1-based line/column, locale variant). */
10
+ errors?: {
11
+ line: number;
12
+ column: number;
13
+ message: string;
14
+ locale?: string | undefined;
15
+ }[];
16
+ /** `rate_limited`: seconds to wait. */
17
+ retryAfter?: number;
18
+ /** `send_text_rejected`: which content checks failed (`money`, `url`, `phone`, `email`, `too_long`, `empty`). */
19
+ reasons?: string[];
20
+ }
21
+ /**
22
+ * A plugin problem: a stable `code` (one of `TELEGRAM_PROBLEM_CODES`), an HTTP `status` (default from
23
+ * `PROBLEM_STATUS`), an optional human `detail` (English, safe to log; never secrets or message text) and the
24
+ * problem `extensions`. `toProblem()` gives the full RFC 9457 body that routes send and `ports.errors` receives.
25
+ *
26
+ * Every error leaving the programmatic API is one of these; unexpected failures are wrapped as `internal_error`
27
+ * with the original error as `cause` (never included in `toProblem()`).
28
+ */
29
+ export declare class TelegramPluginError extends Error {
30
+ /** Always `"TelegramPluginError"`. */
31
+ readonly name = "TelegramPluginError";
32
+ /** Stable machine code; map it to your own error codes or to `labels.problem.<code>` strings. */
33
+ readonly code: TelegramProblemCode;
34
+ /** HTTP status (401, 403, 404, 409, 422, …). */
35
+ readonly status: number;
36
+ /** Short English explanation, or undefined. */
37
+ readonly detail: string | undefined;
38
+ /** Extension members (`issues`, `errors`, `retryAfter`, `reasons`). Empty object when none. */
39
+ readonly extensions: TelegramProblemExtensions;
40
+ constructor(code: TelegramProblemCode, detail?: string, opts?: {
41
+ status?: number;
42
+ extensions?: TelegramProblemExtensions;
43
+ cause?: unknown;
44
+ });
45
+ /** `problemTitle(code)`. */
46
+ get title(): string;
47
+ /** The full problem+json body (`type`, `title`, `status`, `code`, `detail`, `instance` and the extensions). */
48
+ toProblem(instance?: string): TelegramProblem;
49
+ }
50
+ /** True for a `TelegramPluginError` (also across duplicate copies of the package, by name and code). */
51
+ export declare function isTelegramPluginError(e: unknown): e is TelegramPluginError;
52
+ /** Any thrown value as a `TelegramPluginError`; anything else becomes `internal_error` with `cause` set. */
53
+ export declare function toPluginError(e: unknown): TelegramPluginError;
54
+ /** A feature that is off (globally, or for this bot). */
55
+ export declare function featureDisabled(feature: string, detail?: string): TelegramPluginError;
56
+ /** Invalid `createTelegramPlugin` options or `define*` input. Thrown synchronously at construction. */
57
+ export declare class TelegramConfigError extends Error {
58
+ /** Always `"TelegramConfigError"`. */
59
+ readonly name = "TelegramConfigError";
60
+ }
@@ -0,0 +1,72 @@
1
+ import { PROBLEM_STATUS } from "../contracts/problems.js";
2
+ // src/engine/errors.ts
3
+ /** The problem+json title for a code (`send_text_rejected` → `send text rejected`). */
4
+ function problemTitle(code) {
5
+ return code.replaceAll("_", " ");
6
+ }
7
+ /**
8
+ * A plugin problem: a stable `code` (one of `TELEGRAM_PROBLEM_CODES`), an HTTP `status` (default from
9
+ * `PROBLEM_STATUS`), an optional human `detail` (English, safe to log; never secrets or message text) and the
10
+ * problem `extensions`. `toProblem()` gives the full RFC 9457 body that routes send and `ports.errors` receives.
11
+ *
12
+ * Every error leaving the programmatic API is one of these; unexpected failures are wrapped as `internal_error`
13
+ * with the original error as `cause` (never included in `toProblem()`).
14
+ */
15
+ var TelegramPluginError = class extends Error {
16
+ /** Always `"TelegramPluginError"`. */
17
+ name = "TelegramPluginError";
18
+ /** Stable machine code; map it to your own error codes or to `labels.problem.<code>` strings. */
19
+ code;
20
+ /** HTTP status (401, 403, 404, 409, 422, …). */
21
+ status;
22
+ /** Short English explanation, or undefined. */
23
+ detail;
24
+ /** Extension members (`issues`, `errors`, `retryAfter`, `reasons`). Empty object when none. */
25
+ extensions;
26
+ constructor(code, detail, opts = {}) {
27
+ super(detail ? `${code}: ${detail}` : code, opts.cause === void 0 ? void 0 : { cause: opts.cause });
28
+ this.code = code;
29
+ this.status = opts.status ?? PROBLEM_STATUS[code];
30
+ this.detail = detail;
31
+ this.extensions = opts.extensions ?? {};
32
+ }
33
+ /** `problemTitle(code)`. */
34
+ get title() {
35
+ return problemTitle(this.code);
36
+ }
37
+ /** The full problem+json body (`type`, `title`, `status`, `code`, `detail`, `instance` and the extensions). */
38
+ toProblem(instance) {
39
+ const x = this.extensions;
40
+ return {
41
+ type: "about:blank",
42
+ title: this.title,
43
+ status: this.status,
44
+ code: this.code,
45
+ ...this.detail ? { detail: this.detail } : {},
46
+ ...instance ? { instance } : {},
47
+ ...x.issues ? { issues: x.issues } : {},
48
+ ...x.errors ? { errors: x.errors } : {},
49
+ ...x.retryAfter !== void 0 ? { retryAfter: x.retryAfter } : {},
50
+ ...x.reasons ? { reasons: x.reasons } : {}
51
+ };
52
+ }
53
+ };
54
+ /** True for a `TelegramPluginError` (also across duplicate copies of the package, by name and code). */
55
+ function isTelegramPluginError(e) {
56
+ return e instanceof TelegramPluginError || e instanceof Error && e.name === "TelegramPluginError" && typeof e.code === "string" && typeof e.toProblem === "function";
57
+ }
58
+ /** Any thrown value as a `TelegramPluginError`; anything else becomes `internal_error` with `cause` set. */
59
+ function toPluginError(e) {
60
+ if (isTelegramPluginError(e)) return e;
61
+ return new TelegramPluginError("internal_error", "unexpected error", { cause: e });
62
+ }
63
+ /** A feature that is off (globally, or for this bot). */
64
+ function featureDisabled(feature, detail) {
65
+ return new TelegramPluginError("feature_disabled", detail ?? `feature "${feature}" is not enabled`);
66
+ }
67
+ /** Invalid `createTelegramPlugin` options or `define*` input. Thrown synchronously at construction. */
68
+ var TelegramConfigError = class extends Error {
69
+ /** Always `"TelegramConfigError"`. */
70
+ name = "TelegramConfigError";
71
+ };
72
+ export { TelegramConfigError, TelegramPluginError, featureDisabled, isTelegramPluginError, problemTitle, toPluginError };
@@ -0,0 +1,5 @@
1
+ import { EventFilter } from "../../contracts/events.js";
2
+ /** One condition. A missing (undefined / null) field only satisfies `ne`. */
3
+ export declare function matchFilter(filter: EventFilter, payload: unknown): boolean;
4
+ /** True when every filter holds (an empty list matches everything). Pure. */
5
+ export declare function matchFilters(filters: readonly EventFilter[], payload: unknown): boolean;
@@ -0,0 +1,82 @@
1
+ // src/engine/events/filters.ts
2
+ const ORDER_OPS = [
3
+ "gt",
4
+ "gte",
5
+ "lt",
6
+ "lte"
7
+ ];
8
+ function isScalar(v) {
9
+ return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
10
+ }
11
+ /** Strings compare case-insensitively for `contains`; everything else is strict. */
12
+ function containsValue(field, value) {
13
+ if (typeof field === "string" && isScalar(value)) return field.toLowerCase().includes(String(value).toLowerCase());
14
+ if (Array.isArray(field)) return field.some((x) => x === value);
15
+ return false;
16
+ }
17
+ function compare(a, b) {
18
+ if (typeof a === "number" && typeof b === "number") return a - b;
19
+ if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0;
20
+ return null;
21
+ }
22
+ /** One condition. A missing (undefined / null) field only satisfies `ne`. */
23
+ function matchFilter(filter, payload) {
24
+ const obj = payload !== null && typeof payload === "object" ? payload : {};
25
+ const v = Object.hasOwn(obj, filter.field) ? obj[filter.field] : void 0;
26
+ if (v === void 0 || v === null) return filter.op === "ne";
27
+ switch (filter.op) {
28
+ case "eq": return v === filter.value;
29
+ case "ne": return v !== filter.value;
30
+ case "in": return Array.isArray(filter.value) && filter.value.some((x) => x === v);
31
+ case "contains": return containsValue(v, filter.value);
32
+ default: {
33
+ const c = compare(v, filter.value);
34
+ if (c === null) return false;
35
+ if (filter.op === "gt") return c > 0;
36
+ if (filter.op === "gte") return c >= 0;
37
+ if (filter.op === "lt") return c < 0;
38
+ return c <= 0;
39
+ }
40
+ }
41
+ }
42
+ /** True when every filter holds (an empty list matches everything). Pure. */
43
+ function matchFilters(filters, payload) {
44
+ return filters.every((f) => matchFilter(f, payload));
45
+ }
46
+ /**
47
+ * Save-time check of one filter: the field must be `filterable` and the op/value must fit the field's type.
48
+ * Returns an error message, or null when the filter is fine. `shape` is the payload's top-level object shape.
49
+ */
50
+ function checkFilter(filter, filterable, shape) {
51
+ if (!filterable.includes(filter.field)) return `field "${filter.field}" is not filterable for this event`;
52
+ const field = shape?.kind === "object" ? shape.fields[filter.field] : void 0;
53
+ const kind = field?.kind ?? "unknown";
54
+ const value = filter.value;
55
+ const op = filter.op;
56
+ if (op === "in") {
57
+ if (!Array.isArray(value) || value.length === 0) return "`in` needs a non-empty list";
58
+ if (kind === "number" && !value.every((x) => typeof x === "number")) return "`in` on a number field needs numbers";
59
+ if ((kind === "string" || kind === "enum") && !value.every((x) => typeof x === "string")) return "`in` on a text field needs strings";
60
+ if (kind === "boolean" || kind === "array" || kind === "object") return `\`in\` does not apply to a ${kind} field`;
61
+ return null;
62
+ }
63
+ if (Array.isArray(value)) return `\`${op}\` needs a single value`;
64
+ if (op === "contains") {
65
+ if (kind === "array") return isScalar(value) ? null : "`contains` needs a value";
66
+ if (kind === "string" || kind === "enum" || kind === "unknown") return typeof value === "string" ? null : "`contains` on a text field needs a string";
67
+ return `\`contains\` does not apply to a ${kind} field`;
68
+ }
69
+ if (ORDER_OPS.includes(op)) {
70
+ if (kind === "number") return typeof value === "number" ? null : `\`${op}\` on a number field needs a number`;
71
+ if (kind === "string" || kind === "date" || kind === "enum") return typeof value === "string" ? null : `\`${op}\` on a text field needs a string`;
72
+ if (kind === "unknown") return typeof value === "boolean" ? `\`${op}\` needs a number or a string` : null;
73
+ return `\`${op}\` does not apply to a ${kind} field`;
74
+ }
75
+ if (kind === "number" && typeof value !== "number") return `\`${op}\` on a number field needs a number`;
76
+ if ((kind === "string" || kind === "enum") && typeof value !== "string") return `\`${op}\` on a text field needs a string`;
77
+ if (kind === "enum" && field?.kind === "enum" && !field.values.includes(value)) return `"${String(value)}" is not one of ${field.values.join(", ")}`;
78
+ if (kind === "boolean" && typeof value !== "boolean") return `\`${op}\` on a yes/no field needs true or false`;
79
+ if (kind === "array" || kind === "object" || kind === "record") return `\`${op}\` does not apply to a ${kind} field`;
80
+ return null;
81
+ }
82
+ export { checkFilter, matchFilter, matchFilters };
@@ -0,0 +1,30 @@
1
+ import { sql } from "../../ports/sql.js";
2
+ import { claimableEvents, runEvents } from "./tick.js";
3
+ import { QUEUE_MARK } from "../outbox/transport.js";
4
+ // src/engine/events/kick.ts
5
+ /** One indexed read: is any event due now? Keeps a plain send-kick from costing a queue message. */
6
+ async function hasDueEvents(rt, now) {
7
+ return await rt.db.first(sql`SELECT 1 AS ok FROM tg_event WHERE ${claimableEvents(rt, now)} LIMIT 1`) !== null;
8
+ }
9
+ /** Starts event processing now when events are due (queue / DO transports only). Never throws. */
10
+ async function kickEvents(rt) {
11
+ if (!rt.features.events) return;
12
+ const name = rt.transport.name;
13
+ if (name !== "queue" && name !== "do") return;
14
+ try {
15
+ if (!await hasDueEvents(rt, rt.clock.now())) return;
16
+ if (name === "queue" && rt.queue) {
17
+ const body = {
18
+ mark: QUEUE_MARK,
19
+ type: "events",
20
+ tenant: rt.tenant
21
+ };
22
+ await rt.queue.send(body);
23
+ return;
24
+ }
25
+ await runEvents(rt, rt.clock.now());
26
+ } catch (e) {
27
+ rt.log.warn("tg.events_kick_failed", { error: e instanceof Error ? e.name : "error" });
28
+ }
29
+ }
30
+ export { hasDueEvents, kickEvents };
@@ -0,0 +1,3 @@
1
+ import { EventDef } from "../registry.js";
2
+ /** A shallow copy of an object payload without the event's `sensitive` fields (`{}` for non-objects). Pure. */
3
+ export declare function stripSensitive(def: EventDef<unknown>, payload: unknown): Record<string, unknown>;
@@ -0,0 +1,16 @@
1
+ import { hostCan } from "../content/providers.js";
2
+ // src/engine/events/payload.ts
3
+ /** A shallow copy of an object payload without the event's `sensitive` fields (`{}` for non-objects). Pure. */
4
+ function stripSensitive(def, payload) {
5
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return {};
6
+ const out = { ...payload };
7
+ for (const f of def.sensitive) delete out[f];
8
+ return out;
9
+ }
10
+ /** The payload variables this audience may see. */
11
+ async function payloadVarsFor(rt, def, payload, actor) {
12
+ if (def.sensitive.length === 0 || payload === null || typeof payload !== "object" || Array.isArray(payload)) return payload !== null && typeof payload === "object" && !Array.isArray(payload) ? { ...payload } : {};
13
+ if (def.sensitivePermission !== null && await hostCan(rt, actor, def.sensitivePermission)) return { ...payload };
14
+ return stripSensitive(def, payload);
15
+ }
16
+ export { payloadVarsFor, stripSensitive };