@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.
Files changed (3) hide show
  1. package/README.md +16 -19
  2. package/package.json +1 -1
  3. 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
- callbacks: {
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
- callbacks: {
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
- query: [
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
- "callbacks:enter:buttons:cancel": {
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
- callbacks: {
172
+ action: {
177
173
  enter?: (ctx) => Promise<void>,
178
174
  update?: (ctx) => Promise<SceneResult | void>,
179
175
  message?: (ctx) => Promise<SceneResult | void>,
180
- query?: Array<{
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 `query`
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
- ### query[]
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
- ### callbacks:enter:buttons:cancel
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
- "callbacks:enter:buttons:cancel": {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monixlite/grammy-scenes",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Scene middleware for grammY with step-based navigation",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
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
- this.steps = {}; {
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.steps[title]) {
15
- this.steps[title] = [];
16
+ if (!this.map[title]) {
17
+ this.map[title] = {
18
+ order: [],
19
+ steps: {},
20
+ };
16
21
  };
17
22
 
18
23
 
19
- this.steps[title].push(step);
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
- ["callbacks:enter:buttons:cancel"]: {
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.scenes.flat().find((x) => (x.scene.title == scene?.title && x.scene.step == scene?.step));
50
+ return (this.map?.[scene?.title]?.steps?.[scene?.step] ?? null);
42
51
  };
43
52
 
44
53
  step = (scene, dir) => {
45
- const list = this.steps[scene?.title]; {
46
- if (!list) return null;
54
+ const entry = this.map?.[scene?.title]; {
55
+ if (!entry) return null;
47
56
  };
48
57
 
49
58
 
50
- const index = list.indexOf(scene?.step); {
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 (list[(index + 1)] ?? null);
56
- if (dir == "prev") return (list[(index - 1)] ?? null);
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?.callbacks?.enter) return;
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 = (extra.reply_markup?.inline_keyboard || []); {
75
- if (this.options?.["callbacks:enter:buttons:cancel"]?.enabled) keyboard.push([
76
- this.options?.["callbacks:enter:buttons:cancel"]?.component,
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
- await current.callbacks.enter(ctx);
92
- ctx.reply = original;
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?.callbacks) return next();
205
+ if (!current?.action) return next();
171
206
  };
172
207
 
173
208
 
174
- if (ctx?.update && current.callbacks?.update) {
175
- const result = await current.callbacks.update(ctx);
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.callbacks?.message) {
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.callbacks.message(ctx);
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?.query) {
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.query) {
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
  };