UNPKG

8.88 kBJavaScriptView Raw
1const events = require('./config/events');
2const Context = require('./interfaces/context');
3
4const { NotImplementedError } = require('./exceptions');
5
6class VkBotSdkCallback {
7 /**
8 * Создает экземпляр
9 *
10 * @param {VkBotSdkClient} client
11 */
12 constructor(client) {
13 this.client = client;
14
15 this.middlewaresHandlers = [];
16 this.eventsHandlers = [];
17 this.messagesHandlers = [];
18 this.errorsHandlers = [];
19
20 this.longpoll = {
21 key: '',
22 server: '',
23 ts: 0
24 };
25
26 this.eventsCallback = this.eventsCallback.bind(this);
27 }
28
29 /**
30 * Добавляет middleware для всех событий
31 * @param {CtxCallback} cb - Callback function
32 */
33 use(cb) {
34 this.middlewaresHandlers.push({ cb: cb });
35 }
36
37 /**
38 * Добавляет обработчик ошибок для всех Callback функций
39 * @param {CtxErrorCallback} cb - Error callback function
40 */
41 onError(cb) {
42 this.errorsHandlers.push({ cb: cb });
43 }
44
45 /**
46 * Добавляет обработчик события
47 *
48 * @param {string} e - Callback event (https://vk.com/dev/groups_events)
49 * @param {CtxCallback} cb - Callback function
50 */
51 on(e, cb) {
52 this.eventsHandlers.push({ event: e, cb: cb });
53 }
54
55 /**
56 * Добавляет обработчик полезной нагрузки
57 *
58 * @param {string|string[]} act - Payload action
59 * @param {CtxParamsCallback} cb - Callback function with params
60 */
61 payload(act, cb) {
62 this.messagesHandlers.push({
63 type: 'payload',
64 actions: Array.isArray(act) ? act : [act],
65 cb: cb
66 });
67 }
68
69 /**
70 * Добавляет обработчик команды
71 *
72 * @param {string|string[]|RegExp|RegExp[]} exp - Command keywords / RegExp
73 * @param {CtxParamsCallback} cb - Callback function with params
74 */
75 command(exp, cb) {
76 this.messagesHandlers.push({
77 type: 'command',
78 expressions: Array.isArray(exp) ? exp : [exp],
79 cb: cb
80 });
81 }
82
83 /**
84 * Добавляет стандартный обработчик команды
85 * @param {CtxParamsCallback} cb - Callback function with params
86 */
87 defaultReply(cb) {
88 this.messagesHandlers.push({ type: 'command', expressions: [], cb: cb });
89 }
90
91 /**
92 * Запускает LongPoll соединение
93 */
94 initLongPoll() {
95 const options = this.client.options;
96
97 if(!options.group_id) {
98 throw new NotImplementedError('options.group_id is required for longpoll');
99 }
100
101 this.request('groups.getLongPollServer', {
102 group_id: options.group_id
103 }).then(async (data) => {
104 this.longpoll.key = data.key;
105 this.longpoll.server = data.server;
106 this.longpoll.ts = data.ts;
107
108 await this.requestLongPoll();
109 });
110 }
111
112 /**
113 * Выполняет очередной запрос к LongPoll серверу
114 */
115 async requestLongPoll() {
116 const request = await this.client.get(this.longpoll.server, {
117 act: 'a_check',
118 key: this.longpoll.key,
119 ts: this.longpoll.ts,
120 wait: 25
121 });
122
123 if('failed' in request) {
124 setTimeout(() => this.initLongPoll(), 1000);
125 return;
126 }
127
128 this.longpoll.ts = request.ts;
129
130 for(let event of request['updates']) {
131 this.handleEvent(event).then();
132 }
133
134 setTimeout(() => this.requestLongPoll(), 1);
135 }
136
137
138 /**
139 * Проксирует новые события из Express в VkBotSdkCallback.handleEvent
140 *
141 * @param {import('express').request} req
142 * @param {import('express').response} res
143 */
144 async eventsCallback(req, res) {
145 /**
146 * Валидация данных
147 */
148 const data = req.body;
149 const options = this.client.options;
150
151 if(!('type' in data) || !('object' in data)) {
152 return req.send('ok');
153 }
154 else if(options.secret && data.secret !== options.secret) {
155 return res.send('Invalid secret');
156 }
157 else if(data.type === events.confirmation) {
158 return res.send(this.client.options.confirmation);
159 }
160 else res.send('ok');
161
162 await this.handleEvent(data);
163 }
164
165
166 /**
167 * Обрабатывает событие из Callback или LongPoll
168 *
169 * @param {Object} data
170 */
171 async handleEvent(data) {
172 const middlewaresHandlers = [...this.middlewaresHandlers];
173 const eventsHandlers = [...this.eventsHandlers];
174 const messagesHandlers = [...this.messagesHandlers];
175 const errorsHandlers = [...this.errorsHandlers];
176
177 const ctx = new Context(this.client, data);
178
179 const handleCallback = async (task, parameter = null) => {
180 try {
181 let result;
182
183 if(parameter instanceof Error) result = task.cb(parameter, ctx, handleNextTask);
184 else if(parameter !== null) result = task.cb(ctx, parameter, handleNextTask);
185 else result = task.cb(ctx, handleNextTask);
186
187 if(result instanceof Promise) return await result;
188 else return result;
189 }
190 catch (e) {
191 handleNextTask(e, ctx, handleNextTask);
192 }
193
194 return null;
195 };
196 const handleNextTask = (err = null) => {
197 if(err instanceof Error) {
198 if(errorsHandlers.length > 0) {
199 const handler = errorsHandlers.splice(0, 1)[0];
200 return handleCallback(handler, err);
201 }
202 }
203 else {
204 if(middlewaresHandlers.length > 0) {
205 const middleware = middlewaresHandlers.splice(0, 1)[0];
206 return handleCallback(middleware);
207 }
208 else if(eventsHandlers.length > 0) {
209 const event = eventsHandlers.splice(0, 1)[0];
210 if(event.event === data.type) return handleCallback(event);
211
212 return handleNextTask();
213 }
214 else if(messagesHandlers.length > 0) {
215 const handler = messagesHandlers.splice(0, 1)[0];
216
217 if(handler.type === 'payload') {
218 const result = this._checkCommandPayloads(ctx, handler);
219 if (result !== false) return handleCallback(handler, result);
220 }
221 else if (handler.type === 'command') {
222 const result = this._checkCommandExpressions(ctx, handler);
223 if (result !== false) return handleCallback(handler, result);
224 }
225
226 return handleNextTask();
227 }
228 }
229 };
230
231 handleNextTask();
232 }
233
234 /**
235 * Проверяет типы полезной нагрузки для обработчика "payload"
236 *
237 * @param {Context} ctx
238 * @param {Object} task
239 * @returns {boolean|*}
240 * @private
241 */
242 _checkCommandPayloads(ctx, task) {
243 if(ctx.payload.length === 0) return false;
244
245 for (let action of task.actions) {
246 if(ctx.payload[0] === action) return ctx.payload[1];
247 }
248
249 return false;
250 }
251
252 /**
253 * Проверяет ключевые слова для обработчика "command"
254 *
255 * @param {Context} ctx
256 * @param {Object} task
257 * @returns {boolean|*}
258 * @private
259 */
260 _checkCommandExpressions(ctx, task) {
261 if(task.expressions.length === 0) return true;
262
263 for (let expression of task.expressions) {
264 if(typeof expression === 'string' && ctx.message === expression) return expression;
265 else if(expression instanceof RegExp) {
266 const result = expression.exec(ctx.message);
267 if (result) return result.slice(1);
268 }
269 }
270
271 return false;
272 }
273
274 /**
275 * Отправляет запрос к API
276 *
277 * @param {string} method
278 * @param {object} params
279 */
280 request(method, params) {
281 return this.client.request(method, params);
282 }
283
284 /**
285 * Загружает файл по URL
286 *
287 * @param {string} url
288 * @param {Buffer|PathLike} file
289 * @param {string} key
290 * @param {string} filename
291 */
292 uploadFile(url, file, key, filename) {
293 return this.client.uploadFile(url, file, key, filename);
294 }
295}
296
297module.exports = VkBotSdkCallback;
\No newline at end of file