@andrey4emk/npm-app-back-b24 3.7.1 → 3.8.1
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.
- package/README.md +128 -12
- package/bitrix24/b24.ts +103 -20
- package/bitrix24/eventB24.ts +54 -5
- package/logs/logs.ts +19 -1
- package/package.json +5 -2
- package/sendMessage/chatApp.ts +192 -95
- package/sendMessage/email.ts +11 -2
- package/sendMessage/smsgold.ts +57 -12
- package/sendMessage/wappi.ts +144 -53
- package/utils/fetchRetry.ts +251 -8
package/sendMessage/smsgold.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getResultData, type B24MethodCaller } from "../bitrix24/b24.ts";
|
|
2
|
-
import { fetchRetry } from "../utils/fetchRetry.ts";
|
|
2
|
+
import { fetchRetry, fetchWithTimeout, isTimeoutError, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
|
|
3
|
+
import { logs } from "../logs/logs.ts";
|
|
3
4
|
|
|
4
5
|
// ==================== Типы ====================
|
|
5
6
|
|
|
@@ -17,6 +18,15 @@ interface SmsMessageData {
|
|
|
17
18
|
fileName?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Папка на диске Bitrix24, куда кладётся вложение перед отправкой SMS.
|
|
23
|
+
* Значение — папка «contract_company» проекта «Документы и договора» портала
|
|
24
|
+
* repacheb.bitrix24.ru. Для опубликованной библиотеки это чужая константа,
|
|
25
|
+
* поэтому она вынесена в параметр конструктора; здесь она осталась дефолтом,
|
|
26
|
+
* иначе смена была бы ломающей для всех текущих потребителей.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_UPLOAD_FOLDER_ID = 2792881;
|
|
29
|
+
|
|
20
30
|
/** Результат отправки SMS */
|
|
21
31
|
interface SmsResult {
|
|
22
32
|
error: boolean;
|
|
@@ -27,6 +37,12 @@ interface SmsResult {
|
|
|
27
37
|
data?: string;
|
|
28
38
|
errorText?: string;
|
|
29
39
|
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Выставляется только при таймауте ожидания ответа SMSGold: запрос ушёл, ответ не пришёл.
|
|
42
|
+
* Означает «сообщение могло уйти» — не «не отправлено». Повторять отправку нельзя,
|
|
43
|
+
* решение принимает человек.
|
|
44
|
+
*/
|
|
45
|
+
unknownDelivery?: boolean;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
// ==================== Класс ====================
|
|
@@ -37,11 +53,14 @@ export class Smsgold {
|
|
|
37
53
|
// Структурный тип, а не B24OAuth: объявляет callMethod сам, поэтому переживёт
|
|
38
54
|
// удаление метода из SDK. Сигнатуру конструктора не сужает — она стала шире
|
|
39
55
|
private b24: B24MethodCaller | null;
|
|
56
|
+
// ID папки на диске B24 для вложений SMS — параметр, а не константа модуля
|
|
57
|
+
private uploadFolderId: number;
|
|
40
58
|
|
|
41
|
-
constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null) {
|
|
59
|
+
constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null, uploadFolderId: number = DEFAULT_UPLOAD_FOLDER_ID) {
|
|
42
60
|
this.user = authParam.user;
|
|
43
61
|
this.pass = authParam.pass;
|
|
44
62
|
this.b24 = b24;
|
|
63
|
+
this.uploadFolderId = uploadFolderId;
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
async sendSms(messageData: SmsMessageData): Promise<SmsResult> {
|
|
@@ -97,13 +116,20 @@ export class Smsgold {
|
|
|
97
116
|
sender: "potolkiRepa",
|
|
98
117
|
}).toString();
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
// Отправка неидемпотентна — повторов нет, единственная защита от зависшего
|
|
120
|
+
// соединения — бюджет времени. 30с взяты с запасом, чтобы «неизвестный статус»
|
|
121
|
+
// случался как можно реже
|
|
122
|
+
const result = await fetchWithTimeout(
|
|
123
|
+
"https://web.smsgold.ru/http2/",
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
128
|
+
},
|
|
129
|
+
body,
|
|
104
130
|
},
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
FETCH_TIMEOUTS.send
|
|
132
|
+
);
|
|
107
133
|
|
|
108
134
|
if (!result.ok) {
|
|
109
135
|
const errorText = await result.text();
|
|
@@ -126,6 +152,23 @@ export class Smsgold {
|
|
|
126
152
|
},
|
|
127
153
|
};
|
|
128
154
|
} catch (error: unknown) {
|
|
155
|
+
// Ответа не дождались: запрос уже ушёл, сообщение могло быть доставлено.
|
|
156
|
+
// Это не «ошибка отправки» — статус неизвестен, и решить это кодом нельзя.
|
|
157
|
+
// Единственное место в классе, где пишем в лог: уровень error уходит в чат B24,
|
|
158
|
+
// потому что разбираться должен человек
|
|
159
|
+
if (isTimeoutError(error)) {
|
|
160
|
+
logs.add(
|
|
161
|
+
`[${phone}] SMS: таймаут ожидания ответа SMSGold ${FETCH_TIMEOUTS.send}мс — статус неизвестен, повторная отправка запрещена`,
|
|
162
|
+
"error"
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
error: true,
|
|
166
|
+
message: `SMSGold не ответил за ${FETCH_TIMEOUTS.send / 1000}с — статус отправки неизвестен: сообщение могло уйти, повторять нельзя`,
|
|
167
|
+
result: null,
|
|
168
|
+
unknownDelivery: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
130
173
|
return { error: true, message: errMsg, result: null };
|
|
131
174
|
}
|
|
@@ -138,7 +181,7 @@ export class Smsgold {
|
|
|
138
181
|
|
|
139
182
|
// Используем метод disk.folder.uploadfile
|
|
140
183
|
const res = await this.b24!.callMethod("disk.folder.uploadfile", {
|
|
141
|
-
id:
|
|
184
|
+
id: this.uploadFolderId,
|
|
142
185
|
data: { NAME: fileName },
|
|
143
186
|
fileContent: [fileName, base64File],
|
|
144
187
|
});
|
|
@@ -153,9 +196,11 @@ export class Smsgold {
|
|
|
153
196
|
private async downloadFileToUrl(fileUrl: string): Promise<{ error: boolean; message?: string; buffer?: Buffer }> {
|
|
154
197
|
try {
|
|
155
198
|
// Скачивание файла идемпотентно — повтор при сетевом сбое безопасен.
|
|
156
|
-
// Отправка SMS (
|
|
157
|
-
// при потерянном ответе повтор отправил бы сообщение второй
|
|
158
|
-
|
|
199
|
+
// Отправка SMS (fetchWithTimeout выше по коду) намеренно остаётся без retry:
|
|
200
|
+
// при потерянном ответе повтор отправил бы сообщение второй раз.
|
|
201
|
+
// Попыток 3, а не 5: при бюджете 60с на попытку пять попыток дали бы до пяти
|
|
202
|
+
// минут ожидания перед самой отправкой SMS
|
|
203
|
+
const response = await fetchRetry(fileUrl, undefined, 3, 500, FETCH_TIMEOUTS.transfer);
|
|
159
204
|
if (!response.ok) {
|
|
160
205
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
161
206
|
}
|
package/sendMessage/wappi.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { fetchWithTimeout, isTimeoutError, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
|
|
2
|
+
import { logs } from "../logs/logs.ts";
|
|
3
|
+
|
|
1
4
|
// ==================== Типы ====================
|
|
2
5
|
|
|
3
6
|
/** Параметры авторизации Wappi-мессенджера */
|
|
@@ -27,6 +30,11 @@ interface WappiResult {
|
|
|
27
30
|
error: boolean;
|
|
28
31
|
message: string;
|
|
29
32
|
data: any;
|
|
33
|
+
/**
|
|
34
|
+
* Ответа не дождались. Отличает «проверка сказала нет» от «проверки не было»:
|
|
35
|
+
* без этого признака вызывающий код принимает таймаут за отрицательный ответ.
|
|
36
|
+
*/
|
|
37
|
+
timeout?: boolean;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
// ==================== Класс ====================
|
|
@@ -85,18 +93,42 @@ export class Wappi {
|
|
|
85
93
|
url = `https://wappi.pro/maxapi/sync/message/send?${sendOpenLine}profile_id=${profile_id}`;
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
let data: any;
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetchWithTimeout(
|
|
99
|
+
url!,
|
|
100
|
+
{
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
Authorization: token!,
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
recipient: phone,
|
|
108
|
+
body: message,
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
FETCH_TIMEOUTS.send
|
|
112
|
+
);
|
|
113
|
+
data = await res.json();
|
|
114
|
+
} catch (error: unknown) {
|
|
115
|
+
// Метод исторически без try/catch — перехватываем только таймаут, который сами
|
|
116
|
+
// и вводим, чтобы не появился новый путь исключений там, где его сегодня нет.
|
|
117
|
+
// Остальные ошибки летят наружу как раньше
|
|
118
|
+
if (!isTimeoutError(error)) throw error;
|
|
119
|
+
|
|
120
|
+
// Уровень error — единственный, который уходит в чат B24. До появления
|
|
121
|
+
// таймаута зависание долетало исключением до потребителя и там порождало
|
|
122
|
+
// задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
|
|
123
|
+
// след, иначе правка сделала бы отказ отправки тише, чем он был
|
|
124
|
+
logs.add(`[${phone}] Wappi: таймаут ${FETCH_TIMEOUTS.send}мс при отправке сообщения через ${messangerType} — статус неизвестен, повторять нельзя`, "error");
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
error: true,
|
|
128
|
+
message: `Wappi: таймаут ${FETCH_TIMEOUTS.send}мс при отправке сообщения через ${messangerType} — статус неизвестен`,
|
|
129
|
+
data: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
100
132
|
|
|
101
133
|
if (data.status !== "done") {
|
|
102
134
|
return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
|
|
@@ -170,21 +202,40 @@ export class Wappi {
|
|
|
170
202
|
url = `https://wappi.pro/maxapi/async/message/file/url/send?${sendOpenLine}profile_id=${profile_id}`;
|
|
171
203
|
}
|
|
172
204
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
let data: any;
|
|
206
|
+
try {
|
|
207
|
+
// В теле уходит base64 файла — бюджет transfer, а не send
|
|
208
|
+
const res = await fetchWithTimeout(
|
|
209
|
+
url!,
|
|
210
|
+
{
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
Authorization: token!,
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
recipient: phone,
|
|
218
|
+
url: fileUrl,
|
|
219
|
+
file_name: fileName,
|
|
220
|
+
caption: message,
|
|
221
|
+
b64_file: base64,
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
FETCH_TIMEOUTS.transfer
|
|
225
|
+
);
|
|
226
|
+
data = await res.json();
|
|
227
|
+
} catch (error: unknown) {
|
|
228
|
+
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
229
|
+
if (!isTimeoutError(error)) throw error;
|
|
230
|
+
|
|
231
|
+
logs.add(`[${phone}] Wappi: таймаут ${FETCH_TIMEOUTS.transfer}мс при отправке файла через ${messangerType} — статус неизвестен, повторять нельзя`, "error");
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
error: true,
|
|
235
|
+
message: `Wappi: таймаут ${FETCH_TIMEOUTS.transfer}мс при отправке файла через ${messangerType} — статус неизвестен`,
|
|
236
|
+
data: null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
188
239
|
|
|
189
240
|
if (data.status !== "done") {
|
|
190
241
|
return { error: true, message: `Ошибка при отправке файла в ChatApp через ${messangerType}`, data };
|
|
@@ -219,6 +270,14 @@ export class Wappi {
|
|
|
219
270
|
return { error: false, message: `Номер ${phone} проверен для мессенджера ${messangerType}`, data: contactCheck.data };
|
|
220
271
|
}
|
|
221
272
|
|
|
273
|
+
// Ответа не дождались — это не «контакта нет». Создавать контакт вслепую нельзя:
|
|
274
|
+
// он мог уже быть в адресной книге, а результат объявил бы номер проверенным,
|
|
275
|
+
// хотя проверки не было
|
|
276
|
+
if (contactCheck.timeout) {
|
|
277
|
+
logs.add(`[${phone}] Wappi: таймаут ${FETCH_TIMEOUTS.api}мс при проверке контакта в Telegram — контакт не создаём`, "warn");
|
|
278
|
+
return { error: true, message: `Wappi: таймаут ${FETCH_TIMEOUTS.api}мс при проверке контакта ${phone} в Telegram`, data: null };
|
|
279
|
+
}
|
|
280
|
+
|
|
222
281
|
// 3 Если не существует, то номера в телеге нет, пробуем создать
|
|
223
282
|
const addContactResult = await this.addContactTelegramWappi(phone);
|
|
224
283
|
|
|
@@ -236,14 +295,29 @@ export class Wappi {
|
|
|
236
295
|
url = `https://wappi.pro/maxapi/sync/contact/check?profile_id=${profile_id}&phone=${phone}`;
|
|
237
296
|
}
|
|
238
297
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
298
|
+
let data: any;
|
|
299
|
+
try {
|
|
300
|
+
const res = await fetchWithTimeout(
|
|
301
|
+
url!,
|
|
302
|
+
{
|
|
303
|
+
method: "GET",
|
|
304
|
+
headers: {
|
|
305
|
+
"Content-Type": "application/json",
|
|
306
|
+
Authorization: token!,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
FETCH_TIMEOUTS.api
|
|
310
|
+
);
|
|
311
|
+
data = await res.json();
|
|
312
|
+
} catch (error: unknown) {
|
|
313
|
+
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
314
|
+
if (!isTimeoutError(error)) throw error;
|
|
315
|
+
|
|
316
|
+
// warn, а не error: проверка номера ничего не меняет, повторить её безопасно
|
|
317
|
+
logs.add(`[${phone}] Wappi: таймаут ${FETCH_TIMEOUTS.api}мс при проверке номера в ${messangerType}`, "warn");
|
|
318
|
+
|
|
319
|
+
return { error: true, message: `Wappi: таймаут ${FETCH_TIMEOUTS.api}мс при проверке номера ${phone}`, data: null };
|
|
320
|
+
}
|
|
247
321
|
|
|
248
322
|
if (data.status !== "done") {
|
|
249
323
|
return { error: true, message: `Ошибка проверки номера ${phone} ${messangerType}`, data };
|
|
@@ -267,13 +341,17 @@ export class Wappi {
|
|
|
267
341
|
const url = `https://wappi.pro/tapi/sync/contact/get?profile_id=${profile_id}&phone=${phone}`;
|
|
268
342
|
|
|
269
343
|
try {
|
|
270
|
-
const res = await
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
344
|
+
const res = await fetchWithTimeout(
|
|
345
|
+
url,
|
|
346
|
+
{
|
|
347
|
+
method: "GET",
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "application/json",
|
|
350
|
+
Authorization: token,
|
|
351
|
+
},
|
|
275
352
|
},
|
|
276
|
-
|
|
353
|
+
FETCH_TIMEOUTS.api
|
|
354
|
+
);
|
|
277
355
|
|
|
278
356
|
const data: any = await res.json();
|
|
279
357
|
|
|
@@ -286,6 +364,13 @@ export class Wappi {
|
|
|
286
364
|
return { error: false, message: `Контакт ${phone} получен из Telegram`, data };
|
|
287
365
|
} catch (error: unknown) {
|
|
288
366
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
367
|
+
|
|
368
|
+
// Отмечаем таймаут отдельно: вызывающий phoneCheckWappi иначе примет
|
|
369
|
+
// «не дождались ответа» за «контакта нет» и полезет создавать контакт
|
|
370
|
+
if (isTimeoutError(error)) {
|
|
371
|
+
return { error: true, message: `Ошибка при запросе контакта: ${errMsg}`, data: null, timeout: true };
|
|
372
|
+
}
|
|
373
|
+
|
|
289
374
|
return { error: true, message: `Ошибка при запросе контакта: ${errMsg}`, data: null };
|
|
290
375
|
}
|
|
291
376
|
}
|
|
@@ -304,17 +389,21 @@ export class Wappi {
|
|
|
304
389
|
const url = `https://wappi.pro/tapi/sync/contact/add?profile_id=${profile_id}`;
|
|
305
390
|
|
|
306
391
|
try {
|
|
307
|
-
const res = await
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
392
|
+
const res = await fetchWithTimeout(
|
|
393
|
+
url,
|
|
394
|
+
{
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
Authorization: token,
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
recipient: phone,
|
|
402
|
+
name: contactName,
|
|
403
|
+
}),
|
|
312
404
|
},
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
name: contactName,
|
|
316
|
-
}),
|
|
317
|
-
});
|
|
405
|
+
FETCH_TIMEOUTS.send
|
|
406
|
+
);
|
|
318
407
|
|
|
319
408
|
const data: any = await res.json();
|
|
320
409
|
if (data.status !== "done") {
|
|
@@ -325,14 +414,16 @@ export class Wappi {
|
|
|
325
414
|
return { error: false, message: `Контакт ${phone} успешно создан в Telegram`, data };
|
|
326
415
|
} catch (error: unknown) {
|
|
327
416
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
328
|
-
|
|
417
|
+
// Создание контакта неидемпотентно: при таймауте ответ потерян, а контакт мог создаться
|
|
418
|
+
const unknownStatus = isTimeoutError(error) ? " — контакт мог быть создан" : "";
|
|
419
|
+
return { error: true, message: `Ошибка при создании контакта: ${errMsg}${unknownStatus}`, data: null };
|
|
329
420
|
}
|
|
330
421
|
}
|
|
331
422
|
|
|
332
423
|
/** Скачиваем файл по ссылке и конвертируем в base64 */
|
|
333
424
|
private async convertToBase64(fileUrl: string): Promise<{ error: boolean; message?: string; data?: string }> {
|
|
334
425
|
try {
|
|
335
|
-
const response = await
|
|
426
|
+
const response = await fetchWithTimeout(fileUrl, undefined, FETCH_TIMEOUTS.transfer);
|
|
336
427
|
if (!response.ok) {
|
|
337
428
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
338
429
|
}
|