@prajwalshete/telegram-plugin 0.1.0 → 0.1.2
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 +18 -1
- package/dist/contracts/events.d.ts +1 -1
- package/dist/contracts/problems.d.ts +4 -4
- package/dist/engine/actions/callback.js +30 -2
- package/dist/engine/dialogs/dialog.js +13 -5
- package/dist/engine/registry.d.ts +13 -0
- package/dist/engine/registry.js +2 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/en.json +1 -0
- package/dist/locales/mr.js +1 -0
- package/dist/locales/mr.json +1 -0
- package/dist/server/version.d.ts +1 -1
- package/dist/server/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,24 @@ and the package uses [semantic versioning](https://semver.org/) (0.x: minor vers
|
|
|
6
6
|
Schema changes are listed under **Schema**. Upgrade by applying the new `reference.sql` statements (or
|
|
7
7
|
regenerating your Drizzle migration); the plugin answers `schema_outdated` until the tables match.
|
|
8
8
|
|
|
9
|
-
## 0.1.
|
|
9
|
+
## 0.1.2 (2026-10-29)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `ActionResult.outcome`: a Telegram HTML line that closes the tapped message. The message is edited to its own
|
|
14
|
+
text plus the line, and its keyboard is removed (for example "Approved by Asha · 29 Oct, 7:05 pm"). If Telegram
|
|
15
|
+
refuses the edit, the buttons are disabled as before.
|
|
16
|
+
- Dialog steps `skipRest: true` (optional steps only): `/skip` there also skips the optional steps right after it, so
|
|
17
|
+
a run of photo steps ends with one `/skip`. The step's hint then reads "Send /skip when you are done."
|
|
18
|
+
(`bot.dialog.skip_rest_hint`, en + mr).
|
|
19
|
+
|
|
20
|
+
No schema change. Wire identifiers (keyring salt, queue mark, sql symbol) unchanged.
|
|
21
|
+
|
|
22
|
+
## 0.1.1 (2026-10-28)
|
|
23
|
+
|
|
24
|
+
No code changes: first release published from GitHub Actions with npm Trusted Publishing.
|
|
25
|
+
|
|
26
|
+
## 0.1.0 (2026-10-28)
|
|
10
27
|
|
|
11
28
|
First release.
|
|
12
29
|
|
|
@@ -41,8 +41,8 @@ export declare const EVENT_STATUSES: readonly ["pending", "processing", "done",
|
|
|
41
41
|
export declare const EventStatus: z.ZodEnum<{
|
|
42
42
|
pending: "pending";
|
|
43
43
|
failed: "failed";
|
|
44
|
-
processing: "processing";
|
|
45
44
|
done: "done";
|
|
45
|
+
processing: "processing";
|
|
46
46
|
ignored: "ignored";
|
|
47
47
|
}>;
|
|
48
48
|
export type EventStatus = z.infer<typeof EventStatus>;
|
|
@@ -4,8 +4,6 @@ export declare const TELEGRAM_PROBLEM_CODES: readonly ["feature_disabled", "bot_
|
|
|
4
4
|
/** Zod enum of {@link TELEGRAM_PROBLEM_CODES}. */
|
|
5
5
|
export declare const TelegramProblemCode: z.ZodEnum<{
|
|
6
6
|
timezone_invalid: "timezone_invalid";
|
|
7
|
-
recipient_empty: "recipient_empty";
|
|
8
|
-
schedule_rule_invalid: "schedule_rule_invalid";
|
|
9
7
|
feature_disabled: "feature_disabled";
|
|
10
8
|
bot_token_invalid: "bot_token_invalid";
|
|
11
9
|
bot_token_mismatch: "bot_token_mismatch";
|
|
@@ -21,6 +19,8 @@ export declare const TelegramProblemCode: z.ZodEnum<{
|
|
|
21
19
|
template_variable_unknown: "template_variable_unknown";
|
|
22
20
|
provider_unknown: "provider_unknown";
|
|
23
21
|
provider_forbidden: "provider_forbidden";
|
|
22
|
+
schedule_rule_invalid: "schedule_rule_invalid";
|
|
23
|
+
recipient_empty: "recipient_empty";
|
|
24
24
|
send_text_rejected: "send_text_rejected";
|
|
25
25
|
file_too_large: "file_too_large";
|
|
26
26
|
renderer_unavailable: "renderer_unavailable";
|
|
@@ -59,8 +59,6 @@ export declare const TelegramProblem: z.ZodObject<{
|
|
|
59
59
|
instance: z.ZodOptional<z.ZodString>;
|
|
60
60
|
code: z.ZodEnum<{
|
|
61
61
|
timezone_invalid: "timezone_invalid";
|
|
62
|
-
recipient_empty: "recipient_empty";
|
|
63
|
-
schedule_rule_invalid: "schedule_rule_invalid";
|
|
64
62
|
feature_disabled: "feature_disabled";
|
|
65
63
|
bot_token_invalid: "bot_token_invalid";
|
|
66
64
|
bot_token_mismatch: "bot_token_mismatch";
|
|
@@ -76,6 +74,8 @@ export declare const TelegramProblem: z.ZodObject<{
|
|
|
76
74
|
template_variable_unknown: "template_variable_unknown";
|
|
77
75
|
provider_unknown: "provider_unknown";
|
|
78
76
|
provider_forbidden: "provider_forbidden";
|
|
77
|
+
schedule_rule_invalid: "schedule_rule_invalid";
|
|
78
|
+
recipient_empty: "recipient_empty";
|
|
79
79
|
send_text_rejected: "send_text_rejected";
|
|
80
80
|
file_too_large: "file_too_large";
|
|
81
81
|
renderer_unavailable: "renderer_unavailable";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHtml } from "../../core/html/escape.js";
|
|
1
2
|
import { shortId } from "../../core/random.js";
|
|
2
3
|
import { sql } from "../../ports/sql.js";
|
|
3
4
|
import { pickVariant, renderInline } from "../content/render.js";
|
|
@@ -10,7 +11,8 @@ import { z } from "zod";
|
|
|
10
11
|
const Markup = z.looseObject({ inline_keyboard: z.array(z.array(z.looseObject({ text: z.string() }))) });
|
|
11
12
|
const ActionResultShape = z.looseObject({
|
|
12
13
|
text: z.string().optional(),
|
|
13
|
-
sql: z.array(z.unknown()).optional()
|
|
14
|
+
sql: z.array(z.unknown()).optional(),
|
|
15
|
+
outcome: z.string().optional()
|
|
14
16
|
});
|
|
15
17
|
/**
|
|
16
18
|
* Cuts text to at most `max` UTF-16 code units without splitting a grapheme (emoji, combined Devanagari) or a
|
|
@@ -68,6 +70,32 @@ async function editMarkup(ctx, markup) {
|
|
|
68
70
|
return r.outcome.kind === "ok";
|
|
69
71
|
});
|
|
70
72
|
}
|
|
73
|
+
/** Telegram's text limit for an edited message. */
|
|
74
|
+
const MESSAGE_TEXT_MAX = 4096;
|
|
75
|
+
/**
|
|
76
|
+
* Closes the tapped message with the action's outcome: its own text (the HTML the plugin sent, else the plain text
|
|
77
|
+
* Telegram returned, escaped) + a blank line + `outcome`, and no keyboard. False when it could not be edited.
|
|
78
|
+
*/
|
|
79
|
+
async function closeWithOutcome(ctx, outcome) {
|
|
80
|
+
const msg = ctx.update.callback_query?.message;
|
|
81
|
+
if (!msg || msg.date === 0 || !ctx.chat) return false;
|
|
82
|
+
const row = await ctx.rt.db.first(sql`SELECT body_html FROM tg_delivery WHERE tenant = ${ctx.rt.tenant} AND chat_ref = ${ctx.chat.id}
|
|
83
|
+
AND tg_message_id = ${msg.message_id} AND kind = 'text' AND body_html IS NOT NULL ORDER BY part DESC LIMIT 1`);
|
|
84
|
+
const plain = msg.text;
|
|
85
|
+
const original = row?.body_html ?? (typeof plain === "string" ? escapeHtml(plain) : "");
|
|
86
|
+
const text = original ? `${original}\n\n${outcome}` : outcome;
|
|
87
|
+
if (text.length > MESSAGE_TEXT_MAX) return false;
|
|
88
|
+
return withToken(ctx.rt, ctx.bot, async (api) => {
|
|
89
|
+
const r = await settle(api.editMessageText({
|
|
90
|
+
chat_id: msg.chat.id,
|
|
91
|
+
message_id: msg.message_id,
|
|
92
|
+
text,
|
|
93
|
+
parse_mode: "HTML"
|
|
94
|
+
}));
|
|
95
|
+
if (r.outcome.kind !== "ok") ctx.rt.log.debug("tg.callback_edit_failed", { code: r.outcome.reason ?? r.outcome.kind });
|
|
96
|
+
return r.outcome.kind === "ok";
|
|
97
|
+
});
|
|
98
|
+
}
|
|
71
99
|
const isCallback = (b) => "callback_data" in b;
|
|
72
100
|
/** Every callback button disabled; the tapped one shows the result. Fallback: no keyboard. */
|
|
73
101
|
async function finishKeyboard(ctx, tokenId, label, original) {
|
|
@@ -185,7 +213,7 @@ async function runAction(ctx, def, token, actor, params) {
|
|
|
185
213
|
outcome: "done"
|
|
186
214
|
});
|
|
187
215
|
await answer(ctx, label);
|
|
188
|
-
await finishKeyboard(ctx, token.id, label, original);
|
|
216
|
+
if (!result.outcome || !await closeWithOutcome(ctx, result.outcome)) await finishKeyboard(ctx, token.id, label, original);
|
|
189
217
|
if (result.text) await ctx.reply(result.text);
|
|
190
218
|
} catch (e) {
|
|
191
219
|
rt.log.warn("tg.action_failed", {
|
|
@@ -57,7 +57,7 @@ async function askStep(ctx, step, actor, state, prefix = "") {
|
|
|
57
57
|
const lines = [prefix, escapeHtml(text(step.ask, locale))];
|
|
58
58
|
if (step.input === "photo") lines.push(rt.i18n.t(locale, "bot.dialog.send_photo"));
|
|
59
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"));
|
|
60
|
+
if (step.optional) lines.push(rt.i18n.t(locale, step.skipRest ? "bot.dialog.skip_rest_hint" : "bot.dialog.skip_hint"));
|
|
61
61
|
lines.push(rt.i18n.t(locale, "bot.dialog.cancel_hint"));
|
|
62
62
|
const markup = choices.length > 0 ? { inline_keyboard: choices.map((c, i) => [{
|
|
63
63
|
text: c.l,
|
|
@@ -164,8 +164,11 @@ async function finish(ctx, def, starterActor, data) {
|
|
|
164
164
|
await ctx.reply(rt.i18n.t(locale, "bot.dialog.failed"));
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
/**
|
|
168
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Saves the answer and asks the next step, or finishes. `skipRest`: a `/skip` on a step with `skipRest` also passes
|
|
169
|
+
* over the optional steps that directly follow it.
|
|
170
|
+
*/
|
|
171
|
+
async function advance(ctx, def, row, data, skipRest = false) {
|
|
169
172
|
const { rt } = ctx;
|
|
170
173
|
const actor = row.user_id ? await rt.actor.forUser(row.user_id) : await ctx.actor();
|
|
171
174
|
if (actor === null || !await hostCan(rt, actor, def.permission)) {
|
|
@@ -174,7 +177,12 @@ async function advance(ctx, def, row, data) {
|
|
|
174
177
|
return "handled";
|
|
175
178
|
}
|
|
176
179
|
const index = def.steps.findIndex((s) => s.id === row.step);
|
|
177
|
-
|
|
180
|
+
let after = def.steps.slice(index + 1);
|
|
181
|
+
if (skipRest) {
|
|
182
|
+
const firstRequired = after.findIndex((s) => !s.optional);
|
|
183
|
+
after = firstRequired === -1 ? [] : after.slice(firstRequired);
|
|
184
|
+
}
|
|
185
|
+
const next = after.find((s) => !Object.hasOwn(data.state, s.id));
|
|
178
186
|
if (!next) {
|
|
179
187
|
if (await endDialog(ctx, row)) await finish(ctx, def, actor, data);
|
|
180
188
|
return "handled";
|
|
@@ -291,7 +299,7 @@ async function dialogHandler(ctx) {
|
|
|
291
299
|
await ctx.reply(await ctx.t("bot.dialog.required"));
|
|
292
300
|
return "handled";
|
|
293
301
|
}
|
|
294
|
-
return advance(ctx, def, row, data);
|
|
302
|
+
return advance(ctx, def, row, data, step.skipRest);
|
|
295
303
|
}
|
|
296
304
|
await endDialog(ctx, row);
|
|
297
305
|
rt.log.info("tg.dialog", {
|
|
@@ -260,6 +260,12 @@ export interface ActionResult {
|
|
|
260
260
|
text?: string;
|
|
261
261
|
/** Host writes committed in the same `db.batch()` as the token update. */
|
|
262
262
|
sql?: SqlFragment[];
|
|
263
|
+
/**
|
|
264
|
+
* Telegram HTML line that closes the tapped message: the message is edited to its own text plus this line and
|
|
265
|
+
* its keyboard is removed (for example "Approved by Asha · 29 Oct, 7:05 pm"). Without it the buttons are only
|
|
266
|
+
* disabled. If the edit fails, the plugin falls back to disabling the buttons.
|
|
267
|
+
*/
|
|
268
|
+
outcome?: string;
|
|
263
269
|
}
|
|
264
270
|
/** A callback-button action (`defineAction`); buttons carry an opaque `a:<token>` callback. */
|
|
265
271
|
export interface ActionDef<P = unknown, A = unknown, E = unknown> {
|
|
@@ -316,6 +322,8 @@ export interface DialogStepDef<A = unknown, E = unknown> {
|
|
|
316
322
|
readonly input: z.ZodType | "text" | "photo" | "document";
|
|
317
323
|
/** The user may `/skip` this step. */
|
|
318
324
|
readonly optional: boolean;
|
|
325
|
+
/** `/skip` here also skips the optional steps right after it (for example "more photos" ended by one `/skip`). */
|
|
326
|
+
readonly skipRest: boolean;
|
|
319
327
|
/** Choices offered as buttons, computed from the answers so far. */
|
|
320
328
|
choices?(ctx: {
|
|
321
329
|
actor: A;
|
|
@@ -333,6 +341,11 @@ export interface DialogStepInput<A, E = unknown> {
|
|
|
333
341
|
input?: z.ZodType | "text" | "photo" | "document";
|
|
334
342
|
/** May be skipped. Default false. */
|
|
335
343
|
optional?: boolean;
|
|
344
|
+
/**
|
|
345
|
+
* `/skip` on this (optional) step also skips the optional steps that directly follow it, so a run of optional
|
|
346
|
+
* steps such as photo 1..3 ends with one `/skip`. Default false.
|
|
347
|
+
*/
|
|
348
|
+
skipRest?: boolean;
|
|
336
349
|
/** Choices offered as buttons. */
|
|
337
350
|
choices?(ctx: {
|
|
338
351
|
actor: A;
|
package/dist/engine/registry.js
CHANGED
|
@@ -150,11 +150,13 @@ function defineDialog(def) {
|
|
|
150
150
|
const input = s.input ?? "text";
|
|
151
151
|
if (typeof input !== "string" && !isZod(input)) fail(what, `step ${s.id}: input must be a Zod schema or a kind`);
|
|
152
152
|
const { choices } = s;
|
|
153
|
+
if (s.skipRest && !s.optional) fail(what, `step ${s.id}: skipRest needs optional: true`);
|
|
153
154
|
return Object.freeze({
|
|
154
155
|
id: s.id,
|
|
155
156
|
ask: s.ask,
|
|
156
157
|
input,
|
|
157
158
|
optional: s.optional ?? false,
|
|
159
|
+
skipRest: s.skipRest ?? false,
|
|
158
160
|
...choices ? { choices } : {}
|
|
159
161
|
});
|
|
160
162
|
});
|
package/dist/locales/en.js
CHANGED
|
@@ -283,6 +283,7 @@ var en_default = {
|
|
|
283
283
|
"invalid": "That does not look right. Try again.",
|
|
284
284
|
"cancel_hint": "Send /cancel to stop.",
|
|
285
285
|
"skip_hint": "Send /skip to leave this out.",
|
|
286
|
+
"skip_rest_hint": "Send /skip when you are done.",
|
|
286
287
|
"send_photo": "Send a photo.",
|
|
287
288
|
"send_document": "Send a file.",
|
|
288
289
|
"pick_choice": "Please pick one of the buttons.",
|
package/dist/locales/en.json
CHANGED
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"invalid": "That does not look right. Try again.",
|
|
285
285
|
"cancel_hint": "Send /cancel to stop.",
|
|
286
286
|
"skip_hint": "Send /skip to leave this out.",
|
|
287
|
+
"skip_rest_hint": "Send /skip when you are done.",
|
|
287
288
|
"send_photo": "Send a photo.",
|
|
288
289
|
"send_document": "Send a file.",
|
|
289
290
|
"pick_choice": "Please pick one of the buttons.",
|
package/dist/locales/mr.js
CHANGED
|
@@ -283,6 +283,7 @@ var mr_default = {
|
|
|
283
283
|
"invalid": "हे बरोबर वाटत नाही. पुन्हा प्रयत्न करा.",
|
|
284
284
|
"cancel_hint": "थांबवण्यासाठी /cancel पाठवा.",
|
|
285
285
|
"skip_hint": "हे वगळण्यासाठी /skip पाठवा.",
|
|
286
|
+
"skip_rest_hint": "पूर्ण झाल्यावर /skip पाठवा.",
|
|
286
287
|
"send_photo": "एक फोटो पाठवा.",
|
|
287
288
|
"send_document": "एक फाइल पाठवा.",
|
|
288
289
|
"pick_choice": "कृपया एक बटण निवडा.",
|
package/dist/locales/mr.json
CHANGED
|
@@ -284,6 +284,7 @@
|
|
|
284
284
|
"invalid": "हे बरोबर वाटत नाही. पुन्हा प्रयत्न करा.",
|
|
285
285
|
"cancel_hint": "थांबवण्यासाठी /cancel पाठवा.",
|
|
286
286
|
"skip_hint": "हे वगळण्यासाठी /skip पाठवा.",
|
|
287
|
+
"skip_rest_hint": "पूर्ण झाल्यावर /skip पाठवा.",
|
|
287
288
|
"send_photo": "एक फोटो पाठवा.",
|
|
288
289
|
"send_document": "एक फाइल पाठवा.",
|
|
289
290
|
"pick_choice": "कृपया एक बटण निवडा.",
|
package/dist/server/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept equal to package.json `version` (a test checks it). Reported by `GET /admin/meta`. */
|
|
2
|
-
export declare const PLUGIN_VERSION = "0.1.
|
|
2
|
+
export declare const PLUGIN_VERSION = "0.1.2";
|
package/dist/server/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prajwalshete/telegram-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Telegram bot plugin (logic only, no UI) for Cloudflare Workers apps on Hono + D1 + R2: linking, commands, templates, schedules, reports, events, broadcasts, actions, dialogs and an AI send bridge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Prajwal Shete",
|