@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.1
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 +91 -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,9 @@
|
|
|
1
|
+
import { adminRoutes } from "./admin.js";
|
|
2
|
+
import { meRoutes } from "./me.js";
|
|
3
|
+
import { webhookRoutes } from "./webhook.js";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
// src/routes/app.ts
|
|
6
|
+
function buildRoutes(deps, meta) {
|
|
7
|
+
return new Hono().route("/", webhookRoutes(deps)).route("/", adminRoutes(deps, meta)).route("/", meRoutes(deps));
|
|
8
|
+
}
|
|
9
|
+
export { buildRoutes };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PageQuery } from "../contracts/common.js";
|
|
2
|
+
import { BroadcastPreviewRequest, BroadcastQuery, BroadcastSave } from "../contracts/broadcasts.js";
|
|
3
|
+
import { broadcastDto, broadcastReport, createBroadcast, deleteBroadcast, listBroadcasts, previewBroadcast, requireBroadcast, sendBroadcast, updateBroadcast } from "../services/broadcasts.js";
|
|
4
|
+
import { decodeCursor, featureGate, idParam, pageOf, requirePermission, zJson, zQuery } from "./http.js";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
// src/routes/broadcasts.ts
|
|
7
|
+
const ListQuery = BroadcastQuery.extend(PageQuery.shape);
|
|
8
|
+
const PERM = "telegram.broadcasts.send";
|
|
9
|
+
function broadcastRoutes(deps) {
|
|
10
|
+
const app = new Hono();
|
|
11
|
+
app.use("/admin/broadcasts", featureGate(deps.features, "broadcasts"));
|
|
12
|
+
app.use("/admin/broadcasts/*", featureGate(deps.features, "broadcasts"));
|
|
13
|
+
return app.get("/admin/broadcasts", requirePermission(PERM), zQuery(ListQuery), async (c) => {
|
|
14
|
+
const q = c.req.valid("query");
|
|
15
|
+
const rows = await listBroadcasts(c.var.tg, q, {
|
|
16
|
+
cursor: decodeCursor(q.cursor),
|
|
17
|
+
limit: q.limit
|
|
18
|
+
});
|
|
19
|
+
return c.json(pageOf(rows, q.limit, broadcastDto));
|
|
20
|
+
}).post("/admin/broadcasts", requirePermission(PERM), zJson(BroadcastSave), async (c) => {
|
|
21
|
+
return c.json(await createBroadcast(c.var.tg, c.var.tgActor, c.req.valid("json")), 201);
|
|
22
|
+
}).get("/admin/broadcasts/:id", requirePermission(PERM), async (c) => {
|
|
23
|
+
return c.json(broadcastDto(await requireBroadcast(c.var.tg, idParam(c))));
|
|
24
|
+
}).put("/admin/broadcasts/:id", requirePermission(PERM), zJson(BroadcastSave), async (c) => {
|
|
25
|
+
return c.json(await updateBroadcast(c.var.tg, c.var.tgActor, idParam(c), c.req.valid("json")));
|
|
26
|
+
}).delete("/admin/broadcasts/:id", requirePermission(PERM), async (c) => {
|
|
27
|
+
await deleteBroadcast(c.var.tg, c.var.tgActor, idParam(c));
|
|
28
|
+
return c.json({ ok: true });
|
|
29
|
+
}).post("/admin/broadcasts/:id/preview", requirePermission(PERM), zJson(BroadcastPreviewRequest), async (c) => {
|
|
30
|
+
return c.json(await previewBroadcast(c.var.tg, c.var.tgActor, idParam(c), c.req.valid("json")));
|
|
31
|
+
}).post("/admin/broadcasts/:id/send", requirePermission(PERM), async (c) => {
|
|
32
|
+
return c.json(await sendBroadcast(c.var.tg, c.var.tgActor, idParam(c)));
|
|
33
|
+
}).get("/admin/broadcasts/:id/report", requirePermission(PERM), async (c) => {
|
|
34
|
+
return c.json(await broadcastReport(c.var.tg, idParam(c)));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export { broadcastRoutes };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CommandConfigSave, CommandName } from "../contracts/commands.js";
|
|
2
|
+
import { TelegramPluginError } from "../engine/errors.js";
|
|
3
|
+
import { loadBot } from "../engine/bot-api.js";
|
|
4
|
+
import { listCommandConfigs, saveCommandConfig } from "../services/commands.js";
|
|
5
|
+
import { syncCommandMenu } from "../services/menu.js";
|
|
6
|
+
import { featureGate, idParam, requirePermission, zJson } from "./http.js";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
// src/routes/commands.ts
|
|
9
|
+
async function requireBot(rt, id) {
|
|
10
|
+
const bot = await loadBot(rt, id);
|
|
11
|
+
if (!bot) throw new TelegramPluginError("not_found", "bot not found");
|
|
12
|
+
return bot;
|
|
13
|
+
}
|
|
14
|
+
function commandRoutes(deps) {
|
|
15
|
+
const app = new Hono();
|
|
16
|
+
app.use("/admin/bots/:id/commands", featureGate(deps.features, "commands"));
|
|
17
|
+
app.use("/admin/bots/:id/commands/*", featureGate(deps.features, "commands"));
|
|
18
|
+
return app.get("/admin/bots/:id/commands", requirePermission("telegram.commands.manage"), async (c) => {
|
|
19
|
+
const bot = await requireBot(c.var.tg, idParam(c));
|
|
20
|
+
return c.json({ items: await listCommandConfigs(c.var.tg, bot) });
|
|
21
|
+
}).post("/admin/bots/:id/commands/sync", requirePermission("telegram.commands.manage"), async (c) => {
|
|
22
|
+
const bot = await requireBot(c.var.tg, idParam(c));
|
|
23
|
+
return c.json(await syncCommandMenu(c.var.tg, bot, { force: true }));
|
|
24
|
+
}).put("/admin/bots/:id/commands/:command", requirePermission("telegram.commands.manage"), zJson(CommandConfigSave), async (c) => {
|
|
25
|
+
const rt = c.var.tg;
|
|
26
|
+
const bot = await requireBot(rt, idParam(c));
|
|
27
|
+
const name = CommandName.safeParse(c.req.param("command"));
|
|
28
|
+
if (!name.success) throw new TelegramPluginError("not_found", "command not found");
|
|
29
|
+
const dto = await saveCommandConfig(rt, c.var.tgActor, bot, name.data, c.req.valid("json"));
|
|
30
|
+
const fresh = await loadBot(rt, bot.id);
|
|
31
|
+
if (fresh) await syncCommandMenu(rt, fresh);
|
|
32
|
+
return c.json(dto);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export { commandRoutes };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { broadcastRoutes } from "./broadcasts.js";
|
|
2
|
+
import { commandRoutes } from "./commands.js";
|
|
3
|
+
import { eventRoutes } from "./events.js";
|
|
4
|
+
import { scheduleRoutes } from "./schedules.js";
|
|
5
|
+
import { sendRoutes } from "./send.js";
|
|
6
|
+
import { templateRoutes } from "./templates.js";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
// src/routes/content.ts
|
|
9
|
+
function contentRoutes(deps) {
|
|
10
|
+
return new Hono().route("/", templateRoutes(deps)).route("/", commandRoutes(deps)).route("/", scheduleRoutes(deps)).route("/", eventRoutes(deps)).route("/", broadcastRoutes(deps)).route("/", sendRoutes(deps));
|
|
11
|
+
}
|
|
12
|
+
export { contentRoutes };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PageQuery } from "../contracts/common.js";
|
|
2
|
+
import { EventRuleQuery, EventRuleSave, EventRuleTestRequest } from "../contracts/events.js";
|
|
3
|
+
import { createRule, deleteRule, eventRuleDto, listRules, registeredEvents, requireRule, testRule, updateRule } from "../services/event-rules.js";
|
|
4
|
+
import { decodeCursor, featureGate, idParam, pageOf, requirePermission, zJson, zQuery } from "./http.js";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
// src/routes/events.ts
|
|
7
|
+
const ListQuery = EventRuleQuery.extend(PageQuery.shape);
|
|
8
|
+
const PERM = "telegram.events.manage";
|
|
9
|
+
function eventRoutes(deps) {
|
|
10
|
+
const app = new Hono();
|
|
11
|
+
app.use("/admin/events", featureGate(deps.features, "events"));
|
|
12
|
+
app.use("/admin/event-rules", featureGate(deps.features, "events"));
|
|
13
|
+
app.use("/admin/event-rules/*", featureGate(deps.features, "events"));
|
|
14
|
+
return app.get("/admin/events", requirePermission(PERM), async (c) => {
|
|
15
|
+
return c.json({ items: registeredEvents(c.var.tg) });
|
|
16
|
+
}).get("/admin/event-rules", requirePermission(PERM), zQuery(ListQuery), async (c) => {
|
|
17
|
+
const q = c.req.valid("query");
|
|
18
|
+
const rows = await listRules(c.var.tg, q, {
|
|
19
|
+
cursor: decodeCursor(q.cursor),
|
|
20
|
+
limit: q.limit
|
|
21
|
+
});
|
|
22
|
+
return c.json(pageOf(rows, q.limit, eventRuleDto));
|
|
23
|
+
}).post("/admin/event-rules", requirePermission(PERM), zJson(EventRuleSave), async (c) => {
|
|
24
|
+
return c.json(await createRule(c.var.tg, c.var.tgActor, c.req.valid("json")), 201);
|
|
25
|
+
}).get("/admin/event-rules/:id", requirePermission(PERM), async (c) => {
|
|
26
|
+
return c.json(eventRuleDto(await requireRule(c.var.tg, idParam(c))));
|
|
27
|
+
}).put("/admin/event-rules/:id", requirePermission(PERM), zJson(EventRuleSave), async (c) => {
|
|
28
|
+
return c.json(await updateRule(c.var.tg, c.var.tgActor, idParam(c), c.req.valid("json")));
|
|
29
|
+
}).delete("/admin/event-rules/:id", requirePermission(PERM), async (c) => {
|
|
30
|
+
await deleteRule(c.var.tg, c.var.tgActor, idParam(c));
|
|
31
|
+
return c.json({ ok: true });
|
|
32
|
+
}).post("/admin/event-rules/:id/test", requirePermission(PERM), zJson(EventRuleTestRequest), async (c) => {
|
|
33
|
+
return c.json(await testRule(c.var.tg, c.var.tgActor, idParam(c), c.req.valid("json").payload));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export { eventRoutes };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ErrorsPort } from "../ports/ports.js";
|
|
2
|
+
import { FeatureFlags } from "../engine/options.js";
|
|
3
|
+
import { Runtime } from "../engine/runtime.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { Context } from "hono";
|
|
6
|
+
export interface TgVariables {
|
|
7
|
+
tg: Runtime<unknown>;
|
|
8
|
+
tgActor: unknown;
|
|
9
|
+
/** Set by `requirePermission` once a permission check passed. */
|
|
10
|
+
tgPermChecked?: boolean;
|
|
11
|
+
/** `idempotencyMiddleware` ran before the permission check: the next `requirePermission` handles the key. */
|
|
12
|
+
tgIdem?: "armed" | "done";
|
|
13
|
+
/** `skipIdempotencyStore`: never store this route's response. */
|
|
14
|
+
tgIdemNoStore?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type TgEnv = {
|
|
17
|
+
Bindings: object;
|
|
18
|
+
Variables: TgVariables;
|
|
19
|
+
};
|
|
20
|
+
export interface RouteDeps {
|
|
21
|
+
runtime(env: unknown, exec?: {
|
|
22
|
+
waitUntil(p: Promise<unknown>): void;
|
|
23
|
+
}): Runtime<unknown>;
|
|
24
|
+
features: FeatureFlags;
|
|
25
|
+
errors: ErrorsPort | undefined;
|
|
26
|
+
/** Databases (keyed by the env mapper's `db` object) that passed the schema check in this isolate. */
|
|
27
|
+
schemaOk: WeakSet<object>;
|
|
28
|
+
/** The cache key for `schemaOk`: the `db` binding / port the env mapper returns for this env. */
|
|
29
|
+
schemaKey(env: unknown): object | undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Id } from "../contracts/common.js";
|
|
2
|
+
import { PROBLEM_CONTENT_TYPE, PROBLEM_STATUS } from "../contracts/problems.js";
|
|
3
|
+
import { TelegramPluginError, featureDisabled, isTelegramPluginError } from "../engine/errors.js";
|
|
4
|
+
import { ensureState } from "../engine/state.js";
|
|
5
|
+
import { IDEMPOTENCY_RENEW_MS, reserveIdempotency } from "../services/idempotency.js";
|
|
6
|
+
import { validator } from "hono/validator";
|
|
7
|
+
// src/routes/http.ts
|
|
8
|
+
const NO_STORE = "private, no-store";
|
|
9
|
+
function problemBody(err, instance) {
|
|
10
|
+
return err.toProblem(instance);
|
|
11
|
+
}
|
|
12
|
+
function problemResponse(err, instance) {
|
|
13
|
+
return new Response(JSON.stringify(problemBody(err, instance)), {
|
|
14
|
+
status: err.status,
|
|
15
|
+
headers: {
|
|
16
|
+
"content-type": PROBLEM_CONTENT_TYPE,
|
|
17
|
+
"cache-control": NO_STORE
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** `onError` for the sub-apps: plugin problems → problem+json (or the host's error through `ports.errors`). */
|
|
22
|
+
function errorHandler(errors) {
|
|
23
|
+
return (err, c) => {
|
|
24
|
+
if (!isTelegramPluginError(err)) throw err;
|
|
25
|
+
const instance = new URL(c.req.url).pathname;
|
|
26
|
+
if (errors) throw errors(err.toProblem(instance), err);
|
|
27
|
+
return problemResponse(err, instance);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function validationError(error, prefix = "") {
|
|
31
|
+
return new TelegramPluginError("validation_failed", "request is not valid", {
|
|
32
|
+
status: PROBLEM_STATUS.validation_failed,
|
|
33
|
+
extensions: { issues: error.issues.slice(0, 50).map((i) => ({
|
|
34
|
+
path: [prefix, ...i.path.map(String)].filter(Boolean).join("."),
|
|
35
|
+
message: i.message
|
|
36
|
+
})) }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function parseWith(schema, value, prefix) {
|
|
40
|
+
const r = schema.safeParse(value);
|
|
41
|
+
if (!r.success) throw validationError(r.error, prefix);
|
|
42
|
+
return r.data;
|
|
43
|
+
}
|
|
44
|
+
const zJson = (schema) => validator("json", (value) => parseWith(schema, value, "body"));
|
|
45
|
+
const zQuery = (schema) => validator("query", (value) => parseWith(schema, value, "query"));
|
|
46
|
+
/** Validates `:id` route params with the contracts `Id`. */
|
|
47
|
+
function idParam(c, name = "id") {
|
|
48
|
+
const r = Id.safeParse(c.req.param(name));
|
|
49
|
+
if (!r.success) throw new TelegramPluginError("not_found", `${name} not found`);
|
|
50
|
+
return r.data;
|
|
51
|
+
}
|
|
52
|
+
/** Opaque cursor = base64url of the last row id. */
|
|
53
|
+
function encodeCursor(id) {
|
|
54
|
+
return btoa(id).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
55
|
+
}
|
|
56
|
+
function decodeCursor(cursor) {
|
|
57
|
+
if (!cursor) return null;
|
|
58
|
+
try {
|
|
59
|
+
const s = cursor.replaceAll("-", "+").replaceAll("_", "/");
|
|
60
|
+
const id = atob(s.padEnd(Math.ceil(s.length / 4) * 4, "="));
|
|
61
|
+
return Id.safeParse(id).success ? id : null;
|
|
62
|
+
} catch {
|
|
63
|
+
throw new TelegramPluginError("validation_failed", "cursor is not valid", { extensions: { issues: [{
|
|
64
|
+
path: "query.cursor",
|
|
65
|
+
message: "invalid cursor"
|
|
66
|
+
}] } });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Rows fetched with `limit + 1` → `{ items, next }`. */
|
|
70
|
+
function pageOf(rows, limit, map) {
|
|
71
|
+
const more = rows.length > limit;
|
|
72
|
+
const slice = more ? rows.slice(0, limit) : rows;
|
|
73
|
+
const last = slice.at(-1);
|
|
74
|
+
return {
|
|
75
|
+
items: slice.map(map),
|
|
76
|
+
next: more && last ? encodeCursor(last.id) : null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function execOf(c) {
|
|
80
|
+
try {
|
|
81
|
+
return c.executionCtx;
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Sets the runtime and `Cache-Control: private, no-store` on every plugin response. */
|
|
87
|
+
function runtimeMiddleware(deps) {
|
|
88
|
+
return async (c, next) => {
|
|
89
|
+
c.set("tg", deps.runtime(c.env, execOf(c)));
|
|
90
|
+
await next();
|
|
91
|
+
c.res.headers.set("cache-control", NO_STORE);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Admin and /me routes refuse to run on a database behind `SCHEMA_VERSION` (503 schema_outdated). */
|
|
95
|
+
function schemaGuard(deps) {
|
|
96
|
+
return async (c, next) => {
|
|
97
|
+
const key = deps.schemaKey(c.env);
|
|
98
|
+
if (!key || !deps.schemaOk.has(key)) {
|
|
99
|
+
const state = await ensureState(c.var.tg);
|
|
100
|
+
if (!state.ok) {
|
|
101
|
+
c.var.tg.log.error("tg.schema_outdated", {
|
|
102
|
+
required: state.required,
|
|
103
|
+
current: state.current,
|
|
104
|
+
missing: state.missing.length
|
|
105
|
+
});
|
|
106
|
+
throw new TelegramPluginError("schema_outdated", `database schema ${state.current ?? "none"} < ${state.required}`);
|
|
107
|
+
}
|
|
108
|
+
if (key) deps.schemaOk.add(key);
|
|
109
|
+
}
|
|
110
|
+
await next();
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Resolves the host actor (`ports.actor.fromRequest`); 401 `unauthenticated` when nobody is signed in. */
|
|
114
|
+
const actorMiddleware = async (c, next) => {
|
|
115
|
+
const actor = await c.var.tg.actor.fromRequest(c);
|
|
116
|
+
if (actor === null || actor === void 0) throw new TelegramPluginError("unauthenticated", "sign in required");
|
|
117
|
+
c.set("tgActor", actor);
|
|
118
|
+
await next();
|
|
119
|
+
};
|
|
120
|
+
/** Requires one plugin permission; then runs an armed `idempotencyMiddleware` (so a replay is never unchecked). */
|
|
121
|
+
function requirePermission(name) {
|
|
122
|
+
return async (c, next) => {
|
|
123
|
+
if (!await c.var.tg.can(c.var.tgActor, name)) throw new TelegramPluginError("forbidden", `missing ${c.var.tg.perm(name)}`);
|
|
124
|
+
c.set("tgPermChecked", true);
|
|
125
|
+
const key = c.var.tgIdem === "armed" ? idempotencyKeyOf(c) : null;
|
|
126
|
+
if (key !== null) {
|
|
127
|
+
c.set("tgIdem", "done");
|
|
128
|
+
return runIdempotent(c, next, key);
|
|
129
|
+
}
|
|
130
|
+
await next();
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** 404 `feature_disabled` for every route of a feature that is off. */
|
|
134
|
+
function featureGate(features, key) {
|
|
135
|
+
return async (_c, next) => {
|
|
136
|
+
if (features[key] !== true) throw featureDisabled(key);
|
|
137
|
+
await next();
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Stable per-caller key part: the host user id, else the role (actors without a user id). */
|
|
141
|
+
function actorKey(c) {
|
|
142
|
+
const d = c.var.tg.describe(c.var.tgActor);
|
|
143
|
+
return d.userId !== null ? `user:${d.userId}` : `role:${d.role ?? ""}`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Routes whose answer holds a secret (plaintext one-time link codes) are never stored. A retry after the first
|
|
147
|
+
* request finished runs again (a fresh code); a retry while it is still in flight gets 409.
|
|
148
|
+
*/
|
|
149
|
+
const NO_STORE_PATHS = ["/admin/links", "/me/link"];
|
|
150
|
+
/** Per-route opt-out: a handler calls this so its response is never written to `tg_state`. */
|
|
151
|
+
function skipIdempotencyStore(c) {
|
|
152
|
+
c.set("tgIdemNoStore", true);
|
|
153
|
+
}
|
|
154
|
+
function isJson(contentType) {
|
|
155
|
+
return contentType !== null && /^application\/([a-z0-9.+-]*\+)?json\b/i.test(contentType);
|
|
156
|
+
}
|
|
157
|
+
async function runIdempotent(c, next, key) {
|
|
158
|
+
const rt = c.var.tg;
|
|
159
|
+
const path = new URL(c.req.url).pathname;
|
|
160
|
+
const slot = await reserveIdempotency(rt, {
|
|
161
|
+
key,
|
|
162
|
+
actor: actorKey(c),
|
|
163
|
+
method: c.req.method,
|
|
164
|
+
path,
|
|
165
|
+
body: await c.req.text()
|
|
166
|
+
});
|
|
167
|
+
if (slot.replay) {
|
|
168
|
+
const r = slot.replay;
|
|
169
|
+
return new Response(r.body === null ? null : JSON.stringify(r.body), {
|
|
170
|
+
status: r.status,
|
|
171
|
+
headers: {
|
|
172
|
+
"content-type": r.contentType ?? "application/json",
|
|
173
|
+
"idempotent-replay": "true"
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const timer = setInterval(() => {
|
|
178
|
+
slot.renew().catch(() => {});
|
|
179
|
+
}, IDEMPOTENCY_RENEW_MS);
|
|
180
|
+
try {
|
|
181
|
+
await next();
|
|
182
|
+
} catch (e) {
|
|
183
|
+
await slot.release();
|
|
184
|
+
throw e;
|
|
185
|
+
} finally {
|
|
186
|
+
clearInterval(timer);
|
|
187
|
+
}
|
|
188
|
+
const contentType = c.res.headers.get("content-type");
|
|
189
|
+
const noStore = c.var.tgIdemNoStore === true || NO_STORE_PATHS.some((p) => path.endsWith(p));
|
|
190
|
+
if (c.error || c.res.status >= 500 || noStore || !isJson(contentType)) {
|
|
191
|
+
await slot.release();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const text = await c.res.clone().text();
|
|
195
|
+
let body = null;
|
|
196
|
+
try {
|
|
197
|
+
body = text === "" ? null : JSON.parse(text);
|
|
198
|
+
} catch {
|
|
199
|
+
await slot.release();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await slot.save({
|
|
203
|
+
status: c.res.status,
|
|
204
|
+
body,
|
|
205
|
+
contentType
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function idempotencyKeyOf(c) {
|
|
209
|
+
if (c.req.method === "GET" || c.req.method === "HEAD") return null;
|
|
210
|
+
return c.req.header("idempotency-key") ?? null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* `Idempotency-Key` on writes: replays the stored response for a repeated key. The replay never runs before the
|
|
214
|
+
* route's permission check: placed before `requirePermission` it only arms it, and the check runs it after passing.
|
|
215
|
+
*/
|
|
216
|
+
const idempotencyMiddleware = async (c, next) => {
|
|
217
|
+
const key = idempotencyKeyOf(c);
|
|
218
|
+
if (key === null) return next();
|
|
219
|
+
if (c.var.tgPermChecked === true) return runIdempotent(c, next, key);
|
|
220
|
+
c.set("tgIdem", "armed");
|
|
221
|
+
await next();
|
|
222
|
+
};
|
|
223
|
+
export { NO_STORE, actorKey, actorMiddleware, decodeCursor, encodeCursor, errorHandler, featureGate, idParam, idempotencyMiddleware, pageOf, problemBody, problemResponse, requirePermission, runtimeMiddleware, schemaGuard, skipIdempotencyStore, validationError, zJson, zQuery };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SelfLinkCreate } from "../contracts/links.js";
|
|
2
|
+
import { SubscriptionSave } from "../contracts/subscriptions.js";
|
|
3
|
+
import { TelegramPluginError } from "../engine/errors.js";
|
|
4
|
+
import { chatDto } from "../engine/rows.js";
|
|
5
|
+
import { myChats, requireOwnChat, unlinkChat } from "../services/chats.js";
|
|
6
|
+
import { confirmPendingLink, createSelfLink, declinePendingLink, listPendingLinks } from "../services/links.js";
|
|
7
|
+
import { listSubscriptions, replaceSubscriptions } from "../services/subscriptions.js";
|
|
8
|
+
import { actorMiddleware, errorHandler, featureGate, idParam, idempotencyMiddleware, requirePermission, runtimeMiddleware, schemaGuard, zJson } from "./http.js";
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
// src/routes/me.ts
|
|
11
|
+
function userIdOf(c) {
|
|
12
|
+
const id = c.var.tg.describe(c.var.tgActor).userId;
|
|
13
|
+
if (!id) throw new TelegramPluginError("forbidden", "only signed-in users have their own chats");
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
const needSelfLink = requirePermission("telegram.self.link");
|
|
17
|
+
/**
|
|
18
|
+
* `telegram.self.link` for every `/me` route except the pending-invite ones: confirming an admin invite needs only
|
|
19
|
+
* a signed-in user whose id is the invite's user (checked in the service), so invited users without self-link can
|
|
20
|
+
* still accept.
|
|
21
|
+
*/
|
|
22
|
+
const selfLinkPermission = async (c, next) => {
|
|
23
|
+
if (c.req.path.includes("/me/links/pending")) return next();
|
|
24
|
+
return needSelfLink(c, next);
|
|
25
|
+
};
|
|
26
|
+
function meRoutes(deps) {
|
|
27
|
+
const app = new Hono();
|
|
28
|
+
app.onError(errorHandler(deps.errors));
|
|
29
|
+
app.use("/me/*", runtimeMiddleware(deps), schemaGuard(deps), actorMiddleware, selfLinkPermission, idempotencyMiddleware);
|
|
30
|
+
app.use("/me/subscriptions", featureGate(deps.features, "subscriptions"));
|
|
31
|
+
return app.post("/me/link", zJson(SelfLinkCreate), async (c) => {
|
|
32
|
+
return c.json(await createSelfLink(c.var.tg, c.var.tgActor, c.req.valid("json").bot), 201);
|
|
33
|
+
}).get("/me/links/pending", async (c) => {
|
|
34
|
+
return c.json({ items: await listPendingLinks(c.var.tg, c.var.tgActor) });
|
|
35
|
+
}).post("/me/links/pending/:id/confirm", async (c) => {
|
|
36
|
+
return c.json({ chat: await confirmPendingLink(c.var.tg, c.var.tgActor, idParam(c)) });
|
|
37
|
+
}).post("/me/links/pending/:id/decline", async (c) => {
|
|
38
|
+
await declinePendingLink(c.var.tg, c.var.tgActor, idParam(c));
|
|
39
|
+
return c.json({ ok: true });
|
|
40
|
+
}).get("/me/chats", async (c) => {
|
|
41
|
+
const page = {
|
|
42
|
+
items: (await myChats(c.var.tg, userIdOf(c))).map(chatDto),
|
|
43
|
+
next: null
|
|
44
|
+
};
|
|
45
|
+
return c.json(page);
|
|
46
|
+
}).delete("/me/chats/:id", async (c) => {
|
|
47
|
+
const chat = await requireOwnChat(c.var.tg, userIdOf(c), idParam(c));
|
|
48
|
+
await unlinkChat(c.var.tg, c.var.tgActor, chat.id);
|
|
49
|
+
return c.json({ ok: true });
|
|
50
|
+
}).get("/me/subscriptions", async (c) => {
|
|
51
|
+
const rows = await myChats(c.var.tg, userIdOf(c));
|
|
52
|
+
return c.json({ items: await listSubscriptions(c.var.tg, rows.map((r) => r.id)) });
|
|
53
|
+
}).put("/me/subscriptions", zJson(SubscriptionSave), async (c) => {
|
|
54
|
+
const body = c.req.valid("json");
|
|
55
|
+
await requireOwnChat(c.var.tg, userIdOf(c), body.chatRef);
|
|
56
|
+
return c.json({ items: await replaceSubscriptions(c.var.tg, body) });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export { meRoutes };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PageQuery } from "../contracts/common.js";
|
|
2
|
+
import { RecipientSpec } from "../contracts/recipients.js";
|
|
3
|
+
import { NextRunsRequest, SchedulePause, ScheduleSave } from "../contracts/schedules.js";
|
|
4
|
+
import { scheduleDto, scheduleRunDto } from "../engine/schedules/rows.js";
|
|
5
|
+
import { decodeCursor, featureGate, idParam, pageOf, requirePermission, validationError, zJson, zQuery } from "./http.js";
|
|
6
|
+
import { createSchedule, deleteSchedule, getSchedule, listRuns, listSchedules, pauseSchedule, previewNextRuns, resumeSchedule, runScheduleNow, updateSchedule } from "../services/schedules.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
// src/routes/schedules.ts
|
|
10
|
+
/** Zone and recipient count are checked by the service, so they answer `timezone_invalid` / `recipient_empty`. */
|
|
11
|
+
const SaveBody = ScheduleSave.extend({
|
|
12
|
+
timezone: z.string().min(1).max(64).optional(),
|
|
13
|
+
recipients: z.array(RecipientSpec).max(100)
|
|
14
|
+
});
|
|
15
|
+
const PERM = "telegram.schedules.manage";
|
|
16
|
+
function scheduleRoutes(deps) {
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
app.use("/admin/schedules", featureGate(deps.features, "schedules"));
|
|
19
|
+
app.use("/admin/schedules/*", featureGate(deps.features, "schedules"));
|
|
20
|
+
return app.get("/admin/schedules", requirePermission(PERM), zQuery(PageQuery), async (c) => {
|
|
21
|
+
const q = c.req.valid("query");
|
|
22
|
+
const rows = await listSchedules(c.var.tg, {
|
|
23
|
+
cursor: decodeCursor(q.cursor),
|
|
24
|
+
limit: q.limit
|
|
25
|
+
});
|
|
26
|
+
return c.json(pageOf(rows, q.limit, scheduleDto));
|
|
27
|
+
}).post("/admin/schedules", requirePermission(PERM), zJson(SaveBody), async (c) => {
|
|
28
|
+
return c.json(await createSchedule(c.var.tg, c.var.tgActor, c.req.valid("json")), 201);
|
|
29
|
+
}).post("/admin/schedules/next-runs", requirePermission(PERM), zJson(NextRunsRequest), async (c) => {
|
|
30
|
+
return c.json(previewNextRuns(c.var.tg, c.req.valid("json")));
|
|
31
|
+
}).get("/admin/schedules/:id", requirePermission(PERM), async (c) => {
|
|
32
|
+
return c.json(await getSchedule(c.var.tg, idParam(c)));
|
|
33
|
+
}).put("/admin/schedules/:id", requirePermission(PERM), zJson(SaveBody), async (c) => {
|
|
34
|
+
return c.json(await updateSchedule(c.var.tg, c.var.tgActor, idParam(c), c.req.valid("json")));
|
|
35
|
+
}).delete("/admin/schedules/:id", requirePermission(PERM), async (c) => {
|
|
36
|
+
await deleteSchedule(c.var.tg, c.var.tgActor, idParam(c));
|
|
37
|
+
return c.json({ ok: true });
|
|
38
|
+
}).post("/admin/schedules/:id/pause", requirePermission(PERM), async (c) => {
|
|
39
|
+
const text = await c.req.text();
|
|
40
|
+
let raw = {};
|
|
41
|
+
try {
|
|
42
|
+
raw = text.trim() === "" ? {} : JSON.parse(text);
|
|
43
|
+
} catch {
|
|
44
|
+
raw = null;
|
|
45
|
+
}
|
|
46
|
+
const body = SchedulePause.safeParse(raw);
|
|
47
|
+
if (!body.success) throw validationError(body.error, "body");
|
|
48
|
+
return c.json(await pauseSchedule(c.var.tg, c.var.tgActor, idParam(c), body.data));
|
|
49
|
+
}).post("/admin/schedules/:id/resume", requirePermission(PERM), async (c) => {
|
|
50
|
+
return c.json(await resumeSchedule(c.var.tg, c.var.tgActor, idParam(c)));
|
|
51
|
+
}).post("/admin/schedules/:id/run-now", requirePermission(PERM), async (c) => {
|
|
52
|
+
return c.json(await runScheduleNow(c.var.tg, c.var.tgActor, idParam(c)));
|
|
53
|
+
}).get("/admin/schedules/:id/runs", requirePermission(PERM), zQuery(PageQuery), async (c) => {
|
|
54
|
+
const q = c.req.valid("query");
|
|
55
|
+
const rows = await listRuns(c.var.tg, idParam(c), {
|
|
56
|
+
cursor: decodeCursor(q.cursor),
|
|
57
|
+
limit: q.limit
|
|
58
|
+
});
|
|
59
|
+
return c.json(pageOf(rows, q.limit, scheduleRunDto));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export { scheduleRoutes };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SendIntent } from "../contracts/send.js";
|
|
2
|
+
import { sha256Hex } from "../core/random.js";
|
|
3
|
+
import { enqueueIntent, previewIntent } from "../services/send.js";
|
|
4
|
+
import { actorKey, requirePermission, zJson } from "./http.js";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
// src/routes/send.ts
|
|
7
|
+
function sendRoutes(_deps) {
|
|
8
|
+
return new Hono().post("/admin/send/preview", requirePermission("telegram.send"), zJson(SendIntent), async (c) => {
|
|
9
|
+
return c.json(await previewIntent(c.var.tg, c.var.tgActor, c.req.valid("json")));
|
|
10
|
+
}).post("/admin/send", requirePermission("telegram.send"), zJson(SendIntent), async (c) => {
|
|
11
|
+
const rt = c.var.tg;
|
|
12
|
+
const key = c.req.header("idempotency-key");
|
|
13
|
+
const intent = c.req.valid("json");
|
|
14
|
+
const dedupeKey = key ? `api:${(await sha256Hex(crypto.subtle, `/admin/send|${actorKey(c)}|${key}|${JSON.stringify(intent)}`)).slice(0, 40)}` : `api:${rt.ids.uuid()}`;
|
|
15
|
+
const sqls = await enqueueIntent(rt, c.var.tgActor, intent, { dedupeKey });
|
|
16
|
+
const queued = (sqls.length > 0 ? await rt.db.batch(sqls) : []).reduce((n, r, i) => n + (/^\s*INSERT INTO tg_delivery/.test(sqls[i]?.text ?? "") ? r.changes : 0), 0);
|
|
17
|
+
rt.waitUntil(rt.transport.kick());
|
|
18
|
+
return c.json({
|
|
19
|
+
queued,
|
|
20
|
+
dedupeKey
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export { sendRoutes };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PageQuery } from "../contracts/common.js";
|
|
2
|
+
import { ReportRenderRequest, TemplatePreviewRequest, TemplateQuery, TemplateSave } from "../contracts/templates.js";
|
|
3
|
+
import { TelegramPluginError } from "../engine/errors.js";
|
|
4
|
+
import { buildReport } from "../services/reports.js";
|
|
5
|
+
import { audienceForPreview } from "../engine/content/audience.js";
|
|
6
|
+
import { requireTemplate, requireTemplateByKey, templateDto, templateModel } from "../engine/content/template-rows.js";
|
|
7
|
+
import { decodeCursor, featureGate, idParam, pageOf, requirePermission, zJson, zQuery } from "./http.js";
|
|
8
|
+
import { createTemplate, deleteTemplate, listTemplates, listVersions, previewTemplate, restoreVersion, updateTemplate } from "../services/templates.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
// src/routes/templates.ts
|
|
12
|
+
const ListQuery = TemplateQuery.extend(PageQuery.shape);
|
|
13
|
+
const Version = z.coerce.number().int().min(1);
|
|
14
|
+
/** RFC 6266 filename (ASCII fallback + UTF-8 form for Marathi names). */
|
|
15
|
+
function disposition(name) {
|
|
16
|
+
return `attachment; filename="${name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_")}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
|
17
|
+
}
|
|
18
|
+
function templateRoutes(deps) {
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.use("/admin/templates", featureGate(deps.features, "templates"));
|
|
21
|
+
app.use("/admin/templates/*", featureGate(deps.features, "templates"));
|
|
22
|
+
app.use("/admin/reports/*", featureGate(deps.features, "reports"));
|
|
23
|
+
return app.get("/admin/templates", requirePermission("telegram.templates.manage"), zQuery(ListQuery), async (c) => {
|
|
24
|
+
const q = c.req.valid("query");
|
|
25
|
+
const rows = await listTemplates(c.var.tg, q, {
|
|
26
|
+
cursor: decodeCursor(q.cursor),
|
|
27
|
+
limit: q.limit
|
|
28
|
+
});
|
|
29
|
+
return c.json(pageOf(rows, q.limit, templateDto));
|
|
30
|
+
}).post("/admin/templates", requirePermission("telegram.templates.manage"), zJson(TemplateSave), async (c) => {
|
|
31
|
+
return c.json(await createTemplate(c.var.tg, c.var.tgActor, c.req.valid("json")), 201);
|
|
32
|
+
}).post("/admin/templates/preview", requirePermission("telegram.templates.manage"), zJson(TemplatePreviewRequest), async (c) => {
|
|
33
|
+
return c.json(await previewTemplate(c.var.tg, c.var.tgActor, c.req.valid("json")));
|
|
34
|
+
}).get("/admin/templates/:id", requirePermission("telegram.templates.manage"), async (c) => {
|
|
35
|
+
return c.json(templateDto(await requireTemplate(c.var.tg, idParam(c))));
|
|
36
|
+
}).put("/admin/templates/:id", requirePermission("telegram.templates.manage"), zJson(TemplateSave), async (c) => {
|
|
37
|
+
return c.json(await updateTemplate(c.var.tg, c.var.tgActor, idParam(c), c.req.valid("json")));
|
|
38
|
+
}).delete("/admin/templates/:id", requirePermission("telegram.templates.manage"), async (c) => {
|
|
39
|
+
await deleteTemplate(c.var.tg, c.var.tgActor, idParam(c));
|
|
40
|
+
return c.json({ ok: true });
|
|
41
|
+
}).get("/admin/templates/:id/versions", requirePermission("telegram.templates.manage"), async (c) => {
|
|
42
|
+
return c.json({ items: await listVersions(c.var.tg, idParam(c)) });
|
|
43
|
+
}).post("/admin/templates/:id/restore/:version", requirePermission("telegram.templates.manage"), async (c) => {
|
|
44
|
+
const v = Version.safeParse(c.req.param("version"));
|
|
45
|
+
if (!v.success) throw new TelegramPluginError("not_found", "version not found");
|
|
46
|
+
return c.json(await restoreVersion(c.var.tg, c.var.tgActor, idParam(c), v.data));
|
|
47
|
+
}).post("/admin/reports/render", requirePermission("telegram.reports.run"), zJson(ReportRenderRequest), async (c) => {
|
|
48
|
+
const rt = c.var.tg;
|
|
49
|
+
const req = c.req.valid("json");
|
|
50
|
+
const template = templateModel(await requireTemplateByKey(rt, req.template));
|
|
51
|
+
const actor = await audienceForPreview(rt, req.as);
|
|
52
|
+
const locale = req.locale ?? rt.describe(actor).locale;
|
|
53
|
+
const built = await buildReport(rt, {
|
|
54
|
+
template,
|
|
55
|
+
actor,
|
|
56
|
+
params: req.params,
|
|
57
|
+
locales: [locale],
|
|
58
|
+
tz: rt.settings.timezone,
|
|
59
|
+
format: req.format,
|
|
60
|
+
now: rt.clock.now(),
|
|
61
|
+
viewer: c.var.tgActor
|
|
62
|
+
});
|
|
63
|
+
return new Response(built.file.body, { headers: {
|
|
64
|
+
"content-type": built.file.mime,
|
|
65
|
+
"content-disposition": disposition(built.file.name),
|
|
66
|
+
...built.file.size === void 0 ? {} : { "content-length": String(built.file.size) }
|
|
67
|
+
} });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export { templateRoutes };
|