@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,369 @@
|
|
|
1
|
+
import { BotDto } from "../contracts/bots.js";
|
|
2
|
+
import { ChatDto } from "../contracts/chats.js";
|
|
3
|
+
import { DeliveryDto } from "../contracts/deliveries.js";
|
|
4
|
+
import { FeatureKey } from "../contracts/features.js";
|
|
5
|
+
import { PendingLinkDto } from "../contracts/links.js";
|
|
6
|
+
import { FilterRegistry } from "../core/template/filters.js";
|
|
7
|
+
import { DbPort } from "../ports/db.js";
|
|
8
|
+
import { ActorPort, AuditPort, AuthorizePort, ClockPort, ErrorsPort, FilesPort, I18nPort, IdsPort, KeyringPort, LogPort, MetricsPort, PortOrFactory, RendererPort, SettingsPort, StoredFile, TransportPort, UsersPort } from "../ports/ports.js";
|
|
9
|
+
import { TelegramClient, TelegramClientOptions } from "../telegram/client.js";
|
|
10
|
+
import { ActionDef, AudienceDef, CommandContext, CommandDef, DialogDef, EventDef, FileSourceDef, ProviderDef } from "./registry.js";
|
|
11
|
+
import { Runtime } from "./runtime.js";
|
|
12
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
13
|
+
/** What the `env` mapper returns (evaluated per invocation; never read globals). */
|
|
14
|
+
export interface TelegramEnv {
|
|
15
|
+
/** D1 binding (or a D1 session), or any `DbPort`. */
|
|
16
|
+
db: D1Database | D1DatabaseSession | DbPort;
|
|
17
|
+
/** `TELEGRAM_KEYRING` secret JSON (`{ active, keys }`), or a `KeyringPort`. */
|
|
18
|
+
keyring: string | {
|
|
19
|
+
active: string;
|
|
20
|
+
keys: Record<string, string>;
|
|
21
|
+
} | KeyringPort;
|
|
22
|
+
/** Optional Queue producer → queue transport (kick messages, retries). */
|
|
23
|
+
queue?: Queue<unknown> | undefined;
|
|
24
|
+
/** Optional TelegramBotDO namespace → DO transport (strict per-bot sender) and DO scheduler wakes. */
|
|
25
|
+
botDO?: DurableObjectNamespace | undefined;
|
|
26
|
+
/** Optional Browser Rendering binding (PDF renderer: `browserRunPdf` is used when no pdf renderer is given). */
|
|
27
|
+
browser?: unknown;
|
|
28
|
+
/** Optional R2 bucket for report files when the host has no `ports.files` (`r2Files` adapter). */
|
|
29
|
+
files?: R2Bucket | undefined;
|
|
30
|
+
/** Public URL where the plugin routes are mounted, for `setWebhook` (`https://app.example.com/api/telegram`). */
|
|
31
|
+
publicBaseUrl?: string | undefined;
|
|
32
|
+
/** `production` disables the dev polling route. Default `production`. */
|
|
33
|
+
appEnv?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Log level for the default `consoleLog`, or `silent` to drop everything. */
|
|
36
|
+
export type TelegramLogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
|
37
|
+
/**
|
|
38
|
+
* The host's services (docs/04 §9). `actor` and `authorize` are required; everything else has a default.
|
|
39
|
+
* `E` is the host's Worker env type (inferred from `options.env`).
|
|
40
|
+
*/
|
|
41
|
+
export interface TelegramPorts<A, E = unknown> {
|
|
42
|
+
/** Who is calling (admin API) and who a linked Telegram user is. Required. */
|
|
43
|
+
actor: ActorPort<A>;
|
|
44
|
+
/** Permission check for plugin permissions (`telegram.*`, with `permissions.prefix`) and host data permissions. Required. */
|
|
45
|
+
authorize: AuthorizePort<A>;
|
|
46
|
+
/** User lookup for admin linking (`find`), the user picker (`search` → `GET /admin/users`) and `{ role }` recipients (`byRole`). */
|
|
47
|
+
users?: UsersPort;
|
|
48
|
+
/**
|
|
49
|
+
* File storage for inbound media, dialog uploads and report files. A port, or `(env) => port` for per-request
|
|
50
|
+
* bindings. Default: `r2Files(env.files)` when the env mapper returns `files`, else none (file features answer
|
|
51
|
+
* `renderer_unavailable` / "not handled").
|
|
52
|
+
*/
|
|
53
|
+
files?: PortOrFactory<FilesPort<A>, E>;
|
|
54
|
+
/** SQL for audit rows in the same batch as each admin write. Default: insert into `tg_audit`. */
|
|
55
|
+
audit?: AuditPort;
|
|
56
|
+
/**
|
|
57
|
+
* Maps plugin problems to the host's error type for the admin/me routes (gets the full problem body). Default:
|
|
58
|
+
* the routes answer `application/problem+json` themselves. Not used by the programmatic API, which throws
|
|
59
|
+
* `TelegramPluginError` (map it yourself with `isTelegramPluginError`).
|
|
60
|
+
*/
|
|
61
|
+
errors?: ErrorsPort;
|
|
62
|
+
/**
|
|
63
|
+
* Structured log: a `LogPort`, or a level for the built-in JSON-lines `consoleLog` (with token redaction).
|
|
64
|
+
* Default `"info"`. Use `"silent"` to drop everything.
|
|
65
|
+
*/
|
|
66
|
+
log?: LogPort | TelegramLogLevel;
|
|
67
|
+
/** Time source. Default `Date.now` + `setTimeout`. */
|
|
68
|
+
clock?: ClockPort;
|
|
69
|
+
/** Id generator. Default UUIDv7 on the clock. */
|
|
70
|
+
ids?: IdsPort;
|
|
71
|
+
/** Locales and optional host translations. Default `{ locales: ["en", "mr"], defaultLocale: "en" }`. */
|
|
72
|
+
i18n?: I18nPort;
|
|
73
|
+
/** Default time zone and money formatting. Default `{ timezone: "UTC" }`. */
|
|
74
|
+
settings?: SettingsPort;
|
|
75
|
+
/** Counters and timings: a port, or `(env) => port` (for example an Analytics Engine binding). Default none. */
|
|
76
|
+
metrics?: PortOrFactory<MetricsPort, E>;
|
|
77
|
+
/** Extra report renderers (receive the invocation's env in `render`). Built-in: `csv`, and `pdf` over `env.browser`. */
|
|
78
|
+
renderers?: readonly RendererPort<E>[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Feature switches (docs/02 §6). Anything not listed is off, except `core` (always on) and `ops` (on by default).
|
|
82
|
+
* Routes of a disabled feature answer 404 `feature_disabled`. A bot can switch a feature off for itself
|
|
83
|
+
* (`tg_bot.features_json`) but never on when it is off here.
|
|
84
|
+
*/
|
|
85
|
+
export interface TelegramFeaturesInput {
|
|
86
|
+
/** Host bot commands (`defineCommand`) and the per-bot command admin. Default off. */
|
|
87
|
+
commands?: boolean;
|
|
88
|
+
/** Message templates (`tg_template`) with versions, validation and previews. Default off. */
|
|
89
|
+
templates?: boolean;
|
|
90
|
+
/** Recurring schedules (`tg_schedule`) fired by the tick or DO alarms. Default off. */
|
|
91
|
+
schedules?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Report templates rendered to files and `/report`. `{ renderers }` limits the formats listed in meta (default:
|
|
94
|
+
* `csv` plus every `ports.renderers` format). Default off.
|
|
95
|
+
*/
|
|
96
|
+
reports?: boolean | {
|
|
97
|
+
renderers?: string[];
|
|
98
|
+
};
|
|
99
|
+
/** Host events (`defineEvent`, `plugin.emit()`) routed by event rules. Default off. */
|
|
100
|
+
events?: boolean;
|
|
101
|
+
/** One-off broadcasts to many recipients, with preview and progress. Default off. */
|
|
102
|
+
broadcasts?: boolean;
|
|
103
|
+
/** Inline-button actions (`defineAction`). Default off. */
|
|
104
|
+
actions?: boolean;
|
|
105
|
+
/** Multi-step dialogs (`defineDialog`). Default off. */
|
|
106
|
+
dialogs?: boolean;
|
|
107
|
+
/** Photos / documents sent outside dialogs, stored via the files port and passed to `hooks.onInbound`. Default off. */
|
|
108
|
+
inbound?: boolean;
|
|
109
|
+
/** Per-chat subscriptions (mute, quiet hours) applied after recipients resolve. Default off. */
|
|
110
|
+
subscriptions?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* AI bridge. `true` = send intents only (`sendIntent` on, `freeText` off). In object form `sendIntent` defaults
|
|
113
|
+
* to true (`aiToolSpec`, AI `send`/`enqueue`) and `freeText` to false (`hooks.onFreeText`). Default off.
|
|
114
|
+
*/
|
|
115
|
+
ai?: boolean | {
|
|
116
|
+
sendIntent?: boolean;
|
|
117
|
+
freeText?: boolean;
|
|
118
|
+
};
|
|
119
|
+
/** Ops admin routes (`/admin/health`, `/admin/stats`, `/admin/deliveries`). Default on. */
|
|
120
|
+
ops?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/** Pacing, retry and budget limits (`options.limits`; defaults in `DEFAULT_LIMITS`). */
|
|
123
|
+
export interface TelegramLimits {
|
|
124
|
+
/** Per-bot send rate (Telegram allows ~30/s). Default 25. */
|
|
125
|
+
perBotPerSec: number;
|
|
126
|
+
/** Per-chat interval between messages. Default 1100 ms. */
|
|
127
|
+
perChatIntervalMs: number;
|
|
128
|
+
/** Groups / channels per minute. Default 18. */
|
|
129
|
+
perGroupPerMin: number;
|
|
130
|
+
/** Send attempts before a delivery is `failed`. Default 8. */
|
|
131
|
+
maxAttempts: number;
|
|
132
|
+
/** Time budget for one drain / tick. Default 45 s. */
|
|
133
|
+
tickBudgetMs: number;
|
|
134
|
+
/** Inbound updates per chat per minute before the bot goes quiet. Default 20. */
|
|
135
|
+
inboundPerMin: number;
|
|
136
|
+
/** One-time link code lifetime. Default 15 min. */
|
|
137
|
+
linkCodeTtlMs: number;
|
|
138
|
+
/** Deliveries claimed per drain slice. Default 25. */
|
|
139
|
+
drainSlice: number;
|
|
140
|
+
/** Parallel sends inside a slice (different chats). Default 4. */
|
|
141
|
+
drainConcurrency: number;
|
|
142
|
+
/** Bot drain lease. Default 70 s. */
|
|
143
|
+
drainLeaseMs: number;
|
|
144
|
+
/** Workers plan. `free` caps sends per tick at 40. */
|
|
145
|
+
plan: "paid" | "free";
|
|
146
|
+
/** Time budget for one provider run (templates, reports, commands). Default 10 s. */
|
|
147
|
+
providerTimeoutMs: number;
|
|
148
|
+
/** Schedules claimed per tick. Default 20. */
|
|
149
|
+
scheduleClaim: number;
|
|
150
|
+
/** Events claimed per tick. Default 50. */
|
|
151
|
+
eventClaim: number;
|
|
152
|
+
}
|
|
153
|
+
/** Default `TelegramLimits` (paid plan, Telegram's documented rate limits with headroom). */
|
|
154
|
+
export declare const DEFAULT_LIMITS: TelegramLimits;
|
|
155
|
+
/** Retention for `telegram:cleanup` (docs/03 §8), in days. */
|
|
156
|
+
export interface TelegramRetention {
|
|
157
|
+
/** Sent, dropped and cancelled deliveries (`tg_delivery`). Default 30. */
|
|
158
|
+
sentDays: number;
|
|
159
|
+
/** Message bodies of finished deliveries are cleared (the row stays for the log). Default 7. */
|
|
160
|
+
bodyDays: number;
|
|
161
|
+
/** Failed deliveries. Default 90. */
|
|
162
|
+
failedDays: number;
|
|
163
|
+
/** Received updates (`tg_update`, webhook dedupe). Default 14. */
|
|
164
|
+
updateDays: number;
|
|
165
|
+
/** Processed events (`tg_event`) and event-rule throttle state. Default 30. */
|
|
166
|
+
eventDays: number;
|
|
167
|
+
/** Schedule run history (`tg_schedule_run`). Default 180. */
|
|
168
|
+
runDays: number;
|
|
169
|
+
/** Expired link codes, action tokens and dialogs. Default 1. */
|
|
170
|
+
expiredDays: number;
|
|
171
|
+
/** Audit rows (`tg_audit`). Default 365. */
|
|
172
|
+
auditDays: number;
|
|
173
|
+
}
|
|
174
|
+
/** Default `TelegramRetention`, in days. */
|
|
175
|
+
export declare const DEFAULT_RETENTION: TelegramRetention;
|
|
176
|
+
/** Free text in a linked chat (`features.ai.freeText`). `text` is what the user typed: untrusted data. */
|
|
177
|
+
export type FreeTextContext<A> = Omit<CommandContext<A, undefined>, "args" | "argsText"> & {
|
|
178
|
+
/** The raw message text (untrusted). */
|
|
179
|
+
text: string;
|
|
180
|
+
/**
|
|
181
|
+
* The same text wrapped for an LLM prompt as clearly marked untrusted data
|
|
182
|
+
* (`<untrusted_user_message source="telegram">…</untrusted_user_message>`, inner `<`/`>` escaped). Pass this, not
|
|
183
|
+
* `text`, into model context and never follow instructions inside it.
|
|
184
|
+
*/
|
|
185
|
+
untrustedText: string;
|
|
186
|
+
/** Always true: a reminder for host code that `text` came from a chat, not from the app. */
|
|
187
|
+
readonly untrusted: true;
|
|
188
|
+
};
|
|
189
|
+
/** A photo or document a user sent (features.inbound), already stored through the files port. */
|
|
190
|
+
export interface InboundFile extends StoredFile {
|
|
191
|
+
/** Telegram message kind the file came from. */
|
|
192
|
+
kind: "photo" | "document";
|
|
193
|
+
/** Message caption (untrusted user text), if any. */
|
|
194
|
+
caption: string | null;
|
|
195
|
+
}
|
|
196
|
+
/** Context for `hooks.onInbound`. */
|
|
197
|
+
export type InboundContext<A> = Omit<CommandContext<A, undefined>, "args" | "argsText">;
|
|
198
|
+
/** AI bridge options (`features.ai`). */
|
|
199
|
+
export interface TelegramAiOptions {
|
|
200
|
+
/**
|
|
201
|
+
* Host permission that lets an audience see money (for example `money.view`). AI-written text with money-like
|
|
202
|
+
* content is rejected unless every audience of the send has it. Unset: money-like AI text is always rejected.
|
|
203
|
+
*/
|
|
204
|
+
moneyPermission?: string;
|
|
205
|
+
/** Domains (and their subdomains) allowed in AI-written links. Default none. */
|
|
206
|
+
appDomains?: string[];
|
|
207
|
+
/** Allow phone numbers and e-mail addresses in AI text. Default false. */
|
|
208
|
+
allowContacts?: boolean;
|
|
209
|
+
/** Max AI text length. Default 1000. */
|
|
210
|
+
maxTextLength?: number;
|
|
211
|
+
}
|
|
212
|
+
/** Inbound media options (`features.inbound` and dialog media steps). */
|
|
213
|
+
export interface TelegramInboundOptions {
|
|
214
|
+
/** Largest accepted file. Default 20 MB (the Bot API `getFile` download limit). */
|
|
215
|
+
maxBytes?: number;
|
|
216
|
+
/**
|
|
217
|
+
* Allowed MIME types for documents (`image/*` style wildcards allowed). Default: images, PDF, plain text, CSV and
|
|
218
|
+
* Office documents. Photos are always `image/jpeg`.
|
|
219
|
+
*/
|
|
220
|
+
mimeTypes?: string[];
|
|
221
|
+
/** Host permission the chat's actor needs to send files outside dialogs. Default none (any linked chat). */
|
|
222
|
+
permission?: string;
|
|
223
|
+
}
|
|
224
|
+
export interface ResolvedAiOptions {
|
|
225
|
+
readonly moneyPermission: string | null;
|
|
226
|
+
readonly appDomains: readonly string[];
|
|
227
|
+
readonly allowContacts: boolean;
|
|
228
|
+
readonly maxTextLength: number;
|
|
229
|
+
}
|
|
230
|
+
export interface ResolvedInboundOptions {
|
|
231
|
+
readonly maxBytes: number;
|
|
232
|
+
readonly mimeTypes: readonly string[];
|
|
233
|
+
readonly permission: string | null;
|
|
234
|
+
}
|
|
235
|
+
/** Group / supergroup rules (`options.groups`). */
|
|
236
|
+
export interface TelegramGroupOptions {
|
|
237
|
+
/**
|
|
238
|
+
* A linked group acts as its audience (data visibility = the audience). With `true` (default), host commands,
|
|
239
|
+
* `/report`, inbound files and AI free text in a group also need the SENDER to have linked their own Telegram on
|
|
240
|
+
* this bot (like action taps and dialogs); unlinked members get a "link your account first" reply. `false` lets
|
|
241
|
+
* every member of a linked group act as the group's audience. Commands can override this with `groupMembers`.
|
|
242
|
+
*/
|
|
243
|
+
requireLinkedSender?: boolean;
|
|
244
|
+
}
|
|
245
|
+
/** `options.groups` after defaults. */
|
|
246
|
+
export interface ResolvedGroupOptions {
|
|
247
|
+
readonly requireLinkedSender: boolean;
|
|
248
|
+
}
|
|
249
|
+
/** Optional hooks (docs/04 §10). They run in the plugin's waitUntil budget; errors are logged, never thrown. */
|
|
250
|
+
export interface TelegramHooks<A> {
|
|
251
|
+
/** A chat was linked to an app user (link code, deep link or admin link). */
|
|
252
|
+
onLinked?(chat: ChatDto, actor: A): MaybePromise<void>;
|
|
253
|
+
/**
|
|
254
|
+
* An admin invite for `userId` was used in Telegram and now waits for that user's confirmation in the app
|
|
255
|
+
* (`/me/links/pending`). Show an in-app notice; if the user already has a linked chat on this bot, the plugin
|
|
256
|
+
* also tells them there.
|
|
257
|
+
*/
|
|
258
|
+
onPendingLink?(pending: PendingLinkDto, userId: string): MaybePromise<void>;
|
|
259
|
+
/** A chat was unlinked or left (`/unlink`, admin unlink, or the bot was removed from the group). */
|
|
260
|
+
onUnlinked?(chat: ChatDto): MaybePromise<void>;
|
|
261
|
+
/** The user blocked the bot (Telegram 403 or `my_chat_member`); queued sends to it are cancelled. */
|
|
262
|
+
onBlocked?(chat: ChatDto): MaybePromise<void>;
|
|
263
|
+
/** A delivery reached `failed` (permanent error or `limits.maxAttempts` used up). */
|
|
264
|
+
onDeliveryFailed?(delivery: DeliveryDto): MaybePromise<void>;
|
|
265
|
+
/** A bot's status changed (for example the token was revoked). */
|
|
266
|
+
onBotStatus?(bot: BotDto): MaybePromise<void>;
|
|
267
|
+
/** Last chance to cancel a send: resolve to `false`. Any other value lets it go. */
|
|
268
|
+
beforeSend?(delivery: DeliveryDto): MaybePromise<unknown>;
|
|
269
|
+
/** Free text in a linked chat (needs `features.ai.freeText`); reply through `ctx.reply`. */
|
|
270
|
+
onFreeText?(ctx: FreeTextContext<A>): MaybePromise<void>;
|
|
271
|
+
/** A `/command` no built-in or host command handles (`text` is the full message). */
|
|
272
|
+
onUnknownCommand?(ctx: FreeTextContext<A>): MaybePromise<void>;
|
|
273
|
+
/**
|
|
274
|
+
* A photo or document outside a dialog (features.inbound), stored through the files port. Resolve to `false`
|
|
275
|
+
* when the host did not handle it (the user gets a polite "not handled" reply); anything else counts as handled.
|
|
276
|
+
*/
|
|
277
|
+
onInbound?(ctx: InboundContext<A>, file: InboundFile): MaybePromise<unknown>;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* How queued deliveries are drained: `auto` (default: `do` when `env.botDO` is set, else `queue` when `env.queue` is
|
|
281
|
+
* set, else `cron`), a fixed choice, or a factory that builds a custom `TransportPort` from the invocation runtime.
|
|
282
|
+
*/
|
|
283
|
+
export type TransportChoice<A = unknown> = "auto" | "cron" | "queue" | "do" | ((rt: Runtime<A>) => TransportPort);
|
|
284
|
+
/** Cron expressions (UTC, croner syntax) of the plugin's jobs; must match the Worker's `triggers.crons`. */
|
|
285
|
+
export interface TelegramCrons {
|
|
286
|
+
/** Default every minute. */
|
|
287
|
+
tick: string;
|
|
288
|
+
/** Default 03:15 UTC daily. */
|
|
289
|
+
cleanup: string;
|
|
290
|
+
/** Default 03:45 UTC daily. */
|
|
291
|
+
rekey: string;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Options for `createTelegramPlugin<E, A>()`. `E` is the host's Worker env type, `A` the host's actor type.
|
|
295
|
+
* Only `env` and `ports` (`actor`, `authorize`) are required; every other option is off or defaulted as noted.
|
|
296
|
+
* Options are validated once at construction (`TelegramConfigError`).
|
|
297
|
+
*/
|
|
298
|
+
export interface TelegramPluginOptions<E, A> {
|
|
299
|
+
/** Maps the Worker env to what the plugin needs (D1, keyring, optional Queue/DO/R2/Browser), per invocation. Required. */
|
|
300
|
+
env: (env: E) => TelegramEnv;
|
|
301
|
+
/** The host's services. `actor` and `authorize` are required. */
|
|
302
|
+
ports: TelegramPorts<A, E>;
|
|
303
|
+
/** Feature switches. Default: only `core` (linking, /start, /help) and `ops`. */
|
|
304
|
+
features?: TelegramFeaturesInput;
|
|
305
|
+
/** Data providers (`defineProvider`) for templates, reports, commands and events. Default none. */
|
|
306
|
+
providers?: readonly ProviderDef<unknown, unknown, A>[];
|
|
307
|
+
/** Host bot commands (`defineCommand`, features.commands). Default none. */
|
|
308
|
+
commands?: readonly CommandDef<unknown, A>[];
|
|
309
|
+
/** Host events (`defineEvent`, features.events) emitted with `emit()`. Default none. */
|
|
310
|
+
events?: readonly EventDef<unknown>[];
|
|
311
|
+
/** Inline-button actions (`defineAction`, features.actions). Default none. */
|
|
312
|
+
actions?: readonly ActionDef<unknown, A>[];
|
|
313
|
+
/** Multi-step dialogs (`defineDialog`, features.dialogs). Default none. */
|
|
314
|
+
dialogs?: readonly DialogDef<A>[];
|
|
315
|
+
/** Recipient kinds beyond users/roles/chats (`defineAudience`). Default none. */
|
|
316
|
+
audiences?: readonly AudienceDef[];
|
|
317
|
+
/** Host file sources (`defineFileSource`) for file sends; `resolve` gets the invocation env. Default none. */
|
|
318
|
+
fileSources?: readonly FileSourceDef<A, E>[];
|
|
319
|
+
/**
|
|
320
|
+
* Custom template filters by name (`{{ amount | lakh }}`), built with `defineFilter` or a plain function. Used
|
|
321
|
+
* when rendering, validating and previewing templates and listed in `GET /admin/meta`. Names match
|
|
322
|
+
* `[a-z][a-zA-Z0-9_]{0,31}` and may not shadow a built-in filter. Default none.
|
|
323
|
+
*/
|
|
324
|
+
filters?: FilterRegistry;
|
|
325
|
+
/** Pacing, retries and budgets. Defaults in `DEFAULT_LIMITS`. */
|
|
326
|
+
limits?: Partial<TelegramLimits>;
|
|
327
|
+
/** Cleanup retention in days. Defaults in `DEFAULT_RETENTION`. */
|
|
328
|
+
retention?: Partial<TelegramRetention>;
|
|
329
|
+
/** Plugin permission names: `prefix` replaces `telegram` (`ops.bots.manage`). Default `telegram`. */
|
|
330
|
+
permissions?: {
|
|
331
|
+
prefix?: string;
|
|
332
|
+
};
|
|
333
|
+
/** Lifecycle hooks (errors are logged, never thrown). Default none. */
|
|
334
|
+
hooks?: TelegramHooks<A>;
|
|
335
|
+
/** Bot API client: a `TelegramClient` (for example over `FakeTelegram`) or options for `fetchTelegram`. Default `fetchTelegram()`. */
|
|
336
|
+
telegram?: TelegramClient | TelegramClientOptions;
|
|
337
|
+
/**
|
|
338
|
+
* Drain transport. `auto` (default): `do` when `env.botDO` is set, else `queue` when `env.queue` is set, else
|
|
339
|
+
* `cron` (the tick drains). `queue` / `do` throw a config error when their binding is missing.
|
|
340
|
+
*/
|
|
341
|
+
transport?: TransportChoice<A>;
|
|
342
|
+
/**
|
|
343
|
+
* Name of the dead-letter queue whose consumer is this Worker. Plugin messages arriving from it are logged and
|
|
344
|
+
* their rows marked failed (`error_code = 'dead_letter'`) instead of being retried. Default none.
|
|
345
|
+
*/
|
|
346
|
+
deadLetterQueue?: string;
|
|
347
|
+
/** Cron expressions of the three jobs (match them in `wrangler.jsonc` `triggers.crons`). Defaults in `TelegramCrons`. */
|
|
348
|
+
crons?: Partial<TelegramCrons>;
|
|
349
|
+
/** Tenant column value (docs/10 D-12), ≤ 64 chars. Default `''`. */
|
|
350
|
+
tenant?: string;
|
|
351
|
+
/** Allow one app user to link more than one Telegram account per bot. Default false (relinking replaces). */
|
|
352
|
+
allowMultipleChatsPerUser?: boolean;
|
|
353
|
+
/** AI bridge rules (features.ai). */
|
|
354
|
+
ai?: TelegramAiOptions;
|
|
355
|
+
/** Inbound media limits (features.inbound and dialog media steps). */
|
|
356
|
+
inbound?: TelegramInboundOptions;
|
|
357
|
+
/** Rules for group and supergroup chats. */
|
|
358
|
+
groups?: TelegramGroupOptions;
|
|
359
|
+
}
|
|
360
|
+
/** Feature flags after defaults. */
|
|
361
|
+
export type FeatureFlags = Readonly<Record<FeatureKey, boolean>> & {
|
|
362
|
+
/** `features.ai.freeText` resolved (false unless `ai` is on). */
|
|
363
|
+
readonly aiFreeText: boolean;
|
|
364
|
+
/** `features.ai.sendIntent` resolved (false unless `ai` is on). */
|
|
365
|
+
readonly aiSendIntent: boolean;
|
|
366
|
+
/** `features.reports.renderers`; empty = all available renderers. */
|
|
367
|
+
readonly reportRenderers: readonly string[];
|
|
368
|
+
};
|
|
369
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/engine/options.ts
|
|
2
|
+
/** Default `TelegramLimits` (paid plan, Telegram's documented rate limits with headroom). */
|
|
3
|
+
const DEFAULT_LIMITS = {
|
|
4
|
+
perBotPerSec: 25,
|
|
5
|
+
perChatIntervalMs: 1100,
|
|
6
|
+
perGroupPerMin: 18,
|
|
7
|
+
maxAttempts: 8,
|
|
8
|
+
tickBudgetMs: 45e3,
|
|
9
|
+
inboundPerMin: 20,
|
|
10
|
+
linkCodeTtlMs: 9e5,
|
|
11
|
+
drainSlice: 25,
|
|
12
|
+
drainConcurrency: 4,
|
|
13
|
+
drainLeaseMs: 7e4,
|
|
14
|
+
plan: "paid",
|
|
15
|
+
providerTimeoutMs: 1e4,
|
|
16
|
+
scheduleClaim: 20,
|
|
17
|
+
eventClaim: 50
|
|
18
|
+
};
|
|
19
|
+
/** Default `TelegramRetention`, in days. */
|
|
20
|
+
const DEFAULT_RETENTION = {
|
|
21
|
+
sentDays: 30,
|
|
22
|
+
bodyDays: 7,
|
|
23
|
+
failedDays: 90,
|
|
24
|
+
updateDays: 14,
|
|
25
|
+
eventDays: 30,
|
|
26
|
+
runDays: 180,
|
|
27
|
+
expiredDays: 1,
|
|
28
|
+
auditDays: 365
|
|
29
|
+
};
|
|
30
|
+
const DEFAULT_INBOUND_MIME = [
|
|
31
|
+
"image/*",
|
|
32
|
+
"application/pdf",
|
|
33
|
+
"text/plain",
|
|
34
|
+
"text/csv",
|
|
35
|
+
"application/vnd.openxmlformats-officedocument.*",
|
|
36
|
+
"application/msword",
|
|
37
|
+
"application/vnd.ms-excel"
|
|
38
|
+
];
|
|
39
|
+
function resolveAiOptions(o = {}) {
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
moneyPermission: o.moneyPermission ?? null,
|
|
42
|
+
appDomains: Object.freeze([...o.appDomains ?? []]),
|
|
43
|
+
allowContacts: o.allowContacts === true,
|
|
44
|
+
maxTextLength: o.maxTextLength ?? 1e3
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function resolveInboundOptions(o = {}) {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
maxBytes: o.maxBytes ?? 20971520,
|
|
50
|
+
mimeTypes: Object.freeze([...o.mimeTypes ?? DEFAULT_INBOUND_MIME]),
|
|
51
|
+
permission: o.permission ?? null
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function resolveGroupOptions(o = {}) {
|
|
55
|
+
return Object.freeze({ requireLinkedSender: o.requireLinkedSender !== false });
|
|
56
|
+
}
|
|
57
|
+
function resolveFeatures(input = {}) {
|
|
58
|
+
const on = (v, dflt = false) => v === void 0 ? dflt : v !== false;
|
|
59
|
+
const ai = input.ai;
|
|
60
|
+
const aiObj = typeof ai === "object" ? ai : null;
|
|
61
|
+
const reports = input.reports;
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
core: true,
|
|
64
|
+
commands: on(input.commands),
|
|
65
|
+
templates: on(input.templates),
|
|
66
|
+
schedules: on(input.schedules),
|
|
67
|
+
reports: on(reports),
|
|
68
|
+
events: on(input.events),
|
|
69
|
+
broadcasts: on(input.broadcasts),
|
|
70
|
+
actions: on(input.actions),
|
|
71
|
+
dialogs: on(input.dialogs),
|
|
72
|
+
inbound: on(input.inbound),
|
|
73
|
+
subscriptions: on(input.subscriptions),
|
|
74
|
+
ai: on(ai),
|
|
75
|
+
ops: on(input.ops, true),
|
|
76
|
+
aiFreeText: aiObj ? aiObj.freeText === true : false,
|
|
77
|
+
aiSendIntent: aiObj ? aiObj.sendIntent !== false : ai === true,
|
|
78
|
+
reportRenderers: Object.freeze(typeof reports === "object" ? [...reports.renderers ?? []] : [])
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export { DEFAULT_INBOUND_MIME, DEFAULT_LIMITS, DEFAULT_RETENTION, resolveAiOptions, resolveFeatures, resolveGroupOptions, resolveInboundOptions };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { sql } from "../../ports/sql.js";
|
|
2
|
+
import { deliveryDto } from "../rows.js";
|
|
3
|
+
import { metricCount } from "../metrics.js";
|
|
4
|
+
import { isPluginMessage } from "./transport.js";
|
|
5
|
+
// src/engine/outbox/dead-letter.ts
|
|
6
|
+
async function handleDeadLetters(rt, batch) {
|
|
7
|
+
const now = rt.clock.now();
|
|
8
|
+
for (const m of batch.messages) {
|
|
9
|
+
if (!isPluginMessage(m.body)) {
|
|
10
|
+
m.ack();
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const body = m.body;
|
|
14
|
+
if (body.tenant !== rt.tenant) {
|
|
15
|
+
m.ack();
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (body.type === "events") {
|
|
19
|
+
rt.log.warn("tg.dead_letter", {
|
|
20
|
+
type: "events",
|
|
21
|
+
attempts: m.attempts
|
|
22
|
+
});
|
|
23
|
+
m.ack();
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (body.type === "update") {
|
|
28
|
+
const updateId = body.update?.update_id;
|
|
29
|
+
if (typeof updateId === "number") await rt.db.run(sql`UPDATE tg_update SET status = 'failed', error = 'dead_letter', processed_at = ${now}
|
|
30
|
+
WHERE tenant = ${rt.tenant} AND bot_id = ${body.botId} AND update_id = ${updateId}
|
|
31
|
+
AND status != 'done'`);
|
|
32
|
+
rt.log.error("tg.dead_letter", {
|
|
33
|
+
botId: body.botId,
|
|
34
|
+
type: "update",
|
|
35
|
+
attempts: m.attempts
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
const failed = await rt.db.all(sql`UPDATE tg_delivery SET status = 'failed', error_code = 'dead_letter', lease_until = NULL,
|
|
39
|
+
lease_token = NULL, updated_at = ${now}
|
|
40
|
+
WHERE tenant = ${rt.tenant} AND bot_id = ${body.botId} AND status = 'sending'
|
|
41
|
+
AND lease_token IS NOT (SELECT b.drain_token FROM tg_bot b
|
|
42
|
+
WHERE b.id = ${body.botId} AND b.drain_lease_until > ${now})
|
|
43
|
+
RETURNING *`);
|
|
44
|
+
rt.log.error("tg.dead_letter", {
|
|
45
|
+
botId: body.botId,
|
|
46
|
+
type: "drain",
|
|
47
|
+
attempts: m.attempts,
|
|
48
|
+
failed: failed.length
|
|
49
|
+
});
|
|
50
|
+
for (const row of failed) {
|
|
51
|
+
metricCount(rt, "tg.send", 1, {
|
|
52
|
+
botId: body.botId,
|
|
53
|
+
outcome: "failed",
|
|
54
|
+
code: "dead_letter"
|
|
55
|
+
});
|
|
56
|
+
await rt.hook("onDeliveryFailed", (fn) => fn({
|
|
57
|
+
...deliveryDto(row, false),
|
|
58
|
+
status: "failed",
|
|
59
|
+
errorCode: "dead_letter",
|
|
60
|
+
nextAttemptAt: null
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
metricCount(rt, "tg.dead_letter", 1, {
|
|
65
|
+
botId: body.botId,
|
|
66
|
+
outcome: body.type
|
|
67
|
+
});
|
|
68
|
+
m.ack();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
rt.log.error("tg.dead_letter_failed", {
|
|
71
|
+
botId: body.botId,
|
|
72
|
+
error: e instanceof Error ? e.name : "error"
|
|
73
|
+
});
|
|
74
|
+
m.retry({ delaySeconds: 30 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export { handleDeadLetters };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Runtime } from "../runtime.js";
|
|
2
|
+
/** What a TelegramBotDO needs from the plugin (a `TelegramPlugin` satisfies it). */
|
|
3
|
+
export interface BotDoHost {
|
|
4
|
+
/** Builds the runtime for one invocation inside the object. */
|
|
5
|
+
runtime(env: never, exec?: {
|
|
6
|
+
waitUntil(p: Promise<unknown>): void;
|
|
7
|
+
}): Runtime<unknown>;
|
|
8
|
+
}
|
|
9
|
+
/** Registers (or replaces) the plugin a TelegramBotDO uses for `tenant`. The last one registered wins. */
|
|
10
|
+
export declare function registerTelegramPlugin(plugin: BotDoHost, tenant?: string): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/engine/outbox/do-registry.ts
|
|
2
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
3
|
+
/** Registers (or replaces) the plugin a TelegramBotDO uses for `tenant`. The last one registered wins. */
|
|
4
|
+
function registerTelegramPlugin(plugin, tenant = "") {
|
|
5
|
+
hosts.set(tenant, plugin);
|
|
6
|
+
}
|
|
7
|
+
function registeredTelegramPlugin(tenant) {
|
|
8
|
+
return hosts.get(tenant);
|
|
9
|
+
}
|
|
10
|
+
export { registerTelegramPlugin, registeredTelegramPlugin };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TransportPort } from "../../ports/ports.js";
|
|
2
|
+
import { Runtime } from "../runtime.js";
|
|
3
|
+
/** The Durable Object transport (`doTransport`): kicks and scheduler wakes go to the bot's TelegramBotDO. */
|
|
4
|
+
export interface DoTransport extends TransportPort {
|
|
5
|
+
/** Always `"do"`. */
|
|
6
|
+
readonly name: "do";
|
|
7
|
+
/** Ask the bot's object to wake at `at` (epoch ms) for due schedules. Earlier wakes win. */
|
|
8
|
+
scheduleWake(botId: string, at: number): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Replace the object's scheduler wake with `at` (null clears it). Used when the tenant's next schedule moved later
|
|
11
|
+
* or went away (delete, pause), so a stale earlier wake is dropped instead of firing for nothing.
|
|
12
|
+
*/
|
|
13
|
+
setWake(botId: string, at: number | null): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface DoKickBody {
|
|
16
|
+
tenant: string;
|
|
17
|
+
botId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DoWakeBody extends DoKickBody {
|
|
20
|
+
/** Wake time (epoch ms); null clears the wake (only with `replace`). */
|
|
21
|
+
at: number | null;
|
|
22
|
+
/** true: set the wake to `at` exactly. Default false: the earlier of the current wake and `at` wins. */
|
|
23
|
+
replace?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** The TelegramBotDO stub of a bot (`idFromName(botId)`). */
|
|
26
|
+
export declare function botStub(ns: DurableObjectNamespace, botId: string): DurableObjectStub;
|
|
27
|
+
/**
|
|
28
|
+
* Transport over a TelegramBotDO namespace. `kick()` without ids kicks every bot with due work. Kick failures are
|
|
29
|
+
* logged, never thrown (the minute tick re-kicks).
|
|
30
|
+
*/
|
|
31
|
+
export declare function doTransport<A>(rt: Runtime<A>, ns: DurableObjectNamespace): DoTransport;
|
|
32
|
+
/** True for a `DoTransport` (has `scheduleWake` / `setWake`). */
|
|
33
|
+
export declare function isDoTransport(t: TransportPort): t is DoTransport;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { botsWithDueWork } from "./drain.js";
|
|
2
|
+
// src/engine/outbox/do-transport.ts
|
|
3
|
+
/** Internal URL the plugin uses to talk to the object (never leaves the Worker). */
|
|
4
|
+
const DO_ORIGIN = "https://telegram-bot.do";
|
|
5
|
+
/** The TelegramBotDO stub of a bot (`idFromName(botId)`). */
|
|
6
|
+
function botStub(ns, botId) {
|
|
7
|
+
return ns.get(ns.idFromName(botId));
|
|
8
|
+
}
|
|
9
|
+
async function post(rt, ns, path, body) {
|
|
10
|
+
try {
|
|
11
|
+
const res = await botStub(ns, body.botId).fetch(`${DO_ORIGIN}${path}`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify(body)
|
|
15
|
+
});
|
|
16
|
+
await res.body?.cancel();
|
|
17
|
+
if (!res.ok) rt.log.error("tg.do_kick_failed", {
|
|
18
|
+
botId: body.botId,
|
|
19
|
+
status: res.status
|
|
20
|
+
});
|
|
21
|
+
} catch (e) {
|
|
22
|
+
rt.log.error("tg.do_kick_failed", {
|
|
23
|
+
botId: body.botId,
|
|
24
|
+
error: e instanceof Error ? e.name : "error"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Transport over a TelegramBotDO namespace. `kick()` without ids kicks every bot with due work. Kick failures are
|
|
30
|
+
* logged, never thrown (the minute tick re-kicks).
|
|
31
|
+
*/
|
|
32
|
+
function doTransport(rt, ns) {
|
|
33
|
+
return {
|
|
34
|
+
name: "do",
|
|
35
|
+
async kick(botIds) {
|
|
36
|
+
const ids = [...new Set(botIds ?? await botsWithDueWork(rt, rt.clock.now()))];
|
|
37
|
+
await Promise.all(ids.map((botId) => post(rt, ns, "/kick", {
|
|
38
|
+
tenant: rt.tenant,
|
|
39
|
+
botId
|
|
40
|
+
})));
|
|
41
|
+
},
|
|
42
|
+
async scheduleWake(botId, at) {
|
|
43
|
+
await post(rt, ns, "/wake", {
|
|
44
|
+
tenant: rt.tenant,
|
|
45
|
+
botId,
|
|
46
|
+
at
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
async setWake(botId, at) {
|
|
50
|
+
await post(rt, ns, "/wake", {
|
|
51
|
+
tenant: rt.tenant,
|
|
52
|
+
botId,
|
|
53
|
+
at,
|
|
54
|
+
replace: true
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** True for a `DoTransport` (has `scheduleWake` / `setWake`). */
|
|
60
|
+
function isDoTransport(t) {
|
|
61
|
+
const d = t;
|
|
62
|
+
return t.name === "do" && typeof d.scheduleWake === "function" && typeof d.setWake === "function";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Tell the scheduler that `botId` has work at `at` (a schedule's new `next_run_at`). Returns true when a DO
|
|
66
|
+
* scheduler took it; false means the minute tick handles it (cron / queue transports).
|
|
67
|
+
*/
|
|
68
|
+
async function scheduleBotWake(rt, botId, at) {
|
|
69
|
+
if (at === null || !isDoTransport(rt.transport)) return false;
|
|
70
|
+
await rt.transport.scheduleWake(botId, at);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
export { DO_ORIGIN, botStub, doTransport, isDoTransport, scheduleBotWake };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BotRow } from "../rows.js";
|
|
2
|
+
/**
|
|
3
|
+
* Who guarantees a single sender per bot. Default: the D1 bot lease (`tg_bot.drain_lease_until` + fencing token).
|
|
4
|
+
* The Durable Object transport passes its own (the object itself is the lease), saving the per-slice renew.
|
|
5
|
+
*/
|
|
6
|
+
export interface DrainLease {
|
|
7
|
+
/** The bot row when this drain may send (usable, not paused, lease taken); null = stop with `lease`. */
|
|
8
|
+
acquire(now: number): Promise<BotRow | null>;
|
|
9
|
+
/** Called before every slice; null = the lease was lost. */
|
|
10
|
+
renew(now: number): Promise<BotRow | null>;
|
|
11
|
+
/** Gives the lease up (end of the drain). */
|
|
12
|
+
release(): Promise<void>;
|
|
13
|
+
}
|