@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,138 @@
|
|
|
1
|
+
import { decodeEntities } from "../../core/html/escape.js";
|
|
2
|
+
import { splitMessage } from "../../core/split/split.js";
|
|
3
|
+
import { parseTemplate } from "../../core/template/parse.js";
|
|
4
|
+
import { validateTemplate } from "../../core/template/validate.js";
|
|
5
|
+
import { renderTemplate } from "../../core/template/render.js";
|
|
6
|
+
import { TelegramPluginError } from "../errors.js";
|
|
7
|
+
// src/engine/content/render.ts
|
|
8
|
+
/** The variant for the first candidate that has one (exact tag, then its language), else the first variant. */
|
|
9
|
+
function pickVariant(body, candidates) {
|
|
10
|
+
for (const c of candidates) {
|
|
11
|
+
if (!c) continue;
|
|
12
|
+
if (Object.hasOwn(body, c)) return {
|
|
13
|
+
locale: c,
|
|
14
|
+
source: body[c]
|
|
15
|
+
};
|
|
16
|
+
const lang = c.split("-")[0];
|
|
17
|
+
if (lang && Object.hasOwn(body, lang)) return {
|
|
18
|
+
locale: lang,
|
|
19
|
+
source: body[lang]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const first = Object.entries(body)[0];
|
|
23
|
+
return first ? {
|
|
24
|
+
locale: first[0],
|
|
25
|
+
source: first[1]
|
|
26
|
+
} : null;
|
|
27
|
+
}
|
|
28
|
+
/** `text` templates: literal `& < >` outside `{{…}}` are escaped, so no tag survives. */
|
|
29
|
+
function escapeTextSource(src) {
|
|
30
|
+
let out = "";
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < src.length) {
|
|
33
|
+
const open = src.indexOf("{{", i);
|
|
34
|
+
const end = open === -1 ? src.length : open;
|
|
35
|
+
out += src.slice(i, end).replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
36
|
+
if (open === -1) break;
|
|
37
|
+
const close = src.indexOf("}}", open + 2);
|
|
38
|
+
if (close === -1) {
|
|
39
|
+
out += src.slice(open);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
out += src.slice(open, close + 2);
|
|
43
|
+
i = close + 2;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
const astCache = /* @__PURE__ */ new Map();
|
|
48
|
+
/** Parses a template source (cached per isolate; sources are small and repeat across a fan-out). */
|
|
49
|
+
function parseSource(source, format) {
|
|
50
|
+
const key = `${format}\u0000${source}`;
|
|
51
|
+
let hit = astCache.get(key);
|
|
52
|
+
if (!hit) {
|
|
53
|
+
hit = parseTemplate(format === "text" ? escapeTextSource(source) : source);
|
|
54
|
+
if (astCache.size > 500) astCache.clear();
|
|
55
|
+
astCache.set(key, hit);
|
|
56
|
+
}
|
|
57
|
+
return hit;
|
|
58
|
+
}
|
|
59
|
+
function toTemplateError(e, locale) {
|
|
60
|
+
const out = {
|
|
61
|
+
line: e.line,
|
|
62
|
+
column: e.column,
|
|
63
|
+
message: e.message
|
|
64
|
+
};
|
|
65
|
+
if (locale) out.locale = locale;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** `variable_unknown` only → `template_variable_unknown`; anything else → `template_invalid`. Both carry `errors[]`. */
|
|
69
|
+
function templateProblem(errors) {
|
|
70
|
+
const onlyVars = errors.length > 0 && errors.every((e) => e.code === "variable_unknown");
|
|
71
|
+
const list = errors.slice(0, 50).map((e) => toTemplateError(e, e.locale));
|
|
72
|
+
return new TelegramPluginError(onlyVars ? "template_variable_unknown" : "template_invalid", onlyVars ? "the template uses variables the provider does not declare" : "the template is not valid", { extensions: { errors: list } });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Checks every locale variant: syntax, then variables against the provider schema (when given) and filters.
|
|
76
|
+
* Returns the problems with their locale; empty means valid.
|
|
77
|
+
*/
|
|
78
|
+
function checkBody(body, format, opts = {}) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const [locale, source] of Object.entries(body)) {
|
|
81
|
+
const { ast, errors } = parseSource(source, format);
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
for (const e of errors) out.push({
|
|
84
|
+
...e,
|
|
85
|
+
locale
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const e of validateTemplate(ast, opts)) out.push({
|
|
90
|
+
...e,
|
|
91
|
+
locale
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Renders one locale variant. Throws `template_invalid` for a source that does not parse. */
|
|
97
|
+
function renderBody(rt, input) {
|
|
98
|
+
const variant = pickVariant(input.body, [...input.locales, rt.i18n.defaultLocale]);
|
|
99
|
+
if (!variant) throw new TelegramPluginError("template_invalid", "the template has no body");
|
|
100
|
+
const { ast, errors } = parseSource(variant.source, input.format);
|
|
101
|
+
if (errors.length > 0) throw templateProblem(errors.map((e) => ({
|
|
102
|
+
...e,
|
|
103
|
+
locale: variant.locale
|
|
104
|
+
})));
|
|
105
|
+
const r = renderTemplate(ast, {
|
|
106
|
+
vars: input.vars,
|
|
107
|
+
locale: variant.locale,
|
|
108
|
+
tz: input.tz,
|
|
109
|
+
currency: rt.settings.currency,
|
|
110
|
+
minorDigits: rt.settings.minorUnitDigits,
|
|
111
|
+
filters: rt.filters
|
|
112
|
+
});
|
|
113
|
+
const html = r.html.trim();
|
|
114
|
+
return {
|
|
115
|
+
locale: variant.locale,
|
|
116
|
+
html,
|
|
117
|
+
parts: input.noSplit || html === "" ? [] : splitMessage(html),
|
|
118
|
+
warnings: r.warnings.map((w) => toTemplateError(w, variant.locale))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Renders a one-line template (file names, URLs) to plain text: entities decoded, tags dropped. */
|
|
122
|
+
function renderInline(rt, source, vars, ctx) {
|
|
123
|
+
const { ast, errors } = parseSource(source, "text");
|
|
124
|
+
if (errors.length > 0) throw templateProblem(errors.map((e) => ({
|
|
125
|
+
...e,
|
|
126
|
+
locale: ctx.locale
|
|
127
|
+
})));
|
|
128
|
+
const r = renderTemplate(ast, {
|
|
129
|
+
vars,
|
|
130
|
+
locale: ctx.locale,
|
|
131
|
+
tz: ctx.tz,
|
|
132
|
+
currency: rt.settings.currency,
|
|
133
|
+
minorDigits: rt.settings.minorUnitDigits,
|
|
134
|
+
filters: rt.filters
|
|
135
|
+
});
|
|
136
|
+
return decodeEntities(r.html);
|
|
137
|
+
}
|
|
138
|
+
export { checkBody, escapeTextSource, parseSource, pickVariant, renderBody, renderInline, templateProblem, toTemplateError };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { escapeHtml } from "../../core/html/escape.js";
|
|
2
|
+
import { TelegramPluginError, featureDisabled } from "../errors.js";
|
|
3
|
+
import { renderBody } from "./render.js";
|
|
4
|
+
import { buildButtons } from "./buttons.js";
|
|
5
|
+
import { fileUrl, storeReportFile } from "./report-files.js";
|
|
6
|
+
import { buildReport, templateData } from "../../services/reports.js";
|
|
7
|
+
import { requireTemplateByKey, templateModel } from "./template-rows.js";
|
|
8
|
+
import { openSource } from "../../services/send.js";
|
|
9
|
+
// src/engine/content/reply.ts
|
|
10
|
+
function options(t, opts) {
|
|
11
|
+
return {
|
|
12
|
+
...t,
|
|
13
|
+
...opts?.silent ? { silent: true } : {},
|
|
14
|
+
...opts?.protectContent ? { protectContent: true } : {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function tooLarge(ctx, ref, name, caption) {
|
|
18
|
+
const { rt } = ctx;
|
|
19
|
+
rt.log.warn("tg.file_too_large", {
|
|
20
|
+
botId: ctx.bot.id,
|
|
21
|
+
code: "file_too_large"
|
|
22
|
+
});
|
|
23
|
+
const locale = await ctx.locale();
|
|
24
|
+
const url = await fileUrl(rt, ref);
|
|
25
|
+
const link = url ? `\n${rt.i18n.t(locale, "bot.report.download")} <a href="${escapeHtml(url)}">${escapeHtml(name)}</a>` : "";
|
|
26
|
+
await ctx.replyDelivery({
|
|
27
|
+
kind: "text",
|
|
28
|
+
html: [caption, `${rt.i18n.t(locale, "bot.report.too_large")}${link}`].filter(Boolean).join("\n\n")
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Renders a report for the chat's audience, stores the file and replies with it as a document. */
|
|
32
|
+
async function replyReport(ctx, t, actor, params, opts) {
|
|
33
|
+
const { rt } = ctx;
|
|
34
|
+
const info = rt.describe(actor);
|
|
35
|
+
const tz = ctx.chat?.timezone ?? rt.settings.timezone;
|
|
36
|
+
const built = await buildReport(rt, {
|
|
37
|
+
template: t,
|
|
38
|
+
actor,
|
|
39
|
+
params,
|
|
40
|
+
locales: [
|
|
41
|
+
ctx.chat?.locale,
|
|
42
|
+
info.locale,
|
|
43
|
+
ctx.from?.language_code,
|
|
44
|
+
ctx.bot.default_locale
|
|
45
|
+
],
|
|
46
|
+
tz,
|
|
47
|
+
format: opts?.format,
|
|
48
|
+
now: ctx.now
|
|
49
|
+
});
|
|
50
|
+
const stored = await storeReportFile(rt, built.file, {
|
|
51
|
+
template: t.key,
|
|
52
|
+
source_kind: "command"
|
|
53
|
+
});
|
|
54
|
+
if (stored.size !== null && stored.size > 52428800) {
|
|
55
|
+
await tooLarge(ctx, stored.ref, stored.name, built.caption.html);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await ctx.replyDelivery({
|
|
59
|
+
kind: "document",
|
|
60
|
+
html: built.caption.html,
|
|
61
|
+
file: {
|
|
62
|
+
ref: stored.ref,
|
|
63
|
+
name: stored.name,
|
|
64
|
+
mime: stored.mime
|
|
65
|
+
},
|
|
66
|
+
options: options(t.options, opts),
|
|
67
|
+
render: {
|
|
68
|
+
template: t.key,
|
|
69
|
+
templateVersion: t.version
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function replyTemplate(ctx, key, params, opts) {
|
|
74
|
+
const { rt } = ctx;
|
|
75
|
+
if (!rt.features.templates) throw featureDisabled("templates");
|
|
76
|
+
const t = templateModel(await requireTemplateByKey(rt, key));
|
|
77
|
+
const actor = await ctx.actor();
|
|
78
|
+
if (actor === null) {
|
|
79
|
+
await ctx.reply(await ctx.t("bot.error.not_linked"));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (t.kind === "report") {
|
|
83
|
+
await replyReport(ctx, t, actor, params, opts);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const info = rt.describe(actor);
|
|
87
|
+
const locales = [
|
|
88
|
+
ctx.chat?.locale,
|
|
89
|
+
info.locale,
|
|
90
|
+
ctx.from?.language_code,
|
|
91
|
+
ctx.bot.default_locale
|
|
92
|
+
];
|
|
93
|
+
const tz = ctx.chat?.timezone ?? rt.settings.timezone;
|
|
94
|
+
const vars = await templateData(rt, {
|
|
95
|
+
template: t,
|
|
96
|
+
actor,
|
|
97
|
+
params,
|
|
98
|
+
tz,
|
|
99
|
+
now: ctx.now,
|
|
100
|
+
locale: rt.i18n.pick(...locales)
|
|
101
|
+
});
|
|
102
|
+
const body = renderBody(rt, {
|
|
103
|
+
body: t.body,
|
|
104
|
+
format: t.format,
|
|
105
|
+
vars,
|
|
106
|
+
locales,
|
|
107
|
+
tz
|
|
108
|
+
});
|
|
109
|
+
if (body.parts.length === 0) return;
|
|
110
|
+
const chat = ctx.chat;
|
|
111
|
+
const buttons = await buildButtons(rt, opts?.buttons ?? t.buttons, {
|
|
112
|
+
vars,
|
|
113
|
+
locale: body.locale,
|
|
114
|
+
tz,
|
|
115
|
+
botId: ctx.bot.id,
|
|
116
|
+
chatRef: chat?.id ?? null,
|
|
117
|
+
userId: chat?.kind === "private" ? chat.user_id : null,
|
|
118
|
+
now: ctx.now
|
|
119
|
+
});
|
|
120
|
+
await ctx.replyDelivery({
|
|
121
|
+
kind: "text",
|
|
122
|
+
html: body.html,
|
|
123
|
+
replyMarkup: buttons.markup,
|
|
124
|
+
options: options(t.options, opts),
|
|
125
|
+
sql: buttons.sql,
|
|
126
|
+
render: {
|
|
127
|
+
template: t.key,
|
|
128
|
+
templateVersion: t.version
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Sends a stored file (a registered file source, or the files port) the chat's actor may read. */
|
|
133
|
+
async function replyFile(ctx, file, opts) {
|
|
134
|
+
const actor = await ctx.actor();
|
|
135
|
+
if (actor === null) throw new TelegramPluginError("forbidden", "the chat is not linked");
|
|
136
|
+
const source = file.source ?? "files";
|
|
137
|
+
const opened = await openSource(ctx.rt, actor, source, file.ref);
|
|
138
|
+
await opened.body.cancel().catch(() => {});
|
|
139
|
+
const caption = file.caption ?? "";
|
|
140
|
+
if (opened.size !== null && opened.size > 52428800) {
|
|
141
|
+
await tooLarge(ctx, opened.ref, opened.name, caption);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await ctx.replyDelivery({
|
|
145
|
+
kind: "document",
|
|
146
|
+
html: caption,
|
|
147
|
+
file: {
|
|
148
|
+
ref: opened.ref,
|
|
149
|
+
name: opened.name,
|
|
150
|
+
mime: opened.mime,
|
|
151
|
+
...source === "files" ? {} : { source }
|
|
152
|
+
},
|
|
153
|
+
options: options({}, opts)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export { replyFile, replyReport, replyTemplate };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TelegramPluginError, featureDisabled } from "../errors.js";
|
|
2
|
+
import { renderInline } from "./render.js";
|
|
3
|
+
import { csvRenderer } from "../../renderers/csv.js";
|
|
4
|
+
import { browserRunPdf, isBrowserRunBinding } from "../../renderers/html.js";
|
|
5
|
+
// src/engine/content/report-files.ts
|
|
6
|
+
const MAX_UPLOAD_BYTES = 52428800;
|
|
7
|
+
const builtinCsv = csvRenderer();
|
|
8
|
+
/** The renderer for a format: `ports.renderers` first, then built-in CSV, then Browser Rendering for PDF. */
|
|
9
|
+
function rendererFor(rt, format) {
|
|
10
|
+
if (!rt.features.reports) throw featureDisabled("reports");
|
|
11
|
+
const allowed = rt.features.reportRenderers;
|
|
12
|
+
if (allowed.length > 0 && !allowed.includes(format)) throw new TelegramPluginError("renderer_unavailable", `renderer "${format}" is not enabled`);
|
|
13
|
+
const host = rt.renderers.find((r) => r.format === format);
|
|
14
|
+
if (host) return host;
|
|
15
|
+
if (format === "csv") return builtinCsv;
|
|
16
|
+
if (format === "pdf" && isBrowserRunBinding(rt.browser)) return browserRunPdf(rt.browser);
|
|
17
|
+
throw new TelegramPluginError("renderer_unavailable", `no renderer for "${format}"`);
|
|
18
|
+
}
|
|
19
|
+
function reportFormatOf(template, override) {
|
|
20
|
+
return override ?? template.report?.renderer ?? "csv";
|
|
21
|
+
}
|
|
22
|
+
function cleanName(name) {
|
|
23
|
+
const n = [...name].map((ch) => ch === "/" || ch === "\\" || ch.charCodeAt(0) < 32 ? "_" : ch).join("").trim();
|
|
24
|
+
return n === "" ? "report" : n.slice(0, 128);
|
|
25
|
+
}
|
|
26
|
+
/** Renders the report file (not stored). */
|
|
27
|
+
async function renderReportFile(rt, input) {
|
|
28
|
+
const format = reportFormatOf(input.template, input.format);
|
|
29
|
+
const renderer = rendererFor(rt, format);
|
|
30
|
+
const base = input.template.report?.fileName ?? input.template.key;
|
|
31
|
+
const fileName = cleanName(renderInline(rt, base, input.vars, {
|
|
32
|
+
locale: input.locale,
|
|
33
|
+
tz: input.tz
|
|
34
|
+
}));
|
|
35
|
+
const named = /\.[a-z0-9]{2,4}$/i.test(fileName) ? fileName.replace(/\.[a-z0-9]{2,4}$/i, `.${format}`) : `${fileName}.${format}`;
|
|
36
|
+
const title = input.template.layout?.title ? input.template.layout.title[input.locale] ?? Object.values(input.template.layout.title)[0] : void 0;
|
|
37
|
+
return renderer.render({
|
|
38
|
+
data: input.vars,
|
|
39
|
+
fileName: named,
|
|
40
|
+
locale: input.locale,
|
|
41
|
+
timezone: input.tz,
|
|
42
|
+
layout: input.template.report?.layout,
|
|
43
|
+
html: input.html,
|
|
44
|
+
title: title ?? named.replace(/\.[a-z0-9]+$/i, ""),
|
|
45
|
+
currency: rt.settings.currency,
|
|
46
|
+
minorDigits: rt.settings.minorUnitDigits,
|
|
47
|
+
labels: {
|
|
48
|
+
yes: rt.i18n.plain(input.locale, "bot.report.yes"),
|
|
49
|
+
no: rt.i18n.plain(input.locale, "bot.report.no")
|
|
50
|
+
}
|
|
51
|
+
}, rt.env);
|
|
52
|
+
}
|
|
53
|
+
/** Stores a rendered file through the files port. */
|
|
54
|
+
async function storeReportFile(rt, file, tags) {
|
|
55
|
+
if (!rt.files) throw new TelegramPluginError("renderer_unavailable", "report files need ports.files or env.files (R2)");
|
|
56
|
+
const stored = await rt.files.put({
|
|
57
|
+
name: file.name,
|
|
58
|
+
mime: file.mime,
|
|
59
|
+
body: file.body,
|
|
60
|
+
tags: {
|
|
61
|
+
source: "telegram",
|
|
62
|
+
...tags
|
|
63
|
+
}
|
|
64
|
+
}, null);
|
|
65
|
+
return {
|
|
66
|
+
...stored,
|
|
67
|
+
size: stored.size ?? file.size ?? null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** A link to a stored file in the app, when the files port can make one. */
|
|
71
|
+
async function fileUrl(rt, ref) {
|
|
72
|
+
try {
|
|
73
|
+
return await rt.files?.url?.(ref) ?? null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export { MAX_UPLOAD_BYTES, fileUrl, renderReportFile, rendererFor, reportFormatOf, storeReportFile };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LocalizedText } from "../../contracts/common.js";
|
|
2
|
+
import { ButtonRows, ReportLayout, ReportSpec, TemplateOptions } from "../../contracts/templates.js";
|
|
3
|
+
import { sql } from "../../ports/sql.js";
|
|
4
|
+
import { TelegramPluginError } from "../errors.js";
|
|
5
|
+
import { parseJson, parseObject } from "../rows.js";
|
|
6
|
+
// src/engine/content/template-rows.ts
|
|
7
|
+
/** Parses a row; invalid JSON columns fall back to empty / null values (never throws). */
|
|
8
|
+
function templateModel(r) {
|
|
9
|
+
const report = parseJson(r.report_json, ReportSpec.nullable(), null);
|
|
10
|
+
return {
|
|
11
|
+
id: r.id,
|
|
12
|
+
key: r.key,
|
|
13
|
+
name: r.name,
|
|
14
|
+
kind: r.kind === "report" ? "report" : "message",
|
|
15
|
+
provider: r.provider,
|
|
16
|
+
providerParams: parseObject(r.provider_params_json),
|
|
17
|
+
body: parseJson(r.body_json, LocalizedText, {}),
|
|
18
|
+
format: r.format === "text" ? "text" : "html",
|
|
19
|
+
report,
|
|
20
|
+
layout: report?.layout ? ReportLayout.safeParse(report.layout).data ?? null : null,
|
|
21
|
+
buttons: parseJson(r.buttons_json, ButtonRows.nullable(), null),
|
|
22
|
+
options: parseJson(r.options_json, TemplateOptions, {}),
|
|
23
|
+
version: r.version
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The admin API DTO of a row. */
|
|
27
|
+
function templateDto(r) {
|
|
28
|
+
const m = templateModel(r);
|
|
29
|
+
return {
|
|
30
|
+
id: m.id,
|
|
31
|
+
key: m.key,
|
|
32
|
+
name: m.name,
|
|
33
|
+
kind: m.kind,
|
|
34
|
+
provider: m.provider,
|
|
35
|
+
providerParams: m.providerParams,
|
|
36
|
+
body: m.body,
|
|
37
|
+
format: m.format,
|
|
38
|
+
report: m.report,
|
|
39
|
+
buttons: m.buttons,
|
|
40
|
+
options: m.options,
|
|
41
|
+
version: m.version,
|
|
42
|
+
updatedBy: r.updated_by,
|
|
43
|
+
updatedAt: r.updated_at
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function templateVersionDto(r) {
|
|
47
|
+
return {
|
|
48
|
+
templateId: r.template_id,
|
|
49
|
+
version: r.version,
|
|
50
|
+
body: parseJson(r.body_json, LocalizedText, {}),
|
|
51
|
+
report: parseJson(r.report_json, ReportSpec.nullable(), null),
|
|
52
|
+
buttons: parseJson(r.buttons_json, ButtonRows.nullable(), null),
|
|
53
|
+
savedBy: r.saved_by,
|
|
54
|
+
savedAt: r.saved_at
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function templateById(rt, id) {
|
|
58
|
+
return rt.db.first(sql`SELECT * FROM tg_template WHERE tenant = ${rt.tenant} AND id = ${id} AND deleted_at IS NULL`);
|
|
59
|
+
}
|
|
60
|
+
/** A live (not deleted) template of this tenant by key, or null. */
|
|
61
|
+
async function templateByKey(rt, key) {
|
|
62
|
+
return rt.db.first(sql`SELECT * FROM tg_template WHERE tenant = ${rt.tenant} AND key = ${key} AND deleted_at IS NULL`);
|
|
63
|
+
}
|
|
64
|
+
async function requireTemplate(rt, id) {
|
|
65
|
+
const row = await templateById(rt, id);
|
|
66
|
+
if (!row) throw new TelegramPluginError("not_found", "template not found");
|
|
67
|
+
return row;
|
|
68
|
+
}
|
|
69
|
+
async function requireTemplateByKey(rt, key) {
|
|
70
|
+
const row = await templateByKey(rt, key);
|
|
71
|
+
if (!row) throw new TelegramPluginError("not_found", `template "${key}" not found`);
|
|
72
|
+
return row;
|
|
73
|
+
}
|
|
74
|
+
export { requireTemplate, requireTemplateByKey, templateById, templateByKey, templateDto, templateModel, templateVersionDto };
|