@andrey4emk/npm-app-back-b24 3.9.0 → 4.0.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 +108 -4
- package/bitrix24/b24/facade.ts +14 -1
- package/bitrix24/b24/proxy.ts +51 -9
- package/bitrix24/b24/retry.ts +19 -6
- package/bitrix24/b24/tokens.ts +8 -6
- package/bitrix24/b24/types.ts +27 -0
- package/bitrix24/b24.ts +43 -12
- package/bitrix24/errTaskB24.ts +100 -6
- 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/bitrix24/errTaskB24.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DateTime } from "luxon";
|
|
2
2
|
import { $b24, getResultData } from "./b24.ts";
|
|
3
|
+
import { isB24NetworkError } from "./b24/retry.ts";
|
|
3
4
|
import { logs } from "../logs/logs.ts";
|
|
4
5
|
import dotEnv from "dotenv";
|
|
5
6
|
|
|
@@ -14,6 +15,10 @@ export interface ErrorTaskData {
|
|
|
14
15
|
createdBy?: number;
|
|
15
16
|
responsibleId?: number;
|
|
16
17
|
deadline?: string;
|
|
18
|
+
/**
|
|
19
|
+
* ID группы задачи. Поле не передано — берётся `B24_ERROR_TASK_GROUP_ID`;
|
|
20
|
+
* явные `null` и `0` означают «без группы» и дефолт не подхватывают
|
|
21
|
+
*/
|
|
17
22
|
groupId?: number | null;
|
|
18
23
|
accomplices?: number[];
|
|
19
24
|
maxTasks?: number;
|
|
@@ -27,7 +32,8 @@ export interface ErrorTaskData {
|
|
|
27
32
|
interface ErrorB24Result {
|
|
28
33
|
error: boolean;
|
|
29
34
|
message: string;
|
|
30
|
-
|
|
35
|
+
/** Ответ портала на tasks.task.add как есть — форму задаёт сам портал, поэтому unknown */
|
|
36
|
+
data?: unknown;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/** Задача из Bitrix24 */
|
|
@@ -40,6 +46,76 @@ interface TaskItem {
|
|
|
40
46
|
|
|
41
47
|
const appName: string = process.env.APP_NAME || "Задай название приложения в .env";
|
|
42
48
|
|
|
49
|
+
/** Имя переменной окружения с группой по умолчанию для задач об ошибках */
|
|
50
|
+
const ERROR_TASK_GROUP_ENV = "B24_ERROR_TASK_GROUP_ID";
|
|
51
|
+
|
|
52
|
+
/** Разобранная группа по умолчанию плюс текст претензии к значению, если она есть */
|
|
53
|
+
interface DefaultGroup {
|
|
54
|
+
/** ID группы либо `null` — «без группы» */
|
|
55
|
+
id: number | null;
|
|
56
|
+
/** Текст строки `warn`; `null` — предупреждать не о чем */
|
|
57
|
+
warning: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Разбирает значение `B24_ERROR_TASK_GROUP_ID`.
|
|
62
|
+
*
|
|
63
|
+
* `Number`, а не `parseInt`: последний молча съел бы «196abc» и вернул 196.
|
|
64
|
+
* Непригодное значение группой не становится — задача создаётся без группы,
|
|
65
|
+
* как до 4.0.1.
|
|
66
|
+
*/
|
|
67
|
+
function parseDefaultGroupId(raw: string | undefined): DefaultGroup {
|
|
68
|
+
const value = (raw ?? "").trim();
|
|
69
|
+
|
|
70
|
+
if (value === "") {
|
|
71
|
+
return {
|
|
72
|
+
id: null,
|
|
73
|
+
warning: `${ERROR_TASK_GROUP_ENV} не задана — задачи об ошибках создаются без группы`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parsed = Number(value);
|
|
78
|
+
|
|
79
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
80
|
+
return {
|
|
81
|
+
id: null,
|
|
82
|
+
warning: `${ERROR_TASK_GROUP_ENV}: значение '${value}' непригодно (нужно целое больше нуля) — задачи об ошибках создаются без группы`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { id: parsed, warning: null };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Значение читается один раз при загрузке модуля — тем же правилом, что и бюджеты
|
|
91
|
+
* таймаутов в `utils/fetchRetry.ts`. Правка `process.env` в рантайме дефолт не двигает.
|
|
92
|
+
*/
|
|
93
|
+
const defaultGroup: DefaultGroup = parseDefaultGroupId(process.env[ERROR_TASK_GROUP_ENV]);
|
|
94
|
+
|
|
95
|
+
/** Строка `warn` о непригодном дефолте пишется один раз за процесс, а не на каждый вызов */
|
|
96
|
+
let defaultGroupWarned = false;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Выбирает группу задачи.
|
|
100
|
+
*
|
|
101
|
+
* Явно переданное значение приоритетнее дефолта: `null` и `0` означают осознанное
|
|
102
|
+
* «без группы» (так же вёл себя прежний `dataTask.groupId || null`), а отсутствие поля
|
|
103
|
+
* и `undefined` — «решай сам», то есть дефолт из окружения.
|
|
104
|
+
*
|
|
105
|
+
* Предупреждение пишется только когда дефолт реально понадобился: у потребителя,
|
|
106
|
+
* который всегда передаёт группу сам, в логе не появится ни строки.
|
|
107
|
+
*/
|
|
108
|
+
function resolveGroupId(explicit: number | null | undefined): number | null {
|
|
109
|
+
if (explicit !== undefined) return explicit || null;
|
|
110
|
+
|
|
111
|
+
if (defaultGroup.warning !== null && !defaultGroupWarned) {
|
|
112
|
+
defaultGroupWarned = true;
|
|
113
|
+
logs.add(defaultGroup.warning, "warn");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return defaultGroup.id;
|
|
117
|
+
}
|
|
118
|
+
|
|
43
119
|
// ==================== Функция ====================
|
|
44
120
|
|
|
45
121
|
/**
|
|
@@ -65,8 +141,9 @@ export async function checkB24Scope(): Promise<void> {
|
|
|
65
141
|
if (missingScopes.length > 0) {
|
|
66
142
|
logs.add(`Отсутствуют скоупы: ${missingScopes.join(", ")}`, "error");
|
|
67
143
|
}
|
|
68
|
-
} catch (error:
|
|
69
|
-
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
logs.add(`Ошибка при получении скоупов: ${message}`, "error");
|
|
70
147
|
}
|
|
71
148
|
}
|
|
72
149
|
|
|
@@ -83,13 +160,16 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
|
|
|
83
160
|
};
|
|
84
161
|
}
|
|
85
162
|
|
|
163
|
+
// Резолв группы стоит до `try`: сама функция не бросает, а номер группы нужен
|
|
164
|
+
// в `catch` — без него по строке отказа не понять, куда задача не попала
|
|
165
|
+
const groupId = resolveGroupId(dataTask.groupId);
|
|
166
|
+
|
|
86
167
|
try {
|
|
87
168
|
const title = dataTask.title;
|
|
88
169
|
const description = dataTask.description || "";
|
|
89
170
|
const createdBy = dataTask.createdBy || 138;
|
|
90
171
|
const responsibleId = dataTask.responsibleId || 1;
|
|
91
172
|
const deadline = dataTask.deadline || DateTime.now().plus({ days: 1 }).toISO();
|
|
92
|
-
const groupId = dataTask.groupId || null;
|
|
93
173
|
const accomplices = dataTask.accomplices || [];
|
|
94
174
|
const maxTasks = dataTask.maxTasks || 100;
|
|
95
175
|
const ufCrmTask = dataTask.ufCrmTask ?? dataTask.entityTypeAbbr ?? "";
|
|
@@ -133,10 +213,24 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
|
|
|
133
213
|
message: "Задача создана в Битрикс24.",
|
|
134
214
|
data: createdTask,
|
|
135
215
|
};
|
|
136
|
-
} catch (error:
|
|
216
|
+
} catch (error: unknown) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
|
|
219
|
+
// Отказ портала иначе не виден нигде: несетевую ошибку retry-слой отдаёт
|
|
220
|
+
// наружу как есть, без единой строки в логе, а `errorB24()` зовут последним
|
|
221
|
+
// средством и её результат обычно не читают — отказ прав или неверный номер
|
|
222
|
+
// группы уходил бы в тишину.
|
|
223
|
+
//
|
|
224
|
+
// Условие обязательно: сетевой отказ retry-слой уже записал сам — строкой
|
|
225
|
+
// про исчерпание попыток либо про отменённый повтор. Без проверки вторая
|
|
226
|
+
// строка ушла бы дублем в чат B24, куда уходит уровень `error`
|
|
227
|
+
if (!isB24NetworkError(error)) {
|
|
228
|
+
logs.add(`errorB24: задача «${dataTask.title}» не создана (группа ${groupId ?? "не задана"}) — ${message}`, "error");
|
|
229
|
+
}
|
|
230
|
+
|
|
137
231
|
return {
|
|
138
232
|
error: true,
|
|
139
|
-
message: `Не удалось создать задачу в Битрикс24: ${
|
|
233
|
+
message: `Не удалось создать задачу в Битрикс24: ${message}`,
|
|
140
234
|
};
|
|
141
235
|
}
|
|
142
236
|
}
|
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.1",
|
|
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 };
|