@monixlite/grammy-scenes 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -20
- package/package.json +2 -2
- package/src/index.js +28 -12
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,7 +37,7 @@ 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,
|
|
@@ -42,22 +59,100 @@ const scene = new Scenes([
|
|
|
42
59
|
|
|
43
60
|
callbacks: {
|
|
44
61
|
enter: async (ctx) => {
|
|
45
|
-
await ctx.reply("
|
|
62
|
+
await ctx.reply("• Enter your phone number:");
|
|
46
63
|
},
|
|
47
64
|
|
|
48
65
|
message: async (ctx) => {
|
|
49
|
-
|
|
66
|
+
ctx.session.phone = ctx.message.text.trim();
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
scene: {
|
|
73
|
+
title: "auth",
|
|
74
|
+
step: "confirm",
|
|
75
|
+
},
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
callbacks: {
|
|
78
|
+
enter: async (ctx) => {
|
|
79
|
+
const { phone } = ctx.session;
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
await ctx.reply(([
|
|
83
|
+
`• Your phone number: ${phone}`,
|
|
84
|
+
`• Is this correct?`
|
|
85
|
+
]).join("\n"), {
|
|
86
|
+
reply_markup: {
|
|
87
|
+
inline_keyboard: [
|
|
88
|
+
[
|
|
89
|
+
{
|
|
90
|
+
text: "• Yes",
|
|
91
|
+
callback_data: "confirm:yes"
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
text: "• No",
|
|
96
|
+
callback_data: "confirm:no"
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
});
|
|
52
102
|
},
|
|
103
|
+
|
|
104
|
+
query: [
|
|
105
|
+
{
|
|
106
|
+
match: /^confirm:yes$/,
|
|
107
|
+
handler: async (ctx) => {
|
|
108
|
+
const { phone } = ctx.session;
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
await ctx.answerCallbackQuery();
|
|
112
|
+
await ctx.reply(`• Thank you! Your phone number ${phone} has been saved.`);
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
return "stop";
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
match: /^confirm:no$/,
|
|
120
|
+
handler: async (ctx) => {
|
|
121
|
+
await ctx.answerCallbackQuery();
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
return "^phone";
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
53
128
|
},
|
|
54
129
|
},
|
|
55
130
|
],
|
|
56
|
-
]
|
|
131
|
+
], {
|
|
132
|
+
["callbacks:enter:buttons:cancel"]: {
|
|
133
|
+
enabled: true,
|
|
134
|
+
|
|
135
|
+
component: {
|
|
136
|
+
text: "• Cancel scene •",
|
|
137
|
+
callback_data: "scene:cancel",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
bot.callbackQuery("scene:cancel", async (ctx) => {
|
|
144
|
+
ctx.session.scene = null;
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
await ctx.editMessageReplyMarkup(null);
|
|
148
|
+
await ctx.answerCallbackQuery();
|
|
149
|
+
|
|
150
|
+
await ctx.reply("• Scene cancelled!");
|
|
151
|
+
});
|
|
57
152
|
|
|
58
153
|
|
|
59
154
|
bot.command("start", async (ctx) => {
|
|
60
|
-
await scene.enter(ctx, {
|
|
155
|
+
return await scene.enter(ctx, {
|
|
61
156
|
title: "auth",
|
|
62
157
|
step: "phone",
|
|
63
158
|
});
|
|
@@ -69,9 +164,11 @@ bot.use(scene.middleware);
|
|
|
69
164
|
bot.start();
|
|
70
165
|
```
|
|
71
166
|
|
|
167
|
+
---
|
|
168
|
+
|
|
72
169
|
## Scene Definition
|
|
73
170
|
|
|
74
|
-
|
|
171
|
+
Each scene step is described by the following structure:
|
|
75
172
|
|
|
76
173
|
```js
|
|
77
174
|
{
|
|
@@ -82,23 +179,74 @@ bot.start();
|
|
|
82
179
|
|
|
83
180
|
callbacks: {
|
|
84
181
|
enter?: (ctx) => Promise<void>,
|
|
85
|
-
message?: (ctx) => Promise<
|
|
182
|
+
message?: (ctx) => Promise<SceneResult | void>,
|
|
183
|
+
query?: Array<{
|
|
184
|
+
match: RegExp,
|
|
185
|
+
handler: (ctx) => Promise<SceneResult | void>,
|
|
186
|
+
}>,
|
|
86
187
|
},
|
|
87
188
|
}
|
|
88
189
|
```
|
|
89
190
|
|
|
90
|
-
|
|
191
|
+
`SceneResult` is a value that determines the next transition (described below).
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Callbacks
|
|
196
|
+
|
|
197
|
+
### callbacks.enter(ctx)
|
|
198
|
+
|
|
199
|
+
Triggered when entering a scene step.
|
|
200
|
+
Used to send messages, keyboards, or initialize data.
|
|
201
|
+
|
|
202
|
+
Details:
|
|
203
|
+
|
|
204
|
+
• Executed automatically on step entry
|
|
205
|
+
• Can freely use `ctx.reply`
|
|
206
|
+
• If the cancel button option is enabled, it is automatically appended to the inline keyboard
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### callbacks.message(ctx)
|
|
91
211
|
|
|
92
|
-
|
|
212
|
+
Triggered when a text message is received while the step is active.
|
|
93
213
|
|
|
94
|
-
|
|
214
|
+
The return value controls navigation:
|
|
95
215
|
|
|
96
|
-
|
|
97
|
-
|
|
216
|
+
• `undefined` or `"next"` → move to the next step
|
|
217
|
+
• `"stop" | "exit" | "!"` → terminate the scene
|
|
218
|
+
• `"<" | "prev"` → move to the previous step
|
|
219
|
+
• `">" | "next"` → move to the next step
|
|
220
|
+
• `"^step"` → jump to a step within the current scene
|
|
221
|
+
• `"^scene:step"` → jump to another scene
|
|
222
|
+
• `{ step: "..." }` → jump to a specific step
|
|
223
|
+
• `{ scene: { title, step } }` → jump to another scene
|
|
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,134 @@ 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.
|
|
117
276
|
|
|
118
|
-
|
|
277
|
+
---
|
|
278
|
+
|
|
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
|
+
### Relative Transitions
|
|
309
|
+
|
|
310
|
+
• `undefined` / `"next"` / `">"` → next step
|
|
311
|
+
• `"prev"` / `"<"` → previous step
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### Absolute Transitions
|
|
316
|
+
|
|
317
|
+
• `"^step"` → step within the current scene
|
|
318
|
+
• `"^scene:step"` → jump to another scene
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### Object-Based Transitions
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
return { step: "confirm" };
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
return {
|
|
330
|
+
scene: {
|
|
331
|
+
title: "auth",
|
|
332
|
+
step: "phone",
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Options
|
|
340
|
+
|
|
341
|
+
Options are passed as the second argument to `new Scenes()`.
|
|
342
|
+
|
|
343
|
+
### callbacks:enter:buttons:cancel
|
|
344
|
+
|
|
345
|
+
Adds a cancel button to all messages sent inside `callbacks.enter`.
|
|
346
|
+
|
|
347
|
+
```js
|
|
348
|
+
{
|
|
349
|
+
"callbacks:enter:buttons:cancel": {
|
|
350
|
+
enabled: true,
|
|
351
|
+
|
|
352
|
+
component: {
|
|
353
|
+
text: "Cancel",
|
|
354
|
+
callback_data: "scene:cancel",
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Behavior:
|
|
361
|
+
|
|
362
|
+
• Works only inside `callbacks.enter`
|
|
363
|
+
• The button is appended automatically
|
|
364
|
+
• Scene cancellation logic must be implemented by the user
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Scenes Class
|
|
369
|
+
|
|
370
|
+
### new Scenes(scenes, options)
|
|
371
|
+
|
|
372
|
+
Creates a scene manager instance.
|
|
373
|
+
|
|
374
|
+
Parameters:
|
|
375
|
+
|
|
376
|
+
• `scenes` — an array of grouped scene steps
|
|
377
|
+
• `options` — configuration object
|
|
378
|
+
|
|
379
|
+
Exports:
|
|
380
|
+
|
|
381
|
+
• `scenes.middleware` — grammY middleware
|
|
382
|
+
• `scenes.enter(ctx, scene)` — method to enter a scene
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
125
386
|
## License
|
|
126
387
|
|
|
127
388
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monixlite/grammy-scenes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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
|
};
|
|
@@ -183,9 +198,10 @@ class ScenesMiddleware {
|
|
|
183
198
|
|
|
184
199
|
|
|
185
200
|
class Scenes {
|
|
186
|
-
constructor(scenes) {
|
|
201
|
+
constructor(scenes, options = {}) {
|
|
187
202
|
this.instance = new ScenesMiddleware({
|
|
188
203
|
scenes: scenes,
|
|
204
|
+
options: options,
|
|
189
205
|
});
|
|
190
206
|
|
|
191
207
|
|