@andrey4emk/npm-app-back-b24 3.7.1 → 3.8.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 +123 -11
- package/bitrix24/b24.ts +103 -20
- package/logs/logs.ts +19 -1
- package/package.json +5 -2
- package/sendMessage/chatApp.ts +192 -95
- package/sendMessage/email.ts +11 -2
- package/sendMessage/smsgold.ts +57 -12
- package/sendMessage/wappi.ts +144 -53
- package/utils/fetchRetry.ts +251 -8
package/sendMessage/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
|
}
|
package/sendMessage/smsgold.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getResultData, type B24MethodCaller } from "../bitrix24/b24.ts";
|
|
2
|
-
import { fetchRetry } from "../utils/fetchRetry.ts";
|
|
2
|
+
import { fetchRetry, fetchWithTimeout, isTimeoutError, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
|
|
3
|
+
import { logs } from "../logs/logs.ts";
|
|
3
4
|
|
|
4
5
|
// ==================== Типы ====================
|
|
5
6
|
|
|
@@ -17,6 +18,15 @@ interface SmsMessageData {
|
|
|
17
18
|
fileName?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Папка на диске Bitrix24, куда кладётся вложение перед отправкой SMS.
|
|
23
|
+
* Значение — папка «contract_company» проекта «Документы и договора» портала
|
|
24
|
+
* repacheb.bitrix24.ru. Для опубликованной библиотеки это чужая константа,
|
|
25
|
+
* поэтому она вынесена в параметр конструктора; здесь она осталась дефолтом,
|
|
26
|
+
* иначе смена была бы ломающей для всех текущих потребителей.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_UPLOAD_FOLDER_ID = 2792881;
|
|
29
|
+
|
|
20
30
|
/** Результат отправки SMS */
|
|
21
31
|
interface SmsResult {
|
|
22
32
|
error: boolean;
|
|
@@ -27,6 +37,12 @@ interface SmsResult {
|
|
|
27
37
|
data?: string;
|
|
28
38
|
errorText?: string;
|
|
29
39
|
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Выставляется только при таймауте ожидания ответа SMSGold: запрос ушёл, ответ не пришёл.
|
|
42
|
+
* Означает «сообщение могло уйти» — не «не отправлено». Повторять отправку нельзя,
|
|
43
|
+
* решение принимает человек.
|
|
44
|
+
*/
|
|
45
|
+
unknownDelivery?: boolean;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
// ==================== Класс ====================
|
|
@@ -37,11 +53,14 @@ export class Smsgold {
|
|
|
37
53
|
// Структурный тип, а не B24OAuth: объявляет callMethod сам, поэтому переживёт
|
|
38
54
|
// удаление метода из SDK. Сигнатуру конструктора не сужает — она стала шире
|
|
39
55
|
private b24: B24MethodCaller | null;
|
|
56
|
+
// ID папки на диске B24 для вложений SMS — параметр, а не константа модуля
|
|
57
|
+
private uploadFolderId: number;
|
|
40
58
|
|
|
41
|
-
constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null) {
|
|
59
|
+
constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null, uploadFolderId: number = DEFAULT_UPLOAD_FOLDER_ID) {
|
|
42
60
|
this.user = authParam.user;
|
|
43
61
|
this.pass = authParam.pass;
|
|
44
62
|
this.b24 = b24;
|
|
63
|
+
this.uploadFolderId = uploadFolderId;
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
async sendSms(messageData: SmsMessageData): Promise<SmsResult> {
|
|
@@ -97,13 +116,20 @@ export class Smsgold {
|
|
|
97
116
|
sender: "potolkiRepa",
|
|
98
117
|
}).toString();
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
// Отправка неидемпотентна — повторов нет, единственная защита от зависшего
|
|
120
|
+
// соединения — бюджет времени. 30с взяты с запасом, чтобы «неизвестный статус»
|
|
121
|
+
// случался как можно реже
|
|
122
|
+
const result = await fetchWithTimeout(
|
|
123
|
+
"https://web.smsgold.ru/http2/",
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
128
|
+
},
|
|
129
|
+
body,
|
|
104
130
|
},
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
FETCH_TIMEOUTS.send
|
|
132
|
+
);
|
|
107
133
|
|
|
108
134
|
if (!result.ok) {
|
|
109
135
|
const errorText = await result.text();
|
|
@@ -126,6 +152,23 @@ export class Smsgold {
|
|
|
126
152
|
},
|
|
127
153
|
};
|
|
128
154
|
} catch (error: unknown) {
|
|
155
|
+
// Ответа не дождались: запрос уже ушёл, сообщение могло быть доставлено.
|
|
156
|
+
// Это не «ошибка отправки» — статус неизвестен, и решить это кодом нельзя.
|
|
157
|
+
// Единственное место в классе, где пишем в лог: уровень error уходит в чат B24,
|
|
158
|
+
// потому что разбираться должен человек
|
|
159
|
+
if (isTimeoutError(error)) {
|
|
160
|
+
logs.add(
|
|
161
|
+
`[${phone}] SMS: таймаут ожидания ответа SMSGold ${FETCH_TIMEOUTS.send}мс — статус неизвестен, повторная отправка запрещена`,
|
|
162
|
+
"error"
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
error: true,
|
|
166
|
+
message: `SMSGold не ответил за ${FETCH_TIMEOUTS.send / 1000}с — статус отправки неизвестен: сообщение могло уйти, повторять нельзя`,
|
|
167
|
+
result: null,
|
|
168
|
+
unknownDelivery: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
130
173
|
return { error: true, message: errMsg, result: null };
|
|
131
174
|
}
|
|
@@ -138,7 +181,7 @@ export class Smsgold {
|
|
|
138
181
|
|
|
139
182
|
// Используем метод disk.folder.uploadfile
|
|
140
183
|
const res = await this.b24!.callMethod("disk.folder.uploadfile", {
|
|
141
|
-
id:
|
|
184
|
+
id: this.uploadFolderId,
|
|
142
185
|
data: { NAME: fileName },
|
|
143
186
|
fileContent: [fileName, base64File],
|
|
144
187
|
});
|
|
@@ -153,9 +196,11 @@ export class Smsgold {
|
|
|
153
196
|
private async downloadFileToUrl(fileUrl: string): Promise<{ error: boolean; message?: string; buffer?: Buffer }> {
|
|
154
197
|
try {
|
|
155
198
|
// Скачивание файла идемпотентно — повтор при сетевом сбое безопасен.
|
|
156
|
-
// Отправка SMS (
|
|
157
|
-
// при потерянном ответе повтор отправил бы сообщение второй
|
|
158
|
-
|
|
199
|
+
// Отправка SMS (fetchWithTimeout выше по коду) намеренно остаётся без retry:
|
|
200
|
+
// при потерянном ответе повтор отправил бы сообщение второй раз.
|
|
201
|
+
// Попыток 3, а не 5: при бюджете 60с на попытку пять попыток дали бы до пяти
|
|
202
|
+
// минут ожидания перед самой отправкой SMS
|
|
203
|
+
const response = await fetchRetry(fileUrl, undefined, 3, 500, FETCH_TIMEOUTS.transfer);
|
|
159
204
|
if (!response.ok) {
|
|
160
205
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
161
206
|
}
|