@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,375 @@
|
|
|
1
|
+
import { escapeHtml } from "../../core/html/escape.js";
|
|
2
|
+
import { sql } from "../../ports/sql.js";
|
|
3
|
+
import { TelegramPluginError, featureDisabled } from "../errors.js";
|
|
4
|
+
import { pickVariant } from "../content/render.js";
|
|
5
|
+
import { hostCan } from "../content/providers.js";
|
|
6
|
+
import { chatDto, parseJson } from "../rows.js";
|
|
7
|
+
import { answer } from "../actions/callback.js";
|
|
8
|
+
import { inboundKind, storeFailureKey, storeInbound } from "../inbound/files.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
// src/engine/dialogs/dialog.ts
|
|
11
|
+
/** `callback_data` prefix of dialog choice buttons (`d:<step>:<index>`); host buttons must not use it. */
|
|
12
|
+
const DIALOG_PREFIX = "d:";
|
|
13
|
+
const MAX_CHOICES = 50;
|
|
14
|
+
const DialogData = z.object({
|
|
15
|
+
state: z.record(z.string(), z.unknown()).default({}),
|
|
16
|
+
files: z.array(z.object({
|
|
17
|
+
ref: z.string(),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
mime: z.string(),
|
|
20
|
+
size: z.number().nullable()
|
|
21
|
+
})).default([]),
|
|
22
|
+
/** Choices offered for the current step: value + the label shown. */
|
|
23
|
+
choices: z.array(z.object({
|
|
24
|
+
v: z.string(),
|
|
25
|
+
l: z.string()
|
|
26
|
+
})).default([])
|
|
27
|
+
});
|
|
28
|
+
const EMPTY = {
|
|
29
|
+
state: {},
|
|
30
|
+
files: [],
|
|
31
|
+
choices: []
|
|
32
|
+
};
|
|
33
|
+
function text(label, locale) {
|
|
34
|
+
return typeof label === "string" ? label : pickVariant(label, [locale])?.source ?? "";
|
|
35
|
+
}
|
|
36
|
+
async function loadDialog(ctx) {
|
|
37
|
+
if (!ctx.chat) return null;
|
|
38
|
+
return ctx.rt.db.first(sql`SELECT * FROM tg_dialog WHERE tenant = ${ctx.rt.tenant} AND chat_ref = ${ctx.chat.id}`);
|
|
39
|
+
}
|
|
40
|
+
async function endDialog(ctx, row) {
|
|
41
|
+
return (await ctx.rt.db.run(sql`DELETE FROM tg_dialog WHERE tenant = ${ctx.rt.tenant} AND chat_ref = ${row.chat_ref}
|
|
42
|
+
AND updated_at = ${row.updated_at} AND step = ${row.step}`)).changes > 0;
|
|
43
|
+
}
|
|
44
|
+
/** Sends a step's question (choices as `d:<step>:<i>` buttons). Returns the choices offered. */
|
|
45
|
+
async function askStep(ctx, step, actor, state, prefix = "") {
|
|
46
|
+
const { rt } = ctx;
|
|
47
|
+
const locale = await ctx.locale();
|
|
48
|
+
const choices = [];
|
|
49
|
+
if (step.choices) for (const c of (await step.choices({
|
|
50
|
+
actor,
|
|
51
|
+
state,
|
|
52
|
+
env: rt.env
|
|
53
|
+
})).slice(0, MAX_CHOICES)) choices.push({
|
|
54
|
+
v: String(c.value),
|
|
55
|
+
l: text(c.label, locale).slice(0, 64) || String(c.value)
|
|
56
|
+
});
|
|
57
|
+
const lines = [prefix, escapeHtml(text(step.ask, locale))];
|
|
58
|
+
if (step.input === "photo") lines.push(rt.i18n.t(locale, "bot.dialog.send_photo"));
|
|
59
|
+
if (step.input === "document") lines.push(rt.i18n.t(locale, "bot.dialog.send_document"));
|
|
60
|
+
if (step.optional) lines.push(rt.i18n.t(locale, "bot.dialog.skip_hint"));
|
|
61
|
+
lines.push(rt.i18n.t(locale, "bot.dialog.cancel_hint"));
|
|
62
|
+
const markup = choices.length > 0 ? { inline_keyboard: choices.map((c, i) => [{
|
|
63
|
+
text: c.l,
|
|
64
|
+
callback_data: `d:${step.id}:${i}`
|
|
65
|
+
}]) } : null;
|
|
66
|
+
await ctx.replyDelivery({
|
|
67
|
+
kind: "text",
|
|
68
|
+
html: lines.filter(Boolean).join("\n"),
|
|
69
|
+
replyMarkup: markup
|
|
70
|
+
});
|
|
71
|
+
return choices;
|
|
72
|
+
}
|
|
73
|
+
/** Who answers a dialog: the chat's linked user (private) or the sender's own linked account (groups). */
|
|
74
|
+
async function starter(ctx) {
|
|
75
|
+
if (ctx.chat?.kind === "private") {
|
|
76
|
+
const actor = await ctx.actor();
|
|
77
|
+
return actor === null ? null : {
|
|
78
|
+
actor,
|
|
79
|
+
userId: ctx.chat.user_id
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return ctx.senderActor();
|
|
83
|
+
}
|
|
84
|
+
/** `ctx.startDialog(key, state?)`: steps already present in `state` are skipped. */
|
|
85
|
+
async function startDialog(ctx, key, initial = {}) {
|
|
86
|
+
const { rt } = ctx;
|
|
87
|
+
if (!rt.features.dialogs) throw featureDisabled("dialogs");
|
|
88
|
+
const def = rt.registry.dialogs.get(key);
|
|
89
|
+
if (!def) throw new TelegramPluginError("validation_failed", `dialog "${key}" is not registered`);
|
|
90
|
+
const chat = ctx.chat;
|
|
91
|
+
const who = chat?.status === "active" ? await starter(ctx) : null;
|
|
92
|
+
if (!chat || !who) {
|
|
93
|
+
await ctx.reply(await ctx.t("bot.error.not_linked"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!await hostCan(rt, who.actor, def.permission)) {
|
|
97
|
+
await ctx.reply(await ctx.t("bot.error.forbidden"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const state = { ...initial };
|
|
101
|
+
const first = def.steps.find((s) => !Object.hasOwn(state, s.id));
|
|
102
|
+
if (!first) {
|
|
103
|
+
await finish(ctx, def, who.actor, {
|
|
104
|
+
state,
|
|
105
|
+
files: [],
|
|
106
|
+
choices: []
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const now = rt.clock.now();
|
|
111
|
+
const placeholder = JSON.stringify({
|
|
112
|
+
state,
|
|
113
|
+
files: [],
|
|
114
|
+
choices: []
|
|
115
|
+
});
|
|
116
|
+
await rt.db.run(sql`INSERT INTO tg_dialog (tenant, chat_ref, dialog, user_id, step, state_json, expires_at, started_at, updated_at)
|
|
117
|
+
VALUES (${rt.tenant}, ${chat.id}, ${def.key}, ${who.userId}, ${first.id}, ${placeholder},
|
|
118
|
+
${now + def.timeoutMinutes * 6e4}, ${now}, ${now})
|
|
119
|
+
ON CONFLICT (tenant, chat_ref) DO UPDATE SET dialog = excluded.dialog, user_id = excluded.user_id,
|
|
120
|
+
step = excluded.step, state_json = excluded.state_json, expires_at = excluded.expires_at,
|
|
121
|
+
started_at = excluded.started_at, updated_at = excluded.updated_at`);
|
|
122
|
+
const choices = await askStep(ctx, first, who.actor, state);
|
|
123
|
+
if (choices.length > 0) await rt.db.run(sql`UPDATE tg_dialog SET state_json = ${JSON.stringify({
|
|
124
|
+
state,
|
|
125
|
+
files: [],
|
|
126
|
+
choices
|
|
127
|
+
})}
|
|
128
|
+
WHERE tenant = ${rt.tenant} AND chat_ref = ${chat.id} AND step = ${first.id} AND updated_at = ${now}`);
|
|
129
|
+
rt.log.info("tg.dialog", {
|
|
130
|
+
botId: ctx.bot.id,
|
|
131
|
+
dialog: def.key,
|
|
132
|
+
outcome: "started"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function finish(ctx, def, starterActor, data) {
|
|
136
|
+
const { rt } = ctx;
|
|
137
|
+
const locale = await ctx.locale();
|
|
138
|
+
const chat = ctx.chat;
|
|
139
|
+
if (!chat) return;
|
|
140
|
+
try {
|
|
141
|
+
const out = await def.finish({
|
|
142
|
+
actor: starterActor,
|
|
143
|
+
state: data.state,
|
|
144
|
+
files: data.files,
|
|
145
|
+
chat: chatDto(chat),
|
|
146
|
+
locale,
|
|
147
|
+
db: rt.db,
|
|
148
|
+
now: ctx.now,
|
|
149
|
+
env: rt.env
|
|
150
|
+
});
|
|
151
|
+
const reply = z.looseObject({ text: z.string() }).safeParse(out);
|
|
152
|
+
await ctx.reply(reply.success ? reply.data.text : rt.i18n.t(locale, "bot.dialog.done"));
|
|
153
|
+
rt.log.info("tg.dialog", {
|
|
154
|
+
botId: ctx.bot.id,
|
|
155
|
+
dialog: def.key,
|
|
156
|
+
outcome: "finished"
|
|
157
|
+
});
|
|
158
|
+
} catch (e) {
|
|
159
|
+
rt.log.warn("tg.dialog_failed", {
|
|
160
|
+
botId: ctx.bot.id,
|
|
161
|
+
dialog: def.key,
|
|
162
|
+
error: e instanceof Error ? e.name : "error"
|
|
163
|
+
});
|
|
164
|
+
await ctx.reply(rt.i18n.t(locale, "bot.dialog.failed"));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Saves the answer and asks the next step, or finishes. */
|
|
168
|
+
async function advance(ctx, def, row, data) {
|
|
169
|
+
const { rt } = ctx;
|
|
170
|
+
const actor = row.user_id ? await rt.actor.forUser(row.user_id) : await ctx.actor();
|
|
171
|
+
if (actor === null || !await hostCan(rt, actor, def.permission)) {
|
|
172
|
+
await endDialog(ctx, row);
|
|
173
|
+
await ctx.reply(await ctx.t("bot.error.forbidden"));
|
|
174
|
+
return "handled";
|
|
175
|
+
}
|
|
176
|
+
const index = def.steps.findIndex((s) => s.id === row.step);
|
|
177
|
+
const next = def.steps.slice(index + 1).find((s) => !Object.hasOwn(data.state, s.id));
|
|
178
|
+
if (!next) {
|
|
179
|
+
if (await endDialog(ctx, row)) await finish(ctx, def, actor, data);
|
|
180
|
+
return "handled";
|
|
181
|
+
}
|
|
182
|
+
const now = rt.clock.now();
|
|
183
|
+
if ((await rt.db.run(sql`UPDATE tg_dialog SET step = ${next.id}, state_json = ${JSON.stringify({
|
|
184
|
+
...data,
|
|
185
|
+
choices: []
|
|
186
|
+
})},
|
|
187
|
+
expires_at = ${now + def.timeoutMinutes * 6e4}, updated_at = ${now}
|
|
188
|
+
WHERE tenant = ${rt.tenant} AND chat_ref = ${row.chat_ref} AND step = ${row.step}
|
|
189
|
+
AND updated_at = ${row.updated_at}`)).changes === 0) return "handled";
|
|
190
|
+
const choices = await askStep(ctx, next, actor, data.state);
|
|
191
|
+
if (choices.length > 0) await rt.db.run(sql`UPDATE tg_dialog SET state_json = ${JSON.stringify({
|
|
192
|
+
...data,
|
|
193
|
+
choices
|
|
194
|
+
})}
|
|
195
|
+
WHERE tenant = ${rt.tenant} AND chat_ref = ${row.chat_ref} AND step = ${next.id} AND updated_at = ${now}`);
|
|
196
|
+
return "handled";
|
|
197
|
+
}
|
|
198
|
+
function acceptText(step, input, choices) {
|
|
199
|
+
const raw = input.trim();
|
|
200
|
+
if (step.input === "photo") return {
|
|
201
|
+
ok: false,
|
|
202
|
+
key: "bot.dialog.send_photo"
|
|
203
|
+
};
|
|
204
|
+
if (step.input === "document") return {
|
|
205
|
+
ok: false,
|
|
206
|
+
key: "bot.dialog.send_document"
|
|
207
|
+
};
|
|
208
|
+
let value = raw;
|
|
209
|
+
if (choices.length > 0) {
|
|
210
|
+
const lower = raw.toLowerCase();
|
|
211
|
+
const hit = choices.find((c) => c.v === raw || c.l.toLowerCase() === lower);
|
|
212
|
+
if (!hit) return {
|
|
213
|
+
ok: false,
|
|
214
|
+
key: "bot.dialog.pick_choice"
|
|
215
|
+
};
|
|
216
|
+
value = hit.v;
|
|
217
|
+
}
|
|
218
|
+
if (step.input === "text") return raw === "" ? {
|
|
219
|
+
ok: false,
|
|
220
|
+
key: "bot.dialog.invalid"
|
|
221
|
+
} : {
|
|
222
|
+
ok: true,
|
|
223
|
+
value
|
|
224
|
+
};
|
|
225
|
+
const parsed = step.input.safeParse(value);
|
|
226
|
+
return parsed.success ? {
|
|
227
|
+
ok: true,
|
|
228
|
+
value: parsed.data
|
|
229
|
+
} : {
|
|
230
|
+
ok: false,
|
|
231
|
+
key: "bot.dialog.invalid"
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function reask(ctx, step, row, key) {
|
|
235
|
+
const actor = row.user_id ? await ctx.rt.actor.forUser(row.user_id) : await ctx.actor();
|
|
236
|
+
if (actor === null) return;
|
|
237
|
+
await askStep(ctx, step, actor, parseJson(row.state_json, DialogData, EMPTY).state, await ctx.t(key));
|
|
238
|
+
}
|
|
239
|
+
async function dialogHandler(ctx) {
|
|
240
|
+
const { rt } = ctx;
|
|
241
|
+
if (!rt.features.dialogs || ctx.chat?.status !== "active") return "next";
|
|
242
|
+
const q = ctx.update.callback_query;
|
|
243
|
+
const isChoice = ctx.cls === "callback" && q?.data?.startsWith("d:") === true;
|
|
244
|
+
if (ctx.cls !== "command" && ctx.cls !== "text" && ctx.cls !== "media" && !isChoice) return "next";
|
|
245
|
+
const cmd = ctx.cls === "command" ? ctx.command : null;
|
|
246
|
+
if (cmd?.mentionedOther) return "next";
|
|
247
|
+
const row = await loadDialog(ctx);
|
|
248
|
+
if (!row) {
|
|
249
|
+
if (cmd?.command === "cancel" || cmd?.command === "skip") {
|
|
250
|
+
await ctx.reply(await ctx.t("bot.dialog.nothing"));
|
|
251
|
+
return "handled";
|
|
252
|
+
}
|
|
253
|
+
return "next";
|
|
254
|
+
}
|
|
255
|
+
if (ctx.chat.kind !== "private" && row.user_id !== null) {
|
|
256
|
+
if ((await ctx.senderActor())?.userId !== row.user_id) {
|
|
257
|
+
if (isChoice) {
|
|
258
|
+
await answer(ctx, rt.i18n.plain(await ctx.locale(), "bot.action.not_yours"));
|
|
259
|
+
return "handled";
|
|
260
|
+
}
|
|
261
|
+
return "next";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const def = rt.registry.dialogs.get(row.dialog);
|
|
265
|
+
const step = def?.steps.find((s) => s.id === row.step);
|
|
266
|
+
if (!def || !step) {
|
|
267
|
+
await endDialog(ctx, row);
|
|
268
|
+
return "next";
|
|
269
|
+
}
|
|
270
|
+
if (row.expires_at <= rt.clock.now()) {
|
|
271
|
+
await endDialog(ctx, row);
|
|
272
|
+
if (cmd && cmd.command !== "cancel" && cmd.command !== "skip") return "next";
|
|
273
|
+
if (isChoice) await answer(ctx, rt.i18n.plain(await ctx.locale(), "bot.dialog.timeout"));
|
|
274
|
+
else await ctx.reply(await ctx.t("bot.dialog.timeout"));
|
|
275
|
+
return "handled";
|
|
276
|
+
}
|
|
277
|
+
const data = parseJson(row.state_json, DialogData, EMPTY);
|
|
278
|
+
if (cmd) {
|
|
279
|
+
if (cmd.command === "cancel") {
|
|
280
|
+
await endDialog(ctx, row);
|
|
281
|
+
await ctx.reply(await ctx.t("bot.dialog.cancelled"));
|
|
282
|
+
rt.log.info("tg.dialog", {
|
|
283
|
+
botId: ctx.bot.id,
|
|
284
|
+
dialog: def.key,
|
|
285
|
+
outcome: "cancelled"
|
|
286
|
+
});
|
|
287
|
+
return "handled";
|
|
288
|
+
}
|
|
289
|
+
if (cmd.command === "skip") {
|
|
290
|
+
if (!step.optional) {
|
|
291
|
+
await ctx.reply(await ctx.t("bot.dialog.required"));
|
|
292
|
+
return "handled";
|
|
293
|
+
}
|
|
294
|
+
return advance(ctx, def, row, data);
|
|
295
|
+
}
|
|
296
|
+
await endDialog(ctx, row);
|
|
297
|
+
rt.log.info("tg.dialog", {
|
|
298
|
+
botId: ctx.bot.id,
|
|
299
|
+
dialog: def.key,
|
|
300
|
+
outcome: "interrupted"
|
|
301
|
+
});
|
|
302
|
+
return "next";
|
|
303
|
+
}
|
|
304
|
+
if (isChoice && q?.data) {
|
|
305
|
+
const [stepId, idx] = q.data.slice(2).split(":");
|
|
306
|
+
const choice = stepId === step.id ? data.choices[Number(idx)] : void 0;
|
|
307
|
+
if (!choice) {
|
|
308
|
+
await answer(ctx, rt.i18n.plain(await ctx.locale(), "bot.action.expired"));
|
|
309
|
+
return "handled";
|
|
310
|
+
}
|
|
311
|
+
await answer(ctx, choice.l);
|
|
312
|
+
const accepted = acceptText(step, choice.v, data.choices);
|
|
313
|
+
if (!accepted.ok) {
|
|
314
|
+
await reask(ctx, step, row, accepted.key);
|
|
315
|
+
return "handled";
|
|
316
|
+
}
|
|
317
|
+
return advance(ctx, def, row, {
|
|
318
|
+
...data,
|
|
319
|
+
state: {
|
|
320
|
+
...data.state,
|
|
321
|
+
[step.id]: accepted.value
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (ctx.cls === "media") {
|
|
326
|
+
const kind = inboundKind(ctx.message);
|
|
327
|
+
const mime = ctx.message?.document?.mime_type ?? "";
|
|
328
|
+
if (!(step.input === "photo" && (kind === "photo" || mime.startsWith("image/")) || step.input === "document" && kind === "document")) {
|
|
329
|
+
await reask(ctx, step, row, "bot.dialog.invalid");
|
|
330
|
+
return "handled";
|
|
331
|
+
}
|
|
332
|
+
const actor = row.user_id ? await rt.actor.forUser(row.user_id) : await ctx.actor();
|
|
333
|
+
if (actor === null) {
|
|
334
|
+
await endDialog(ctx, row);
|
|
335
|
+
await ctx.reply(await ctx.t("bot.error.forbidden"));
|
|
336
|
+
return "handled";
|
|
337
|
+
}
|
|
338
|
+
const stored = await storeInbound(ctx, actor, {
|
|
339
|
+
source_kind: "dialog",
|
|
340
|
+
dialog: def.key,
|
|
341
|
+
step: step.id
|
|
342
|
+
});
|
|
343
|
+
if (!stored.ok) {
|
|
344
|
+
await reask(ctx, step, row, storeFailureKey(stored.reason));
|
|
345
|
+
return "handled";
|
|
346
|
+
}
|
|
347
|
+
const { ref, name, mime: fileMime, size } = stored.file;
|
|
348
|
+
return advance(ctx, def, row, {
|
|
349
|
+
...data,
|
|
350
|
+
state: {
|
|
351
|
+
...data.state,
|
|
352
|
+
[step.id]: ref
|
|
353
|
+
},
|
|
354
|
+
files: [...data.files, {
|
|
355
|
+
ref,
|
|
356
|
+
name,
|
|
357
|
+
mime: fileMime,
|
|
358
|
+
size
|
|
359
|
+
}]
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const accepted = acceptText(step, ctx.message?.text ?? "", data.choices);
|
|
363
|
+
if (!accepted.ok) {
|
|
364
|
+
await reask(ctx, step, row, accepted.key);
|
|
365
|
+
return "handled";
|
|
366
|
+
}
|
|
367
|
+
return advance(ctx, def, row, {
|
|
368
|
+
...data,
|
|
369
|
+
state: {
|
|
370
|
+
...data.state,
|
|
371
|
+
[step.id]: accepted.value
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
export { DIALOG_PREFIX, dialogHandler, startDialog };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ChatTopics } from "../../contracts/chats.js";
|
|
2
|
+
import { escapeHtml } from "../../core/html/escape.js";
|
|
3
|
+
import { sql } from "../../ports/sql.js";
|
|
4
|
+
import { parseJson } from "../rows.js";
|
|
5
|
+
import { commandRows, describe, effectiveCommands } from "../../services/commands.js";
|
|
6
|
+
import { commandAllowed } from "./commands.js";
|
|
7
|
+
import { hostContext, senderMayAct } from "./context.js";
|
|
8
|
+
// src/engine/dispatch/builtins.ts
|
|
9
|
+
/** Built-in menu entries per chat kind (descriptions: `bot.cmd.<name>`). Phase 3 adds host commands. */
|
|
10
|
+
const BUILTIN_MENU = {
|
|
11
|
+
private: [
|
|
12
|
+
"start",
|
|
13
|
+
"help",
|
|
14
|
+
"stop",
|
|
15
|
+
"settings",
|
|
16
|
+
"whoami",
|
|
17
|
+
"language"
|
|
18
|
+
],
|
|
19
|
+
group: ["help", "whoami"]
|
|
20
|
+
};
|
|
21
|
+
/** Built-ins that work for this runtime: without features.commands only /start and /help. */
|
|
22
|
+
function builtinMenu(rt, kind) {
|
|
23
|
+
return rt.features.commands ? BUILTIN_MENU[kind] : BUILTIN_MENU[kind].filter((c) => c === "start" || c === "help");
|
|
24
|
+
}
|
|
25
|
+
function isGroupKind(type) {
|
|
26
|
+
return type === "group" || type === "supergroup";
|
|
27
|
+
}
|
|
28
|
+
/** Help for this chat: built-ins plus registered commands the actor may use. */
|
|
29
|
+
async function helpText(ctx) {
|
|
30
|
+
const { rt } = ctx;
|
|
31
|
+
const locale = await ctx.locale();
|
|
32
|
+
const isPrivate = ctx.tgChat?.type === "private";
|
|
33
|
+
if (isPrivate && ctx.chat?.status !== "active") return rt.i18n.t(locale, "bot.start.unlinked");
|
|
34
|
+
const lines = [];
|
|
35
|
+
for (const name of builtinMenu(rt, isPrivate ? "private" : "group")) lines.push(`/${name} — ${rt.i18n.t(locale, `bot.cmd.${name}`)}`);
|
|
36
|
+
const actor = await ctx.actor();
|
|
37
|
+
if (rt.features.commands && actor !== null) {
|
|
38
|
+
if (rt.features.reports && await senderMayAct(ctx)) lines.push(`/report — ${rt.i18n.t(locale, "bot.cmd.report")}`);
|
|
39
|
+
const kind = ctx.tgChat?.type ?? "private";
|
|
40
|
+
for (const c of effectiveCommands(rt, await commandRows(rt, ctx.bot.id))) {
|
|
41
|
+
if (!c.enabled || !c.showInMenu || !c.def.scope.includes(kind)) continue;
|
|
42
|
+
if (!await senderMayAct(ctx, c.def.groupMembers)) continue;
|
|
43
|
+
if (!await commandAllowed(ctx, c, actor)) continue;
|
|
44
|
+
lines.push(`/${c.def.command} — ${escapeHtml(describe(c.description, locale))}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return lines.length === 0 ? rt.i18n.t(locale, "bot.help.none") : `${rt.i18n.t(locale, "bot.help.header")}\n${lines.join("\n")}`;
|
|
48
|
+
}
|
|
49
|
+
async function isMuted(ctx, chatRef) {
|
|
50
|
+
return (await ctx.rt.db.first(sql`SELECT muted FROM tg_subscription WHERE tenant = ${ctx.rt.tenant} AND chat_ref = ${chatRef} AND topic = '*'`))?.muted === 1;
|
|
51
|
+
}
|
|
52
|
+
function languageName(ctx, locale, code) {
|
|
53
|
+
const key = `bot.language.names.${code}`;
|
|
54
|
+
const name = ctx.rt.i18n.plain(locale, key);
|
|
55
|
+
return name === key ? code : name;
|
|
56
|
+
}
|
|
57
|
+
async function onHere(ctx) {
|
|
58
|
+
const { rt } = ctx;
|
|
59
|
+
const chat = ctx.chat;
|
|
60
|
+
const thread = ctx.message?.message_thread_id;
|
|
61
|
+
const name = (ctx.command?.argsText ?? "").trim().slice(0, 128);
|
|
62
|
+
if (chat?.status !== "active" || !isGroupKind(ctx.tgChat?.type) || thread === void 0 || name === "") {
|
|
63
|
+
await ctx.reply(await ctx.t("bot.here.not_topic"));
|
|
64
|
+
return "handled";
|
|
65
|
+
}
|
|
66
|
+
const sender = await ctx.senderActor();
|
|
67
|
+
if (!sender || !await rt.can(sender.actor, "telegram.chats.manage")) {
|
|
68
|
+
await ctx.reply(await ctx.t("bot.error.forbidden"));
|
|
69
|
+
return "handled";
|
|
70
|
+
}
|
|
71
|
+
const topics = {
|
|
72
|
+
...parseJson(chat.topics_json, ChatTopics, {}),
|
|
73
|
+
[name]: thread
|
|
74
|
+
};
|
|
75
|
+
await rt.db.batch([sql`UPDATE tg_chat SET topics_json = ${JSON.stringify(topics)}, updated_at = ${ctx.now}
|
|
76
|
+
WHERE tenant = ${rt.tenant} AND id = ${chat.id}`, ...rt.audit({
|
|
77
|
+
actor: sender.userId,
|
|
78
|
+
entity: "telegram_chat",
|
|
79
|
+
entityId: chat.id,
|
|
80
|
+
action: "topic",
|
|
81
|
+
diff: { topic: name },
|
|
82
|
+
at: ctx.now
|
|
83
|
+
})]);
|
|
84
|
+
ctx.chat = {
|
|
85
|
+
...chat,
|
|
86
|
+
topics_json: JSON.stringify(topics)
|
|
87
|
+
};
|
|
88
|
+
await ctx.reply(await ctx.t("bot.here.saved", { name }));
|
|
89
|
+
return "handled";
|
|
90
|
+
}
|
|
91
|
+
/** Commands that work in any chat state: /help, /whoami, /here. The rest need a linked chat. */
|
|
92
|
+
async function builtinsHandler(ctx) {
|
|
93
|
+
const cmd = ctx.command;
|
|
94
|
+
if (ctx.cls !== "command" || !cmd || cmd.mentionedOther) return "next";
|
|
95
|
+
const { rt } = ctx;
|
|
96
|
+
const chat = ctx.chat;
|
|
97
|
+
const linked = chat?.status === "active";
|
|
98
|
+
const isPrivate = ctx.tgChat?.type === "private";
|
|
99
|
+
if (cmd.command === "here") return onHere(ctx);
|
|
100
|
+
if (!rt.features.commands && cmd.command !== "help") return "next";
|
|
101
|
+
switch (cmd.command) {
|
|
102
|
+
case "help":
|
|
103
|
+
await ctx.reply(await helpText(ctx));
|
|
104
|
+
return "handled";
|
|
105
|
+
case "whoami": {
|
|
106
|
+
const info = linked ? await ctx.actorInfo() : null;
|
|
107
|
+
if (!info || !chat) {
|
|
108
|
+
await ctx.reply(await ctx.t("bot.whoami.unlinked"));
|
|
109
|
+
return "handled";
|
|
110
|
+
}
|
|
111
|
+
const name = isPrivate ? info.name ?? "" : chat.title ?? "";
|
|
112
|
+
await ctx.reply(await ctx.t("bot.whoami.linked", {
|
|
113
|
+
name,
|
|
114
|
+
role: info.role ?? ""
|
|
115
|
+
}));
|
|
116
|
+
return "handled";
|
|
117
|
+
}
|
|
118
|
+
case "stop":
|
|
119
|
+
case "settings":
|
|
120
|
+
case "language": break;
|
|
121
|
+
default: return "next";
|
|
122
|
+
}
|
|
123
|
+
if (!linked || !chat) {
|
|
124
|
+
if (isPrivate) await ctx.reply(await ctx.t("bot.start.unlinked"));
|
|
125
|
+
return isPrivate ? "handled" : "ignored";
|
|
126
|
+
}
|
|
127
|
+
if (!isPrivate && cmd.command !== "settings") {
|
|
128
|
+
const sender = await ctx.senderActor();
|
|
129
|
+
if (!sender || !await rt.can(sender.actor, "telegram.chats.manage")) {
|
|
130
|
+
await ctx.reply(await ctx.t("bot.error.forbidden"));
|
|
131
|
+
return "handled";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const locale = await ctx.locale();
|
|
135
|
+
if (cmd.command === "stop") {
|
|
136
|
+
await rt.db.run(sql`INSERT INTO tg_subscription (tenant, chat_ref, topic, muted, quiet_json, updated_at)
|
|
137
|
+
VALUES (${rt.tenant}, ${chat.id}, '*', 1, NULL, ${ctx.now})
|
|
138
|
+
ON CONFLICT (tenant, chat_ref, topic) DO UPDATE SET muted = 1, updated_at = excluded.updated_at`);
|
|
139
|
+
rt.log.info("tg.muted", {
|
|
140
|
+
botId: ctx.bot.id,
|
|
141
|
+
chatRef: chat.id
|
|
142
|
+
});
|
|
143
|
+
await ctx.reply(rt.i18n.t(locale, "bot.stop.done"));
|
|
144
|
+
return "handled";
|
|
145
|
+
}
|
|
146
|
+
if (cmd.command === "settings") {
|
|
147
|
+
const muted = await isMuted(ctx, chat.id);
|
|
148
|
+
const state = rt.i18n.plain(locale, muted ? "bot.settings.muted" : "bot.settings.unmuted");
|
|
149
|
+
await ctx.reply([
|
|
150
|
+
rt.i18n.t(locale, "bot.settings.header"),
|
|
151
|
+
rt.i18n.t(locale, "bot.settings.all", { state }),
|
|
152
|
+
rt.i18n.t(locale, "bot.settings.language", { language: languageName(ctx, locale, locale) })
|
|
153
|
+
].join("\n"));
|
|
154
|
+
return "handled";
|
|
155
|
+
}
|
|
156
|
+
const wanted = cmd.args[0];
|
|
157
|
+
if (!wanted) {
|
|
158
|
+
const list = rt.i18n.locales.map((l) => `/language ${l} — ${escapeHtml(languageName(ctx, locale, l))}`);
|
|
159
|
+
await ctx.reply(`${rt.i18n.t(locale, "bot.language.choose")}\n${list.join("\n")}`);
|
|
160
|
+
return "handled";
|
|
161
|
+
}
|
|
162
|
+
const match = rt.i18n.match(wanted.toLowerCase());
|
|
163
|
+
if (!match) {
|
|
164
|
+
await ctx.reply(rt.i18n.t(locale, "bot.language.unknown", { list: rt.i18n.locales.join(", ") }));
|
|
165
|
+
return "handled";
|
|
166
|
+
}
|
|
167
|
+
await rt.db.run(sql`UPDATE tg_chat SET locale = ${match}, updated_at = ${ctx.now} WHERE tenant = ${rt.tenant} AND id = ${chat.id}`);
|
|
168
|
+
ctx.chat = {
|
|
169
|
+
...chat,
|
|
170
|
+
locale: match
|
|
171
|
+
};
|
|
172
|
+
await ctx.reply(rt.i18n.t(match, "bot.language.set"));
|
|
173
|
+
return "handled";
|
|
174
|
+
}
|
|
175
|
+
/** Last stop: unknown commands get a hint (groups only when addressed to this bot); other text is ignored. */
|
|
176
|
+
async function fallbackHandler(ctx) {
|
|
177
|
+
const isPrivate = ctx.tgChat?.type === "private";
|
|
178
|
+
const cmd = ctx.command;
|
|
179
|
+
if (ctx.cls === "command" && cmd && !cmd.mentionedOther) {
|
|
180
|
+
if (!isPrivate && cmd.mention === null) return "ignored";
|
|
181
|
+
if (isPrivate && ctx.chat?.status !== "active") {
|
|
182
|
+
await ctx.reply(await ctx.t("bot.start.unlinked"));
|
|
183
|
+
return "handled";
|
|
184
|
+
}
|
|
185
|
+
if (ctx.rt.hooks.onUnknownCommand) {
|
|
186
|
+
const host = await hostContext(ctx, ctx.message?.text ?? "");
|
|
187
|
+
await ctx.rt.hook("onUnknownCommand", (fn) => fn(host));
|
|
188
|
+
}
|
|
189
|
+
await ctx.reply(await ctx.t("bot.error.unknown_command"));
|
|
190
|
+
return "handled";
|
|
191
|
+
}
|
|
192
|
+
if (isPrivate && (ctx.cls === "text" || ctx.cls === "media")) {
|
|
193
|
+
await ctx.reply(await ctx.t(ctx.chat?.status === "active" ? "bot.error.unknown_command" : "bot.start.unlinked"));
|
|
194
|
+
return "handled";
|
|
195
|
+
}
|
|
196
|
+
return "ignored";
|
|
197
|
+
}
|
|
198
|
+
export { BUILTIN_MENU, builtinMenu, builtinsHandler, fallbackHandler, helpText };
|