@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/bitrix24/eventB24.ts
CHANGED
|
@@ -3,7 +3,18 @@ import { $b24, getResultData, type B24MethodCaller } from "./b24.ts";
|
|
|
3
3
|
// ==================== Типы ====================
|
|
4
4
|
|
|
5
5
|
/** Сообщение из коннектора */
|
|
6
|
-
interface ConnectorMessage {
|
|
6
|
+
export interface ConnectorMessage {
|
|
7
|
+
/**
|
|
8
|
+
* `MESSAGE_ID` записи очереди офлайн-событий — ключ для `clear()`.
|
|
9
|
+
* Идентифицирует запись, а не отдельное сообщение: у сообщений одной записи
|
|
10
|
+
* он общий, поэтому для дедупликации сообщений не годится (для этого `im.id`)
|
|
11
|
+
*/
|
|
12
|
+
messageId: string;
|
|
13
|
+
/**
|
|
14
|
+
* `ID` записи очереди офлайн-событий. Поле информационное, для сверки
|
|
15
|
+
* с `event.offline.list`. В `clear()` не передавать — метод принимает `MESSAGE_ID`
|
|
16
|
+
*/
|
|
17
|
+
eventId: string;
|
|
7
18
|
connectorId: string;
|
|
8
19
|
lineId: string;
|
|
9
20
|
chatId: string;
|
|
@@ -14,7 +25,7 @@ interface ConnectorMessage {
|
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
/** События коннектора */
|
|
17
|
-
interface ConnectorEvents {
|
|
28
|
+
export interface ConnectorEvents {
|
|
18
29
|
processId: string | null;
|
|
19
30
|
message: ConnectorMessage[];
|
|
20
31
|
}
|
|
@@ -26,7 +37,7 @@ interface MessageEntityLink {
|
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
/** События сущностей (сделки, смарт-процессы) */
|
|
29
|
-
interface EntityEvents {
|
|
40
|
+
export interface EntityEvents {
|
|
30
41
|
processId: string | null;
|
|
31
42
|
entitysId: string[];
|
|
32
43
|
arrMessageIdAndEntityId: MessageEntityLink[];
|
|
@@ -41,6 +52,7 @@ interface StandardResult {
|
|
|
41
52
|
|
|
42
53
|
/** Офлайн-событие Bitrix24 */
|
|
43
54
|
interface OfflineEvent {
|
|
55
|
+
ID: string;
|
|
44
56
|
MESSAGE_ID: string;
|
|
45
57
|
EVENT_NAME: string;
|
|
46
58
|
EVENT_DATA: any;
|
|
@@ -148,7 +160,19 @@ export class Event {
|
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
/**
|
|
151
|
-
* Получает офлайн-события по
|
|
163
|
+
* Получает офлайн-события по имени.
|
|
164
|
+
*
|
|
165
|
+
* Гранулярность очистки — запись очереди, а не сообщение. Для коннектора одна
|
|
166
|
+
* запись (один `MESSAGE_ID`) может нести несколько сообщений, и `get()` раскладывает
|
|
167
|
+
* их через `flatMap` — у таких сообщений `messageId` совпадает. Очистка по этому
|
|
168
|
+
* ключу удалит запись целиком: если из двух сообщений одной записи ушло одно,
|
|
169
|
+
* `clear()` заберёт и неотправленное. Поэлементную очистку с точностью до
|
|
170
|
+
* сообщения на этом ключе построить нельзя.
|
|
171
|
+
*
|
|
172
|
+
* Обратная сторона: запись, из которой не разобрано ни одного сообщения
|
|
173
|
+
* (пустой или битый `MESSAGES`), в `data.message` не попадает вовсе — очистка
|
|
174
|
+
* по собранным `messageId` её не заберёт, и она останется зарезервированной
|
|
175
|
+
* под тем же `process_id`. Безопасный режим — `clear(processId)` целиком.
|
|
152
176
|
*/
|
|
153
177
|
async get(eventName: string): Promise<StandardResult> {
|
|
154
178
|
try {
|
|
@@ -184,6 +208,10 @@ export class Event {
|
|
|
184
208
|
return normalizeMessages(eventData.MESSAGES)
|
|
185
209
|
.filter((messageData) => messageData?.chat && messageData?.message)
|
|
186
210
|
.map((messageData) => ({
|
|
211
|
+
// Портал отдаёт идентификаторы то строкой, то числом —
|
|
212
|
+
// приводим, чтобы тип `string` не врал
|
|
213
|
+
messageId: String(event.MESSAGE_ID ?? ""),
|
|
214
|
+
eventId: String(event.ID ?? ""),
|
|
187
215
|
connectorId: eventData.CONNECTOR,
|
|
188
216
|
lineId: eventData.LINE,
|
|
189
217
|
chatId: messageData.chat.id,
|
|
@@ -210,6 +238,9 @@ export class Event {
|
|
|
210
238
|
|
|
211
239
|
if (arrEventsToClear.length > 0) {
|
|
212
240
|
const arrMessageIdToClear = arrEventsToClear.map((event) => event.MESSAGE_ID);
|
|
241
|
+
// Результат намеренно не проверяется: отказ здесь не фатален — те же
|
|
242
|
+
// записи придут следующим опросом, а бросок лишил бы потребителя всей
|
|
243
|
+
// выборки активных событий
|
|
213
244
|
await this.b24.callMethod("event.offline.clear", {
|
|
214
245
|
process_id: arrOfflineEvents.process_id,
|
|
215
246
|
message_id: arrMessageIdToClear,
|
|
@@ -243,11 +274,29 @@ export class Event {
|
|
|
243
274
|
*/
|
|
244
275
|
async clear(processId: string, messageId?: string | string[]): Promise<StandardResult> {
|
|
245
276
|
try {
|
|
277
|
+
// Пустой массив в JavaScript истинен и уезжал в теле запроса как `message_id: []` —
|
|
278
|
+
// это не документированный режим «очистить весь пакет» (что портал делает с
|
|
279
|
+
// пустым списком, не проверялось). Считаем пустой список за «очищать нечего»
|
|
280
|
+
// и до API не доходим: молча сносить весь пакет по пустому списку опасно —
|
|
281
|
+
// зарезервированный пакет портал повторно не выдаёт
|
|
282
|
+
if (Array.isArray(messageId) && messageId.length === 0) {
|
|
283
|
+
return { error: false, message: "Список записей пуст, очищать нечего", data: null };
|
|
284
|
+
}
|
|
285
|
+
|
|
246
286
|
const params: any = { process_id: processId };
|
|
247
287
|
if (messageId) {
|
|
248
288
|
params.message_id = messageId;
|
|
249
289
|
}
|
|
250
|
-
await this.b24.callMethod("event.offline.clear", params);
|
|
290
|
+
const response = await this.b24.callMethod("event.offline.clear", params);
|
|
291
|
+
// «Мягкую» ошибку (например, ERROR_ENTITY_NOT_FOUND на протухшем process_id)
|
|
292
|
+
// SDK не бросает, а возвращает AjaxResult с isSuccess: false. Без проверки
|
|
293
|
+
// метод рапортовал бы «События очищены» при неочищенной очереди.
|
|
294
|
+
// Именно isSuccess, а не getResultData(): та бросает ещё и на result == null,
|
|
295
|
+
// а что портал кладёт в result этого метода, не проверено
|
|
296
|
+
if (!response.isSuccess) {
|
|
297
|
+
const messages = response.getErrorMessages().join("; ") || "неизвестная ошибка";
|
|
298
|
+
throw new Error(`event.offline.clear: Bitrix24 вернул ошибку — ${messages}`);
|
|
299
|
+
}
|
|
251
300
|
|
|
252
301
|
return { error: false, message: "События очищены", data: null };
|
|
253
302
|
} catch (error: any) {
|
package/logs/logs.ts
CHANGED
|
@@ -30,6 +30,15 @@ const B24_CLIENT_ID = "6rf74zyu842h6op1z186b08k7aavqq7z";
|
|
|
30
30
|
/** Лимит длины сообщения для B24 чата (безопасный, с запасом на BB-обёртку) */
|
|
31
31
|
const B24_TEXT_LIMIT = 4000;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Бюджет времени на отправку error-лога в чат B24, мс.
|
|
35
|
+
*
|
|
36
|
+
* Своя константа, а не `FETCH_TIMEOUTS` из `utils/fetchRetry.ts`: тот модуль импортирует
|
|
37
|
+
* `logs` первой строкой, и обратный импорт дал бы цикл. Дублирование одного числа
|
|
38
|
+
* дешевле цикла зависимостей.
|
|
39
|
+
*/
|
|
40
|
+
const B24_CHAT_TIMEOUT_MS = 5_000;
|
|
41
|
+
|
|
33
42
|
// ==================== Настройки ====================
|
|
34
43
|
|
|
35
44
|
const configDir: string = process.env.CONFIG_DIR || "../config";
|
|
@@ -149,7 +158,10 @@ class LogsAPI {
|
|
|
149
158
|
|
|
150
159
|
/**
|
|
151
160
|
* Отправляет error-сообщение в чат B24 через webhook (fire-and-forget)
|
|
152
|
-
* Использует нативный fetch, а не fetchRetry — во избежание
|
|
161
|
+
* Использует нативный fetch, а не fetchRetry/fetchWithTimeout — во избежание
|
|
162
|
+
* циклической зависимости, поэтому и таймаут выставляется здесь вручную через
|
|
163
|
+
* `AbortSignal.timeout`: без него зависшее соединение никого бы не разбудило,
|
|
164
|
+
* вызов идёт без await
|
|
153
165
|
* Ошибки не пробрасываются, только console.error
|
|
154
166
|
*/
|
|
155
167
|
private sendToB24Chat(message: string, jsonData?: unknown): void {
|
|
@@ -195,6 +207,7 @@ class LogsAPI {
|
|
|
195
207
|
DIALOG_ID: this.b24ChatId,
|
|
196
208
|
MESSAGE: text,
|
|
197
209
|
}),
|
|
210
|
+
signal: AbortSignal.timeout(B24_CHAT_TIMEOUT_MS),
|
|
198
211
|
})
|
|
199
212
|
.then((response) => {
|
|
200
213
|
if (!response.ok) {
|
|
@@ -202,6 +215,11 @@ class LogsAPI {
|
|
|
202
215
|
}
|
|
203
216
|
})
|
|
204
217
|
.catch((error: unknown) => {
|
|
218
|
+
// Проверка локальная, без импорта isTimeoutError — тот модуль импортирует logs
|
|
219
|
+
if ((error as { name?: unknown })?.name === "TimeoutError") {
|
|
220
|
+
console.error(`Logs B24 Chat: не ответил за ${B24_CHAT_TIMEOUT_MS / 1000}с, сообщение не доставлено`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
205
223
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
206
224
|
console.error(`Logs B24 Chat: ошибка отправки — ${errMsg}`);
|
|
207
225
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrey4emk/npm-app-back-b24",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Bitrix24 OAuth helpers for Node.js projects",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"url": "git+ssh://git@github.com/andrey4emk/npm_appBackB24.git"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.3.0"
|
|
23
|
+
},
|
|
21
24
|
"author": "andrey4emk",
|
|
22
25
|
"license": "MIT",
|
|
23
26
|
"bugs": {
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"utils/fetchRetry.ts"
|
|
38
41
|
],
|
|
39
42
|
"dependencies": {
|
|
40
|
-
"@bitrix24/b24jssdk": "2.
|
|
43
|
+
"@bitrix24/b24jssdk": "2.2.0",
|
|
41
44
|
"@types/express": "5.0.6",
|
|
42
45
|
"@types/luxon": "3.7.4",
|
|
43
46
|
"@types/node": "25.9.5",
|
package/sendMessage/chatApp.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
|
/** Параметры для создания токена ChatApp */
|
|
@@ -85,6 +88,30 @@ export class ChatApp {
|
|
|
85
88
|
this.telegramType = ChatApp.resolveMessengerType(typeParam?.telegram);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Общая часть URL до раздела запроса: лицензия и разрешённый тип мессенджера.
|
|
93
|
+
* `null` — тип из конфига не разрешился, обращаться некуда. Проверка типа идёт
|
|
94
|
+
* первой: при отсутствующей секции конфига `this.type.whatsApp` тоже undefined,
|
|
95
|
+
* и до чтения licenseId дело не доходит.
|
|
96
|
+
*/
|
|
97
|
+
private resolveBaseUrl(messangerType: MessengerType): string | null {
|
|
98
|
+
const resolvedType = messangerType === "whatsApp" ? this.whatsAppType : this.telegramType;
|
|
99
|
+
if (!resolvedType) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const licenseId = messangerType === "whatsApp" ? this.type.whatsApp.licenseId : this.type.telegram.licenseId;
|
|
103
|
+
return `https://api.chatapp.online/v1/licenses/${licenseId}/messengers/${resolvedType}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Отказ гейта — один текст на все три публичных метода.
|
|
108
|
+
* Не бросаем: экземпляр создаётся на уровне модуля, и бросок уронил бы импорт
|
|
109
|
+
* всего модуля отправки вместе с остальными каналами.
|
|
110
|
+
*/
|
|
111
|
+
private static denyMessenger(messangerType: MessengerType): ChatAppResult {
|
|
112
|
+
return { error: true, message: `ChatApp: не разрешён тип мессенджера ${messangerType} (нет конфига или пуст messenger)`, data: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
async sendMessageChatApp(messangerType: MessengerType, messageData: MessageData): Promise<ChatAppResult> {
|
|
89
116
|
// Проверяем что верно указан messangerType и в messageData есть phone и message
|
|
90
117
|
if (!["whatsApp", "telegram"].includes(messangerType)) {
|
|
@@ -94,9 +121,9 @@ export class ChatApp {
|
|
|
94
121
|
return { error: true, message: "Отсутствует phone или message", data: null };
|
|
95
122
|
}
|
|
96
123
|
// Тип мессенджера не разрешился из конфига — отправлять некуда
|
|
97
|
-
const
|
|
98
|
-
if (!
|
|
99
|
-
return
|
|
124
|
+
const baseUrl = this.resolveBaseUrl(messangerType);
|
|
125
|
+
if (!baseUrl) {
|
|
126
|
+
return ChatApp.denyMessenger(messangerType);
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
const check = await this.checkTokenChatApp();
|
|
@@ -107,25 +134,42 @@ export class ChatApp {
|
|
|
107
134
|
const phone = messageData.phone;
|
|
108
135
|
const message = messageData.message;
|
|
109
136
|
|
|
110
|
-
|
|
111
|
-
if (messangerType === "whatsApp") {
|
|
112
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${resolvedType}/chats/${phone}/messages/text`;
|
|
113
|
-
}
|
|
114
|
-
if (messangerType === "telegram") {
|
|
115
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${resolvedType}/chats/${phone}/messages/text`;
|
|
116
|
-
}
|
|
137
|
+
const url = `${baseUrl}/chats/${phone}/messages/text`;
|
|
117
138
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
let data: any;
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetchWithTimeout(
|
|
142
|
+
url,
|
|
143
|
+
{
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
Lang: "en",
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
Accept: "application/json",
|
|
149
|
+
Authorization: this.auth.accessToken,
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({ text: message }),
|
|
152
|
+
},
|
|
153
|
+
FETCH_TIMEOUTS.send
|
|
154
|
+
);
|
|
155
|
+
data = await res.json();
|
|
156
|
+
} catch (error: unknown) {
|
|
157
|
+
// Метод исторически без try/catch — перехватываем только таймаут, который сами
|
|
158
|
+
// и вводим. Остальные ошибки летят наружу как раньше
|
|
159
|
+
if (!isTimeoutError(error)) throw error;
|
|
160
|
+
|
|
161
|
+
// Уровень error — единственный, который уходит в чат B24. До появления
|
|
162
|
+
// таймаута зависание долетало исключением до потребителя и там порождало
|
|
163
|
+
// задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
|
|
164
|
+
// след, иначе правка сделала бы отказ отправки тише, чем он был
|
|
165
|
+
logs.add(`[${phone}] ChatApp: таймаут ${FETCH_TIMEOUTS.send}мс при отправке сообщения через ${messangerType} — статус неизвестен, повторять нельзя`, "error");
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
error: true,
|
|
169
|
+
message: `ChatApp: таймаут ${FETCH_TIMEOUTS.send}мс при отправке сообщения через ${messangerType} — статус неизвестен`,
|
|
170
|
+
data: null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
129
173
|
|
|
130
174
|
if (!data.success) {
|
|
131
175
|
return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
|
|
@@ -149,9 +193,9 @@ export class ChatApp {
|
|
|
149
193
|
return { error: true, message: "Отсутствует phone или message", data: null };
|
|
150
194
|
}
|
|
151
195
|
// Тип мессенджера не разрешился из конфига — отправлять некуда
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
154
|
-
return
|
|
196
|
+
const baseUrl = this.resolveBaseUrl(messangerType);
|
|
197
|
+
if (!baseUrl) {
|
|
198
|
+
return ChatApp.denyMessenger(messangerType);
|
|
155
199
|
}
|
|
156
200
|
|
|
157
201
|
const check = await this.checkTokenChatApp();
|
|
@@ -164,25 +208,38 @@ export class ChatApp {
|
|
|
164
208
|
const file = messageData.fileUrl;
|
|
165
209
|
const fileName = messageData.fileName;
|
|
166
210
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
const url = `${baseUrl}/chats/${phone}/messages/file`;
|
|
212
|
+
|
|
213
|
+
let data: any;
|
|
214
|
+
try {
|
|
215
|
+
// Файл по ссылке забирает сам ChatApp, поэтому ответа можно ждать дольше — бюджет transfer
|
|
216
|
+
const res = await fetchWithTimeout(
|
|
217
|
+
url,
|
|
218
|
+
{
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: {
|
|
221
|
+
Lang: "en",
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
Accept: "application/json",
|
|
224
|
+
Authorization: this.auth.accessToken,
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify({ file, fileName, caption }),
|
|
227
|
+
},
|
|
228
|
+
FETCH_TIMEOUTS.transfer
|
|
229
|
+
);
|
|
230
|
+
data = await res.json();
|
|
231
|
+
} catch (error: unknown) {
|
|
232
|
+
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
233
|
+
if (!isTimeoutError(error)) throw error;
|
|
174
234
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
body: JSON.stringify({ file, fileName, caption }),
|
|
184
|
-
});
|
|
185
|
-
const data: any = await res.json();
|
|
235
|
+
logs.add(`[${phone}] ChatApp: таймаут ${FETCH_TIMEOUTS.transfer}мс при отправке файла через ${messangerType} — статус неизвестен, повторять нельзя`, "error");
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
error: true,
|
|
239
|
+
message: `ChatApp: таймаут ${FETCH_TIMEOUTS.transfer}мс при отправке файла через ${messangerType} — статус неизвестен`,
|
|
240
|
+
data: null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
186
243
|
|
|
187
244
|
if (!data.success) {
|
|
188
245
|
return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
|
|
@@ -197,14 +254,23 @@ export class ChatApp {
|
|
|
197
254
|
return { error: false, message: `Сообщение успешно отправлено в ChatApp через ${messangerType}`, data, auth: newAuth };
|
|
198
255
|
}
|
|
199
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Проверка телефона. Результат — поле `check` в `data` (значение `data.data.exist` ответа API).
|
|
259
|
+
*
|
|
260
|
+
* Изменение поведения от 16.08.2026: ответ с `success: true`, но без объекта `data`
|
|
261
|
+
* раньше давал `TypeError` наружу (метод не обёрнут в try/catch). Теперь такой ответ
|
|
262
|
+
* возвращается как обычный для этого класса отказ `{ error: true, ... }` с сырым ответом
|
|
263
|
+
* в `data`. Молча подставлять `check: undefined` нельзя: «телефон не найден» и «ответ
|
|
264
|
+
* непонятной формы» — разные факты, и вызывающий отличил бы их только по исключению.
|
|
265
|
+
*/
|
|
200
266
|
async phoneCheckChatApp(messangerType: MessengerType, phone: string): Promise<ChatAppResult> {
|
|
201
267
|
if (!["whatsApp", "telegram"].includes(messangerType)) {
|
|
202
268
|
return { error: true, message: "Неверный тип мессенджера", data: null };
|
|
203
269
|
}
|
|
204
270
|
// Тип мессенджера не разрешился из конфига — проверять телефон негде
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
207
|
-
return
|
|
271
|
+
const baseUrl = this.resolveBaseUrl(messangerType);
|
|
272
|
+
if (!baseUrl) {
|
|
273
|
+
return ChatApp.denyMessenger(messangerType);
|
|
208
274
|
}
|
|
209
275
|
|
|
210
276
|
const check = await this.checkTokenChatApp();
|
|
@@ -212,23 +278,32 @@ export class ChatApp {
|
|
|
212
278
|
return { error: true, message: `Ошибка с токеном ChatApp в функции phoneCheckChatApp класса ChatApp\n${check.message}`, data: check };
|
|
213
279
|
}
|
|
214
280
|
|
|
215
|
-
|
|
216
|
-
if (messangerType === "whatsApp") {
|
|
217
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${resolvedType}/phones/${phone}/check`;
|
|
218
|
-
}
|
|
219
|
-
if (messangerType === "telegram") {
|
|
220
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${resolvedType}/phones/${phone}/check`;
|
|
221
|
-
}
|
|
281
|
+
const url = `${baseUrl}/phones/${phone}/check`;
|
|
222
282
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
283
|
+
let data: any;
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetchWithTimeout(
|
|
286
|
+
url,
|
|
287
|
+
{
|
|
288
|
+
method: "GET",
|
|
289
|
+
headers: {
|
|
290
|
+
Lang: "en",
|
|
291
|
+
Accept: "application/json",
|
|
292
|
+
Authorization: this.auth.accessToken,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
FETCH_TIMEOUTS.api
|
|
296
|
+
);
|
|
297
|
+
data = await res.json();
|
|
298
|
+
} catch (error: unknown) {
|
|
299
|
+
// Перехватываем только таймаут, остальные ошибки ведут себя как раньше
|
|
300
|
+
if (!isTimeoutError(error)) throw error;
|
|
301
|
+
|
|
302
|
+
// warn, а не error: проверка номера ничего не меняет, повторить её безопасно
|
|
303
|
+
logs.add(`[${phone}] ChatApp: таймаут ${FETCH_TIMEOUTS.api}мс при проверке телефона через ${messangerType}`, "warn");
|
|
304
|
+
|
|
305
|
+
return { error: true, message: `ChatApp: таймаут ${FETCH_TIMEOUTS.api}мс при проверке телефона через ${messangerType}`, data: null };
|
|
306
|
+
}
|
|
232
307
|
|
|
233
308
|
if (!data.success) {
|
|
234
309
|
return { error: true, message: `Ошибка при проверке телефона в ChatApp через ${messangerType}`, data };
|
|
@@ -240,20 +315,30 @@ export class ChatApp {
|
|
|
240
315
|
newAuth = this.auth;
|
|
241
316
|
this.updateToken = false;
|
|
242
317
|
}
|
|
318
|
+
// Ответ разбирается как any — noUncheckedIndexedAccess здесь ничего не ловит,
|
|
319
|
+
// а обращение к data.data.exist без проверки давало TypeError мимо всякого try/catch
|
|
320
|
+
if (typeof data.data !== "object" || data.data === null) {
|
|
321
|
+
return { error: true, message: `Ответ ChatApp без поля data при проверке телефона через ${messangerType}`, data, auth: newAuth };
|
|
322
|
+
}
|
|
323
|
+
|
|
243
324
|
data.check = data.data.exist;
|
|
244
325
|
return { error: false, message: `Телефон успешно проверен в ChatApp через ${messangerType}`, data, auth: newAuth };
|
|
245
326
|
}
|
|
246
327
|
|
|
247
328
|
async checkTokenChatApp(): Promise<{ error: boolean; message: string; data?: null }> {
|
|
248
329
|
try {
|
|
249
|
-
const response = await
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
330
|
+
const response = await fetchWithTimeout(
|
|
331
|
+
"https://api.chatapp.online/v1/tokens/check",
|
|
332
|
+
{
|
|
333
|
+
method: "GET",
|
|
334
|
+
headers: {
|
|
335
|
+
Lang: "en",
|
|
336
|
+
Accept: "application/json",
|
|
337
|
+
Authorization: this.auth.accessToken,
|
|
338
|
+
},
|
|
255
339
|
},
|
|
256
|
-
|
|
340
|
+
FETCH_TIMEOUTS.quick
|
|
341
|
+
);
|
|
257
342
|
|
|
258
343
|
const data: any = await response.json();
|
|
259
344
|
if (!data.success) {
|
|
@@ -270,19 +355,23 @@ export class ChatApp {
|
|
|
270
355
|
async makeTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
|
|
271
356
|
console.warn("makeTokenChatApp called");
|
|
272
357
|
try {
|
|
273
|
-
const response = await
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
358
|
+
const response = await fetchWithTimeout(
|
|
359
|
+
"https://api.chatapp.online/v1/tokens",
|
|
360
|
+
{
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: {
|
|
363
|
+
Lang: "en",
|
|
364
|
+
"Content-Type": "application/json",
|
|
365
|
+
Accept: "application/json",
|
|
366
|
+
},
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
email: this.make.email,
|
|
369
|
+
password: this.make.pass,
|
|
370
|
+
appId: this.make.appId,
|
|
371
|
+
}),
|
|
279
372
|
},
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
password: this.make.pass,
|
|
283
|
-
appId: this.make.appId,
|
|
284
|
-
}),
|
|
285
|
-
});
|
|
373
|
+
FETCH_TIMEOUTS.quick
|
|
374
|
+
);
|
|
286
375
|
|
|
287
376
|
const data: any = await response.json();
|
|
288
377
|
if (!data.success) {
|
|
@@ -303,15 +392,19 @@ export class ChatApp {
|
|
|
303
392
|
|
|
304
393
|
async refreshTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
|
|
305
394
|
try {
|
|
306
|
-
const response = await
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
395
|
+
const response = await fetchWithTimeout(
|
|
396
|
+
"https://api.chatapp.online/v1/tokens/refresh",
|
|
397
|
+
{
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: {
|
|
400
|
+
Lang: "en",
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
Accept: "application/json",
|
|
403
|
+
},
|
|
404
|
+
body: JSON.stringify({ refreshToken: this.auth.refreshToken }),
|
|
312
405
|
},
|
|
313
|
-
|
|
314
|
-
|
|
406
|
+
FETCH_TIMEOUTS.quick
|
|
407
|
+
);
|
|
315
408
|
|
|
316
409
|
const data: any = await response.json();
|
|
317
410
|
if (!data.success) {
|
|
@@ -336,14 +429,18 @@ export class ChatApp {
|
|
|
336
429
|
|
|
337
430
|
async getLicensesChatApp(): Promise<{ error: boolean; message: string; data: unknown }> {
|
|
338
431
|
try {
|
|
339
|
-
const response = await
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
432
|
+
const response = await fetchWithTimeout(
|
|
433
|
+
"https://api.chatapp.online/v1/licenses",
|
|
434
|
+
{
|
|
435
|
+
method: "GET",
|
|
436
|
+
headers: {
|
|
437
|
+
Lang: "en",
|
|
438
|
+
Accept: "application/json",
|
|
439
|
+
Authorization: this.auth.accessToken,
|
|
440
|
+
},
|
|
345
441
|
},
|
|
346
|
-
|
|
442
|
+
FETCH_TIMEOUTS.api
|
|
443
|
+
);
|
|
347
444
|
const data: any = await response.json();
|
|
348
445
|
|
|
349
446
|
if (!data.success) {
|
package/sendMessage/email.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import nodemailer from "nodemailer";
|
|
2
|
+
import { fetchWithTimeout, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
|
|
2
3
|
|
|
3
4
|
// ==================== Типы ====================
|
|
4
5
|
|
|
@@ -42,6 +43,12 @@ export class Email {
|
|
|
42
43
|
host: "smtp.yandex.ru",
|
|
43
44
|
port: 465, // 465 = SMTPS, 587 = STARTTLS
|
|
44
45
|
secure: true, // true для 465
|
|
46
|
+
// Дефолты nodemailer (соединение 2 мин, приветствие 30с, сокет 10 мин) слишком щедры:
|
|
47
|
+
// письмо с вложением уходит на Яндекс за секунды, а 10 минут молчания сокета
|
|
48
|
+
// блокируют вызывающую очередь
|
|
49
|
+
connectionTimeout: 15_000,
|
|
50
|
+
greetingTimeout: 10_000,
|
|
51
|
+
socketTimeout: 120_000,
|
|
45
52
|
});
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -51,7 +58,9 @@ export class Email {
|
|
|
51
58
|
if (dataMail.fileUrl) {
|
|
52
59
|
const resDownloadFile = await this.downloadFileToUrl(dataMail.fileUrl);
|
|
53
60
|
if (resDownloadFile.error) {
|
|
54
|
-
|
|
61
|
+
// Подстановка вместо возможного undefined: при exactOptionalPropertyTypes
|
|
62
|
+
// у потребителя явный undefined не подходит полю message?: string
|
|
63
|
+
return { error: true, message: resDownloadFile.message ?? "Не удалось скачать файл по fileUrl" };
|
|
55
64
|
}
|
|
56
65
|
dataMail.attachments = [
|
|
57
66
|
{
|
|
@@ -80,7 +89,7 @@ export class Email {
|
|
|
80
89
|
|
|
81
90
|
private async downloadFileToUrl(fileUrl: string): Promise<{ error: boolean; message?: string; buffer?: Buffer }> {
|
|
82
91
|
try {
|
|
83
|
-
const response = await
|
|
92
|
+
const response = await fetchWithTimeout(fileUrl, undefined, FETCH_TIMEOUTS.transfer);
|
|
84
93
|
if (!response.ok) {
|
|
85
94
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
86
95
|
}
|