@andrey4emk/npm-app-back-b24 3.7.0 → 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 +134 -10
- package/bitrix24/b24.ts +103 -20
- package/logs/logs.ts +19 -1
- package/package.json +12 -9
- package/sendMessage/chatApp.ts +218 -86
- 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 */
|
|
@@ -57,12 +60,56 @@ export class ChatApp {
|
|
|
57
60
|
private auth: ChatAppAuthParam;
|
|
58
61
|
private type: ChatAppTypeParam;
|
|
59
62
|
private updateToken: boolean;
|
|
63
|
+
private readonly whatsAppType: string | null;
|
|
64
|
+
private readonly telegramType: string | null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Тип мессенджера из конфига. Массив приходит из внешнего источника
|
|
68
|
+
* (config/chatApp.json обновляется данными API ChatApp), поэтому проверяем:
|
|
69
|
+
* пустой messenger дал бы URL с undefined и «отправку в никуда».
|
|
70
|
+
* Не бросаем: экземпляр создаётся на уровне модуля, и бросок уронил бы
|
|
71
|
+
* импорт всего модуля отправки вместе с остальными каналами. Отказ локальный —
|
|
72
|
+
* его возвращают методы того мессенджера, который не разрешился.
|
|
73
|
+
*/
|
|
74
|
+
private static resolveMessengerType(config: MessengerConfig | undefined): string | null {
|
|
75
|
+
const first = config?.messenger?.[0];
|
|
76
|
+
if (typeof first?.type !== "string" || !first.type.trim()) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return first.type;
|
|
80
|
+
}
|
|
60
81
|
|
|
61
82
|
constructor(makeParam: ChatAppMakeParam, authParam: ChatAppAuthParam, typeParam: ChatAppTypeParam) {
|
|
62
83
|
this.make = makeParam;
|
|
63
84
|
this.auth = authParam;
|
|
64
85
|
this.type = typeParam;
|
|
65
86
|
this.updateToken = false;
|
|
87
|
+
this.whatsAppType = ChatApp.resolveMessengerType(typeParam?.whatsApp);
|
|
88
|
+
this.telegramType = ChatApp.resolveMessengerType(typeParam?.telegram);
|
|
89
|
+
}
|
|
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 };
|
|
66
113
|
}
|
|
67
114
|
|
|
68
115
|
async sendMessageChatApp(messangerType: MessengerType, messageData: MessageData): Promise<ChatAppResult> {
|
|
@@ -73,6 +120,11 @@ export class ChatApp {
|
|
|
73
120
|
if (!messageData.phone || !messageData.message) {
|
|
74
121
|
return { error: true, message: "Отсутствует phone или message", data: null };
|
|
75
122
|
}
|
|
123
|
+
// Тип мессенджера не разрешился из конфига — отправлять некуда
|
|
124
|
+
const baseUrl = this.resolveBaseUrl(messangerType);
|
|
125
|
+
if (!baseUrl) {
|
|
126
|
+
return ChatApp.denyMessenger(messangerType);
|
|
127
|
+
}
|
|
76
128
|
|
|
77
129
|
const check = await this.checkTokenChatApp();
|
|
78
130
|
if (check.error) {
|
|
@@ -82,25 +134,42 @@ export class ChatApp {
|
|
|
82
134
|
const phone = messageData.phone;
|
|
83
135
|
const message = messageData.message;
|
|
84
136
|
|
|
85
|
-
|
|
86
|
-
if (messangerType === "whatsApp") {
|
|
87
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${this.type.whatsApp.messenger[0].type}/chats/${phone}/messages/text`;
|
|
88
|
-
}
|
|
89
|
-
if (messangerType === "telegram") {
|
|
90
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${this.type.telegram.messenger[0].type}/chats/${phone}/messages/text`;
|
|
91
|
-
}
|
|
137
|
+
const url = `${baseUrl}/chats/${phone}/messages/text`;
|
|
92
138
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|
|
104
173
|
|
|
105
174
|
if (!data.success) {
|
|
106
175
|
return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
|
|
@@ -123,6 +192,11 @@ export class ChatApp {
|
|
|
123
192
|
if (!messageData.phone || !messageData.message) {
|
|
124
193
|
return { error: true, message: "Отсутствует phone или message", data: null };
|
|
125
194
|
}
|
|
195
|
+
// Тип мессенджера не разрешился из конфига — отправлять некуда
|
|
196
|
+
const baseUrl = this.resolveBaseUrl(messangerType);
|
|
197
|
+
if (!baseUrl) {
|
|
198
|
+
return ChatApp.denyMessenger(messangerType);
|
|
199
|
+
}
|
|
126
200
|
|
|
127
201
|
const check = await this.checkTokenChatApp();
|
|
128
202
|
if (check.error) {
|
|
@@ -134,25 +208,38 @@ export class ChatApp {
|
|
|
134
208
|
const file = messageData.fileUrl;
|
|
135
209
|
const fileName = messageData.fileName;
|
|
136
210
|
|
|
137
|
-
|
|
138
|
-
if (messangerType === "whatsApp") {
|
|
139
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${this.type.whatsApp.messenger[0].type}/chats/${phone}/messages/file`;
|
|
140
|
-
}
|
|
141
|
-
if (messangerType === "telegram") {
|
|
142
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${this.type.telegram.messenger[0].type}/chats/${phone}/messages/file`;
|
|
143
|
-
}
|
|
211
|
+
const url = `${baseUrl}/chats/${phone}/messages/file`;
|
|
144
212
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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;
|
|
234
|
+
|
|
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
|
+
}
|
|
156
243
|
|
|
157
244
|
if (!data.success) {
|
|
158
245
|
return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
|
|
@@ -167,33 +254,56 @@ export class ChatApp {
|
|
|
167
254
|
return { error: false, message: `Сообщение успешно отправлено в ChatApp через ${messangerType}`, data, auth: newAuth };
|
|
168
255
|
}
|
|
169
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
|
+
*/
|
|
170
266
|
async phoneCheckChatApp(messangerType: MessengerType, phone: string): Promise<ChatAppResult> {
|
|
171
267
|
if (!["whatsApp", "telegram"].includes(messangerType)) {
|
|
172
268
|
return { error: true, message: "Неверный тип мессенджера", data: null };
|
|
173
269
|
}
|
|
270
|
+
// Тип мессенджера не разрешился из конфига — проверять телефон негде
|
|
271
|
+
const baseUrl = this.resolveBaseUrl(messangerType);
|
|
272
|
+
if (!baseUrl) {
|
|
273
|
+
return ChatApp.denyMessenger(messangerType);
|
|
274
|
+
}
|
|
174
275
|
|
|
175
276
|
const check = await this.checkTokenChatApp();
|
|
176
277
|
if (check.error) {
|
|
177
278
|
return { error: true, message: `Ошибка с токеном ChatApp в функции phoneCheckChatApp класса ChatApp\n${check.message}`, data: check };
|
|
178
279
|
}
|
|
179
280
|
|
|
180
|
-
|
|
181
|
-
if (messangerType === "whatsApp") {
|
|
182
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${this.type.whatsApp.messenger[0].type}/phones/${phone}/check`;
|
|
183
|
-
}
|
|
184
|
-
if (messangerType === "telegram") {
|
|
185
|
-
url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${this.type.telegram.messenger[0].type}/phones/${phone}/check`;
|
|
186
|
-
}
|
|
281
|
+
const url = `${baseUrl}/phones/${phone}/check`;
|
|
187
282
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
307
|
|
|
198
308
|
if (!data.success) {
|
|
199
309
|
return { error: true, message: `Ошибка при проверке телефона в ChatApp через ${messangerType}`, data };
|
|
@@ -205,20 +315,30 @@ export class ChatApp {
|
|
|
205
315
|
newAuth = this.auth;
|
|
206
316
|
this.updateToken = false;
|
|
207
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
|
+
|
|
208
324
|
data.check = data.data.exist;
|
|
209
325
|
return { error: false, message: `Телефон успешно проверен в ChatApp через ${messangerType}`, data, auth: newAuth };
|
|
210
326
|
}
|
|
211
327
|
|
|
212
328
|
async checkTokenChatApp(): Promise<{ error: boolean; message: string; data?: null }> {
|
|
213
329
|
try {
|
|
214
|
-
const response = await
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
},
|
|
220
339
|
},
|
|
221
|
-
|
|
340
|
+
FETCH_TIMEOUTS.quick
|
|
341
|
+
);
|
|
222
342
|
|
|
223
343
|
const data: any = await response.json();
|
|
224
344
|
if (!data.success) {
|
|
@@ -235,19 +355,23 @@ export class ChatApp {
|
|
|
235
355
|
async makeTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
|
|
236
356
|
console.warn("makeTokenChatApp called");
|
|
237
357
|
try {
|
|
238
|
-
const response = await
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}),
|
|
244
372
|
},
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
password: this.make.pass,
|
|
248
|
-
appId: this.make.appId,
|
|
249
|
-
}),
|
|
250
|
-
});
|
|
373
|
+
FETCH_TIMEOUTS.quick
|
|
374
|
+
);
|
|
251
375
|
|
|
252
376
|
const data: any = await response.json();
|
|
253
377
|
if (!data.success) {
|
|
@@ -268,15 +392,19 @@ export class ChatApp {
|
|
|
268
392
|
|
|
269
393
|
async refreshTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
|
|
270
394
|
try {
|
|
271
|
-
const response = await
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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 }),
|
|
277
405
|
},
|
|
278
|
-
|
|
279
|
-
|
|
406
|
+
FETCH_TIMEOUTS.quick
|
|
407
|
+
);
|
|
280
408
|
|
|
281
409
|
const data: any = await response.json();
|
|
282
410
|
if (!data.success) {
|
|
@@ -301,14 +429,18 @@ export class ChatApp {
|
|
|
301
429
|
|
|
302
430
|
async getLicensesChatApp(): Promise<{ error: boolean; message: string; data: unknown }> {
|
|
303
431
|
try {
|
|
304
|
-
const response = await
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
},
|
|
310
441
|
},
|
|
311
|
-
|
|
442
|
+
FETCH_TIMEOUTS.api
|
|
443
|
+
);
|
|
312
444
|
const data: any = await response.json();
|
|
313
445
|
|
|
314
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
|
}
|