@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.
Files changed (3) hide show
  1. package/README.md +281 -20
  2. package/package.json +2 -2
  3. package/src/index.js +28 -12
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # @monixlite/grammy-scenes
2
2
 
3
- Scene middleware for grammY with step-based navigation.
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
- * Node.js >= 16
14
- * grammY >= 1.19
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
- await ctx.reply(`Номер: ${ctx.message.text.trim()}`);
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
- return "stop";
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<"next" | "stop" | void>,
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
- ### callbacks.enter
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
- ### callbacks.message
214
+ The return value controls navigation:
95
215
 
96
- Вызывается при получении сообщения пользователя.
97
- Возврат `"stop"` завершает сцену.
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
- ### Enter scene
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
- ### Cancel scene manually
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
- ## Middleware
275
+ After this, the middleware stops processing scene events.
117
276
 
118
- Middleware должен быть подключён **после** session middleware:
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.0.0",
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({ scenes }) {
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