@andrey4emk/npm-app-back-b24 3.9.0 → 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 +98 -3
- package/bitrix24/b24/facade.ts +14 -1
- package/bitrix24/b24/proxy.ts +51 -9
- package/bitrix24/b24/retry.ts +16 -4
- package/bitrix24/b24/tokens.ts +8 -6
- package/bitrix24/b24/types.ts +27 -0
- package/bitrix24/b24.ts +43 -12
- package/bitrix24/errTaskB24.ts +8 -5
- package/bitrix24/eventB24.ts +71 -35
- package/index.ts +4 -1
- package/package.json +17 -6
- package/sendMessage/chatApp.ts +101 -24
- package/sendMessage/smsgold.ts +11 -2
- package/sendMessage/wappi.ts +41 -15
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- [Логирование](#логирование)
|
|
18
18
|
- [fetchRetry и таймауты](#fetchretry-и-таймауты)
|
|
19
19
|
- [Переменные окружения](#переменные-окружения)
|
|
20
|
+
- [Миграция на 4.0.0](#миграция-на-400)
|
|
20
21
|
- [Скрипты](#скрипты)
|
|
21
22
|
- [Лицензия](#лицензия)
|
|
22
23
|
|
|
@@ -26,6 +27,15 @@
|
|
|
26
27
|
npm install @andrey4emk/npm-app-back-b24
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
`nodemailer` — **опциональная peer-зависимость**: её ставит потребитель, и только если пользуется классом `Email`. Всем остальным она не нужна и не приезжает.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Только тем, кто шлёт почту
|
|
34
|
+
npm install nodemailer@9.0.5
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Типы (`@types/node`, `@types/luxon` и прочие) пакет **не транслирует** — с 4.0.0 они лежат в его `devDependencies`. Потребителю, который гоняет `tsc`, нужны свои: подробности в разделе [Миграция на 4.0.0](#миграция-на-400).
|
|
38
|
+
|
|
29
39
|
### Точки входа
|
|
30
40
|
|
|
31
41
|
| Импорт | Что отдаёт | Поднимает OAuth-модуль |
|
|
@@ -33,6 +43,7 @@ npm install @andrey4emk/npm-app-back-b24
|
|
|
33
43
|
| `@andrey4emk/npm-app-back-b24` | всё | да |
|
|
34
44
|
| `@andrey4emk/npm-app-back-b24/logs` | `logs` | нет |
|
|
35
45
|
| `@andrey4emk/npm-app-back-b24/fetchRetry` | `fetchRetry`, `fetchWithTimeout`, `isNetworkError`, `isPreConnectionError`, `isTimeoutError`, `isAbortError`, `maskUrl`, `FETCH_TIMEOUTS`, `DEFAULT_FETCH_TIMEOUT_MS` | нет |
|
|
46
|
+
| `@andrey4emk/npm-app-back-b24/email` | `Email` | нет |
|
|
36
47
|
|
|
37
48
|
Корневой импорт — barrel: он тянет модуль OAuth, который при загрузке читает `authB24.json` и, если приложение авторизовано, запускает таймер проактивного обновления токена. Проектам, которые ходят в B24 по **входящему вебхуку** или берут из пакета только логгер и `fetchRetry`, удобнее импортировать по подпутям — тогда OAuth-модуль не загружается вообще.
|
|
38
49
|
|
|
@@ -42,6 +53,13 @@ import { logs } from "@andrey4emk/npm-app-back-b24/logs";
|
|
|
42
53
|
import { fetchRetry, maskUrl } from "@andrey4emk/npm-app-back-b24/fetchRetry";
|
|
43
54
|
```
|
|
44
55
|
|
|
56
|
+
Подпуть `./email` стоит особняком: `Email` вынесен из barrel'а не ради экономии импорта, а ради зависимости. Он единственный тянет `nodemailer`, и пока класс лежал в barrel'е, почтовая библиотека приезжала во все восемь проектов — включая те шесть, что почту не шлют.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// Почту шлём — ставим nodemailer и берём Email из подпути
|
|
60
|
+
import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
61
|
+
```
|
|
62
|
+
|
|
45
63
|
Корневой импорт при этом остаётся безопасным: если `APP_B24_CLIENT_ID` и `APP_B24_CLIENT_SECRET` не заданы, пакет считает, что OAuth в проекте не используется, молча выставляет `$b24 = null` и пишет об этом только в `debug`.
|
|
46
64
|
|
|
47
65
|
### Строгость типов
|
|
@@ -101,7 +119,6 @@ import {
|
|
|
101
119
|
Event,
|
|
102
120
|
ChatApp,
|
|
103
121
|
Smsgold,
|
|
104
|
-
Email,
|
|
105
122
|
Wappi,
|
|
106
123
|
logs,
|
|
107
124
|
fetchRetry,
|
|
@@ -112,6 +129,9 @@ import {
|
|
|
112
129
|
FETCH_TIMEOUTS,
|
|
113
130
|
} from "@andrey4emk/npm-app-back-b24";
|
|
114
131
|
|
|
132
|
+
// Email живёт в подпути: он единственный тянет optional peer nodemailer
|
|
133
|
+
import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
134
|
+
|
|
115
135
|
// $b24 — готовый экземпляр B24OAuth (или null, если токены не настроены)
|
|
116
136
|
if ($b24) {
|
|
117
137
|
const result = await $b24.callMethod("crm.contact.list", { limit: 5 });
|
|
@@ -704,7 +724,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
704
724
|
- **`Email`** — класс для отправки email через SMTP Яндекса. Экземпляр нужно создать самостоятельно.
|
|
705
725
|
|
|
706
726
|
```js
|
|
707
|
-
import { Email } from "@andrey4emk/npm-app-back-b24";
|
|
727
|
+
import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
708
728
|
|
|
709
729
|
const emailClient = new Email({
|
|
710
730
|
user: "your-email@yandex.ru",
|
|
@@ -743,7 +763,9 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
743
763
|
- **Отвергнутый получатель — это ошибка.** Ответ транспорта разбирается: адрес ищется в `accepted` и `rejected`, элементы принимаются и строкой, и объектом `{ name, address }`. Текст ошибки несёт адрес и ответ сервера. Сверка идёт по **нормализованному** адресу: снимается регистр, снимаются угловые скобки, домен приводится к punycode. Иначе живой адрес объявлялся бы недоставленным — почтовый сервер возвращает адреса из конверта, а не в том виде, как их передали: `ivan@пример.рф` приходит обратно как `ivan@xn--e1afmkfd.xn--p1ai`, а `<a@b.ru>` — как `a@b.ru`. Если транспорт не сообщил ни `accepted`, ни `rejected`, результат считается успешным и в лог уходит одна строка `warn`: объявить доставленное письмо ошибкой было бы хуже исходного дефекта.
|
|
744
764
|
- **Входной объект не мутируется.** Прежняя реализация записывала вложения обратно в переданный `dataMail`, затирая `attachments` вызывающего.
|
|
745
765
|
- **Второй аргумент конструктора — подмена транспорта.** `new Email(auth, transport)` принимает любой объект с методом `sendMail(options)` (тип `MailTransport`). Нужен тестам и потребителю со своим SMTP; без него класс поднимает транспорт Яндекса сам.
|
|
746
|
-
-
|
|
766
|
+
- **Класс живёт в подпути `@andrey4emk/npm-app-back-b24/email`, а не в корне.** Из barrel'а он убран в 4.0.0.
|
|
767
|
+
- Транспорт — `nodemailer`, **опциональная peer-зависимость** с диапазоном `^9.0.5 || ^10.0.0`. Ставит его потребитель. Нижняя граница — CVE (GHSA-p6gq-j5cr-w38f, уязвимы версии `<= 9.0.0`), верхняя — проверенная совместимость с десяткой.
|
|
768
|
+
- `@types/nodemailer` нужен **только на девятке**. У `nodemailer` 10 типы свои, и TypeScript предпочтёт их; отдельный пакет типов там лишний.
|
|
747
769
|
- Вложения передаются готовым `Buffer` — файл по `fileUrl` пакет скачивает сам. Поля `path` и `href` намеренно не поддерживаются: nodemailer не должен ходить по URL. Скачивание идёт через `fetchWithTimeout` с бюджетом 60 секунд.
|
|
748
770
|
- Таймауты SMTP-транспорта переопределены: соединение 15 с, приветствие 10 с, сокет 120 с. Дефолты nodemailer (2 мин / 30 с / 10 мин) слишком щедры — письмо на Яндекс уходит за секунды, а 10 минут молчания сокета блокируют вызывающую очередь.
|
|
749
771
|
|
|
@@ -999,6 +1021,79 @@ CHATAPP_PASS=your-password
|
|
|
999
1021
|
CHATAPP_APP_ID=your-app-id
|
|
1000
1022
|
```
|
|
1001
1023
|
|
|
1024
|
+
## Миграция на 4.0.0
|
|
1025
|
+
|
|
1026
|
+
Выпуск ломающий. Правок у потребителя четыре, все механические.
|
|
1027
|
+
|
|
1028
|
+
### 1. `Email` переехал в подпуть
|
|
1029
|
+
|
|
1030
|
+
Из общего импорта его убрать, добавить отдельной строкой:
|
|
1031
|
+
|
|
1032
|
+
```diff
|
|
1033
|
+
-import { $b24, logs, Email } from "@andrey4emk/npm-app-back-b24";
|
|
1034
|
+
+import { $b24, logs } from "@andrey4emk/npm-app-back-b24";
|
|
1035
|
+
+import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
Затрагивает два проекта: `b24_conector/server/api/sendMessage.ts` и `b24_desktop/server/api/sendMessage.js` (первая строка в обоих). Больше `Email` из пакета никто не импортирует.
|
|
1039
|
+
|
|
1040
|
+
### 2. `nodemailer` — теперь зависимость потребителя
|
|
1041
|
+
|
|
1042
|
+
Кто пользуется `Email`, ставит его себе: `pnpm add nodemailer@9.0.5` (оба проекта на pnpm). Диапазон peer — `^9.0.5 || ^10.0.0`. Тем же двум проектам `@types/nodemailer` в `devDependencies`, если остаются на девятке; на десятке типы приходят с самим пакетом.
|
|
1043
|
+
|
|
1044
|
+
Шаг обязателен обоим, но по разным причинам:
|
|
1045
|
+
|
|
1046
|
+
- **`b24_conector`** — `nodemailer` в его `package.json` отсутствует вовсе, библиотека приезжала транзитивно из наших `dependencies`. После 4.0.0 не приедет, и первый же импорт `server/api/sendMessage.ts` упадёт с `ERR_MODULE_NOT_FOUND`. Вместе с почтой встанут `ChatApp`, `Wappi` и SemySMS — они живут в том же модуле. Пропустить шаг нельзя;
|
|
1047
|
+
- **`b24_desktop`** — зависимость уже прописана в его `package.json`, поставленная версия `9.0.5` подходит под диапазон. Здесь достаточно сверки.
|
|
1048
|
+
|
|
1049
|
+
Под pnpm несовпадение опционального peer не блокирует установку: менеджер пишет `WARN` и завершает работу с нулевым кодом. Кому нужен жёсткий блок — `strict-peer-dependencies=true` в своём `.npmrc`.
|
|
1050
|
+
|
|
1051
|
+
Остальным шести проектам делать нечего — почтовая библиотека к ним больше не приезжает.
|
|
1052
|
+
|
|
1053
|
+
### 3. `@types/*` больше не приезжают из пакета
|
|
1054
|
+
|
|
1055
|
+
Они переехали в `devDependencies` и потребителю не транслируются. Кто гоняет `tsc`, держит свои:
|
|
1056
|
+
|
|
1057
|
+
- `@types/node` — всем;
|
|
1058
|
+
- `@types/luxon` — всем, кто импортирует корень (`errTaskB24.ts` тянет `luxon` в рантайме);
|
|
1059
|
+
- `@types/express` — **больше не нужен из-за нас никому**: пакет типы `express` не импортирует вовсе.
|
|
1060
|
+
|
|
1061
|
+
Без `@types/luxon` тайпчек упадёт с `TS7016` на `bitrix24/errTaskB24.ts`. Семь из восьми живых потребителей его уже держат.
|
|
1062
|
+
|
|
1063
|
+
### 4. Изменённые публичные типы
|
|
1064
|
+
|
|
1065
|
+
`any` в поверхности пакета заменён на `unknown` — читающий поле напрямую добавляет приведение.
|
|
1066
|
+
|
|
1067
|
+
| Что | Было | Стало |
|
|
1068
|
+
| ---------------------------------------------- | ------- | --------------------------- |
|
|
1069
|
+
| `data` у `event.get()` и `event.clear()` | `any` | `unknown` |
|
|
1070
|
+
| `ConnectorMessage.file` | `any[] \| null` | `TelegramFile[] \| null` |
|
|
1071
|
+
| `ConnectorMessage.attachments` | `any[] \| null` | `unknown[] \| null` |
|
|
1072
|
+
| `ConnectorMessage.im` | `any \| null` | `Record<string, unknown> \| null` |
|
|
1073
|
+
| `data` у методов `Wappi` | `any` | `unknown` |
|
|
1074
|
+
| `data` у `errorB24()` | `any` | `unknown` |
|
|
1075
|
+
|
|
1076
|
+
```diff
|
|
1077
|
+
-const { data } = await event.get("ONCRMDEALUPDATE");
|
|
1078
|
+
-for (const id of data.entitysId) { /* ... */ }
|
|
1079
|
+
+const { data } = await event.get("ONCRMDEALUPDATE");
|
|
1080
|
+
+const events = data as EntityEvents;
|
|
1081
|
+
+for (const id of events.entitysId) { /* ... */ }
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
Тип `TelegramFile` экспортируется из пакета — своё объявление в `b24_conector` можно убрать, но замена не drop-in: у них поле объявлено `name?: string`, в пакете оно `name: string`. Разбор пакета всегда отдаёт строку (на битом элементе — пустую), поэтому чтение вида `fileInfo.name || fallback` продолжит работать. Сверки потребуют их собственные места, где `TelegramFile` создаётся, — в том числе очередь ретраев `confB24Outbox.ts`: объект без `name` в наш тип не ляжет и упадёт тайпчеком.
|
|
1085
|
+
|
|
1086
|
+
`saveAuthB24Handler` больше не типизирован типами `express`, но по-прежнему принимает `Request`/`Response` без правок: `app.post("/saveAuthB24", saveAuthB24Handler)` работает как раньше.
|
|
1087
|
+
|
|
1088
|
+
Дефолты дженериков `callMethod`, `callBatch`, `fetchListMethod` и `getResultData` остались `any` намеренно — код вида `chunk.forEach((x) => x.ID)` продолжает компилироваться.
|
|
1089
|
+
|
|
1090
|
+
### Что изменилось само, правок не требует
|
|
1091
|
+
|
|
1092
|
+
- Вместе с `Email` из барреля уехали и его типы — `MailTransport` и `SendMailInfo`. Теперь они в подпути `@andrey4emk/npm-app-back-b24/email`. Из потребителей их никто не импортирует.
|
|
1093
|
+
- `saveAuthB24Handler` на запрос **без тела** отвечает `400 Не заполнены обязательные поля.` вместо прежних `500`. Раньше разбор пустого тела падал и уходил в общий `catch`.
|
|
1094
|
+
- Он же отвечает `400` на **нестроковые поля** тела: объект в `access_token`, число в `member_id`, нечисловой `expires_in`. Раньше такой запрос давал `500` — а в 4.0.0 до этой правки успевал затереть рабочий `config/authB24.json` строкой `"[object Object]"` и ответить `201`. Файл токенов в этом случае не трогается.
|
|
1095
|
+
- `expires_in`, пришедший строкой (`"3600"`), считается правильно. Раньше склейка строк в расчёте `expires` давала абсурдную дату.
|
|
1096
|
+
|
|
1002
1097
|
## Скрипты
|
|
1003
1098
|
|
|
1004
1099
|
- `npm run test` — прогон юнит-тестов из `tests/` через встроенный `node --test`. Дополнительных зависимостей не нужно: `.ts` запускаются на нативном стирании типов Node, поэтому прогону нужен Node 22.6+ (`engines: ">=20.3.0"` — это требование к потребителю пакета, а не к разработке). Сети тесты не касаются, в тарбол папка не попадает.
|
package/bitrix24/b24/facade.ts
CHANGED
|
@@ -31,6 +31,11 @@ export const FETCH_LIST_MAX_PAGES = 20000;
|
|
|
31
31
|
* Функция превращает такой ответ в понятную ошибку вместо падения на undefined.
|
|
32
32
|
* Также отсекает случай, когда запрос успешен, но result пустой (null/undefined).
|
|
33
33
|
*
|
|
34
|
+
* Дефолт дженерика — `any`, и это осознанно: потребители пишут
|
|
35
|
+
* `getResultData(response, "tasks.task.add").task.id` без параметра типа.
|
|
36
|
+
* Замена дефолта на `unknown` сломала бы каждое такое обращение, ничего не дав
|
|
37
|
+
* взамен: узкий тип задаётся на месте вызова — `getResultData<Deal>(response, method)`.
|
|
38
|
+
*
|
|
34
39
|
* @param response — результат $b24.callMethod()
|
|
35
40
|
* @param methodName — имя метода B24, попадёт в текст ошибки
|
|
36
41
|
*/
|
|
@@ -82,8 +87,16 @@ export function readV2Envelope(response: AjaxResult, method: string): V2Envelope
|
|
|
82
87
|
* идемпотентности (`getRetryBlockReason`) видит те же аргументы, что и раньше,
|
|
83
88
|
* — retry оборачивает фасад снаружи, а преобразование в объект опций
|
|
84
89
|
* происходит уже внутри.
|
|
90
|
+
*
|
|
91
|
+
* `any` в сигнатурах ниже (`callMethod<T = any>`, `params?: any`, `AsyncGenerator<any[]>`,
|
|
92
|
+
* `Array<any>`, а также `call.make<any>` внутри `fetchListMethod` — тип страницы
|
|
93
|
+
* задаёт вызывающий) оставлен НАМЕРЕННО и снятию по T-53 не подлежит. Это дефолты дженериков
|
|
94
|
+
* публичной поверхности, дословно повторяющие `AbstractB24`: на них стоит код потребителей
|
|
95
|
+
* (`for await (const chunk of $b24.fetchListMethod(...)) chunk.forEach((x) => x.ID)`).
|
|
96
|
+
* `unknown` в дефолте уронил бы каждое такое место, не дав взамен ничего — узкий тип
|
|
97
|
+
* задаётся параметром типа на месте вызова (`callMethod<Deal>(...)`).
|
|
85
98
|
*/
|
|
86
|
-
export function createFacade(target: B24OAuth):
|
|
99
|
+
export function createFacade(target: B24OAuth): FacadeMethods {
|
|
87
100
|
// Геттер actions при каждом обращении проверяет инициализацию экземпляра —
|
|
88
101
|
// читаем его в момент вызова, а не один раз при создании фасада
|
|
89
102
|
const v2 = () => target.actions.v2;
|
package/bitrix24/b24/proxy.ts
CHANGED
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
import type { B24OAuth } from "@bitrix24/b24jssdk";
|
|
2
2
|
import { createFacade } from "./facade.ts";
|
|
3
3
|
import { withRetry } from "./retry.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type { AsyncFn } from "./retry.ts";
|
|
5
|
+
import type { B24Client, FacadeMethods } from "./types.ts";
|
|
5
6
|
|
|
6
|
-
// ==================== Константы ====================
|
|
7
|
+
// ==================== Константы и типы ====================
|
|
8
|
+
|
|
9
|
+
/** Имя метода, который Proxy резолвит в реализацию пакета */
|
|
10
|
+
type FacadeMethodName = keyof FacadeMethods;
|
|
7
11
|
|
|
8
12
|
/** Методы фасада, оборачиваемые retry-логикой (fetchListMethod — async-генератор, его оборачивать нельзя) */
|
|
9
|
-
|
|
13
|
+
const RETRYABLE_METHOD_NAMES = ["callMethod", "callListMethod", "callBatch"] as const;
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Имена, которые Proxy резолвит в собственную реализацию пакета поверх `actions.v2.*`.
|
|
13
17
|
*
|
|
14
|
-
* Шире, чем
|
|
18
|
+
* Шире, чем RETRYABLE_METHOD_NAMES: `fetchListMethod` и `callBatchByChunk` мы реализуем,
|
|
15
19
|
* но не ретраим. `callBatchByChunk` в наборе обязателен — пока хоть один deprecated-метод
|
|
16
20
|
* SDK достижим через `$b24`, он будет писать предупреждение об устаревании.
|
|
21
|
+
*
|
|
22
|
+
* `satisfies` ловит опечатку и переименование: имя, которого нет среди методов фасада,
|
|
23
|
+
* уронит тайпчек здесь, а не молча уедет мимо Proxy в SDK.
|
|
24
|
+
*/
|
|
25
|
+
const FACADE_METHOD_NAMES = ["callMethod", "callListMethod", "fetchListMethod", "callBatch", "callBatchByChunk"] as const satisfies readonly FacadeMethodName[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Обратная страховка к `satisfies` выше: ловит НЕдостачу.
|
|
29
|
+
* Забыли имя — `Exclude` перестанет быть `never`, и тип станет `never`,
|
|
30
|
+
* а `true` в него не присвоится. Значение нигде не используется
|
|
17
31
|
*/
|
|
18
|
-
|
|
32
|
+
const _facadeNamesAreExhaustive: Exclude<FacadeMethodName, (typeof FACADE_METHOD_NAMES)[number]> extends never ? true : never = true;
|
|
33
|
+
void _facadeNamesAreExhaustive;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Имя метода фасада, который дополнительно оборачивается retry.
|
|
37
|
+
*
|
|
38
|
+
* Уже, чем `FacadeMethodName`: `fetchListMethod` — async-генератор, он возвращает
|
|
39
|
+
* не `Promise`, под `AsyncFn` не подходит и в `withRetry` не попадает даже по типу.
|
|
40
|
+
* Тип выведен из самого списка, поэтому список и тип разойтись не могут
|
|
41
|
+
*/
|
|
42
|
+
type RetryableMethodName = (typeof RETRYABLE_METHOD_NAMES)[number];
|
|
43
|
+
|
|
44
|
+
/** Тип `ReadonlySet<string>`, а не `Set<RetryableMethodName>`: `has()` вызывается с произвольным ключом Proxy */
|
|
45
|
+
export const RETRYABLE_METHODS: ReadonlySet<string> = new Set<string>(RETRYABLE_METHOD_NAMES);
|
|
46
|
+
|
|
47
|
+
/** См. FACADE_METHOD_NAMES */
|
|
48
|
+
export const FACADE_METHODS: ReadonlySet<string> = new Set<string>(FACADE_METHOD_NAMES);
|
|
49
|
+
|
|
50
|
+
/** Метод резолвится в реализацию пакета, а не SDK */
|
|
51
|
+
function isFacadeMethod(prop: string): prop is FacadeMethodName {
|
|
52
|
+
return FACADE_METHODS.has(prop);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Метод фасада оборачивается retry-логикой */
|
|
56
|
+
function isRetryableMethod(prop: string): prop is RetryableMethodName {
|
|
57
|
+
return RETRYABLE_METHODS.has(prop);
|
|
58
|
+
}
|
|
19
59
|
|
|
20
60
|
// ==================== Proxy-обёртка ====================
|
|
21
61
|
|
|
@@ -40,10 +80,12 @@ export function wrapB24WithRetry(b24: B24OAuth): B24Client {
|
|
|
40
80
|
get(target, prop) {
|
|
41
81
|
// Имена фасада проверяем до Reflect.get: пока SDK ещё объявляет свои
|
|
42
82
|
// deprecated-методы, иначе мы отдавали бы их, а не свои
|
|
43
|
-
if (typeof prop === "string" &&
|
|
83
|
+
if (typeof prop === "string" && isFacadeMethod(prop)) {
|
|
44
84
|
if (!methodCache.has(prop)) {
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
// Индексируем внутри ветки: после сужения prop до RetryableMethodName
|
|
86
|
+
// из типа facade[prop] уходит fetchListMethod, и withRetry принимает
|
|
87
|
+
// остаток без приведения
|
|
88
|
+
methodCache.set(prop, isRetryableMethod(prop) ? withRetry(facade[prop], target, prop) : facade[prop]);
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
return methodCache.get(prop);
|
|
@@ -59,7 +101,7 @@ export function wrapB24WithRetry(b24: B24OAuth): B24Client {
|
|
|
59
101
|
|
|
60
102
|
if (!methodCache.has(prop)) {
|
|
61
103
|
const wrapped = RETRYABLE_METHODS.has(prop)
|
|
62
|
-
? withRetry(value as
|
|
104
|
+
? withRetry(value as AsyncFn, target, prop)
|
|
63
105
|
: value.bind(target);
|
|
64
106
|
methodCache.set(prop, wrapped);
|
|
65
107
|
}
|
package/bitrix24/b24/retry.ts
CHANGED
|
@@ -2,6 +2,18 @@ import { AjaxError, RefreshTokenError, SdkError } from "@bitrix24/b24jssdk";
|
|
|
2
2
|
import { logs } from "../../logs/logs.ts";
|
|
3
3
|
import { isNetworkError, isPreConnectionError } from "../../utils/fetchRetry.ts";
|
|
4
4
|
|
|
5
|
+
// ==================== Типы ====================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Любая асинхронная функция.
|
|
9
|
+
*
|
|
10
|
+
* `never[]` в параметрах делает тип надмножеством всех сигнатур: `never`
|
|
11
|
+
* присваивается чему угодно, поэтому под ограничение подходит и `callMethod`,
|
|
12
|
+
* и `callBatch`. Возврат `Promise<unknown>` отсекает `fetchListMethod` —
|
|
13
|
+
* это async-генератор, и оборачивать его retry-обёрткой нельзя (`for await` ломается).
|
|
14
|
+
*/
|
|
15
|
+
export type AsyncFn = (...args: never[]) => Promise<unknown>;
|
|
16
|
+
|
|
5
17
|
// ==================== Константы ====================
|
|
6
18
|
|
|
7
19
|
/** Количество попыток при сетевых ошибках */
|
|
@@ -302,7 +314,7 @@ function joinReasons(creating: string[], reserving: string[]): string | null {
|
|
|
302
314
|
* не удалось, вызов считается создающим. Ошибиться в сторону лишней осторожности
|
|
303
315
|
* дешевле — потребитель получит ошибку вместо тихого дубликата.
|
|
304
316
|
*/
|
|
305
|
-
export function getRetryBlockReason(sdkMethod: string, args:
|
|
317
|
+
export function getRetryBlockReason(sdkMethod: string, args: readonly unknown[]): string | null {
|
|
306
318
|
const first = args[0];
|
|
307
319
|
|
|
308
320
|
// callMethod(method, params) и callListMethod(method, params, ...)
|
|
@@ -430,10 +442,10 @@ export async function runWithRetry<T>(fn: () => Promise<T>, label: string, block
|
|
|
430
442
|
}
|
|
431
443
|
|
|
432
444
|
/** Оборачивает async-функцию retry-логикой при сетевых ошибках */
|
|
433
|
-
export function withRetry<T extends (
|
|
434
|
-
return (async (...args:
|
|
445
|
+
export function withRetry<T extends AsyncFn>(fn: T, context: object, methodName: string): T {
|
|
446
|
+
return (async (...args: Parameters<T>) => {
|
|
435
447
|
// Состав вызова между попытками не меняется — разбираем один раз
|
|
436
|
-
const blockReason = getRetryBlockReason(methodName, args);
|
|
448
|
+
const blockReason = getRetryBlockReason(methodName, args as readonly unknown[]);
|
|
437
449
|
return runWithRetry(() => fn.apply(context, args), `$b24.${methodName}`, blockReason);
|
|
438
450
|
}) as T;
|
|
439
451
|
}
|
package/bitrix24/b24/tokens.ts
CHANGED
|
@@ -64,9 +64,10 @@ export function saveTokens(authData: AuthData): SaveResult {
|
|
|
64
64
|
confAuthB24.set(APP_ENV, { ...authData, domain: cleanDomain(authData.domain) });
|
|
65
65
|
logs.add("Токены Bitrix24 сохранены", "debug");
|
|
66
66
|
return { error: false, message: "Токены сохранены." };
|
|
67
|
-
} catch (error:
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
logs.add(`Ошибка сохранения токенов: ${message}`, "error");
|
|
70
|
+
return { error: true, message };
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -234,9 +235,10 @@ export async function refreshAndSaveTokens(): Promise<SaveResult> {
|
|
|
234
235
|
try {
|
|
235
236
|
const authData = await refreshAuthWithMutex();
|
|
236
237
|
return saveTokens(authData);
|
|
237
|
-
} catch (error:
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
} catch (error: unknown) {
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
logs.add(`Ошибка обновления токенов: ${message}`, "error");
|
|
241
|
+
return { error: true, message };
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
|
package/bitrix24/b24/types.ts
CHANGED
|
@@ -8,6 +8,26 @@ export interface SaveResult {
|
|
|
8
8
|
message: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Минимальный контракт HTTP-запроса, который читает `saveAuthB24Handler`.
|
|
13
|
+
*
|
|
14
|
+
* Своя структура вместо `Request` из `@types/express`: три потребителя
|
|
15
|
+
* (`b24_rnp`, `b24_ai`, `marketing_vk_ads`) `express` не ставят вовсе и жили
|
|
16
|
+
* на нашей копии типов, приезжавшей транзитивно. С 4.0.0 `@types/*` в
|
|
17
|
+
* `devDependencies`, поэтому такой копии больше нет.
|
|
18
|
+
*
|
|
19
|
+
* Express подставляет сюда свой `Request` без правок у потребителя:
|
|
20
|
+
* параметры контравариантны, и `Request.body: any` подходит под `body: unknown`.
|
|
21
|
+
*/
|
|
22
|
+
export interface AuthSaveRequest {
|
|
23
|
+
body: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Минимальный контракт HTTP-ответа, который пишет `saveAuthB24Handler` */
|
|
27
|
+
export interface AuthSaveResponse {
|
|
28
|
+
status(code: number): { json(body: unknown): unknown };
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
/**
|
|
12
32
|
* `B24OAuth` плюс пять методов, которые пакет реализует сам поверх `actions.v2.*`.
|
|
13
33
|
*
|
|
@@ -24,6 +44,13 @@ export interface SaveResult {
|
|
|
24
44
|
* SDK 2.2.0 останется зелёным, то есть пропажа обнаружится только у потребителя.
|
|
25
45
|
* Страховка от «уборки дублей» — `tests/client-type.test.ts`: он читает текст этого
|
|
26
46
|
* файла и падает, если хоть одно объявление отсюда исчезло.
|
|
47
|
+
*
|
|
48
|
+
* `any` в сигнатурах оставлен НАМЕРЕННО и снятию по T-53 не подлежит: это дефолты
|
|
49
|
+
* дженериков публичной поверхности, дословно повторяющие `AbstractB24`. На них стоит
|
|
50
|
+
* код потребителей — `for await (const chunk of $b24.fetchListMethod(...))
|
|
51
|
+
* chunk.forEach((x) => x.ID)` и `callBatch([["crm.deal.get", { id }]])`. `unknown`
|
|
52
|
+
* в дефолте уронил бы каждое такое место, ничего не дав взамен: узкий тип задаётся
|
|
53
|
+
* параметром типа на месте вызова (`callMethod<Deal>(...)`).
|
|
27
54
|
*/
|
|
28
55
|
export interface B24Client extends B24OAuth {
|
|
29
56
|
callMethod<T = any>(method: string, params?: object, start?: number): Promise<AjaxResult<T>>;
|
package/bitrix24/b24.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { B24OAuth } from "@bitrix24/b24jssdk";
|
|
2
|
-
import type { Request, Response } from "express";
|
|
3
2
|
import { logs } from "../logs/logs.ts";
|
|
4
3
|
|
|
5
4
|
import { createB24Instance } from "./b24/instance.ts";
|
|
6
5
|
import { wrapB24WithRetry } from "./b24/proxy.ts";
|
|
7
6
|
import { $b24, setB24Instance } from "./b24/state.ts";
|
|
8
7
|
import { refreshAndSaveTokens, resetRefreshMutex, saveTokens, startProactiveRefresh, stopProactiveRefresh } from "./b24/tokens.ts";
|
|
9
|
-
import type { SaveResult } from "./b24/types.ts";
|
|
8
|
+
import type { AuthSaveRequest, AuthSaveResponse, SaveResult } from "./b24/types.ts";
|
|
10
9
|
|
|
11
10
|
// ==================== Реэкспорт публичного API ====================
|
|
12
11
|
|
|
@@ -62,12 +61,43 @@ export async function reinitializeB24(): Promise<SaveResult> {
|
|
|
62
61
|
return refreshResult;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/** Непустая строка — единственная форма, пригодная для поля токена */
|
|
65
|
+
function isFilledString(value: unknown): value is string {
|
|
66
|
+
return typeof value === "string" && value.length > 0;
|
|
67
|
+
}
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* HTTP-обработчик для сохранения токенов с фронта.
|
|
71
|
+
*
|
|
72
|
+
* Типы запроса и ответа — собственные структурные (`AuthSaveRequest`/`AuthSaveResponse`),
|
|
73
|
+
* а не `Request`/`Response` из `@types/express`: пакет больше не тянет типы express
|
|
74
|
+
* потребителю. Express подставляет свои объекты сюда без правок на его стороне
|
|
75
|
+
*/
|
|
76
|
+
export async function saveAuthB24Handler(req: AuthSaveRequest, res: AuthSaveResponse): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
// req.body теперь unknown — разбираем явно, деструктуризация по нему не пройдёт
|
|
79
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
80
|
+
const access_token = body["access_token"];
|
|
81
|
+
const refresh_token = body["refresh_token"];
|
|
82
|
+
const domain = body["domain"];
|
|
83
|
+
const expires_in = body["expires_in"];
|
|
84
|
+
const member_id = body["member_id"];
|
|
85
|
+
|
|
86
|
+
// expires_in участвует и в самом объекте токенов, и в расчёте expires —
|
|
87
|
+
// приводим один раз в отдельную переменную
|
|
88
|
+
const expiresIn = Number(expires_in);
|
|
89
|
+
|
|
90
|
+
// Проверяем именно тип, а не «истинность» после String(): приведение объекта
|
|
91
|
+
// дало бы непустую строку "[object Object]", она прошла бы валидацию
|
|
92
|
+
// в saveTokens и затёрла рабочие токены в authB24.json
|
|
93
|
+
if (
|
|
94
|
+
!isFilledString(access_token) ||
|
|
95
|
+
!isFilledString(refresh_token) ||
|
|
96
|
+
!isFilledString(domain) ||
|
|
97
|
+
!isFilledString(member_id) ||
|
|
98
|
+
!Number.isFinite(expiresIn) ||
|
|
99
|
+
expiresIn <= 0
|
|
100
|
+
) {
|
|
71
101
|
res.status(400).json({ status: "error", message: "Не заполнены обязательные поля." });
|
|
72
102
|
return;
|
|
73
103
|
}
|
|
@@ -76,9 +106,9 @@ export async function saveAuthB24Handler(req: Request, res: Response): Promise<v
|
|
|
76
106
|
access_token,
|
|
77
107
|
refresh_token,
|
|
78
108
|
domain,
|
|
79
|
-
expires_in,
|
|
109
|
+
expires_in: expiresIn,
|
|
80
110
|
member_id,
|
|
81
|
-
expires: Math.floor(Date.now() / 1000) +
|
|
111
|
+
expires: Math.floor(Date.now() / 1000) + expiresIn,
|
|
82
112
|
});
|
|
83
113
|
|
|
84
114
|
if (result.error) {
|
|
@@ -100,9 +130,10 @@ export async function saveAuthB24Handler(req: Request, res: Response): Promise<v
|
|
|
100
130
|
} else {
|
|
101
131
|
res.status(201).json({ status: "ok", message: "Токены сохранены и применены." });
|
|
102
132
|
}
|
|
103
|
-
} catch (error:
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
} catch (error: unknown) {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
logs.add(`Ошибка в saveAuthB24Handler: ${message}`, "error");
|
|
136
|
+
res.status(500).json({ status: "error", message });
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
|
package/bitrix24/errTaskB24.ts
CHANGED
|
@@ -27,7 +27,8 @@ export interface ErrorTaskData {
|
|
|
27
27
|
interface ErrorB24Result {
|
|
28
28
|
error: boolean;
|
|
29
29
|
message: string;
|
|
30
|
-
|
|
30
|
+
/** Ответ портала на tasks.task.add как есть — форму задаёт сам портал, поэтому unknown */
|
|
31
|
+
data?: unknown;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/** Задача из Bitrix24 */
|
|
@@ -65,8 +66,9 @@ export async function checkB24Scope(): Promise<void> {
|
|
|
65
66
|
if (missingScopes.length > 0) {
|
|
66
67
|
logs.add(`Отсутствуют скоупы: ${missingScopes.join(", ")}`, "error");
|
|
67
68
|
}
|
|
68
|
-
} catch (error:
|
|
69
|
-
|
|
69
|
+
} catch (error: unknown) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
logs.add(`Ошибка при получении скоупов: ${message}`, "error");
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -133,10 +135,11 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
|
|
|
133
135
|
message: "Задача создана в Битрикс24.",
|
|
134
136
|
data: createdTask,
|
|
135
137
|
};
|
|
136
|
-
} catch (error:
|
|
138
|
+
} catch (error: unknown) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
140
|
return {
|
|
138
141
|
error: true,
|
|
139
|
-
message: `Не удалось создать задачу в Битрикс24: ${
|
|
142
|
+
message: `Не удалось создать задачу в Битрикс24: ${message}`,
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
}
|
package/bitrix24/eventB24.ts
CHANGED
|
@@ -20,9 +20,21 @@ export interface ConnectorMessage {
|
|
|
20
20
|
lineId: string;
|
|
21
21
|
chatId: string;
|
|
22
22
|
text: string | null;
|
|
23
|
-
file:
|
|
24
|
-
attachments:
|
|
25
|
-
im:
|
|
23
|
+
file: TelegramFile[] | null;
|
|
24
|
+
attachments: unknown[] | null;
|
|
25
|
+
im: Record<string, unknown> | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Файл сообщения, приведённый к формату Telegram.
|
|
30
|
+
*
|
|
31
|
+
* Раньше метод отдавал `any[]`, и единственный потребитель (`b24_conector`)
|
|
32
|
+
* объявлял такую же структуру у себя. Тип экспортируется, чтобы объявление было одно
|
|
33
|
+
*/
|
|
34
|
+
export interface TelegramFile {
|
|
35
|
+
type: "image" | "video" | "file" | "document";
|
|
36
|
+
downloadLink: string;
|
|
37
|
+
name: string;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
/** События коннектора */
|
|
@@ -48,7 +60,12 @@ export interface EntityEvents {
|
|
|
48
60
|
interface StandardResult {
|
|
49
61
|
error: boolean;
|
|
50
62
|
message: string;
|
|
51
|
-
|
|
63
|
+
/**
|
|
64
|
+
* `ConnectorEvents`, `EntityEvents` либо `null` — какой именно, зависит от имени
|
|
65
|
+
* события, переданного в `get()`. Разобрать это типом нельзя, поэтому `unknown`:
|
|
66
|
+
* потребитель приводит результат сам, как и делал до 4.0.0
|
|
67
|
+
*/
|
|
68
|
+
data: unknown;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
/** Офлайн-событие Bitrix24 */
|
|
@@ -56,7 +73,7 @@ interface OfflineEvent {
|
|
|
56
73
|
ID: string;
|
|
57
74
|
MESSAGE_ID: string;
|
|
58
75
|
EVENT_NAME: string;
|
|
59
|
-
EVENT_DATA:
|
|
76
|
+
EVENT_DATA: unknown;
|
|
60
77
|
EVENT_ADDITIONAL: {
|
|
61
78
|
user_id: string;
|
|
62
79
|
};
|
|
@@ -105,7 +122,7 @@ const SYSTEM_USER_ID = "138";
|
|
|
105
122
|
* внутри записи: защита только одной из них читалась бы как решение «здесь
|
|
106
123
|
* нормализация не нужна», а словарь на месте списка ронял бы весь вызов.
|
|
107
124
|
*/
|
|
108
|
-
function normalizeCollection(value: unknown):
|
|
125
|
+
function normalizeCollection(value: unknown): unknown[] {
|
|
109
126
|
if (Array.isArray(value)) return value;
|
|
110
127
|
if (value && typeof value === "object") return Object.values(value);
|
|
111
128
|
return [];
|
|
@@ -234,15 +251,23 @@ export class Event {
|
|
|
234
251
|
/**
|
|
235
252
|
* Преобразует файлы из формата Bitrix24 в формат TelegramFile
|
|
236
253
|
*/
|
|
237
|
-
private convertB24FilesToTelegramFormat(b24Files:
|
|
254
|
+
private convertB24FilesToTelegramFormat(b24Files: unknown[] | null | undefined): TelegramFile[] | null {
|
|
238
255
|
if (!b24Files || !Array.isArray(b24Files) || b24Files.length === 0) {
|
|
239
256
|
return null;
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
return b24Files.map((
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
259
|
+
return b24Files.map((rawFile) => {
|
|
260
|
+
// Элемент приходит как unknown: сужаем через asRecord, как и запись очереди.
|
|
261
|
+
// Битый элемент (null, строка) даёт пустые имя и ссылку — по той же причине,
|
|
262
|
+
// по которой устойчив разбор записи: исключение здесь остановило бы очередь
|
|
263
|
+
const file = asRecord(rawFile) ?? {};
|
|
264
|
+
|
|
265
|
+
// Проверка типа, а не приведение: нестроковое `name` уронило бы
|
|
266
|
+
// detectFileType на toLowerCase(), get() вернул бы ошибку — и очередь встала
|
|
267
|
+
const fileName = typeof file["name"] === "string" ? file["name"] : "";
|
|
268
|
+
const link = typeof file["link"] === "string" ? file["link"] : typeof file["url"] === "string" ? file["url"] : "";
|
|
269
|
+
const rawType = file["type"];
|
|
270
|
+
const fileType = this.detectFileType(fileName, typeof rawType === "string" ? rawType : undefined);
|
|
246
271
|
|
|
247
272
|
return {
|
|
248
273
|
type: fileType,
|
|
@@ -348,24 +373,33 @@ export class Event {
|
|
|
348
373
|
// здесь означало бы, что get() вернёт ошибку, потребитель не вызовет
|
|
349
374
|
// clear(), и те же события придут следующим опросом — очередь встала бы
|
|
350
375
|
// навсегда на одном кривом сообщении
|
|
351
|
-
data.message = allEvents.flatMap((
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
376
|
+
data.message = allEvents.flatMap((rawEvent) => {
|
|
377
|
+
// Запись очереди приходит из normalizeCollection как unknown —
|
|
378
|
+
// сужаем через asRecord, тем же способом, что и classifyEntityEvent
|
|
379
|
+
const record = asRecord(rawEvent) ?? {};
|
|
380
|
+
const eventData = asRecord(record["EVENT_DATA"]) ?? {};
|
|
381
|
+
|
|
382
|
+
return normalizeCollection(eventData["MESSAGES"])
|
|
383
|
+
.map((rawMessage) => asRecord(rawMessage))
|
|
384
|
+
.filter((messageData): messageData is Record<string, unknown> => Boolean(messageData?.["chat"] && messageData?.["message"]))
|
|
385
|
+
.map((messageData) => {
|
|
386
|
+
const chat = asRecord(messageData["chat"]) ?? {};
|
|
387
|
+
const message = asRecord(messageData["message"]) ?? {};
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
// Портал отдаёт идентификаторы то строкой, то числом —
|
|
391
|
+
// приводим, чтобы тип `string` не врал
|
|
392
|
+
messageId: String(record["MESSAGE_ID"] ?? ""),
|
|
393
|
+
eventId: String(record["ID"] ?? ""),
|
|
394
|
+
connectorId: readId(eventData["CONNECTOR"]) ?? "",
|
|
395
|
+
lineId: readId(eventData["LINE"]) ?? "",
|
|
396
|
+
chatId: readId(chat["id"]) ?? "",
|
|
397
|
+
text: (message["text"] as string | undefined) || null,
|
|
398
|
+
file: this.convertB24FilesToTelegramFormat(message["files"] as unknown[] | undefined),
|
|
399
|
+
attachments: (message["attachments"] as unknown[] | undefined) || null,
|
|
400
|
+
im: (messageData["im"] as Record<string, unknown> | undefined) || null,
|
|
401
|
+
};
|
|
402
|
+
});
|
|
369
403
|
});
|
|
370
404
|
}
|
|
371
405
|
|
|
@@ -382,7 +416,7 @@ export class Event {
|
|
|
382
416
|
// Тип OfflineEvent описывает то, что портал обещает, а не то, что присылает:
|
|
383
417
|
// EVENT_ADDITIONAL и EVENT_DATA приходили и без обязательных полей (T-50).
|
|
384
418
|
// Параметр колбэка объявлен unknown намеренно — так классификация видит запись
|
|
385
|
-
// такой, какая она
|
|
419
|
+
// такой, какая она есть
|
|
386
420
|
const verdicts = allEvents.map((raw: unknown) => classifyEntityEvent(raw, eventName));
|
|
387
421
|
|
|
388
422
|
// Битую запись обработать нечем, и о её пропаже иначе не узнает никто:
|
|
@@ -479,10 +513,11 @@ export class Event {
|
|
|
479
513
|
}
|
|
480
514
|
|
|
481
515
|
return { error: false, message: "События получены", data };
|
|
482
|
-
} catch (error:
|
|
516
|
+
} catch (error: unknown) {
|
|
517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
483
518
|
return {
|
|
484
519
|
error: true,
|
|
485
|
-
message: `Не удалось получить события. ${
|
|
520
|
+
message: `Не удалось получить события. ${message}`,
|
|
486
521
|
data: null,
|
|
487
522
|
};
|
|
488
523
|
}
|
|
@@ -502,7 +537,7 @@ export class Event {
|
|
|
502
537
|
return { error: false, message: "Список записей пуст, очищать нечего", data: null };
|
|
503
538
|
}
|
|
504
539
|
|
|
505
|
-
const params:
|
|
540
|
+
const params: Record<string, unknown> = { process_id: processId };
|
|
506
541
|
if (messageId) {
|
|
507
542
|
params.message_id = messageId;
|
|
508
543
|
}
|
|
@@ -518,10 +553,11 @@ export class Event {
|
|
|
518
553
|
}
|
|
519
554
|
|
|
520
555
|
return { error: false, message: "События очищены", data: null };
|
|
521
|
-
} catch (error:
|
|
556
|
+
} catch (error: unknown) {
|
|
557
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
522
558
|
return {
|
|
523
559
|
error: true,
|
|
524
|
-
message: `Не удалось очистить события. ${
|
|
560
|
+
message: `Не удалось очистить события. ${message}`,
|
|
525
561
|
data: null,
|
|
526
562
|
};
|
|
527
563
|
}
|
package/index.ts
CHANGED
|
@@ -6,9 +6,12 @@ export * from "./bitrix24/errTaskB24.ts";
|
|
|
6
6
|
export * from "./bitrix24/eventB24.ts";
|
|
7
7
|
|
|
8
8
|
// Мессенджеры и уведомления
|
|
9
|
+
// `Email` здесь намеренно отсутствует: с 4.0.0 он живёт в подпуте
|
|
10
|
+
// `@andrey4emk/npm-app-back-b24/email`, чтобы optional peer `nodemailer`
|
|
11
|
+
// не требовался проектам, которые почту не шлют. Возврат `export *` сюда
|
|
12
|
+
// вернёт зависимость в баррель — это ловит tests/public-api.test.ts
|
|
9
13
|
export * from "./sendMessage/chatApp.ts";
|
|
10
14
|
export * from "./sendMessage/smsgold.ts";
|
|
11
|
-
export * from "./sendMessage/email.ts";
|
|
12
15
|
export * from "./sendMessage/wappi.ts";
|
|
13
16
|
|
|
14
17
|
// Логирование
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrey4emk/npm-app-back-b24",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Bitrix24 OAuth helpers for Node.js projects",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.ts",
|
|
9
9
|
"./logs": "./logs/logs.ts",
|
|
10
|
-
"./fetchRetry": "./utils/fetchRetry.ts"
|
|
10
|
+
"./fetchRetry": "./utils/fetchRetry.ts",
|
|
11
|
+
"./email": "./sendMessage/email.ts"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
13
14
|
"test": "DOTENV_CONFIG_QUIET=true node --test --test-timeout=30000 \"tests/**/*.test.ts\"",
|
|
@@ -49,13 +50,23 @@
|
|
|
49
50
|
],
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@bitrix24/b24jssdk": "2.2.0",
|
|
53
|
+
"conf": "15.1.0",
|
|
54
|
+
"dotenv": "17.4.2",
|
|
55
|
+
"luxon": "3.7.2"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
52
58
|
"@types/express": "5.0.6",
|
|
53
59
|
"@types/luxon": "3.7.4",
|
|
54
60
|
"@types/node": "25.9.5",
|
|
55
61
|
"@types/nodemailer": "8.0.1",
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"nodemailer": "^9.0.5"
|
|
62
|
+
"nodemailer": "9.0.5"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"nodemailer": "^9.0.5 || ^10.0.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"nodemailer": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
60
71
|
}
|
|
61
72
|
}
|
package/sendMessage/chatApp.ts
CHANGED
|
@@ -45,6 +45,63 @@ interface FileMessageData extends MessageData {
|
|
|
45
45
|
fileName: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Ответ ChatApp в той части, которую разбирает класс.
|
|
50
|
+
*
|
|
51
|
+
* Описан по фактически читаемым полям, а не по всему API: остальное уезжает
|
|
52
|
+
* потребителю в `data` как есть и типом не сужается
|
|
53
|
+
*/
|
|
54
|
+
interface ChatAppResponse {
|
|
55
|
+
success?: boolean;
|
|
56
|
+
/** Полезная нагрузка ответа. Форму задаёт сам ChatApp, разбираем на месте */
|
|
57
|
+
data?: unknown;
|
|
58
|
+
/** Дописывается классом в phoneCheckChatApp. В ответе ChatApp этого поля нет */
|
|
59
|
+
check?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Комплект токенов ChatApp — то, что сервис обещает в ответе на выдачу и обновление.
|
|
64
|
+
*
|
|
65
|
+
* Поля необязательные намеренно: обязательность в чужом JSON ничем не подкреплена,
|
|
66
|
+
* а усечённый ответ клал бы `undefined` под типом `string`. Комплект проверяется
|
|
67
|
+
* на месте разбора
|
|
68
|
+
*/
|
|
69
|
+
interface ChatAppTokens {
|
|
70
|
+
accessToken?: string;
|
|
71
|
+
accessTokenEndTime?: string;
|
|
72
|
+
refreshToken?: string;
|
|
73
|
+
refreshTokenEndTime?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Ответ на выдачу или обновление токена */
|
|
77
|
+
interface ChatAppTokenResponse {
|
|
78
|
+
success?: boolean;
|
|
79
|
+
data?: ChatAppTokens;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Проверяет, что в ответе пришёл весь комплект токенов.
|
|
84
|
+
*
|
|
85
|
+
* Частичный ответ до этой проверки клал `undefined` в `this.auth` под типом `string`:
|
|
86
|
+
* метод отчитывался успехом, а следующее обновление уходило с пустым refresh-токеном
|
|
87
|
+
* и не восстанавливалось до перезапуска процесса
|
|
88
|
+
*/
|
|
89
|
+
function isFullTokenSet(tokens: ChatAppTokens | undefined): tokens is ChatAppAuthParam {
|
|
90
|
+
return !!(
|
|
91
|
+
tokens &&
|
|
92
|
+
typeof tokens.accessToken === "string" && tokens.accessToken.length > 0 &&
|
|
93
|
+
typeof tokens.accessTokenEndTime === "string" &&
|
|
94
|
+
typeof tokens.refreshToken === "string" && tokens.refreshToken.length > 0 &&
|
|
95
|
+
typeof tokens.refreshTokenEndTime === "string"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Ответ на запрос списка лицензий */
|
|
100
|
+
interface ChatAppLicensesResponse {
|
|
101
|
+
success?: boolean;
|
|
102
|
+
licenses?: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
48
105
|
/** Стандартный результат операции */
|
|
49
106
|
interface ChatAppResult {
|
|
50
107
|
error: boolean;
|
|
@@ -136,7 +193,7 @@ export class ChatApp {
|
|
|
136
193
|
|
|
137
194
|
const url = `${baseUrl}/chats/${phone}/messages/text`;
|
|
138
195
|
|
|
139
|
-
let data:
|
|
196
|
+
let data: ChatAppResponse;
|
|
140
197
|
try {
|
|
141
198
|
const res = await fetchWithTimeout(
|
|
142
199
|
url,
|
|
@@ -152,7 +209,7 @@ export class ChatApp {
|
|
|
152
209
|
},
|
|
153
210
|
FETCH_TIMEOUTS.send
|
|
154
211
|
);
|
|
155
|
-
data = await res.json();
|
|
212
|
+
data = (await res.json()) as ChatAppResponse;
|
|
156
213
|
} catch (error: unknown) {
|
|
157
214
|
// Метод исторически без try/catch — перехватываем только таймаут, который сами
|
|
158
215
|
// и вводим. Остальные ошибки летят наружу как раньше
|
|
@@ -210,7 +267,7 @@ export class ChatApp {
|
|
|
210
267
|
|
|
211
268
|
const url = `${baseUrl}/chats/${phone}/messages/file`;
|
|
212
269
|
|
|
213
|
-
let data:
|
|
270
|
+
let data: ChatAppResponse;
|
|
214
271
|
try {
|
|
215
272
|
// Файл по ссылке забирает сам ChatApp, поэтому ответа можно ждать дольше — бюджет transfer
|
|
216
273
|
const res = await fetchWithTimeout(
|
|
@@ -227,7 +284,7 @@ export class ChatApp {
|
|
|
227
284
|
},
|
|
228
285
|
FETCH_TIMEOUTS.transfer
|
|
229
286
|
);
|
|
230
|
-
data = await res.json();
|
|
287
|
+
data = (await res.json()) as ChatAppResponse;
|
|
231
288
|
} catch (error: unknown) {
|
|
232
289
|
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
233
290
|
if (!isTimeoutError(error)) throw error;
|
|
@@ -280,7 +337,7 @@ export class ChatApp {
|
|
|
280
337
|
|
|
281
338
|
const url = `${baseUrl}/phones/${phone}/check`;
|
|
282
339
|
|
|
283
|
-
let data:
|
|
340
|
+
let data: ChatAppResponse;
|
|
284
341
|
try {
|
|
285
342
|
const res = await fetchWithTimeout(
|
|
286
343
|
url,
|
|
@@ -294,7 +351,7 @@ export class ChatApp {
|
|
|
294
351
|
},
|
|
295
352
|
FETCH_TIMEOUTS.api
|
|
296
353
|
);
|
|
297
|
-
data = await res.json();
|
|
354
|
+
data = (await res.json()) as ChatAppResponse;
|
|
298
355
|
} catch (error: unknown) {
|
|
299
356
|
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
300
357
|
if (!isTimeoutError(error)) throw error;
|
|
@@ -315,13 +372,14 @@ export class ChatApp {
|
|
|
315
372
|
newAuth = this.auth;
|
|
316
373
|
this.updateToken = false;
|
|
317
374
|
}
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
375
|
+
// Полезная нагрузка ответа объявлена unknown: обращение к data.data.exist
|
|
376
|
+
// без проверки давало TypeError мимо всякого try/catch
|
|
377
|
+
const payload = data.data;
|
|
378
|
+
if (typeof payload !== "object" || payload === null) {
|
|
321
379
|
return { error: true, message: `Ответ ChatApp без поля data при проверке телефона через ${messangerType}`, data, auth: newAuth };
|
|
322
380
|
}
|
|
323
381
|
|
|
324
|
-
data.check =
|
|
382
|
+
data.check = (payload as Record<string, unknown>)["exist"];
|
|
325
383
|
return { error: false, message: `Телефон успешно проверен в ChatApp через ${messangerType}`, data, auth: newAuth };
|
|
326
384
|
}
|
|
327
385
|
|
|
@@ -340,9 +398,14 @@ export class ChatApp {
|
|
|
340
398
|
FETCH_TIMEOUTS.quick
|
|
341
399
|
);
|
|
342
400
|
|
|
343
|
-
const data
|
|
401
|
+
const data = (await response.json()) as ChatAppResponse;
|
|
344
402
|
if (!data.success) {
|
|
345
|
-
|
|
403
|
+
// Результат обновления не проглатываем: раньше «Токен обновлен» уходило
|
|
404
|
+
// и когда обновление провалилось, а мёртвый токен доезжал до отправки
|
|
405
|
+
const refreshResult = await this.refreshTokenChatApp();
|
|
406
|
+
if (refreshResult.error) {
|
|
407
|
+
return { error: true, message: `Не удалось обновить токен ChatApp: ${refreshResult.message ?? ""}`, data: null };
|
|
408
|
+
}
|
|
346
409
|
return { error: false, message: "Токен обновлен" };
|
|
347
410
|
}
|
|
348
411
|
return { error: false, message: "Токен действителен" };
|
|
@@ -353,7 +416,6 @@ export class ChatApp {
|
|
|
353
416
|
}
|
|
354
417
|
|
|
355
418
|
async makeTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
|
|
356
|
-
console.warn("makeTokenChatApp called");
|
|
357
419
|
try {
|
|
358
420
|
const response = await fetchWithTimeout(
|
|
359
421
|
"https://api.chatapp.online/v1/tokens",
|
|
@@ -373,15 +435,24 @@ export class ChatApp {
|
|
|
373
435
|
FETCH_TIMEOUTS.quick
|
|
374
436
|
);
|
|
375
437
|
|
|
376
|
-
const data
|
|
438
|
+
const data = (await response.json()) as ChatAppTokenResponse;
|
|
377
439
|
if (!data.success) {
|
|
378
440
|
return { error: true, message: "Не удалось получить токен ChatApp", data };
|
|
379
441
|
}
|
|
380
442
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.auth
|
|
443
|
+
// Успех без комплекта токенов раньше давал TypeError на undefined и уходил
|
|
444
|
+
// в catch общим сообщением. Отказ явный, флаг ошибки тот же.
|
|
445
|
+
// Проверяем весь комплект, а не наличие контейнера: усечённый ответ клал бы
|
|
446
|
+
// undefined в this.auth, и обновление токена не восстановилось бы до перезапуска
|
|
447
|
+
const tokens = data.data;
|
|
448
|
+
if (!isFullTokenSet(tokens)) {
|
|
449
|
+
return { error: true, message: "ChatApp: в ответе нет данных токена", data };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.auth.accessToken = tokens.accessToken;
|
|
453
|
+
this.auth.accessTokenEndTime = tokens.accessTokenEndTime;
|
|
454
|
+
this.auth.refreshToken = tokens.refreshToken;
|
|
455
|
+
this.auth.refreshTokenEndTime = tokens.refreshTokenEndTime;
|
|
385
456
|
this.updateToken = true;
|
|
386
457
|
return { error: false };
|
|
387
458
|
} catch (error: unknown) {
|
|
@@ -406,7 +477,7 @@ export class ChatApp {
|
|
|
406
477
|
FETCH_TIMEOUTS.quick
|
|
407
478
|
);
|
|
408
479
|
|
|
409
|
-
const data
|
|
480
|
+
const data = (await response.json()) as ChatAppTokenResponse;
|
|
410
481
|
if (!data.success) {
|
|
411
482
|
const resMakeToken = await this.makeTokenChatApp();
|
|
412
483
|
if (resMakeToken.error) {
|
|
@@ -415,10 +486,16 @@ export class ChatApp {
|
|
|
415
486
|
return { error: false };
|
|
416
487
|
}
|
|
417
488
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
489
|
+
// Тот же явный отказ, что и в makeTokenChatApp: успех без комплекта токенов
|
|
490
|
+
const tokens = data.data;
|
|
491
|
+
if (!isFullTokenSet(tokens)) {
|
|
492
|
+
return { error: true, message: "ChatApp: в ответе нет данных токена", data };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.auth.accessToken = tokens.accessToken;
|
|
496
|
+
this.auth.accessTokenEndTime = tokens.accessTokenEndTime;
|
|
497
|
+
this.auth.refreshToken = tokens.refreshToken;
|
|
498
|
+
this.auth.refreshTokenEndTime = tokens.refreshTokenEndTime;
|
|
422
499
|
this.updateToken = true;
|
|
423
500
|
return { error: false };
|
|
424
501
|
} catch (error: unknown) {
|
|
@@ -441,7 +518,7 @@ export class ChatApp {
|
|
|
441
518
|
},
|
|
442
519
|
FETCH_TIMEOUTS.api
|
|
443
520
|
);
|
|
444
|
-
const data
|
|
521
|
+
const data = (await response.json()) as ChatAppLicensesResponse;
|
|
445
522
|
|
|
446
523
|
if (!data.success) {
|
|
447
524
|
return { error: true, message: "Не удалось получить лицензии ChatApp", data: null };
|
package/sendMessage/smsgold.ts
CHANGED
|
@@ -27,6 +27,15 @@ interface SmsMessageData {
|
|
|
27
27
|
*/
|
|
28
28
|
const DEFAULT_UPLOAD_FOLDER_ID = 2792881;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Файл, загруженный на диск Bitrix24 — то, что классу нужно от ответа
|
|
32
|
+
* `disk.folder.uploadfile`. Идентификатор портал отдаёт то строкой, то числом,
|
|
33
|
+
* и класс лишь передаёт его дальше в `disk.file.getExternalLink`
|
|
34
|
+
*/
|
|
35
|
+
interface UploadedFile {
|
|
36
|
+
ID: string | number;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
/** Результат отправки SMS */
|
|
31
40
|
interface SmsResult {
|
|
32
41
|
error: boolean;
|
|
@@ -175,7 +184,7 @@ export class Smsgold {
|
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
/** Загружает файл в папку Битрикс24 через метод disk.folder.uploadfile */
|
|
178
|
-
private async uploadFileInFolderB24(fileBuffer: Buffer, fileName: string): Promise<{ error: boolean; message?: string; data?:
|
|
187
|
+
private async uploadFileInFolderB24(fileBuffer: Buffer, fileName: string): Promise<{ error: boolean; message?: string; data?: UploadedFile }> {
|
|
179
188
|
try {
|
|
180
189
|
const base64File = fileBuffer.toString("base64");
|
|
181
190
|
|
|
@@ -185,7 +194,7 @@ export class Smsgold {
|
|
|
185
194
|
data: { NAME: fileName },
|
|
186
195
|
fileContent: [fileName, base64File],
|
|
187
196
|
});
|
|
188
|
-
const data = getResultData(res, "disk.folder.uploadfile");
|
|
197
|
+
const data = getResultData<UploadedFile>(res, "disk.folder.uploadfile");
|
|
189
198
|
return { error: false, data };
|
|
190
199
|
} catch (error: unknown) {
|
|
191
200
|
const errMsg = error instanceof Error ? error.message : String(error);
|
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
|
}
|