@monixlite/grammy-scenes 1.2.2 → 1.3.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 +16 -19
- package/package.json +1 -1
- package/src/index.js +74 -32
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# @monixlite/grammy-scenes
|
|
2
2
|
|
|
3
|
-
A lightweight Finite State Machine (FSM) engine for Telegram bots built on grammY.
|
|
4
|
-
The library provides step-based scenes with declarative definitions, session-backed state, deterministic transitions, and extensible navigation.
|
|
3
|
+
A lightweight Finite State Machine (FSM) engine for Telegram bots built on grammY. The library provides step-based scenes with declarative definitions, session-backed state, deterministic transitions, and extensible navigation.
|
|
5
4
|
|
|
6
5
|
---
|
|
7
6
|
|
|
@@ -11,11 +10,6 @@ The library provides step-based scenes with declarative definitions, session-bac
|
|
|
11
10
|
npm install @monixlite/grammy-scenes
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
## Requirements
|
|
15
|
-
|
|
16
|
-
* Node.js >= 16
|
|
17
|
-
* grammY >= 1.19
|
|
18
|
-
|
|
19
13
|
---
|
|
20
14
|
|
|
21
15
|
## Core Concepts
|
|
@@ -55,7 +49,7 @@ const scenes = new Scenes([
|
|
|
55
49
|
step: "phone",
|
|
56
50
|
},
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
action: {
|
|
59
53
|
enter: async (ctx) => {
|
|
60
54
|
await ctx.reply("• Enter your phone number:");
|
|
61
55
|
},
|
|
@@ -72,7 +66,7 @@ const scenes = new Scenes([
|
|
|
72
66
|
step: "confirm",
|
|
73
67
|
},
|
|
74
68
|
|
|
75
|
-
|
|
69
|
+
action: {
|
|
76
70
|
enter: async (ctx) => {
|
|
77
71
|
const { phone } = ctx.session;
|
|
78
72
|
|
|
@@ -88,6 +82,7 @@ const scenes = new Scenes([
|
|
|
88
82
|
text: "• Yes",
|
|
89
83
|
callback_data: "confirm:yes",
|
|
90
84
|
},
|
|
85
|
+
|
|
91
86
|
{
|
|
92
87
|
text: "• No",
|
|
93
88
|
callback_data: "confirm:no",
|
|
@@ -98,7 +93,7 @@ const scenes = new Scenes([
|
|
|
98
93
|
});
|
|
99
94
|
},
|
|
100
95
|
|
|
101
|
-
|
|
96
|
+
callbacks: [
|
|
102
97
|
{
|
|
103
98
|
match: /^confirm:yes$/,
|
|
104
99
|
handler: async (ctx) => {
|
|
@@ -112,6 +107,7 @@ const scenes = new Scenes([
|
|
|
112
107
|
return "stop";
|
|
113
108
|
},
|
|
114
109
|
},
|
|
110
|
+
|
|
115
111
|
{
|
|
116
112
|
match: /^confirm:no$/,
|
|
117
113
|
handler: async (ctx) => {
|
|
@@ -126,7 +122,7 @@ const scenes = new Scenes([
|
|
|
126
122
|
},
|
|
127
123
|
],
|
|
128
124
|
], {
|
|
129
|
-
"
|
|
125
|
+
["action:enter:extra:reply_markup:inline_keyboard:cancel"]: {
|
|
130
126
|
enabled: true,
|
|
131
127
|
|
|
132
128
|
component: {
|
|
@@ -173,11 +169,11 @@ bot.start();
|
|
|
173
169
|
step: string,
|
|
174
170
|
},
|
|
175
171
|
|
|
176
|
-
|
|
172
|
+
action: {
|
|
177
173
|
enter?: (ctx) => Promise<void>,
|
|
178
174
|
update?: (ctx) => Promise<SceneResult | void>,
|
|
179
175
|
message?: (ctx) => Promise<SceneResult | void>,
|
|
180
|
-
|
|
176
|
+
callbacks?: Array<{
|
|
181
177
|
match: RegExp,
|
|
182
178
|
handler: (ctx) => Promise<SceneResult | void>,
|
|
183
179
|
}>,
|
|
@@ -202,7 +198,7 @@ Executed on step entry.
|
|
|
202
198
|
|
|
203
199
|
Executed on any update while the step is active.
|
|
204
200
|
|
|
205
|
-
* Runs before `message` and `
|
|
201
|
+
* Runs before `message` and `callbacks`
|
|
206
202
|
* Suitable for guards, timeouts, media
|
|
207
203
|
* Return value controls navigation
|
|
208
204
|
|
|
@@ -210,7 +206,7 @@ Executed on any update while the step is active.
|
|
|
210
206
|
|
|
211
207
|
Triggered on text messages during the step.
|
|
212
208
|
|
|
213
|
-
###
|
|
209
|
+
### callbacks[]
|
|
214
210
|
|
|
215
211
|
CallbackQuery handlers matched by regexp.
|
|
216
212
|
The handler return value follows standard transition rules.
|
|
@@ -281,14 +277,15 @@ ctx.session.scene = null;
|
|
|
281
277
|
|
|
282
278
|
## Options
|
|
283
279
|
|
|
284
|
-
###
|
|
280
|
+
### action:enter:extra:reply_markup:inline_keyboard:cancel
|
|
285
281
|
|
|
286
282
|
Injects a cancel button into all `enter` replies.
|
|
287
283
|
|
|
288
284
|
```
|
|
289
285
|
{
|
|
290
|
-
"
|
|
286
|
+
["action:enter:extra:reply_markup:inline_keyboard:cancel"]: {
|
|
291
287
|
enabled: true,
|
|
288
|
+
|
|
292
289
|
component: {
|
|
293
290
|
text: "Cancel",
|
|
294
291
|
callback_data: "scene:cancel",
|
|
@@ -306,9 +303,9 @@ Notes:
|
|
|
306
303
|
|
|
307
304
|
## API
|
|
308
305
|
|
|
309
|
-
### new Scenes(scenes, options)
|
|
306
|
+
### new Scenes(scenes, options, DEV?)
|
|
310
307
|
|
|
311
|
-
Creates a scene manager
|
|
308
|
+
Creates a scene manager backed by `ScenesMiddleware`.
|
|
312
309
|
|
|
313
310
|
Exports:
|
|
314
311
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,59 +1,68 @@
|
|
|
1
1
|
class ScenesMiddleware {
|
|
2
2
|
constructor({
|
|
3
3
|
scenes,
|
|
4
|
-
options
|
|
4
|
+
options,
|
|
5
|
+
DEV,
|
|
5
6
|
}) {
|
|
6
7
|
this.scenes = scenes;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
this.map = {}; {
|
|
9
11
|
for (const group of scenes) {
|
|
10
12
|
for (const item of group) {
|
|
11
13
|
const { title, step } = item.scene;
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
if (!this.
|
|
15
|
-
this.
|
|
16
|
+
if (!this.map[title]) {
|
|
17
|
+
this.map[title] = {
|
|
18
|
+
order: [],
|
|
19
|
+
steps: {},
|
|
20
|
+
};
|
|
16
21
|
};
|
|
17
22
|
|
|
18
23
|
|
|
19
|
-
this.
|
|
24
|
+
this.map[title].order.push(step);
|
|
25
|
+
this.map[title].steps[step] = item;
|
|
20
26
|
};
|
|
21
27
|
};
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
this.options = {
|
|
26
|
-
["
|
|
32
|
+
["action:enter:extra:reply_markup:inline_keyboard:cancel"]: {
|
|
27
33
|
enabled: true,
|
|
28
34
|
|
|
29
35
|
component: {
|
|
30
|
-
text: "
|
|
36
|
+
text: "Cancel",
|
|
31
37
|
callback_data: "scene:cancel",
|
|
32
38
|
},
|
|
33
39
|
},
|
|
34
40
|
|
|
35
41
|
...options,
|
|
36
42
|
};
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
this.DEV = DEV;
|
|
37
46
|
};
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
scene = (scene) => {
|
|
41
|
-
return this.
|
|
50
|
+
return (this.map?.[scene?.title]?.steps?.[scene?.step] ?? null);
|
|
42
51
|
};
|
|
43
52
|
|
|
44
53
|
step = (scene, dir) => {
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
54
|
+
const entry = this.map?.[scene?.title]; {
|
|
55
|
+
if (!entry) return null;
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
|
|
50
|
-
const index =
|
|
59
|
+
const index = entry.order.indexOf(scene?.step); {
|
|
51
60
|
if (index == -1) return null;
|
|
52
61
|
};
|
|
53
62
|
|
|
54
63
|
|
|
55
|
-
if (dir == "next") return (
|
|
56
|
-
if (dir == "prev") return (
|
|
64
|
+
if (dir == "next") return (entry.order[(index + 1)] ?? null);
|
|
65
|
+
if (dir == "prev") return (entry.order[(index - 1)] ?? null);
|
|
57
66
|
|
|
58
67
|
|
|
59
68
|
return null;
|
|
@@ -65,15 +74,19 @@ class ScenesMiddleware {
|
|
|
65
74
|
|
|
66
75
|
|
|
67
76
|
const current = this.scene(scene); {
|
|
68
|
-
if (!current?.
|
|
77
|
+
if (!current?.action?.enter) return;
|
|
69
78
|
};
|
|
70
79
|
|
|
71
80
|
|
|
72
|
-
const original = ctx.reply.bind(ctx);
|
|
81
|
+
const original = ctx.reply.bind(ctx);
|
|
82
|
+
|
|
83
|
+
try {
|
|
73
84
|
ctx.reply = (text, extra = {}) => {
|
|
74
|
-
const keyboard =
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const keyboard = [
|
|
86
|
+
...(extra.reply_markup?.inline_keyboard || []),
|
|
87
|
+
]; {
|
|
88
|
+
if (this.options?.["action:enter:extra:reply_markup:inline_keyboard:cancel"]?.enabled) keyboard.push([
|
|
89
|
+
this.options?.["action:enter:extra:reply_markup:inline_keyboard:cancel"]?.component,
|
|
77
90
|
]);
|
|
78
91
|
};
|
|
79
92
|
|
|
@@ -81,15 +94,17 @@ class ScenesMiddleware {
|
|
|
81
94
|
return original(text, {
|
|
82
95
|
...extra,
|
|
83
96
|
reply_markup: {
|
|
97
|
+
...(extra?.reply_markup || {}),
|
|
84
98
|
inline_keyboard: keyboard,
|
|
85
99
|
},
|
|
86
100
|
});
|
|
87
101
|
};
|
|
88
|
-
};
|
|
89
102
|
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
await current.action.enter(ctx);
|
|
105
|
+
} finally {
|
|
106
|
+
ctx.reply = original;
|
|
107
|
+
};
|
|
93
108
|
};
|
|
94
109
|
|
|
95
110
|
handle = async (ctx, scene, result) => {
|
|
@@ -103,7 +118,17 @@ class ScenesMiddleware {
|
|
|
103
118
|
const step = this.step({
|
|
104
119
|
title: scene.title,
|
|
105
120
|
step: scene.step,
|
|
106
|
-
}, "prev");
|
|
121
|
+
}, "prev"); {
|
|
122
|
+
if (!step) {
|
|
123
|
+
if (this.DEV) {
|
|
124
|
+
console.warn(`[grammy-scenes] No previous step found for scene "${scene.title}" (current step: "${scene.step}")`);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
ctx.session.scene = null;
|
|
129
|
+
return;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
107
132
|
|
|
108
133
|
|
|
109
134
|
return await this.enter(ctx, {
|
|
@@ -116,7 +141,17 @@ class ScenesMiddleware {
|
|
|
116
141
|
const step = this.step({
|
|
117
142
|
title: scene.title,
|
|
118
143
|
step: scene.step,
|
|
119
|
-
}, "next");
|
|
144
|
+
}, "next"); {
|
|
145
|
+
if (!step) {
|
|
146
|
+
if (this.DEV) {
|
|
147
|
+
console.warn(`[grammy-scenes] No next step found for scene "${scene.title}" (current step: "${scene.step}")`);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
ctx.session.scene = null;
|
|
152
|
+
return;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
120
155
|
|
|
121
156
|
|
|
122
157
|
return await this.enter(ctx, {
|
|
@@ -167,32 +202,38 @@ class ScenesMiddleware {
|
|
|
167
202
|
|
|
168
203
|
|
|
169
204
|
const current = this.scene(scene); {
|
|
170
|
-
if (!current?.
|
|
205
|
+
if (!current?.action) return next();
|
|
171
206
|
};
|
|
172
207
|
|
|
173
208
|
|
|
174
|
-
if (ctx?.update && current.
|
|
175
|
-
const result = await current.
|
|
209
|
+
if (ctx?.update && current.action?.update) {
|
|
210
|
+
const result = await current.action.update(ctx);
|
|
176
211
|
|
|
177
212
|
return await this.handle(ctx, scene, result);
|
|
178
213
|
};
|
|
179
214
|
|
|
180
215
|
|
|
181
|
-
if (ctx?.message && current.
|
|
216
|
+
if (ctx?.message && current.action?.message) {
|
|
182
217
|
if (ctx?.message?.caption) ctx.message.text ??= ctx.message.caption;
|
|
183
218
|
|
|
184
219
|
|
|
185
|
-
const result = await current.
|
|
220
|
+
const result = await current.action.message(ctx);
|
|
186
221
|
|
|
187
222
|
return await this.handle(ctx, scene, result);
|
|
188
223
|
};
|
|
189
224
|
|
|
190
225
|
|
|
191
|
-
if (ctx?.callbackQuery && current.callbacks
|
|
226
|
+
if (ctx?.callbackQuery && current.action?.callbacks) {
|
|
227
|
+
if (!Array.isArray(current.action.callbacks)) return next();
|
|
228
|
+
|
|
229
|
+
|
|
192
230
|
const data = ctx.callbackQuery.data;
|
|
193
231
|
|
|
194
232
|
|
|
195
|
-
for (const q of current.callbacks
|
|
233
|
+
for (const q of current.action.callbacks) {
|
|
234
|
+
if (!q?.match?.test || !q?.handler) continue;
|
|
235
|
+
|
|
236
|
+
|
|
196
237
|
if (q.match.test(data)) {
|
|
197
238
|
const result = await q.handler(ctx);
|
|
198
239
|
|
|
@@ -208,14 +249,15 @@ class ScenesMiddleware {
|
|
|
208
249
|
|
|
209
250
|
|
|
210
251
|
class Scenes {
|
|
211
|
-
constructor(scenes, options = {}) {
|
|
252
|
+
constructor(scenes = [], options = {}, DEV = false) {
|
|
212
253
|
this.instance = new ScenesMiddleware({
|
|
213
254
|
scenes: scenes,
|
|
214
255
|
options: options,
|
|
256
|
+
DEV: DEV,
|
|
215
257
|
});
|
|
216
258
|
|
|
217
259
|
|
|
218
|
-
this.middleware = this.instance.middleware;
|
|
260
|
+
this.middleware = this.instance.middleware.bind(this.instance);
|
|
219
261
|
this.enter = this.instance.enter.bind(this.instance);
|
|
220
262
|
};
|
|
221
263
|
};
|