@andrey4emk/npm-app-back-b24 3.8.2 → 4.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.
- package/README.md +165 -13
- package/bitrix24/b24/config.ts +24 -0
- package/bitrix24/b24/facade.ts +371 -0
- package/bitrix24/b24/instance.ts +186 -0
- package/bitrix24/b24/proxy.ts +120 -0
- package/bitrix24/b24/retry.ts +490 -0
- package/bitrix24/b24/state.ts +39 -0
- package/bitrix24/b24/tokens.ts +280 -0
- package/bitrix24/b24/types.ts +93 -0
- package/bitrix24/b24.ts +97 -955
- package/bitrix24/errTaskB24.ts +8 -5
- package/bitrix24/eventB24.ts +301 -63
- package/index.ts +4 -1
- package/package.json +26 -7
- package/sendMessage/chatApp.ts +101 -24
- package/sendMessage/email.ts +233 -32
- package/sendMessage/smsgold.ts +11 -2
- package/sendMessage/wappi.ts +41 -15
- package/utils/fetchRetry.ts +70 -9
package/sendMessage/wappi.ts
CHANGED
|
@@ -25,11 +25,31 @@ interface WappiFileMessageData extends WappiMessageData {
|
|
|
25
25
|
fileName: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Ответ Wappi в той части, которую разбирает класс.
|
|
30
|
+
*
|
|
31
|
+
* Описан по фактически читаемым полям, а не по всему API. Поля `check` и `haveContact`
|
|
32
|
+
* в ответе Wappi отсутствуют — их дописывает сам класс в объект ответа, поэтому
|
|
33
|
+
* в интерфейсе они обязаны быть, иначе присваивание не пройдёт тайпчек
|
|
34
|
+
*/
|
|
35
|
+
interface WappiResponse {
|
|
36
|
+
status?: string;
|
|
37
|
+
detail?: string;
|
|
38
|
+
on_max?: boolean;
|
|
39
|
+
on_whatsapp?: boolean;
|
|
40
|
+
contact?: unknown;
|
|
41
|
+
/** Дописывается классом: номер пригоден для отправки */
|
|
42
|
+
check?: boolean;
|
|
43
|
+
/** Дописывается классом: контакт найден или создан */
|
|
44
|
+
haveContact?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/** Стандартный результат операции */
|
|
29
48
|
interface WappiResult {
|
|
30
49
|
error: boolean;
|
|
31
50
|
message: string;
|
|
32
|
-
|
|
51
|
+
/** Ответ Wappi как есть — форму задаёт сервис, потребитель приводит сам */
|
|
52
|
+
data: unknown;
|
|
33
53
|
/**
|
|
34
54
|
* Ответа не дождались. Отличает «проверка сказала нет» от «проверки не было»:
|
|
35
55
|
* без этого признака вызывающий код принимает таймаут за отрицательный ответ.
|
|
@@ -93,7 +113,7 @@ export class Wappi {
|
|
|
93
113
|
url = `https://wappi.pro/maxapi/sync/message/send?${sendOpenLine}profile_id=${profile_id}`;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
|
-
let data:
|
|
116
|
+
let data: WappiResponse;
|
|
97
117
|
try {
|
|
98
118
|
const res = await fetchWithTimeout(
|
|
99
119
|
url!,
|
|
@@ -110,7 +130,7 @@ export class Wappi {
|
|
|
110
130
|
},
|
|
111
131
|
FETCH_TIMEOUTS.send
|
|
112
132
|
);
|
|
113
|
-
data = await res.json();
|
|
133
|
+
data = (await res.json()) as WappiResponse;
|
|
114
134
|
} catch (error: unknown) {
|
|
115
135
|
// Метод исторически без try/catch — перехватываем только таймаут, который сами
|
|
116
136
|
// и вводим, чтобы не появился новый путь исключений там, где его сегодня нет.
|
|
@@ -202,7 +222,7 @@ export class Wappi {
|
|
|
202
222
|
url = `https://wappi.pro/maxapi/async/message/file/url/send?${sendOpenLine}profile_id=${profile_id}`;
|
|
203
223
|
}
|
|
204
224
|
|
|
205
|
-
let data:
|
|
225
|
+
let data: WappiResponse;
|
|
206
226
|
try {
|
|
207
227
|
// В теле уходит base64 файла — бюджет transfer, а не send
|
|
208
228
|
const res = await fetchWithTimeout(
|
|
@@ -223,7 +243,7 @@ export class Wappi {
|
|
|
223
243
|
},
|
|
224
244
|
FETCH_TIMEOUTS.transfer
|
|
225
245
|
);
|
|
226
|
-
data = await res.json();
|
|
246
|
+
data = (await res.json()) as WappiResponse;
|
|
227
247
|
} catch (error: unknown) {
|
|
228
248
|
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
229
249
|
if (!isTimeoutError(error)) throw error;
|
|
@@ -265,9 +285,14 @@ export class Wappi {
|
|
|
265
285
|
const contactCheck = await this.getContactTelegramWappi(phone);
|
|
266
286
|
|
|
267
287
|
// 2 Если существует, то номер в телеге есть, можем отправлять сообщение
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
288
|
+
// Приведение, а не проверка: `data` объявлен unknown только в публичном типе,
|
|
289
|
+
// внутри класса это всегда разобранный ответ Wappi. На ошибке там null,
|
|
290
|
+
// но до чтения поля не доходит — первым проверяется `!contactCheck.error`,
|
|
291
|
+
// порядок вычисления тот же, что и до 4.0.0
|
|
292
|
+
const contactData = contactCheck.data as WappiResponse;
|
|
293
|
+
if (!contactCheck.error && contactData.haveContact) {
|
|
294
|
+
contactData.check = true;
|
|
295
|
+
return { error: false, message: `Номер ${phone} проверен для мессенджера ${messangerType}`, data: contactData };
|
|
271
296
|
}
|
|
272
297
|
|
|
273
298
|
// Ответа не дождались — это не «контакта нет». Создавать контакт вслепую нельзя:
|
|
@@ -280,14 +305,15 @@ export class Wappi {
|
|
|
280
305
|
|
|
281
306
|
// 3 Если не существует, то номера в телеге нет, пробуем создать
|
|
282
307
|
const addContactResult = await this.addContactTelegramWappi(phone);
|
|
308
|
+
const addedData = addContactResult.data as WappiResponse;
|
|
283
309
|
|
|
284
310
|
if (!addContactResult.error) {
|
|
285
311
|
// Если получается создать, отправляем информацию
|
|
286
|
-
|
|
287
|
-
return { error: false, message: `Номер ${phone} успешно добавлен и проверен для мессенджера ${messangerType}`, data:
|
|
312
|
+
addedData.check = true;
|
|
313
|
+
return { error: false, message: `Номер ${phone} успешно добавлен и проверен для мессенджера ${messangerType}`, data: addedData };
|
|
288
314
|
}
|
|
289
315
|
// Если не получается, то отправляем информацию об ошибке
|
|
290
|
-
return { error: true, message: `Не удалось проверить и добавить номер ${phone} в Telegram: ${addContactResult.message}`, data:
|
|
316
|
+
return { error: true, message: `Не удалось проверить и добавить номер ${phone} в Telegram: ${addContactResult.message}`, data: addedData };
|
|
291
317
|
}
|
|
292
318
|
if (messangerType === "max") {
|
|
293
319
|
token = this.maxAuth.token;
|
|
@@ -295,7 +321,7 @@ export class Wappi {
|
|
|
295
321
|
url = `https://wappi.pro/maxapi/sync/contact/check?profile_id=${profile_id}&phone=${phone}`;
|
|
296
322
|
}
|
|
297
323
|
|
|
298
|
-
let data:
|
|
324
|
+
let data: WappiResponse;
|
|
299
325
|
try {
|
|
300
326
|
const res = await fetchWithTimeout(
|
|
301
327
|
url!,
|
|
@@ -308,7 +334,7 @@ export class Wappi {
|
|
|
308
334
|
},
|
|
309
335
|
FETCH_TIMEOUTS.api
|
|
310
336
|
);
|
|
311
|
-
data = await res.json();
|
|
337
|
+
data = (await res.json()) as WappiResponse;
|
|
312
338
|
} catch (error: unknown) {
|
|
313
339
|
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
314
340
|
if (!isTimeoutError(error)) throw error;
|
|
@@ -353,7 +379,7 @@ export class Wappi {
|
|
|
353
379
|
FETCH_TIMEOUTS.api
|
|
354
380
|
);
|
|
355
381
|
|
|
356
|
-
const data
|
|
382
|
+
const data = (await res.json()) as WappiResponse;
|
|
357
383
|
|
|
358
384
|
if (data.status !== "done") {
|
|
359
385
|
return { error: true, message: `Ошибка получения контакта ${phone} из Telegram`, data };
|
|
@@ -405,7 +431,7 @@ export class Wappi {
|
|
|
405
431
|
FETCH_TIMEOUTS.send
|
|
406
432
|
);
|
|
407
433
|
|
|
408
|
-
const data
|
|
434
|
+
const data = (await res.json()) as WappiResponse;
|
|
409
435
|
if (data.status !== "done") {
|
|
410
436
|
return { error: true, message: `Ошибка создания контакта ${phone} в Telegram`, data };
|
|
411
437
|
}
|
package/utils/fetchRetry.ts
CHANGED
|
@@ -31,7 +31,15 @@ function isValidTimeout(value: number): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Верхняя граница подозрительного бюджета: значение от 1 до 999 мс почти наверняка
|
|
35
|
+
* означает, что человек написал секунды вместо миллисекунд.
|
|
36
|
+
*
|
|
37
|
+
* Ноль сюда не входит — это задокументированное выключение таймаута.
|
|
38
|
+
*/
|
|
39
|
+
const SUSPICIOUS_TIMEOUT_MAX_MS = 999;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Читает бюджет запроса из переменной окружения `name`.
|
|
35
43
|
*
|
|
36
44
|
* `dotenv.config()` вызывается в `logs/logs.ts`, а этот модуль импортирует `logs`
|
|
37
45
|
* первой строкой — значит к моменту чтения `process.env` файл `.env` уже загружен.
|
|
@@ -39,15 +47,38 @@ function isValidTimeout(value: number): boolean {
|
|
|
39
47
|
* Значение принимается, только если это целое число >= 0 (0 означает «без таймаута»).
|
|
40
48
|
* Любой мусор в переменной не должен молча превращаться в `NaN` или в обрезанное
|
|
41
49
|
* `parseInt`-число, поэтому проверяем фактом и падаем на дефолт с записью в лог.
|
|
50
|
+
* Пустая строка и строка из пробелов считаются «переменная не задана» — молча дефолт.
|
|
51
|
+
*
|
|
52
|
+
* Значение от 1 до 999 применяется как есть, но с записью `warn`: столько миллисекунд
|
|
53
|
+
* не хватит ни одному живому запросу, и это типовая путаница секунд с миллисекундами.
|
|
54
|
+
* Молча такое значение пропускать нельзя — на бюджетах отправки оно превращается
|
|
55
|
+
* в массовую недоставку с «неизвестным статусом» и потоком `error` в чат портала.
|
|
56
|
+
*
|
|
57
|
+
* Экспортируется по двум причинам: одинаковая семантика для всех бюджетов пакета
|
|
58
|
+
* (дефолт `fetchRetry` и каждый из `FETCH_TIMEOUTS` читаются одной и той же функцией)
|
|
59
|
+
* и проверяемость тестом без запуска дочернего процесса.
|
|
60
|
+
*
|
|
61
|
+
* Важно: все чтения происходят **один раз при загрузке модуля**. Правка `process.env`
|
|
62
|
+
* в рантайме на уже вычисленные константы не влияет.
|
|
63
|
+
*
|
|
64
|
+
* @param name — имя переменной окружения
|
|
65
|
+
* @param fallback — значение, если переменная не задана или непригодна
|
|
66
|
+
* @returns бюджет в мс
|
|
42
67
|
*/
|
|
43
|
-
function readTimeoutFromEnv(): number {
|
|
44
|
-
const raw = process.env
|
|
45
|
-
if (raw === undefined || raw.trim() === "") return
|
|
68
|
+
export function readTimeoutFromEnv(name: string, fallback: number): number {
|
|
69
|
+
const raw = process.env[name];
|
|
70
|
+
if (raw === undefined || raw.trim() === "") return fallback;
|
|
46
71
|
|
|
47
72
|
const parsed = Number(raw);
|
|
48
73
|
if (!isValidTimeout(parsed)) {
|
|
49
|
-
logs.add(
|
|
50
|
-
return
|
|
74
|
+
logs.add(`${name}: значение '${raw}' непригодно (нужно целое от 0 до ${MAX_TIMEOUT_MS}) — берём ${fallback}мс`, "warn");
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Значение применяем как есть: аварийный рычаг остаётся в руках у человека,
|
|
79
|
+
// мы только делаем подозрительный ввод видимым
|
|
80
|
+
if (parsed >= 1 && parsed <= SUSPICIOUS_TIMEOUT_MAX_MS) {
|
|
81
|
+
logs.add(`${name}: значение '${raw}' похоже на секунды, а нужны миллисекунды — бюджет ${parsed}мс применён как есть`, "warn");
|
|
51
82
|
}
|
|
52
83
|
|
|
53
84
|
return parsed;
|
|
@@ -58,7 +89,13 @@ function readTimeoutFromEnv(): number {
|
|
|
58
89
|
* из `FETCH_TIMEOUT_MS`, иначе 60 000. Значение 0 полностью выключает таймаут —
|
|
59
90
|
* это аварийный рычаг для случая, когда дефолт обрывает живой долгий запрос.
|
|
60
91
|
*/
|
|
61
|
-
export const DEFAULT_FETCH_TIMEOUT_MS: number = readTimeoutFromEnv();
|
|
92
|
+
export const DEFAULT_FETCH_TIMEOUT_MS: number = readTimeoutFromEnv("FETCH_TIMEOUT_MS", FALLBACK_TIMEOUT_MS);
|
|
93
|
+
|
|
94
|
+
/** Дефолты именованных бюджетов до чтения окружения, мс */
|
|
95
|
+
const FALLBACK_TIMEOUTS = { quick: 10_000, api: 20_000, send: 30_000, transfer: 60_000 } as const;
|
|
96
|
+
|
|
97
|
+
/** Имя именованного бюджета пакета */
|
|
98
|
+
export type FetchTimeoutName = keyof typeof FALLBACK_TIMEOUTS;
|
|
62
99
|
|
|
63
100
|
/**
|
|
64
101
|
* Именованные бюджеты для вызовов внутри пакета: смысл каждого запроса тут известен,
|
|
@@ -68,8 +105,32 @@ export const DEFAULT_FETCH_TIMEOUT_MS: number = readTimeoutFromEnv();
|
|
|
68
105
|
* - `api` — короткое чтение (проверка номера, список лицензий, получение контакта);
|
|
69
106
|
* - `send` — отправка текста, создание контакта, отправка SMS;
|
|
70
107
|
* - `transfer` — скачивание файла и отправка файла/вложения.
|
|
108
|
+
*
|
|
109
|
+
* Каждый бюджет переопределяется своей переменной окружения:
|
|
110
|
+
*
|
|
111
|
+
* | Ключ | Переменная | Дефолт, мс |
|
|
112
|
+
* |---|---|---|
|
|
113
|
+
* | `quick` | `FETCH_TIMEOUT_QUICK_MS` | 10 000 |
|
|
114
|
+
* | `api` | `FETCH_TIMEOUT_API_MS` | 20 000 |
|
|
115
|
+
* | `send` | `FETCH_TIMEOUT_SEND_MS` | 30 000 |
|
|
116
|
+
* | `transfer` | `FETCH_TIMEOUT_TRANSFER_MS` | 60 000 |
|
|
117
|
+
*
|
|
118
|
+
* Значения читаются один раз при загрузке модуля: правка `process.env` в рантайме
|
|
119
|
+
* их не двигает, менять бюджет нужно до старта процесса.
|
|
120
|
+
*
|
|
121
|
+
* Значение 0 выключает таймаут для этого бюджета целиком. Это аварийный рычаг,
|
|
122
|
+
* и у отправок (`send`, `transfer`) он снимает заодно защиту «неизвестный статус
|
|
123
|
+
* доставки»: при нуле зависшая отправка снова держится до таймаутов undici,
|
|
124
|
+
* а `unknownDelivery` в `Smsgold` не выставится никогда.
|
|
125
|
+
*
|
|
126
|
+
* Непригодное значение защиту молча не выключает: одна строка `warn` и дефолт.
|
|
71
127
|
*/
|
|
72
|
-
export const FETCH_TIMEOUTS
|
|
128
|
+
export const FETCH_TIMEOUTS: Readonly<Record<FetchTimeoutName, number>> = {
|
|
129
|
+
quick: readTimeoutFromEnv("FETCH_TIMEOUT_QUICK_MS", FALLBACK_TIMEOUTS.quick),
|
|
130
|
+
api: readTimeoutFromEnv("FETCH_TIMEOUT_API_MS", FALLBACK_TIMEOUTS.api),
|
|
131
|
+
send: readTimeoutFromEnv("FETCH_TIMEOUT_SEND_MS", FALLBACK_TIMEOUTS.send),
|
|
132
|
+
transfer: readTimeoutFromEnv("FETCH_TIMEOUT_TRANSFER_MS", FALLBACK_TIMEOUTS.transfer),
|
|
133
|
+
};
|
|
73
134
|
|
|
74
135
|
/** Читает `name` с произвольного значения ошибки, не полагаясь на её тип */
|
|
75
136
|
function getErrorName(error: unknown): string | null {
|
|
@@ -135,7 +196,7 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
135
196
|
|
|
136
197
|
/**
|
|
137
198
|
* Коды, доказывающие, что соединение не было установлено: DNS не разрешился либо
|
|
138
|
-
* хост отверг подключение.
|
|
199
|
+
* хост отверг подключение. Соединение с целевым хостом не состоялось.
|
|
139
200
|
*/
|
|
140
201
|
const PRE_CONNECTION_ERROR_CODES = ["ENOTFOUND", "EAI_AGAIN", "EAI_NODATA", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH", "ENETDOWN"];
|
|
141
202
|
|