@monixlite/grammy-scenes 1.0.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 +127 -0
  2. package/package.json +20 -0
  3. package/src/index.js +199 -0
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @monixlite/grammy-scenes
2
+
3
+ Scene middleware for grammY with step-based navigation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @monixlite/grammy-scenes
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ * Node.js >= 16
14
+ * grammY >= 1.19
15
+
16
+ ## Basic Usage
17
+
18
+ ```js
19
+ const { Bot, session } = require("grammy");
20
+ const { Scenes } = require("@monixlite/grammy-scenes");
21
+
22
+
23
+ const bot = new Bot(""); {
24
+ bot.use(session({
25
+ initial: () => ({
26
+ scene: null,
27
+ step: null,
28
+ name: null,
29
+ age: null,
30
+ }),
31
+ }));
32
+ };
33
+
34
+
35
+ const scene = new Scenes([
36
+ [
37
+ {
38
+ scene: {
39
+ title: "auth",
40
+ step: "phone",
41
+ },
42
+
43
+ callbacks: {
44
+ enter: async (ctx) => {
45
+ await ctx.reply("Введите номер:");
46
+ },
47
+
48
+ message: async (ctx) => {
49
+ await ctx.reply(`Номер: ${ctx.message.text.trim()}`);
50
+
51
+ return "stop";
52
+ },
53
+ },
54
+ },
55
+ ],
56
+ ]);
57
+
58
+
59
+ bot.command("start", async (ctx) => {
60
+ await scene.enter(ctx, {
61
+ title: "auth",
62
+ step: "phone",
63
+ });
64
+ });
65
+
66
+
67
+ bot.use(scene.middleware);
68
+
69
+ bot.start();
70
+ ```
71
+
72
+ ## Scene Definition
73
+
74
+ Каждый шаг сцены описывается объектом:
75
+
76
+ ```js
77
+ {
78
+ scene: {
79
+ title: string,
80
+ step: string,
81
+ },
82
+
83
+ callbacks: {
84
+ enter?: (ctx) => Promise<void>,
85
+ message?: (ctx) => Promise<"next" | "stop" | void>,
86
+ },
87
+ }
88
+ ```
89
+
90
+ ### callbacks.enter
91
+
92
+ Вызывается при входе в шаг сцены.
93
+
94
+ ### callbacks.message
95
+
96
+ Вызывается при получении сообщения пользователя.
97
+ Возврат `"stop"` завершает сцену.
98
+
99
+ ## Scene Control
100
+
101
+ ### Enter scene
102
+
103
+ ```js
104
+ await scenes.enter(ctx, {
105
+ title: "auth",
106
+ step: "phone",
107
+ });
108
+ ```
109
+
110
+ ### Cancel scene manually
111
+
112
+ ```js
113
+ ctx.session.scene = null;
114
+ ```
115
+
116
+ ## Middleware
117
+
118
+ Middleware должен быть подключён **после** session middleware:
119
+
120
+ ```js
121
+ bot.use(session(...));
122
+ bot.use(scenes.middleware);
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@monixlite/grammy-scenes",
3
+ "version": "1.0.0",
4
+ "description": "Scene middleware for grammY with step-based navigation",
5
+ "main": "src/index.js",
6
+ "type": "commonjs",
7
+ "keywords": [
8
+ "grammy",
9
+ "telegram",
10
+ "bot",
11
+ "scenes",
12
+ "middleware",
13
+ "monixlite"
14
+ ],
15
+ "author": "monixlite",
16
+ "license": "MIT",
17
+ "peerDependencies": {
18
+ "grammy": "^1.0.0"
19
+ }
20
+ }
package/src/index.js ADDED
@@ -0,0 +1,199 @@
1
+ class ScenesMiddleware {
2
+ constructor({ scenes }) {
3
+ this.scenes = scenes;
4
+
5
+
6
+ this.steps = {}; {
7
+ for (const group of scenes) {
8
+ for (const item of group) {
9
+ const { title, step } = item.scene;
10
+
11
+
12
+ if (!this.steps[title]) {
13
+ this.steps[title] = [];
14
+ };
15
+
16
+
17
+ this.steps[title].push(step);
18
+ };
19
+ };
20
+ };
21
+ };
22
+
23
+
24
+ scene = (scene) => {
25
+ return this.scenes.flat().find((x) => (x.scene.title == scene?.title && x.scene.step == scene?.step));
26
+ };
27
+
28
+ step = (scene, dir) => {
29
+ const list = this.steps[scene?.title]; {
30
+ if (!list) return null;
31
+ };
32
+
33
+
34
+ const index = list.indexOf(scene?.step); {
35
+ if (index == -1) return null;
36
+ };
37
+
38
+
39
+ if (dir == "next") return (list[(index + 1)] ?? null);
40
+ if (dir == "prev") return (list[(index - 1)] ?? null);
41
+
42
+
43
+ return null;
44
+ };
45
+
46
+
47
+ enter = async (ctx, scene) => {
48
+ ctx.session.scene = scene;
49
+
50
+
51
+ const current = this.scene(scene); {
52
+ if (!current?.callbacks?.enter) return;
53
+ };
54
+
55
+
56
+ const original = ctx.reply.bind(ctx); {
57
+ ctx.reply = (text, extra = {}) => {
58
+ return original(text, {
59
+ ...extra,
60
+ reply_markup: {
61
+ inline_keyboard: [
62
+ ...(extra.reply_markup?.inline_keyboard || []),
63
+ [
64
+ {
65
+ text: "Отменить",
66
+ callback_data: "scene:cancel",
67
+ },
68
+ ],
69
+ ],
70
+ },
71
+ });
72
+ };
73
+ };
74
+
75
+
76
+ await current.callbacks.enter(ctx);
77
+ ctx.reply = original;
78
+ };
79
+
80
+ handle = async (ctx, scene, result) => {
81
+ if (result?.exit || (["!", "exit", "stop"]).includes(result)) {
82
+ ctx.session.scene = null;
83
+ return;
84
+ };
85
+
86
+
87
+ if (result?.prev || (["<", "prev"]).includes(result)) {
88
+ const step = this.step({
89
+ title: scene.title,
90
+ step: scene.step,
91
+ }, "prev");
92
+
93
+
94
+ return await this.enter(ctx, {
95
+ title: scene.title,
96
+ step: step,
97
+ });
98
+ };
99
+
100
+ if (!result || result?.next || ([">", "next"]).includes(result)) {
101
+ const step = this.step({
102
+ title: scene.title,
103
+ step: scene.step,
104
+ }, "next");
105
+
106
+
107
+ return await this.enter(ctx, {
108
+ title: scene.title,
109
+ step: step,
110
+ });
111
+ };
112
+
113
+
114
+ if (result?.scene) return await this.enter(ctx, {
115
+ title: result.scene.title,
116
+ step: result.scene.step,
117
+ });
118
+
119
+ if (result?.step) return await this.enter(ctx, {
120
+ title: scene.title,
121
+ step: result.step,
122
+ });
123
+
124
+
125
+ const transition = String(result).match(/^\^(.*)$/); {
126
+ if (!transition || !transition?.[1]) {
127
+ ctx.session.scene = null;
128
+ return;
129
+ };
130
+
131
+
132
+ const parts = transition[1].split(":"); {
133
+ if (parts.length == 1) return await this.enter(ctx, {
134
+ title: scene.title,
135
+ step: parts[0],
136
+ });
137
+ };
138
+
139
+
140
+ return await this.enter(ctx, {
141
+ title: parts[0],
142
+ step: parts[1],
143
+ });
144
+ };
145
+ };
146
+
147
+
148
+ middleware = async (ctx, next) => {
149
+ const { scene } = ctx.session; {
150
+ if (!scene?.title || !scene?.step) return next();
151
+ };
152
+
153
+
154
+ const current = this.scene(scene); {
155
+ if (!current?.callbacks) return next();
156
+ };
157
+
158
+
159
+ if (ctx.message && current.callbacks.message) {
160
+ const result = await current.callbacks.message(ctx);
161
+
162
+ return await this.handle(ctx, scene, result);
163
+ };
164
+
165
+
166
+ if (ctx.callbackQuery && current.callbacks.query) {
167
+ const data = ctx.callbackQuery.data;
168
+
169
+
170
+ for (const q of current.callbacks.query) {
171
+ if (q.match.test(data)) {
172
+ const result = await q.handler(ctx);
173
+
174
+ return await this.handle(ctx, scene, result);
175
+ };
176
+ };
177
+ };
178
+
179
+
180
+ return next();
181
+ };
182
+ };
183
+
184
+
185
+ class Scenes {
186
+ constructor(scenes) {
187
+ this.instance = new ScenesMiddleware({
188
+ scenes: scenes,
189
+ });
190
+
191
+
192
+ this.middleware = this.instance.middleware;
193
+ this.enter = this.instance.enter.bind(this.instance);
194
+ };
195
+ };
196
+
197
+ module.exports = {
198
+ Scenes,
199
+ };