@skroz/telegram-bot 1.0.28 → 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.
@@ -0,0 +1,430 @@
1
+ const indexBackup = {};
2
+ export default indexBackup;
3
+ /*
4
+ import TelegramApi from '@skroz/telegram-api';
5
+ import {
6
+ TG_BotCommand,
7
+ TG_CallbackQuery,
8
+ TG_ChatResponse,
9
+ TG_DeleteMessageInput,
10
+ TG_Message,
11
+ TG_PreCheckoutQuery,
12
+ TG_SendActionInput,
13
+ TG_SendMessageInput,
14
+ TG_SendMessageResponse,
15
+ TG_SendPhotoInput,
16
+ TG_SetMyCommandsResponse,
17
+ TG_SuccessfulPayment,
18
+ TG_Update,
19
+ } from '@skroz/telegram-api/dist/TelegramTypes';
20
+
21
+ /!* eslint-disable @typescript-eslint/naming-convention *!/
22
+
23
+ /!* хранит уникальные колбэки, чтобы исключить повторное нажатие *!/
24
+ const processedCallbacks = new Set();
25
+ /!* разделитель для параметров колбэка *!/
26
+ const callbackDivider = '^:^:^';
27
+ /!* по умолчанию истекающее сообщение *!/
28
+ const expiresSecondsDefault = 5 * 60; // 5 минут
29
+
30
+ /!* не больше 64 символов по документации, внутри защита от повторных кликов *!/
31
+ export const createCallbackData = (
32
+ telegramId: number | string,
33
+ data: (string | number)[]
34
+ ): string =>
35
+ [`${Date.now()}_${telegramId}`, ...data].join(callbackDivider).slice(0, 64);
36
+
37
+ /!* не больше 64 символов по документации, внутри защита от повторных кликов *!/
38
+ export const getCallbackData = (data: string): string[] =>
39
+ data.split(callbackDivider).slice(1);
40
+
41
+ const expiredMessagesList: {
42
+ message: TG_Message;
43
+ // user: TG_User;
44
+ expiresAt: number;
45
+ onExpire: (message: TG_Message) => void;
46
+ }[] = [];
47
+
48
+ const pushMessageToExpireList = (
49
+ message: TG_Message,
50
+ // user: TG_User,
51
+ expires: ExpiresInput
52
+ ) => {
53
+ const expiresAt =
54
+ Date.now() + (expires.seconds || expiresSecondsDefault) * 1000;
55
+
56
+ expiredMessagesList.push({
57
+ message,
58
+ // user,
59
+ expiresAt,
60
+ onExpire: expires.onExpire,
61
+ });
62
+ };
63
+
64
+ // забирает и сразу удаляет
65
+ const shiftExpiredMessages = (): {
66
+ message: TG_Message;
67
+ // user: TG_User;
68
+ // from: TG_User;
69
+ onExpire: (
70
+ message: TG_Message
71
+ // , user: TG_User
72
+ ) => void;
73
+ }[] => {
74
+ const now = Date.now();
75
+ const expired = expiredMessagesList.filter((msg) => msg.expiresAt <= now);
76
+ expiredMessagesList.splice(0, expired.length);
77
+ return expired;
78
+ };
79
+
80
+ // убираем из списка удаленные сообщения
81
+ const deleteMessageFromExpiringList = (message: TG_Message) => {
82
+ const index = expiredMessagesList.findIndex(
83
+ (msg) => msg.message.message_id === message.message_id
84
+ );
85
+ if (index !== -1) {
86
+ expiredMessagesList.splice(index, 1);
87
+ }
88
+ };
89
+
90
+ /!* обрабатываем истекшие сообщения по таймеру *!/
91
+ setInterval(async () => {
92
+ const expired = shiftExpiredMessages();
93
+ if (expired.length > 0) {
94
+ await Promise.all(
95
+ expired.map(async (ex) => {
96
+ await ex.onExpire(ex.message);
97
+ })
98
+ );
99
+ }
100
+ }, 15000);
101
+
102
+ type ExpiresInput = {
103
+ seconds?: number;
104
+ onExpire: (resultMessage: TG_Message) => void;
105
+ };
106
+
107
+ /!* Тип функции отправки в телеграм *!/
108
+ /!* type TelegramSendFunction<TInput, TResponse> = (
109
+ input: TInput
110
+ ) => Promise<TResponse>; *!/
111
+
112
+ /!* Тип типов функций отправки *!/
113
+ /!* type TG_SendFunction =
114
+ | TelegramSendFunction<TG_SendMessageInput, TG_SendMessageResponse>
115
+ | TelegramSendFunction<TG_SendPhotoInput, TG_SendMessageResponse>; *!/
116
+ // в будущем:
117
+ // | TelegramSendFunction<TG_SendDocumentInput, TG_SendDocumentResponse>
118
+
119
+ // interface TelegramMessageHandler<TInput, TResponse> {
120
+ // send: (input: TInput) => Promise<TResponse>;
121
+ // }
122
+
123
+ class TelegramBot {
124
+ constructor(
125
+ private readonly botToken: string,
126
+ /!* messages: {
127
+ text?: Record<
128
+ string,
129
+ (input: TG_SendMessageInput) => TG_SendMessageInput
130
+ >;
131
+ photo?: Record<string, (input: TG_SendPhotoInput) => TG_SendPhotoInput>;
132
+ }, *!/
133
+ private readonly expiresMessageSeconds?: number,
134
+ private readonly protect_content?: boolean
135
+ ) {
136
+ /!* this.messages = {
137
+ text: this.wrapTextMessages(messages.text ?? {}),
138
+ photo: this.wrapPhotoMessages(messages.photo ?? {}),
139
+ }; *!/
140
+ }
141
+
142
+ // публичный контейнер
143
+ /!* readonly messages: {
144
+ text: Record<
145
+ string,
146
+ {
147
+ send: (
148
+ input: TG_SendMessageInput,
149
+ message?: TG_Message,
150
+ onExpire?: (resultMessage: TG_Message) => void
151
+ ) => Promise<TG_SendMessageResponse>;
152
+ }
153
+ >;
154
+ photo: Record<
155
+ string,
156
+ {
157
+ send: (
158
+ input: TG_SendPhotoInput,
159
+ message?: TG_Message,
160
+ onExpire?: (resultMessage: TG_Message) => void
161
+ ) => Promise<TG_SendMessageResponse>;
162
+ }
163
+ >;
164
+ }; *!/
165
+
166
+ async handleUpdate(
167
+ update: TG_Update,
168
+ handleCallback: (
169
+ callbackQuery: TG_CallbackQuery,
170
+ callbackDataParams: string[]
171
+ ) => Promise<void>,
172
+ handleMessage: (message: TG_Message) => void,
173
+ handlePreCheckoutQuery: (
174
+ preCheckoutQuery: TG_PreCheckoutQuery
175
+ ) => Promise<{ ok: boolean; error_message?: string }>,
176
+ handleSuccessfulPayment: (
177
+ successfulPayment: TG_SuccessfulPayment,
178
+ message: TG_Message
179
+ ) => Promise<void>
180
+ ): Promise<void> {
181
+ const callbackQuery = update.callback_query;
182
+ if (callbackQuery) {
183
+ const { data } = callbackQuery;
184
+
185
+ if (!data) {
186
+ // 1. Коллбэк пришел от WebApp
187
+ // 2. Игровая кнопка
188
+ // 3. Кнопка с удаленным сообщением
189
+ return; // todo sendError
190
+ }
191
+
192
+ // data - всегда уникален, потому что в начале стоит текущий Date.now()
193
+ if (processedCallbacks.has(data)) {
194
+ // чтобы избежать дублирования нажатия на кнопку
195
+ return; // Уже обработан
196
+ }
197
+ processedCallbacks.add(data);
198
+
199
+ // Удаляем из памяти через 60 секунд
200
+ setTimeout(() => {
201
+ processedCallbacks.delete(data);
202
+ }, 60 * 1000);
203
+
204
+ await handleCallback(callbackQuery, getCallbackData(data));
205
+ }
206
+ /!* Часть оплаты *!/
207
+ const preCheckoutQuery = update.pre_checkout_query;
208
+ if (preCheckoutQuery) {
209
+ /!* Нужно ответить в течение 10 секунд внутри метода или заказ отменяется автоматически *!/
210
+ const { ok, error_message } = await handlePreCheckoutQuery(
211
+ preCheckoutQuery
212
+ );
213
+
214
+ await TelegramApi.answerPreCheckoutQuery(
215
+ {
216
+ pre_checkout_query_id: preCheckoutQuery.id,
217
+ ok,
218
+ error_message,
219
+ },
220
+ this.botToken
221
+ );
222
+ return;
223
+ }
224
+
225
+ /!* обработаем сообщение *!/
226
+ const { message } = update; // его нет, если это нажатие на кнопку callback_query или при редактировании предыдущего сообщения
227
+
228
+ if (message) {
229
+ const { successful_payment } = message;
230
+ if (successful_payment) {
231
+ /!* Успешная оплата *!/
232
+ await handleSuccessfulPayment(successful_payment, message);
233
+ return;
234
+ }
235
+
236
+ if (!callbackQuery) {
237
+ /!* разбираем входящий текст *!/
238
+ await handleMessage(message);
239
+ }
240
+ }
241
+ }
242
+
243
+ async sendPhoto(
244
+ input: TG_SendPhotoInput,
245
+ message?: TG_Message,
246
+ onExpire?: (resultMessage: TG_Message) => void
247
+ ): Promise<TG_SendMessageResponse> {
248
+ let sendResult: TG_SendMessageResponse;
249
+
250
+ const sendPhotoFunction = () =>
251
+ TelegramApi.sendPhoto(
252
+ {
253
+ protect_content: this.protect_content, // переписывается инпутом
254
+ parse_mode: 'HTML',
255
+ ...input,
256
+ },
257
+ this.botToken
258
+ );
259
+
260
+ if (message) {
261
+ try {
262
+ sendResult = await TelegramApi.editMessageMedia(
263
+ {
264
+ media: {
265
+ type: 'photo',
266
+ media: input.photo as string,
267
+ caption: input.caption,
268
+ parse_mode: 'HTML',
269
+ },
270
+ ...input,
271
+ message_id: message.message_id,
272
+ },
273
+ this.botToken
274
+ );
275
+ } catch (e) {
276
+ sendResult = await sendPhotoFunction();
277
+ }
278
+ } else {
279
+ sendResult = await sendPhotoFunction();
280
+ }
281
+
282
+ if (sendResult.ok && sendResult.result) {
283
+ if (onExpire) {
284
+ pushMessageToExpireList(sendResult.result, {
285
+ seconds: this.expiresMessageSeconds,
286
+ onExpire,
287
+ });
288
+ } else {
289
+ deleteMessageFromExpiringList(sendResult.result);
290
+ }
291
+ }
292
+
293
+ return sendResult;
294
+ }
295
+
296
+ async sendMessage(
297
+ input: TG_SendMessageInput,
298
+ message?: TG_Message,
299
+ onExpire?: (resultMessage: TG_Message) => void
300
+ ): Promise<TG_SendMessageResponse> {
301
+ let sendResult: TG_SendMessageResponse;
302
+
303
+ const sendPhotoFunction = () =>
304
+ TelegramApi.sendMessage(
305
+ {
306
+ parse_mode: 'HTML',
307
+ protect_content: this.protect_content, // переписывается инпутом
308
+ ...input,
309
+ },
310
+ this.botToken
311
+ );
312
+
313
+ if (message) {
314
+ try {
315
+ sendResult = await TelegramApi.editMessageText(
316
+ {
317
+ message_id: message.message_id,
318
+ ...input,
319
+ },
320
+ this.botToken
321
+ );
322
+ } catch (e) {
323
+ sendResult = await sendPhotoFunction();
324
+ }
325
+ } else {
326
+ sendResult = await sendPhotoFunction();
327
+ }
328
+
329
+ if (sendResult.ok && sendResult.result) {
330
+ if (onExpire) {
331
+ pushMessageToExpireList(sendResult.result, {
332
+ seconds: this.expiresMessageSeconds,
333
+ onExpire,
334
+ });
335
+ } else {
336
+ deleteMessageFromExpiringList(sendResult.result);
337
+ }
338
+ }
339
+
340
+ return sendResult;
341
+ }
342
+
343
+ async getUpdates(offset?: number) {
344
+ return TelegramApi.getUpdates(this.botToken, offset);
345
+ }
346
+
347
+ async setWebhook(webhookUrl: string) {
348
+ return TelegramApi.setWebhook(this.botToken, webhookUrl);
349
+ }
350
+
351
+ async deleteWebhook() {
352
+ return TelegramApi.deleteWebhook(this.botToken);
353
+ }
354
+
355
+ async setMyCommands(
356
+ commands: TG_BotCommand[]
357
+ ): Promise<TG_SetMyCommandsResponse> {
358
+ return TelegramApi.setMyCommands(commands, this.botToken);
359
+ }
360
+
361
+ async getChatInfo(telegramId: string | number): Promise<TG_ChatResponse> {
362
+ return TelegramApi.getChat(telegramId, this.botToken);
363
+ }
364
+
365
+ async sendChatAction(input: TG_SendActionInput): Promise<boolean> {
366
+ return TelegramApi.sendChatAction(input, this.botToken);
367
+ }
368
+
369
+ async deleteMessage(input: TG_DeleteMessageInput): Promise<boolean> {
370
+ return TelegramApi.deleteMessage(input, this.botToken);
371
+ }
372
+
373
+ // генераторы типовых методов
374
+ /!* private wrapTextMessages(
375
+ defs: Record<string, (input: TG_SendMessageInput) => TG_SendMessageInput>
376
+ ) {
377
+ const result: Record<
378
+ string,
379
+ {
380
+ send: (
381
+ input: TG_SendMessageInput,
382
+ message?: TG_Message,
383
+ onExpire?: (m: TG_Message) => void
384
+ ) => Promise<TG_SendMessageResponse>;
385
+ }
386
+ > = {};
387
+
388
+ Object.entries(defs).forEach(([key, builder]) => {
389
+ result[key] = {
390
+ send: (input, message, onExpire) =>
391
+ this.sendMessage(
392
+ builder(input ?? ({} as TG_SendMessageInput)),
393
+ message,
394
+ onExpire
395
+ ),
396
+ };
397
+ });
398
+ return result;
399
+ }
400
+
401
+ private wrapPhotoMessages(
402
+ defs: Record<string, (input: TG_SendPhotoInput) => TG_SendPhotoInput>
403
+ ) {
404
+ const result: Record<
405
+ string,
406
+ {
407
+ send: (
408
+ input: TG_SendPhotoInput,
409
+ message?: TG_Message,
410
+ onExpire?: (m: TG_Message) => void
411
+ ) => Promise<TG_SendMessageResponse>;
412
+ }
413
+ > = {};
414
+
415
+ Object.entries(defs).forEach(([key, builder]) => {
416
+ result[key] = {
417
+ send: (input, message, onExpire) =>
418
+ this.sendPhoto(
419
+ builder(input ?? ({} as TG_SendPhotoInput)),
420
+ message,
421
+ onExpire
422
+ ),
423
+ };
424
+ });
425
+ return result;
426
+ } *!/
427
+ }
428
+
429
+ export default TelegramBot;
430
+ */