@monixlite/grammy-scenes 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -23
- package/package.json +2 -2
- package/src/index.js +40 -14
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @monixlite/grammy-scenes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight FSM (Finite State Machine) engine for Telegram bots built on grammY.
|
|
4
|
+
The module implements step-based scenes with declarative step definitions, automatic transitions, session-based state management, and an extensible navigation system.
|
|
5
|
+
|
|
6
|
+
---
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -10,8 +13,22 @@ npm install @monixlite/grammy-scenes
|
|
|
10
13
|
|
|
11
14
|
## Requirements
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
• Node.js >= 16
|
|
17
|
+
• grammY >= 1.19
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Core Concept
|
|
22
|
+
|
|
23
|
+
A scene is a sequence of steps united under a common title.
|
|
24
|
+
Each step is described by an object containing:
|
|
25
|
+
|
|
26
|
+
• scene and step identifiers
|
|
27
|
+
• a set of callbacks reacting to Telegram events (enter, message, callbackQuery)
|
|
28
|
+
|
|
29
|
+
The current scene state is stored in `ctx.session.scene`.
|
|
30
|
+
|
|
31
|
+
---
|
|
15
32
|
|
|
16
33
|
## Basic Usage
|
|
17
34
|
|
|
@@ -20,13 +37,10 @@ const { Bot, session } = require("grammy");
|
|
|
20
37
|
const { Scenes } = require("@monixlite/grammy-scenes");
|
|
21
38
|
|
|
22
39
|
|
|
23
|
-
const bot = new Bot(""); {
|
|
40
|
+
const bot = new Bot("..."); {
|
|
24
41
|
bot.use(session({
|
|
25
42
|
initial: () => ({
|
|
26
43
|
scene: null,
|
|
27
|
-
step: null,
|
|
28
|
-
name: null,
|
|
29
|
-
age: null,
|
|
30
44
|
}),
|
|
31
45
|
}));
|
|
32
46
|
};
|
|
@@ -42,22 +56,100 @@ const scene = new Scenes([
|
|
|
42
56
|
|
|
43
57
|
callbacks: {
|
|
44
58
|
enter: async (ctx) => {
|
|
45
|
-
await ctx.reply("
|
|
59
|
+
await ctx.reply("• Enter your phone number:");
|
|
46
60
|
},
|
|
47
61
|
|
|
48
62
|
message: async (ctx) => {
|
|
49
|
-
|
|
63
|
+
ctx.session.phone = ctx.message.text.trim();
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
scene: {
|
|
70
|
+
title: "auth",
|
|
71
|
+
step: "confirm",
|
|
72
|
+
},
|
|
50
73
|
|
|
51
|
-
|
|
74
|
+
callbacks: {
|
|
75
|
+
enter: async (ctx) => {
|
|
76
|
+
const { phone } = ctx.session;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
await ctx.reply(([
|
|
80
|
+
`• Your phone number: ${phone}`,
|
|
81
|
+
`• Is this correct?`
|
|
82
|
+
]).join("\n"), {
|
|
83
|
+
reply_markup: {
|
|
84
|
+
inline_keyboard: [
|
|
85
|
+
[
|
|
86
|
+
{
|
|
87
|
+
text: "• Yes",
|
|
88
|
+
callback_data: "confirm:yes"
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
text: "• No",
|
|
93
|
+
callback_data: "confirm:no"
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
52
99
|
},
|
|
100
|
+
|
|
101
|
+
query: [
|
|
102
|
+
{
|
|
103
|
+
match: /^confirm:yes$/,
|
|
104
|
+
handler: async (ctx) => {
|
|
105
|
+
const { phone } = ctx.session;
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
await ctx.answerCallbackQuery();
|
|
109
|
+
await ctx.reply(`• Thank you! Your phone number ${phone} has been saved.`);
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
return "stop";
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
match: /^confirm:no$/,
|
|
117
|
+
handler: async (ctx) => {
|
|
118
|
+
await ctx.answerCallbackQuery();
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
return "^phone";
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
53
125
|
},
|
|
54
126
|
},
|
|
55
127
|
],
|
|
56
|
-
]
|
|
128
|
+
], {
|
|
129
|
+
["callbacks:enter:buttons:cancel"]: {
|
|
130
|
+
enabled: true,
|
|
131
|
+
|
|
132
|
+
component: {
|
|
133
|
+
text: "• Cancel scene •",
|
|
134
|
+
callback_data: "scene:cancel",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
bot.callbackQuery("scene:cancel", async (ctx) => {
|
|
141
|
+
ctx.session.scene = null;
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
await ctx.editMessageReplyMarkup(null);
|
|
145
|
+
await ctx.answerCallbackQuery();
|
|
146
|
+
|
|
147
|
+
await ctx.reply("• Scene cancelled!");
|
|
148
|
+
});
|
|
57
149
|
|
|
58
150
|
|
|
59
151
|
bot.command("start", async (ctx) => {
|
|
60
|
-
await scene.enter(ctx, {
|
|
152
|
+
return await scene.enter(ctx, {
|
|
61
153
|
title: "auth",
|
|
62
154
|
step: "phone",
|
|
63
155
|
});
|
|
@@ -69,9 +161,11 @@ bot.use(scene.middleware);
|
|
|
69
161
|
bot.start();
|
|
70
162
|
```
|
|
71
163
|
|
|
164
|
+
---
|
|
165
|
+
|
|
72
166
|
## Scene Definition
|
|
73
167
|
|
|
74
|
-
|
|
168
|
+
Each scene step is described by the following structure:
|
|
75
169
|
|
|
76
170
|
```js
|
|
77
171
|
{
|
|
@@ -82,23 +176,77 @@ bot.start();
|
|
|
82
176
|
|
|
83
177
|
callbacks: {
|
|
84
178
|
enter?: (ctx) => Promise<void>,
|
|
85
|
-
|
|
179
|
+
update?: (ctx) => Promise<SceneResult | void>,
|
|
180
|
+
message?: (ctx) => Promise<SceneResult | void>,
|
|
181
|
+
query?: Array<{
|
|
182
|
+
match: RegExp,
|
|
183
|
+
handler: (ctx) => Promise<SceneResult | void>,
|
|
184
|
+
}>,
|
|
86
185
|
},
|
|
87
186
|
}
|
|
88
187
|
```
|
|
89
188
|
|
|
90
|
-
|
|
189
|
+
`SceneResult` is a value that determines the next transition (described below).
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Callbacks
|
|
194
|
+
|
|
195
|
+
### callbacks.enter(ctx)
|
|
196
|
+
|
|
197
|
+
Triggered when entering a scene step.
|
|
198
|
+
Used to send messages, keyboards, or initialize data.
|
|
199
|
+
|
|
200
|
+
Details:
|
|
201
|
+
|
|
202
|
+
• Executed automatically on step entry
|
|
203
|
+
• Can freely use `ctx.reply`
|
|
204
|
+
• If the cancel button option is enabled, it is automatically appended to the inline keyboard
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### callbacks.update(ctx)
|
|
209
|
+
|
|
210
|
+
Triggered on **any update** while the step is active.
|
|
211
|
+
|
|
212
|
+
Details:
|
|
91
213
|
|
|
92
|
-
|
|
214
|
+
• Executed before `message` and `query`
|
|
215
|
+
• Receives the raw grammY context
|
|
216
|
+
• Can be used for global step logic (timeouts, guards, media handling)
|
|
217
|
+
• Return value controls navigation using standard SceneResult rules
|
|
93
218
|
|
|
94
|
-
|
|
219
|
+
---
|
|
95
220
|
|
|
96
|
-
|
|
97
|
-
|
|
221
|
+
### callbacks.message(ctx)
|
|
222
|
+
|
|
223
|
+
Triggered when a text message is received while the step is active.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### callbacks.query
|
|
228
|
+
|
|
229
|
+
An array of `callbackQuery` handlers.
|
|
230
|
+
|
|
231
|
+
Each handler has the following shape:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
{
|
|
235
|
+
match: RegExp,
|
|
236
|
+
handler: async (ctx) => SceneResult
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
When `match` matches `ctx.callbackQuery.data`, the corresponding handler is executed.
|
|
241
|
+
Its return value is processed using the same transition logic as `callbacks.message`.
|
|
242
|
+
|
|
243
|
+
---
|
|
98
244
|
|
|
99
245
|
## Scene Control
|
|
100
246
|
|
|
101
|
-
###
|
|
247
|
+
### scenes.enter(ctx, scene)
|
|
248
|
+
|
|
249
|
+
Forces entry into a scene and step.
|
|
102
250
|
|
|
103
251
|
```js
|
|
104
252
|
await scenes.enter(ctx, {
|
|
@@ -107,21 +255,147 @@ await scenes.enter(ctx, {
|
|
|
107
255
|
});
|
|
108
256
|
```
|
|
109
257
|
|
|
110
|
-
|
|
258
|
+
Behavior:
|
|
259
|
+
|
|
260
|
+
• Writes the scene into `ctx.session.scene`
|
|
261
|
+
• Locates the step definition
|
|
262
|
+
• Executes `callbacks.enter` if present
|
|
263
|
+
• Temporarily overrides `ctx.reply` to append the cancel button
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Cancel a Scene Manually
|
|
268
|
+
|
|
269
|
+
A scene can be terminated manually:
|
|
111
270
|
|
|
112
271
|
```js
|
|
113
272
|
ctx.session.scene = null;
|
|
114
273
|
```
|
|
115
274
|
|
|
116
|
-
|
|
275
|
+
After this, the middleware stops processing scene events.
|
|
276
|
+
|
|
277
|
+
---
|
|
117
278
|
|
|
118
|
-
Middleware
|
|
279
|
+
## Middleware
|
|
119
280
|
|
|
120
281
|
```js
|
|
121
282
|
bot.use(session(...));
|
|
122
283
|
bot.use(scenes.middleware);
|
|
123
284
|
```
|
|
124
285
|
|
|
286
|
+
The middleware:
|
|
287
|
+
|
|
288
|
+
• Checks for an active scene in the session
|
|
289
|
+
• Resolves the current step
|
|
290
|
+
• Delegates updates to the appropriate callbacks
|
|
291
|
+
• Executes step transitions
|
|
292
|
+
|
|
293
|
+
The middleware **must** be registered after the session middleware.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Step Navigation
|
|
298
|
+
|
|
299
|
+
Transitions are determined by the value returned from callbacks.
|
|
300
|
+
|
|
301
|
+
### Terminating a Scene
|
|
302
|
+
|
|
303
|
+
• `"stop"`, `"exit"`, `"!"`
|
|
304
|
+
→ `ctx.session.scene = null`
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### All Transitions
|
|
309
|
+
|
|
310
|
+
• `undefined` or `"next"` → move to the next step
|
|
311
|
+
• `"stop" | "exit" | "!"` → terminate the scene
|
|
312
|
+
• `"<" | "prev"` → move to the previous step
|
|
313
|
+
• `">" | "next"` → move to the next step
|
|
314
|
+
• `"^step"` → jump to a step within the current scene
|
|
315
|
+
• `"^scene:step"` → jump to another scene
|
|
316
|
+
• `{ step: "..." }` → jump to a specific step
|
|
317
|
+
• `{ scene: { title, step } }` → jump to another scene
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### Relative Transitions
|
|
322
|
+
|
|
323
|
+
• `undefined` / `"next"` / `">"` → next step
|
|
324
|
+
• `"prev"` / `"<"` → previous step
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### Absolute Transitions
|
|
329
|
+
|
|
330
|
+
• `"^step"` → step within the current scene
|
|
331
|
+
• `"^scene:step"` → jump to another scene
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
### Object-Based Transitions
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
return { step: "confirm" };
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
return {
|
|
343
|
+
scene: {
|
|
344
|
+
title: "auth",
|
|
345
|
+
step: "phone",
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Options
|
|
353
|
+
|
|
354
|
+
Options are passed as the second argument to `new Scenes()`.
|
|
355
|
+
|
|
356
|
+
### callbacks:enter:buttons:cancel
|
|
357
|
+
|
|
358
|
+
Adds a cancel button to all messages sent inside `callbacks.enter`.
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
{
|
|
362
|
+
"callbacks:enter:buttons:cancel": {
|
|
363
|
+
enabled: true,
|
|
364
|
+
|
|
365
|
+
component: {
|
|
366
|
+
text: "Cancel",
|
|
367
|
+
callback_data: "scene:cancel",
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Behavior:
|
|
374
|
+
|
|
375
|
+
• Works only inside `callbacks.enter`
|
|
376
|
+
• The button is appended automatically
|
|
377
|
+
• Scene cancellation logic must be implemented by the user
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Scenes Class
|
|
382
|
+
|
|
383
|
+
### new Scenes(scenes, options)
|
|
384
|
+
|
|
385
|
+
Creates a scene manager instance.
|
|
386
|
+
|
|
387
|
+
Parameters:
|
|
388
|
+
|
|
389
|
+
• `scenes` — an array of grouped scene steps
|
|
390
|
+
• `options` — configuration object
|
|
391
|
+
|
|
392
|
+
Exports:
|
|
393
|
+
|
|
394
|
+
• `scenes.middleware` — grammY middleware
|
|
395
|
+
• `scenes.enter(ctx, scene)` — method to enter a scene
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
125
399
|
## License
|
|
126
400
|
|
|
127
401
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monixlite/grammy-scenes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Scene middleware for grammY with step-based navigation",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -17,4 +17,4 @@
|
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"grammy": "^1.0.0"
|
|
19
19
|
}
|
|
20
|
-
}
|
|
20
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
class ScenesMiddleware {
|
|
2
|
-
constructor({
|
|
2
|
+
constructor({
|
|
3
|
+
scenes,
|
|
4
|
+
options = {},
|
|
5
|
+
}) {
|
|
3
6
|
this.scenes = scenes;
|
|
4
7
|
|
|
5
|
-
|
|
6
8
|
this.steps = {}; {
|
|
7
9
|
for (const group of scenes) {
|
|
8
10
|
for (const item of group) {
|
|
@@ -18,6 +20,20 @@ class ScenesMiddleware {
|
|
|
18
20
|
};
|
|
19
21
|
};
|
|
20
22
|
};
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
this.options = {
|
|
26
|
+
["callbacks:enter:buttons:cancel"]: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
|
|
29
|
+
component: {
|
|
30
|
+
text: "Отменить",
|
|
31
|
+
callback_data: "scene:cancel",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
...options,
|
|
36
|
+
};
|
|
21
37
|
};
|
|
22
38
|
|
|
23
39
|
|
|
@@ -55,18 +71,17 @@ class ScenesMiddleware {
|
|
|
55
71
|
|
|
56
72
|
const original = ctx.reply.bind(ctx); {
|
|
57
73
|
ctx.reply = (text, extra = {}) => {
|
|
74
|
+
const keyboard = (extra.reply_markup?.inline_keyboard || []); {
|
|
75
|
+
if (this.options?.["callbacks:enter:buttons:cancel"]?.enabled) keyboard.push([
|
|
76
|
+
this.options?.["callbacks:enter:buttons:cancel"]?.component,
|
|
77
|
+
]);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
|
|
58
81
|
return original(text, {
|
|
59
82
|
...extra,
|
|
60
83
|
reply_markup: {
|
|
61
|
-
inline_keyboard:
|
|
62
|
-
...(extra.reply_markup?.inline_keyboard || []),
|
|
63
|
-
[
|
|
64
|
-
{
|
|
65
|
-
text: "Отменить",
|
|
66
|
-
callback_data: "scene:cancel",
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
],
|
|
84
|
+
inline_keyboard: keyboard,
|
|
70
85
|
},
|
|
71
86
|
});
|
|
72
87
|
};
|
|
@@ -156,14 +171,24 @@ class ScenesMiddleware {
|
|
|
156
171
|
};
|
|
157
172
|
|
|
158
173
|
|
|
159
|
-
if (ctx
|
|
174
|
+
if (ctx?.update && current.callbacks?.update) {
|
|
175
|
+
const result = await current.callbacks.update(ctx);
|
|
176
|
+
|
|
177
|
+
return await this.handle(ctx, scene, result);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if (ctx?.message && current.callbacks?.message) {
|
|
182
|
+
if (ctx?.message?.caption) ctx.message.text ??= ctx.message.caption;
|
|
183
|
+
|
|
184
|
+
|
|
160
185
|
const result = await current.callbacks.message(ctx);
|
|
161
186
|
|
|
162
187
|
return await this.handle(ctx, scene, result);
|
|
163
188
|
};
|
|
164
189
|
|
|
165
190
|
|
|
166
|
-
if (ctx
|
|
191
|
+
if (ctx?.callbackQuery && current.callbacks?.query) {
|
|
167
192
|
const data = ctx.callbackQuery.data;
|
|
168
193
|
|
|
169
194
|
|
|
@@ -183,9 +208,10 @@ class ScenesMiddleware {
|
|
|
183
208
|
|
|
184
209
|
|
|
185
210
|
class Scenes {
|
|
186
|
-
constructor(scenes) {
|
|
211
|
+
constructor(scenes, options = {}) {
|
|
187
212
|
this.instance = new ScenesMiddleware({
|
|
188
213
|
scenes: scenes,
|
|
214
|
+
options: options,
|
|
189
215
|
});
|
|
190
216
|
|
|
191
217
|
|