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