@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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/adapters/clock.d.ts +3 -0
- package/dist/adapters/clock.js +4 -0
- package/dist/adapters/console-log.d.ts +13 -0
- package/dist/adapters/console-log.js +41 -0
- package/dist/adapters/d1-db.d.ts +5 -0
- package/dist/adapters/d1-db.js +37 -0
- package/dist/adapters/ids.d.ts +5 -0
- package/dist/adapters/ids.js +25 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/keyring.d.ts +50 -0
- package/dist/adapters/keyring.js +174 -0
- package/dist/adapters/metrics.d.ts +28 -0
- package/dist/adapters/metrics.js +48 -0
- package/dist/adapters/r2-files.d.ts +20 -0
- package/dist/adapters/r2-files.js +53 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +45 -0
- package/dist/client/broadcasts.d.ts +35 -0
- package/dist/client/commands.d.ts +23 -0
- package/dist/client/endpoint.d.ts +34 -0
- package/dist/client/events.d.ts +33 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.js +3 -0
- package/dist/client/schedules.d.ts +42 -0
- package/dist/client/schema.d.ts +181 -0
- package/dist/client/send.d.ts +15 -0
- package/dist/client/templates.d.ts +47 -0
- package/dist/contracts/bots.d.ts +129 -0
- package/dist/contracts/bots.js +70 -0
- package/dist/contracts/broadcasts.d.ts +116 -0
- package/dist/contracts/broadcasts.js +63 -0
- package/dist/contracts/chats.d.ts +97 -0
- package/dist/contracts/chats.js +66 -0
- package/dist/contracts/commands.d.ts +52 -0
- package/dist/contracts/commands.js +53 -0
- package/dist/contracts/common.d.ts +38 -0
- package/dist/contracts/common.js +41 -0
- package/dist/contracts/deliveries.d.ts +103 -0
- package/dist/contracts/deliveries.js +74 -0
- package/dist/contracts/destinations.d.ts +24 -0
- package/dist/contracts/destinations.js +28 -0
- package/dist/contracts/events.d.ts +165 -0
- package/dist/contracts/events.js +98 -0
- package/dist/contracts/features.d.ts +20 -0
- package/dist/contracts/features.js +21 -0
- package/dist/contracts/health.d.ts +55 -0
- package/dist/contracts/health.js +58 -0
- package/dist/contracts/index.d.ts +22 -0
- package/dist/contracts/index.js +22 -0
- package/dist/contracts/labels.d.ts +38 -0
- package/dist/contracts/labels.js +52 -0
- package/dist/contracts/links.d.ts +51 -0
- package/dist/contracts/links.js +50 -0
- package/dist/contracts/meta.d.ts +132 -0
- package/dist/contracts/meta.js +85 -0
- package/dist/contracts/permissions.d.ts +24 -0
- package/dist/contracts/permissions.js +27 -0
- package/dist/contracts/problems.d.ts +106 -0
- package/dist/contracts/problems.js +101 -0
- package/dist/contracts/recipients.d.ts +36 -0
- package/dist/contracts/recipients.js +22 -0
- package/dist/contracts/schedules.d.ts +276 -0
- package/dist/contracts/schedules.js +143 -0
- package/dist/contracts/send.d.ts +126 -0
- package/dist/contracts/send.js +69 -0
- package/dist/contracts/subscriptions.d.ts +38 -0
- package/dist/contracts/subscriptions.js +31 -0
- package/dist/contracts/templates.d.ts +358 -0
- package/dist/contracts/templates.js +174 -0
- package/dist/contracts/users.d.ts +15 -0
- package/dist/contracts/users.js +19 -0
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.js +47 -0
- package/dist/core/html/escape.d.ts +2 -0
- package/dist/core/html/escape.js +36 -0
- package/dist/core/html/tokenize.js +65 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/pacing.d.ts +1 -0
- package/dist/core/pacing.js +86 -0
- package/dist/core/random.d.ts +2 -0
- package/dist/core/random.js +38 -0
- package/dist/core/recipients.d.ts +1 -0
- package/dist/core/recipients.js +86 -0
- package/dist/core/schedule/describe.d.ts +3 -0
- package/dist/core/schedule/describe.js +74 -0
- package/dist/core/schedule/index.d.ts +4 -0
- package/dist/core/schedule/policy.d.ts +1 -0
- package/dist/core/schedule/policy.js +37 -0
- package/dist/core/schedule/rule.d.ts +25 -0
- package/dist/core/schedule/rule.js +142 -0
- package/dist/core/schedule/tz.d.ts +2 -0
- package/dist/core/schedule/tz.js +59 -0
- package/dist/core/split/plain.d.ts +2 -0
- package/dist/core/split/plain.js +23 -0
- package/dist/core/split/split.d.ts +15 -0
- package/dist/core/split/split.js +158 -0
- package/dist/core/template/ast.d.ts +58 -0
- package/dist/core/template/ast.js +5 -0
- package/dist/core/template/expr.js +108 -0
- package/dist/core/template/filters.d.ts +20 -0
- package/dist/core/template/filters.js +127 -0
- package/dist/core/template/format.d.ts +17 -0
- package/dist/core/template/format.js +88 -0
- package/dist/core/template/index.d.ts +20 -0
- package/dist/core/template/index.js +29 -0
- package/dist/core/template/parse.d.ts +8 -0
- package/dist/core/template/parse.js +369 -0
- package/dist/core/template/render.d.ts +17 -0
- package/dist/core/template/render.js +118 -0
- package/dist/core/template/shape.d.ts +28 -0
- package/dist/core/template/shape.js +151 -0
- package/dist/core/template/tags.js +105 -0
- package/dist/core/template/validate.d.ts +10 -0
- package/dist/core/template/validate.js +144 -0
- package/dist/core/template/variables.d.ts +15 -0
- package/dist/core/template/variables.js +41 -0
- package/dist/do/index.d.ts +3 -0
- package/dist/do/index.js +3 -0
- package/dist/do/telegram-bot-do.d.ts +45 -0
- package/dist/do/telegram-bot-do.js +219 -0
- package/dist/engine/actions/callback.js +256 -0
- package/dist/engine/actions/tokens.d.ts +2 -0
- package/dist/engine/actions/tokens.js +51 -0
- package/dist/engine/bot-api.js +20 -0
- package/dist/engine/bot-features.js +39 -0
- package/dist/engine/chat-state.js +66 -0
- package/dist/engine/content/audience.js +54 -0
- package/dist/engine/content/buttons.d.ts +1 -0
- package/dist/engine/content/buttons.js +113 -0
- package/dist/engine/content/fanout.js +284 -0
- package/dist/engine/content/intersect.d.ts +6 -0
- package/dist/engine/content/intersect.js +26 -0
- package/dist/engine/content/providers.d.ts +6 -0
- package/dist/engine/content/providers.js +142 -0
- package/dist/engine/content/recipients.js +128 -0
- package/dist/engine/content/render.js +138 -0
- package/dist/engine/content/reply.js +156 -0
- package/dist/engine/content/report-files.js +78 -0
- package/dist/engine/content/template-rows.js +74 -0
- package/dist/engine/dialogs/dialog.d.ts +2 -0
- package/dist/engine/dialogs/dialog.js +375 -0
- package/dist/engine/dispatch/builtins.js +198 -0
- package/dist/engine/dispatch/commands.js +194 -0
- package/dist/engine/dispatch/context.d.ts +2 -0
- package/dist/engine/dispatch/context.js +194 -0
- package/dist/engine/dispatch/dispatcher.js +66 -0
- package/dist/engine/dispatch/handlers.d.ts +1 -0
- package/dist/engine/dispatch/handlers.js +103 -0
- package/dist/engine/dispatch/lifecycle.js +85 -0
- package/dist/engine/dispatch/link-chat.js +31 -0
- package/dist/engine/dispatch/linking.js +273 -0
- package/dist/engine/errors.d.ts +60 -0
- package/dist/engine/errors.js +72 -0
- package/dist/engine/events/filters.d.ts +5 -0
- package/dist/engine/events/filters.js +82 -0
- package/dist/engine/events/kick.js +30 -0
- package/dist/engine/events/payload.d.ts +3 -0
- package/dist/engine/events/payload.js +16 -0
- package/dist/engine/events/tick.js +208 -0
- package/dist/engine/i18n.d.ts +12 -0
- package/dist/engine/i18n.js +55 -0
- package/dist/engine/inbound/files.js +121 -0
- package/dist/engine/inbound/handler.js +46 -0
- package/dist/engine/jobs.d.ts +32 -0
- package/dist/engine/jobs.js +85 -0
- package/dist/engine/link-codes.js +46 -0
- package/dist/engine/metrics.js +12 -0
- package/dist/engine/options.d.ts +369 -0
- package/dist/engine/options.js +81 -0
- package/dist/engine/outbox/dead-letter.js +78 -0
- package/dist/engine/outbox/do-registry.d.ts +10 -0
- package/dist/engine/outbox/do-registry.js +10 -0
- package/dist/engine/outbox/do-transport.d.ts +33 -0
- package/dist/engine/outbox/do-transport.js +73 -0
- package/dist/engine/outbox/drain.d.ts +13 -0
- package/dist/engine/outbox/drain.js +381 -0
- package/dist/engine/outbox/enqueue.d.ts +1 -0
- package/dist/engine/outbox/enqueue.js +63 -0
- package/dist/engine/outbox/select.js +24 -0
- package/dist/engine/outbox/send.js +362 -0
- package/dist/engine/outbox/transport.d.ts +35 -0
- package/dist/engine/outbox/transport.js +175 -0
- package/dist/engine/outbox/wake.js +28 -0
- package/dist/engine/registry.d.ts +502 -0
- package/dist/engine/registry.js +258 -0
- package/dist/engine/rows.d.ts +25 -0
- package/dist/engine/rows.js +131 -0
- package/dist/engine/runtime.d.ts +95 -0
- package/dist/engine/runtime.js +149 -0
- package/dist/engine/schedules/fire.js +208 -0
- package/dist/engine/schedules/resume.js +75 -0
- package/dist/engine/schedules/rows.js +69 -0
- package/dist/engine/schedules/tick.js +30 -0
- package/dist/engine/schedules/wake.js +50 -0
- package/dist/engine/state.d.ts +11 -0
- package/dist/engine/state.js +78 -0
- package/dist/locales/en.js +339 -0
- package/dist/locales/en.json +341 -0
- package/dist/locales/mr.js +339 -0
- package/dist/locales/mr.json +341 -0
- package/dist/ports/db.d.ts +22 -0
- package/dist/ports/index.d.ts +4 -0
- package/dist/ports/ports.d.ts +257 -0
- package/dist/ports/sql.d.ts +39 -0
- package/dist/ports/sql.js +76 -0
- package/dist/renderers/csv.d.ts +14 -0
- package/dist/renderers/csv.js +51 -0
- package/dist/renderers/html.d.ts +44 -0
- package/dist/renderers/html.js +81 -0
- package/dist/renderers/index.d.ts +5 -0
- package/dist/renderers/index.js +4 -0
- package/dist/renderers/table.d.ts +30 -0
- package/dist/renderers/table.js +114 -0
- package/dist/routes/admin.js +146 -0
- package/dist/routes/app.d.ts +3111 -0
- package/dist/routes/app.js +9 -0
- package/dist/routes/broadcasts.js +37 -0
- package/dist/routes/commands.js +35 -0
- package/dist/routes/content.js +12 -0
- package/dist/routes/events.js +36 -0
- package/dist/routes/http.d.ts +30 -0
- package/dist/routes/http.js +223 -0
- package/dist/routes/me.js +59 -0
- package/dist/routes/schedules.js +62 -0
- package/dist/routes/send.js +24 -0
- package/dist/routes/templates.js +70 -0
- package/dist/routes/webhook.js +147 -0
- package/dist/schema/columns.js +307 -0
- package/dist/schema/ddl.d.ts +7 -0
- package/dist/schema/ddl.js +45 -0
- package/dist/schema/index.d.ts +4255 -0
- package/dist/schema/index.js +44 -0
- package/dist/schema/reference.sql +359 -0
- package/dist/schema/tables.d.ts +4243 -0
- package/dist/schema/tables.js +327 -0
- package/dist/schema/version.d.ts +5 -0
- package/dist/schema/version.js +7 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.js +46 -0
- package/dist/server/plugin.d.ts +81 -0
- package/dist/server/plugin.js +247 -0
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.js +4 -0
- package/dist/services/ai-tool.d.ts +26 -0
- package/dist/services/ai-tool.js +148 -0
- package/dist/services/bots.js +300 -0
- package/dist/services/broadcasts.js +390 -0
- package/dist/services/chats.js +83 -0
- package/dist/services/cleanup.js +83 -0
- package/dist/services/commands.js +140 -0
- package/dist/services/deliveries.js +53 -0
- package/dist/services/destinations.js +91 -0
- package/dist/services/event-rules.js +229 -0
- package/dist/services/events.js +20 -0
- package/dist/services/idempotency.js +64 -0
- package/dist/services/links.js +200 -0
- package/dist/services/menu.js +95 -0
- package/dist/services/ops.js +90 -0
- package/dist/services/rekey.js +63 -0
- package/dist/services/reports.js +87 -0
- package/dist/services/schedules.js +214 -0
- package/dist/services/send.d.ts +14 -0
- package/dist/services/send.js +421 -0
- package/dist/services/subscriptions.js +28 -0
- package/dist/services/templates.js +259 -0
- package/dist/telegram/classify.d.ts +30 -0
- package/dist/telegram/classify.js +164 -0
- package/dist/telegram/client.d.ts +73 -0
- package/dist/telegram/client.js +135 -0
- package/dist/telegram/errors.d.ts +37 -0
- package/dist/telegram/errors.js +62 -0
- package/dist/telegram/inbound.d.ts +40 -0
- package/dist/telegram/inbound.js +72 -0
- package/dist/telegram/index.d.ts +8 -0
- package/dist/telegram/multipart.d.ts +5 -0
- package/dist/telegram/multipart.js +47 -0
- package/dist/telegram/types.d.ts +271 -0
- package/dist/telegram/update.d.ts +833 -0
- package/dist/telegram/update.js +129 -0
- package/dist/testing/fake-html.d.ts +9 -0
- package/dist/testing/fake-html.js +83 -0
- package/dist/testing/fake-telegram.d.ts +206 -0
- package/dist/testing/fake-telegram.js +856 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/reference-sql.d.ts +4 -0
- package/dist/testing/reference-sql.js +8 -0
- package/dist/testing/test-plugin.d.ts +126 -0
- package/dist/testing/test-plugin.js +182 -0
- package/dist/testing/updates.d.ts +100 -0
- package/dist/testing/updates.js +212 -0
- package/package.json +113 -3
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { nextBackoff, throttleKey } from "../../core/pacing.js";
|
|
2
|
+
import { topics } from "../../core/recipients.js";
|
|
3
|
+
import { sql } from "../../ports/sql.js";
|
|
4
|
+
import { isTelegramPluginError } from "../errors.js";
|
|
5
|
+
import { templateById, templateModel } from "../content/template-rows.js";
|
|
6
|
+
import { matchFilters } from "./filters.js";
|
|
7
|
+
import { payloadVarsFor } from "./payload.js";
|
|
8
|
+
import { TRANSIENT_FANOUT_ERRORS, fanOut } from "../content/fanout.js";
|
|
9
|
+
import { resolveRecipients } from "../content/recipients.js";
|
|
10
|
+
import { eventProvider, eventRuleModel } from "../../services/event-rules.js";
|
|
11
|
+
// src/engine/events/tick.ts
|
|
12
|
+
/** How long a claimed event stays with one processor before another may re-claim it. */
|
|
13
|
+
const EVENT_CLAIM_MS = 12e4;
|
|
14
|
+
/** Pending events that are due (no backoff, or its time has come), or processing ones with an expired claim. */
|
|
15
|
+
function claimableEvents(rt, now) {
|
|
16
|
+
return sql`tenant = ${rt.tenant} AND ((status = 'pending' AND (claim_until IS NULL OR claim_until <= ${now}))
|
|
17
|
+
OR (status = 'processing' AND claim_until < ${now}))`;
|
|
18
|
+
}
|
|
19
|
+
/** Atomically claims up to `limits.eventClaim` due events. */
|
|
20
|
+
async function claimEvents(rt, now, token) {
|
|
21
|
+
const claimable = claimableEvents(rt, now);
|
|
22
|
+
return rt.db.all(sql`UPDATE tg_event SET status = 'processing', claim_until = ${now + EVENT_CLAIM_MS}, claim_token = ${token},
|
|
23
|
+
attempts = attempts + 1
|
|
24
|
+
WHERE id IN (SELECT id FROM tg_event WHERE ${claimable} ORDER BY occurred_at, id LIMIT ${rt.limits.eventClaim})
|
|
25
|
+
AND ${claimable}
|
|
26
|
+
RETURNING id, event, payload_json, occurred_at, attempts`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Throttle as one conditional upsert in `tg_state` (`thr:<rule>:<value>` → last event id, `updated_at` = its
|
|
30
|
+
* time). It passes when the key is new, its window has passed, or this same event took it (re-processing after a
|
|
31
|
+
* crash), so two processors can never both pass in one window.
|
|
32
|
+
*/
|
|
33
|
+
async function takeThrottle(rt, rule, ev, payload) {
|
|
34
|
+
if (!rule.throttle) return true;
|
|
35
|
+
const key = throttleKey(rule.id, rule.throttle, payload);
|
|
36
|
+
const since = ev.occurred_at - rule.throttle.perMinutes * 6e4;
|
|
37
|
+
return (await rt.db.all(sql`INSERT INTO tg_state (tenant, key, value, updated_at) VALUES (${rt.tenant}, ${key}, ${ev.id}, ${ev.occurred_at})
|
|
38
|
+
ON CONFLICT (tenant, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
39
|
+
WHERE tg_state.updated_at <= ${since} OR tg_state.value = excluded.value
|
|
40
|
+
RETURNING key`)).length > 0;
|
|
41
|
+
}
|
|
42
|
+
async function rulesFor(rt, events) {
|
|
43
|
+
const out = /* @__PURE__ */ new Map();
|
|
44
|
+
const keys = [...new Set(events)];
|
|
45
|
+
if (keys.length === 0) return out;
|
|
46
|
+
const rows = await rt.db.all(sql`SELECT * FROM tg_event_rule WHERE tenant = ${rt.tenant} AND event IN (${sql.join(keys)})
|
|
47
|
+
AND enabled = 1 AND deleted_at IS NULL ORDER BY created_at, id`);
|
|
48
|
+
for (const r of rows) out.set(r.event, [...out.get(r.event) ?? [], eventRuleModel(r)]);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
const ignored = (error) => ({
|
|
52
|
+
status: "ignored",
|
|
53
|
+
error,
|
|
54
|
+
botIds: [],
|
|
55
|
+
deliveries: 0,
|
|
56
|
+
retry: false
|
|
57
|
+
});
|
|
58
|
+
async function processOne(rt, ev, rules, templates) {
|
|
59
|
+
const def = rt.registry.events.get(ev.event);
|
|
60
|
+
if (!def) return ignored("event_unknown");
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = JSON.parse(ev.payload_json);
|
|
64
|
+
} catch {
|
|
65
|
+
return ignored("payload_invalid");
|
|
66
|
+
}
|
|
67
|
+
const parsed = def.payload.safeParse(raw);
|
|
68
|
+
if (!parsed.success) return ignored("payload_invalid");
|
|
69
|
+
const payload = parsed.data;
|
|
70
|
+
const errors = /* @__PURE__ */ new Set();
|
|
71
|
+
const botIds = /* @__PURE__ */ new Set();
|
|
72
|
+
let deliveries = 0;
|
|
73
|
+
for (const rule of rules) {
|
|
74
|
+
if (!matchFilters(rule.filters, payload)) continue;
|
|
75
|
+
let template = templates.get(rule.templateId);
|
|
76
|
+
if (template === void 0) {
|
|
77
|
+
const row = await templateById(rt, rule.templateId);
|
|
78
|
+
template = row ? templateModel(row) : null;
|
|
79
|
+
templates.set(rule.templateId, template);
|
|
80
|
+
}
|
|
81
|
+
if (!template) {
|
|
82
|
+
rt.log.warn("tg.event_rule_template_missing", {
|
|
83
|
+
rule: rule.id,
|
|
84
|
+
code: "not_found"
|
|
85
|
+
});
|
|
86
|
+
errors.add("template_missing");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!await takeThrottle(rt, rule, ev, payload)) continue;
|
|
90
|
+
const now = rt.clock.now();
|
|
91
|
+
const recipients = await resolveRecipients(rt, rule.recipients, {
|
|
92
|
+
topic: topics.event(def.key),
|
|
93
|
+
now
|
|
94
|
+
});
|
|
95
|
+
if (recipients.send.length + recipients.held.length === 0) continue;
|
|
96
|
+
const r = await fanOut(rt, {
|
|
97
|
+
template,
|
|
98
|
+
provider: eventProvider(def, template, payload),
|
|
99
|
+
recipients,
|
|
100
|
+
personalize: true,
|
|
101
|
+
source: "event",
|
|
102
|
+
sourceId: ev.id,
|
|
103
|
+
dedupeKey: (chat) => `evt:${ev.id}:${rule.id}:${chat.chatRef}${chat.messageThreadId ? `#${chat.messageThreadId}` : ""}`,
|
|
104
|
+
now,
|
|
105
|
+
tz: rt.settings.timezone,
|
|
106
|
+
vars: (aud) => payloadVarsFor(rt, def, payload, aud.actor),
|
|
107
|
+
meta: {
|
|
108
|
+
event: def.key,
|
|
109
|
+
rule: rule.id
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
deliveries += r.deliveries;
|
|
113
|
+
for (const e of r.errors) errors.add(e);
|
|
114
|
+
if (r.chats > 0) for (const c of [...recipients.send, ...recipients.held.map((h) => h.chat)]) botIds.add(c.botId);
|
|
115
|
+
}
|
|
116
|
+
const error = errors.size > 0 ? [...errors].join(",").slice(0, 200) : null;
|
|
117
|
+
const retry = [...errors].some((e) => TRANSIENT_FANOUT_ERRORS.includes(e));
|
|
118
|
+
return {
|
|
119
|
+
status: "done",
|
|
120
|
+
error,
|
|
121
|
+
botIds: [...botIds],
|
|
122
|
+
deliveries,
|
|
123
|
+
retry
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Claims and processes due events. Safe to run concurrently (claims + dedupe keys + the throttle upsert). */
|
|
127
|
+
async function runEvents(rt, now) {
|
|
128
|
+
const result = {
|
|
129
|
+
claimed: 0,
|
|
130
|
+
done: 0,
|
|
131
|
+
ignored: 0,
|
|
132
|
+
failed: 0,
|
|
133
|
+
retried: 0,
|
|
134
|
+
deliveries: 0
|
|
135
|
+
};
|
|
136
|
+
if (!rt.features.events) return result;
|
|
137
|
+
const token = rt.ids.uuid();
|
|
138
|
+
const claimed = await claimEvents(rt, now, token);
|
|
139
|
+
result.claimed = claimed.length;
|
|
140
|
+
if (claimed.length === 0) return result;
|
|
141
|
+
const rules = await rulesFor(rt, claimed.map((e) => e.event));
|
|
142
|
+
const templates = /* @__PURE__ */ new Map();
|
|
143
|
+
const botIds = /* @__PURE__ */ new Set();
|
|
144
|
+
const started = rt.clock.now();
|
|
145
|
+
const budget = rt.limits.tickBudgetMs / 2;
|
|
146
|
+
const fenced = (id) => sql`WHERE tenant = ${rt.tenant} AND id = ${id} AND claim_token = ${token}`;
|
|
147
|
+
/** Puts the event back to `pending` with a backoff, or ends it as `finalStatus` after the last attempt. */
|
|
148
|
+
const backOff = async (ev, finalStatus, code) => {
|
|
149
|
+
const final = ev.attempts >= 5;
|
|
150
|
+
const at = rt.clock.now();
|
|
151
|
+
const notBefore = final ? null : at + nextBackoff(ev.attempts - 1, Math.random);
|
|
152
|
+
await rt.db.run(sql`UPDATE tg_event SET status = ${final ? finalStatus : "pending"}, error = ${code.slice(0, 200)},
|
|
153
|
+
claim_until = ${notBefore}, claim_token = NULL, processed_at = ${final ? at : null} ${fenced(ev.id)}`);
|
|
154
|
+
return final;
|
|
155
|
+
};
|
|
156
|
+
for (const [i, ev] of claimed.entries()) {
|
|
157
|
+
if (rt.clock.now() - started > budget) {
|
|
158
|
+
const rest = claimed.slice(i).map((e) => e.id);
|
|
159
|
+
await rt.db.run(sql`UPDATE tg_event SET status = 'pending', claim_until = NULL, claim_token = NULL, attempts = attempts - 1
|
|
160
|
+
WHERE tenant = ${rt.tenant} AND claim_token = ${token} AND id IN (${sql.join(rest)})`);
|
|
161
|
+
result.retried += rest.length;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
let outcome;
|
|
165
|
+
try {
|
|
166
|
+
outcome = await processOne(rt, ev, rules.get(ev.event) ?? [], templates);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
const code = isTelegramPluginError(e) ? e.code : e instanceof Error ? e.name : "error";
|
|
169
|
+
rt.log.error("tg.event_failed", {
|
|
170
|
+
event: ev.event,
|
|
171
|
+
attempts: ev.attempts,
|
|
172
|
+
code
|
|
173
|
+
});
|
|
174
|
+
if (await backOff(ev, "failed", code)) result.failed++;
|
|
175
|
+
else result.retried++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
result.deliveries += outcome.deliveries;
|
|
179
|
+
for (const b of outcome.botIds) botIds.add(b);
|
|
180
|
+
if (outcome.retry && ev.attempts < 5) {
|
|
181
|
+
rt.log.warn("tg.event_retry", {
|
|
182
|
+
event: ev.event,
|
|
183
|
+
attempts: ev.attempts,
|
|
184
|
+
code: outcome.error ?? "error"
|
|
185
|
+
});
|
|
186
|
+
await backOff(ev, "done", outcome.error ?? "provider_error");
|
|
187
|
+
result.retried++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (outcome.retry) rt.log.warn("tg.event_retry_exhausted", {
|
|
191
|
+
event: ev.event,
|
|
192
|
+
attempts: ev.attempts
|
|
193
|
+
});
|
|
194
|
+
await rt.db.run(sql`UPDATE tg_event SET status = ${outcome.status}, error = ${outcome.error}, processed_at = ${rt.clock.now()},
|
|
195
|
+
claim_until = NULL, claim_token = NULL ${fenced(ev.id)}`);
|
|
196
|
+
if (outcome.status === "done") result.done++;
|
|
197
|
+
else result.ignored++;
|
|
198
|
+
}
|
|
199
|
+
if (botIds.size > 0 && rt.transport.name !== "outbox") await rt.transport.kick([...botIds]);
|
|
200
|
+
rt.log.info("tg.events", { ...result });
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/** The tick stage (`tickStages()`): `runEvents` without the summary. */
|
|
204
|
+
async function processEvents(rt, now) {
|
|
205
|
+
if (!rt.features.events) return;
|
|
206
|
+
await runEvents(rt, now);
|
|
207
|
+
}
|
|
208
|
+
export { EVENT_CLAIM_MS, claimableEvents, processEvents, runEvents };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Translator {
|
|
2
|
+
/** HTML-safe text (variables escaped). */
|
|
3
|
+
t(locale: string, key: string, vars?: Record<string, unknown>): string;
|
|
4
|
+
/** Raw text (variables not escaped), for menus and plain-text fields. */
|
|
5
|
+
plain(locale: string, key: string, vars?: Record<string, unknown>): string;
|
|
6
|
+
/** First supported locale out of the candidates (exact, then language part), else the default. */
|
|
7
|
+
pick(...candidates: (string | null | undefined)[]): string;
|
|
8
|
+
/** The supported locale for a tag (exact, then language part), or null. */
|
|
9
|
+
match(candidate: string | null | undefined): string | null;
|
|
10
|
+
readonly locales: readonly string[];
|
|
11
|
+
readonly defaultLocale: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { escapeHtml } from "../core/html/escape.js";
|
|
2
|
+
import en_default from "../locales/en.js";
|
|
3
|
+
import mr_default from "../locales/mr.js";
|
|
4
|
+
// src/engine/i18n.ts
|
|
5
|
+
const BUILTIN = {
|
|
6
|
+
en: en_default,
|
|
7
|
+
mr: mr_default
|
|
8
|
+
};
|
|
9
|
+
Object.keys(BUILTIN);
|
|
10
|
+
function lookup(tree, key) {
|
|
11
|
+
let node = tree;
|
|
12
|
+
for (const part of key.split(".")) {
|
|
13
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
14
|
+
node = node[part];
|
|
15
|
+
}
|
|
16
|
+
return typeof node === "string" ? node : void 0;
|
|
17
|
+
}
|
|
18
|
+
function interpolate(text, vars, escapeVars) {
|
|
19
|
+
if (!vars) return text;
|
|
20
|
+
return text.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_, name) => {
|
|
21
|
+
const v = Object.hasOwn(vars, name) ? vars[name] : void 0;
|
|
22
|
+
const s = v === void 0 || v === null ? "" : String(v);
|
|
23
|
+
return escapeVars ? escapeHtml(s) : s;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function createTranslator(port) {
|
|
27
|
+
const locales = port.locales.length > 0 ? port.locales : ["en"];
|
|
28
|
+
const supported = new Set(locales);
|
|
29
|
+
const base = (locale, key, vars, escapeVars) => {
|
|
30
|
+
const host = port.t?.(locale, key, vars);
|
|
31
|
+
if (host !== void 0) return host;
|
|
32
|
+
return interpolate(lookup(BUILTIN[locale], key) ?? lookup(BUILTIN[locale.split("-")[0] ?? ""], key) ?? lookup(en_default, key) ?? key, vars, escapeVars);
|
|
33
|
+
};
|
|
34
|
+
const match = (c) => {
|
|
35
|
+
if (!c) return null;
|
|
36
|
+
if (supported.has(c)) return c;
|
|
37
|
+
const lang = c.split("-")[0]?.toLowerCase();
|
|
38
|
+
return lang && supported.has(lang) ? lang : null;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
locales,
|
|
42
|
+
defaultLocale: port.defaultLocale,
|
|
43
|
+
t: (locale, key, vars) => base(locale, key, vars, true),
|
|
44
|
+
plain: (locale, key, vars) => base(locale, key, vars, false),
|
|
45
|
+
match,
|
|
46
|
+
pick(...candidates) {
|
|
47
|
+
for (const c of candidates) {
|
|
48
|
+
const m = match(c);
|
|
49
|
+
if (m) return m;
|
|
50
|
+
}
|
|
51
|
+
return port.defaultLocale;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export { createTranslator };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { settle } from "../../telegram/classify.js";
|
|
2
|
+
import { withToken } from "../bot-api.js";
|
|
3
|
+
// src/engine/inbound/files.ts
|
|
4
|
+
function inboundKind(message) {
|
|
5
|
+
if (message?.photo && message.photo.length > 0) return "photo";
|
|
6
|
+
if (message?.document) return "document";
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
function mimeAllowed(allow, mime) {
|
|
10
|
+
const m = mime.toLowerCase();
|
|
11
|
+
return allow.some((a) => {
|
|
12
|
+
const p = a.toLowerCase();
|
|
13
|
+
return p.endsWith("*") ? m.startsWith(p.slice(0, -1)) : m === p;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** No path separators or control characters in stored names. */
|
|
17
|
+
function safeName(name) {
|
|
18
|
+
return Array.from(name, (ch) => ch === "/" || ch === "\\" || ch.charCodeAt(0) < 32 ? "_" : ch).join("").slice(0, 128);
|
|
19
|
+
}
|
|
20
|
+
function pick(message, maxBytes) {
|
|
21
|
+
if (message.photo && message.photo.length > 0) {
|
|
22
|
+
const fit = [...message.photo].sort((a, b) => (b.file_size ?? 0) - (a.file_size ?? 0)).find((p) => (p.file_size ?? 0) <= maxBytes);
|
|
23
|
+
if (!fit) return "too_large";
|
|
24
|
+
return {
|
|
25
|
+
kind: "photo",
|
|
26
|
+
fileId: fit.file_id,
|
|
27
|
+
name: `photo-${message.message_id}.jpg`,
|
|
28
|
+
mime: "image/jpeg",
|
|
29
|
+
size: fit.file_size ?? null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const d = message.document;
|
|
33
|
+
if (!d) return null;
|
|
34
|
+
return {
|
|
35
|
+
kind: "document",
|
|
36
|
+
fileId: d.file_id,
|
|
37
|
+
name: safeName(d.file_name ?? `file-${message.message_id}`),
|
|
38
|
+
mime: d.mime_type ?? "application/octet-stream",
|
|
39
|
+
size: d.file_size ?? null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Downloads the message's photo / document and stores it as `actor`. Never throws for user-caused problems. */
|
|
43
|
+
async function storeInbound(ctx, actor, tags) {
|
|
44
|
+
const { rt } = ctx;
|
|
45
|
+
const message = ctx.message;
|
|
46
|
+
if (!message) return {
|
|
47
|
+
ok: false,
|
|
48
|
+
reason: "unavailable"
|
|
49
|
+
};
|
|
50
|
+
const files = rt.files;
|
|
51
|
+
if (!files) return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: "unavailable"
|
|
54
|
+
};
|
|
55
|
+
const max = rt.inbound.maxBytes;
|
|
56
|
+
const p = pick(message, max);
|
|
57
|
+
if (p === null) return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: "unavailable"
|
|
60
|
+
};
|
|
61
|
+
if (p === "too_large" || p.size !== null && p.size > max) return {
|
|
62
|
+
ok: false,
|
|
63
|
+
reason: "too_large"
|
|
64
|
+
};
|
|
65
|
+
if (p.kind === "document" && !mimeAllowed(rt.inbound.mimeTypes, p.mime)) return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: "type_not_allowed"
|
|
68
|
+
};
|
|
69
|
+
const bytes = await withToken(rt, ctx.bot, async (api) => {
|
|
70
|
+
const info = await settle(api.getFile({ file_id: p.fileId }));
|
|
71
|
+
if (info.outcome.kind !== "ok" || !info.value?.file_path) return null;
|
|
72
|
+
if ((info.value.file_size ?? 0) > max) return "too_large";
|
|
73
|
+
try {
|
|
74
|
+
const res = await api.downloadFile(info.value.file_path);
|
|
75
|
+
if (!res.ok) return null;
|
|
76
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
77
|
+
return buf.byteLength > max ? "too_large" : buf;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
if (bytes === "too_large") return {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: "too_large"
|
|
85
|
+
};
|
|
86
|
+
if (bytes === null) {
|
|
87
|
+
rt.log.warn("tg.inbound_download_failed", { botId: ctx.bot.id });
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: "download_failed"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const stored = await files.put({
|
|
94
|
+
name: p.name,
|
|
95
|
+
mime: p.mime,
|
|
96
|
+
body: bytes,
|
|
97
|
+
tags: {
|
|
98
|
+
source: "telegram",
|
|
99
|
+
kind: p.kind,
|
|
100
|
+
...tags
|
|
101
|
+
}
|
|
102
|
+
}, actor);
|
|
103
|
+
rt.log.info("tg.inbound_stored", {
|
|
104
|
+
botId: ctx.bot.id,
|
|
105
|
+
kind: p.kind,
|
|
106
|
+
size: bytes.byteLength
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
file: {
|
|
111
|
+
...stored,
|
|
112
|
+
kind: p.kind,
|
|
113
|
+
caption: message.caption ?? null
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The bot string for a failed store. */
|
|
118
|
+
function storeFailureKey(reason) {
|
|
119
|
+
return `bot.inbound.${reason}`;
|
|
120
|
+
}
|
|
121
|
+
export { inboundKind, storeFailureKey, storeInbound };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { hostCan } from "../content/providers.js";
|
|
2
|
+
import { storeFailureKey, storeInbound } from "./files.js";
|
|
3
|
+
import { hostContext, senderMayAct } from "../dispatch/context.js";
|
|
4
|
+
// src/engine/inbound/handler.ts
|
|
5
|
+
async function inboundHandler(ctx) {
|
|
6
|
+
const { rt } = ctx;
|
|
7
|
+
if (ctx.cls !== "media" || !rt.features.inbound) return "next";
|
|
8
|
+
const actor = await ctx.actor();
|
|
9
|
+
if (actor === null) {
|
|
10
|
+
await ctx.reply(await ctx.t(ctx.tgChat?.type === "private" ? "bot.start.unlinked" : "bot.error.not_linked"));
|
|
11
|
+
return "handled";
|
|
12
|
+
}
|
|
13
|
+
if (!await senderMayAct(ctx)) {
|
|
14
|
+
await ctx.reply(await ctx.t("bot.error.sender_not_linked"));
|
|
15
|
+
return "handled";
|
|
16
|
+
}
|
|
17
|
+
if (!await hostCan(rt, actor, rt.inbound.permission)) {
|
|
18
|
+
await ctx.reply(await ctx.t("bot.error.forbidden"));
|
|
19
|
+
return "handled";
|
|
20
|
+
}
|
|
21
|
+
const onInbound = rt.hooks.onInbound;
|
|
22
|
+
if (!onInbound) {
|
|
23
|
+
await ctx.reply(await ctx.t("bot.inbound.not_handled"));
|
|
24
|
+
return "handled";
|
|
25
|
+
}
|
|
26
|
+
const stored = await storeInbound(ctx, actor, { source_kind: "inbound" });
|
|
27
|
+
if (!stored.ok) {
|
|
28
|
+
await ctx.reply(await ctx.t(storeFailureKey(stored.reason)));
|
|
29
|
+
return "handled";
|
|
30
|
+
}
|
|
31
|
+
const host = await hostContext(ctx, stored.file.caption ?? "");
|
|
32
|
+
let handled = true;
|
|
33
|
+
try {
|
|
34
|
+
handled = await onInbound(host, stored.file) !== false;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
rt.log.warn("tg.hook_error", {
|
|
37
|
+
hook: "onInbound",
|
|
38
|
+
error: e instanceof Error ? e.name : "unknown"
|
|
39
|
+
});
|
|
40
|
+
await ctx.reply(await ctx.t("bot.error.generic"));
|
|
41
|
+
return "handled";
|
|
42
|
+
}
|
|
43
|
+
if (!handled) await ctx.reply(await ctx.t("bot.inbound.not_handled"));
|
|
44
|
+
return "handled";
|
|
45
|
+
}
|
|
46
|
+
export { inboundHandler };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** What a plugin job's `run` receives from the host's cron dispatcher. */
|
|
2
|
+
export interface TelegramJobContext<E> {
|
|
3
|
+
/** The Worker env of the scheduled invocation. */
|
|
4
|
+
env: E;
|
|
5
|
+
/** Scheduled time (ms or Date); default now. */
|
|
6
|
+
now?: number | Date;
|
|
7
|
+
/** `ctx.waitUntil` of the scheduled event, for hook and kick work. */
|
|
8
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
9
|
+
/** The cron expression that fired (informational). */
|
|
10
|
+
cron?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Outcome of one job run (for the host's job log). */
|
|
13
|
+
export interface TelegramJobResult {
|
|
14
|
+
/** `ok` = did work, `noop` = nothing to do, `skipped` = not run (for example `schema_outdated`). */
|
|
15
|
+
status: "ok" | "noop" | "skipped";
|
|
16
|
+
/** Short summary without personal data (`sent 3`, `deleted 12…`). */
|
|
17
|
+
detail?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A plugin cron job (`telegram:tick`, `telegram:cleanup`, `telegram:rekey`), structural so it fits a host `Job`
|
|
21
|
+
* registry. Every job checks the schema version first and skips when it is outdated.
|
|
22
|
+
*/
|
|
23
|
+
export interface TelegramJob<E> {
|
|
24
|
+
/** Job name (`telegram:tick`, …). */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Cron expression (`options.crons`); the Worker's `triggers.crons` must include it. */
|
|
27
|
+
readonly cron: string;
|
|
28
|
+
/** One-line description for admin job lists. */
|
|
29
|
+
readonly description: string;
|
|
30
|
+
/** Runs the job: D1 reads/writes and, for the tick, Telegram sends. */
|
|
31
|
+
run(ctx: TelegramJobContext<E>): Promise<TelegramJobResult>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { STATE_KEYS, ensureState, setState } from "./state.js";
|
|
2
|
+
import { sweepStuckUpdates } from "./dispatch/dispatcher.js";
|
|
3
|
+
import { processEvents } from "./events/tick.js";
|
|
4
|
+
import { drainBots } from "./outbox/transport.js";
|
|
5
|
+
import { cleanup } from "../services/cleanup.js";
|
|
6
|
+
import { rekeyBots } from "../services/rekey.js";
|
|
7
|
+
import { resumeStuckRuns } from "./schedules/resume.js";
|
|
8
|
+
import { fireDueSchedules } from "./schedules/tick.js";
|
|
9
|
+
import { armScheduleWake } from "./schedules/wake.js";
|
|
10
|
+
// src/engine/jobs.ts
|
|
11
|
+
/**
|
|
12
|
+
* Schedules stage: fire what is due (under the DO transport re-arm the scheduler bot after a fire), then resume
|
|
13
|
+
* runs cut off mid fan-out (`schedules/resume.ts`).
|
|
14
|
+
*/
|
|
15
|
+
async function scheduleStage(rt, now) {
|
|
16
|
+
if (await fireDueSchedules(rt, now) > 0) await armScheduleWake(rt);
|
|
17
|
+
await resumeStuckRuns(rt, now);
|
|
18
|
+
}
|
|
19
|
+
function tickStages() {
|
|
20
|
+
return [scheduleStage, processEvents];
|
|
21
|
+
}
|
|
22
|
+
const DEFAULT_CRONS = {
|
|
23
|
+
tick: "* * * * *",
|
|
24
|
+
cleanup: "15 3 * * *",
|
|
25
|
+
rekey: "45 3 * * *"
|
|
26
|
+
};
|
|
27
|
+
async function runTick(rt) {
|
|
28
|
+
const started = rt.clock.now();
|
|
29
|
+
const schema = await ensureState(rt);
|
|
30
|
+
if (!schema.ok) {
|
|
31
|
+
rt.log.error("tg.schema_outdated", {
|
|
32
|
+
required: schema.required,
|
|
33
|
+
current: schema.current
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
status: "skipped",
|
|
37
|
+
detail: "schema_outdated"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
await setState(rt, STATE_KEYS.lastTickAt, String(started), started);
|
|
41
|
+
const swept = await sweepStuckUpdates(rt, started);
|
|
42
|
+
for (const stage of tickStages()) try {
|
|
43
|
+
await stage(rt, rt.clock.now());
|
|
44
|
+
} catch (e) {
|
|
45
|
+
rt.log.error("tg.tick_stage_failed", { error: e instanceof Error ? e.name : "error" });
|
|
46
|
+
}
|
|
47
|
+
let sent = 0;
|
|
48
|
+
if (rt.transport.name === "queue" || rt.transport.name === "do") await rt.transport.kick();
|
|
49
|
+
else {
|
|
50
|
+
const left = rt.limits.tickBudgetMs - (rt.clock.now() - started);
|
|
51
|
+
for (const r of await drainBots(rt, void 0, left)) sent += r.sent;
|
|
52
|
+
}
|
|
53
|
+
rt.log.info("tg.tick", {
|
|
54
|
+
swept,
|
|
55
|
+
sent,
|
|
56
|
+
ms: rt.clock.now() - started
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
status: sent > 0 || swept > 0 ? "ok" : "noop",
|
|
60
|
+
detail: `sent ${sent}`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function runCleanup(rt) {
|
|
64
|
+
if (!(await ensureState(rt)).ok) return {
|
|
65
|
+
status: "skipped",
|
|
66
|
+
detail: "schema_outdated"
|
|
67
|
+
};
|
|
68
|
+
const r = await cleanup(rt);
|
|
69
|
+
return {
|
|
70
|
+
status: "ok",
|
|
71
|
+
detail: `deleted ${Object.values(r.deleted).reduce((a, b) => a + b, 0)}, bodies ${r.bodiesCleared}, bots ${r.botsChecked}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function runRekey(rt) {
|
|
75
|
+
if (!(await ensureState(rt)).ok) return {
|
|
76
|
+
status: "skipped",
|
|
77
|
+
detail: "schema_outdated"
|
|
78
|
+
};
|
|
79
|
+
const r = await rekeyBots(rt);
|
|
80
|
+
return {
|
|
81
|
+
status: r.rekeyed > 0 ? "ok" : "noop",
|
|
82
|
+
detail: `rekeyed ${r.rekeyed}, remaining ${r.remaining}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export { DEFAULT_CRONS, runCleanup, runRekey, runTick, tickStages };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomCode, sha256Hex } from "../core/random.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// src/engine/link-codes.ts
|
|
4
|
+
function newLinkCode(rand) {
|
|
5
|
+
return randomCode(rand, 32);
|
|
6
|
+
}
|
|
7
|
+
function hashLinkCode(code) {
|
|
8
|
+
return sha256Hex(crypto.subtle, `tg-link:${code}`);
|
|
9
|
+
}
|
|
10
|
+
/** `tg_state` key prefix of pending invites. */
|
|
11
|
+
const PENDING_LINK_PREFIX = "link_pending:";
|
|
12
|
+
/** How long a redeemed invite waits for the target user's confirmation. */
|
|
13
|
+
const PENDING_LINK_TTL_MS = 864e5;
|
|
14
|
+
/** `tg_state` key of one pending invite. */
|
|
15
|
+
function pendingLinkKey(id) {
|
|
16
|
+
return `${PENDING_LINK_PREFIX}${id}`;
|
|
17
|
+
}
|
|
18
|
+
/** Stored value of a pending invite (`tg_state.value`, JSON). */
|
|
19
|
+
const PendingLinkState = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
botId: z.string(),
|
|
22
|
+
/** Telegram chat id of the private chat that redeemed the code. */
|
|
23
|
+
chatId: z.string(),
|
|
24
|
+
/** `tg_chat.id` to use (the existing row's id, or a fresh one). */
|
|
25
|
+
chatRef: z.string(),
|
|
26
|
+
userId: z.string(),
|
|
27
|
+
createdBy: z.string().nullable(),
|
|
28
|
+
tgName: z.string().nullable(),
|
|
29
|
+
tgUsername: z.string().nullable(),
|
|
30
|
+
locale: z.string().nullable(),
|
|
31
|
+
createdAt: z.number(),
|
|
32
|
+
expiresAt: z.number()
|
|
33
|
+
});
|
|
34
|
+
/** API view of a pending invite (`GET /me/links/pending`, `hooks.onPendingLink`). */
|
|
35
|
+
function pendingLinkDto(p) {
|
|
36
|
+
return {
|
|
37
|
+
id: p.id,
|
|
38
|
+
bot: p.botId,
|
|
39
|
+
telegramName: p.tgName,
|
|
40
|
+
telegramUsername: p.tgUsername,
|
|
41
|
+
invitedBy: p.createdBy,
|
|
42
|
+
createdAt: p.createdAt,
|
|
43
|
+
expiresAt: p.expiresAt
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export { PENDING_LINK_PREFIX, PENDING_LINK_TTL_MS, PendingLinkState, hashLinkCode, newLinkCode, pendingLinkDto, pendingLinkKey };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/engine/metrics.ts
|
|
2
|
+
function metricCount(rt, name, n = 1, tags = {}) {
|
|
3
|
+
try {
|
|
4
|
+
rt.metrics?.count(name, n, tags);
|
|
5
|
+
} catch {}
|
|
6
|
+
}
|
|
7
|
+
function metricTiming(rt, name, ms, tags = {}) {
|
|
8
|
+
try {
|
|
9
|
+
rt.metrics?.timing(name, ms, tags);
|
|
10
|
+
} catch {}
|
|
11
|
+
}
|
|
12
|
+
export { metricCount, metricTiming };
|