@ihazz/bitrix24 1.1.12 → 1.1.14
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 +77 -4
- package/dist/src/api.d.ts +10 -5
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +42 -8
- package/dist/src/api.js.map +1 -1
- package/dist/src/channel.d.ts +20 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +2303 -81
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.d.ts +1 -0
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +79 -68
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.d.ts +10 -0
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/inbound-handler.js +281 -16
- package/dist/src/inbound-handler.js.map +1 -1
- package/dist/src/media-service.d.ts +4 -0
- package/dist/src/media-service.d.ts.map +1 -1
- package/dist/src/media-service.js +147 -14
- package/dist/src/media-service.js.map +1 -1
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/message-utils.js +113 -4
- package/dist/src/message-utils.js.map +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/send-service.d.ts +2 -1
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +34 -5
- package/dist/src/send-service.js.map +1 -1
- package/dist/src/state-paths.d.ts +1 -0
- package/dist/src/state-paths.d.ts.map +1 -1
- package/dist/src/state-paths.js +9 -0
- package/dist/src/state-paths.js.map +1 -1
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +62 -13
- package/src/channel.ts +3746 -843
- package/src/i18n.ts +81 -68
- package/src/inbound-handler.ts +357 -17
- package/src/media-service.ts +185 -15
- package/src/message-utils.ts +144 -4
- package/src/runtime.ts +1 -0
- package/src/send-service.ts +52 -4
- package/src/state-paths.ts +11 -0
- package/src/types.ts +122 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -19,6 +19,8 @@ import type {
|
|
|
19
19
|
import { Dedup } from './dedup.js';
|
|
20
20
|
import { createVerboseLogger, defaultLogger } from './utils.js';
|
|
21
21
|
|
|
22
|
+
const USER_SCOPE_GROUP_MIRROR_DEBOUNCE_MS = 1200;
|
|
23
|
+
|
|
22
24
|
/** Normalized fetch command context passed to onFetchCommand callback. */
|
|
23
25
|
export interface FetchCommandContext {
|
|
24
26
|
commandId: number;
|
|
@@ -77,6 +79,11 @@ export class InboundHandler {
|
|
|
77
79
|
private config: Bitrix24AccountConfig;
|
|
78
80
|
private logger: Logger;
|
|
79
81
|
private verboseLog: boolean;
|
|
82
|
+
private knownBotConversations = new Map<string, number>();
|
|
83
|
+
private pendingUserScopeGroupMessages = new Map<string, {
|
|
84
|
+
timer: ReturnType<typeof setTimeout>;
|
|
85
|
+
resolve: (value: boolean) => void;
|
|
86
|
+
}>();
|
|
80
87
|
private onMessage?: (ctx: B24MsgContext) => void | Promise<void>;
|
|
81
88
|
private onJoinChat?: (ctx: FetchJoinChatContext) => void | Promise<void>;
|
|
82
89
|
private onReactionChange?: (ctx: FetchReactionContext) => void | Promise<void>;
|
|
@@ -95,6 +102,112 @@ export class InboundHandler {
|
|
|
95
102
|
this.onBotDelete = opts.onBotDelete;
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
private pruneKnownBotConversations(now = Date.now()): void {
|
|
106
|
+
for (const [key, expiresAt] of this.knownBotConversations.entries()) {
|
|
107
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
108
|
+
this.knownBotConversations.delete(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private buildConversationMirrorKeys(dialogId: string, chatId: string): string[] {
|
|
114
|
+
const keys: string[] = [];
|
|
115
|
+
const normalizedDialogId = String(dialogId ?? '').trim();
|
|
116
|
+
const normalizedChatId = String(chatId ?? '').trim();
|
|
117
|
+
|
|
118
|
+
if (normalizedDialogId) {
|
|
119
|
+
keys.push(`dialog:${normalizedDialogId}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (normalizedChatId) {
|
|
123
|
+
keys.push(`chat:${normalizedChatId}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return keys;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private rememberBotConversation(dialogId: string, chatId: string): void {
|
|
130
|
+
const keys = this.buildConversationMirrorKeys(dialogId, chatId);
|
|
131
|
+
if (keys.length === 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.pruneKnownBotConversations();
|
|
136
|
+
const expiresAt = Date.now() + (5 * 60 * 1000);
|
|
137
|
+
keys.forEach((key) => {
|
|
138
|
+
this.knownBotConversations.set(key, expiresAt);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private isKnownBotConversation(dialogId: string, chatId: string): boolean {
|
|
143
|
+
const keys = this.buildConversationMirrorKeys(dialogId, chatId);
|
|
144
|
+
if (keys.length === 0) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.pruneKnownBotConversations();
|
|
149
|
+
return keys.some((key) => this.knownBotConversations.has(key));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private shouldSkipMirroredUserEvent(params: {
|
|
153
|
+
dialogId: string;
|
|
154
|
+
chatId: string;
|
|
155
|
+
isP2P: boolean;
|
|
156
|
+
botId: string;
|
|
157
|
+
}): boolean {
|
|
158
|
+
if (!this.config.agentMode) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (params.isP2P) {
|
|
163
|
+
return params.dialogId === params.botId;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return this.isKnownBotConversation(params.dialogId, params.chatId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private buildPendingUserScopeGroupMessageKey(messageId: string, chatId: string): string {
|
|
170
|
+
return `${chatId}:${messageId}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private consumePendingUserScopeGroupMessage(messageId: string, chatId: string): boolean {
|
|
174
|
+
const key = this.buildPendingUserScopeGroupMessageKey(messageId, chatId);
|
|
175
|
+
const pendingEntry = this.pendingUserScopeGroupMessages.get(key);
|
|
176
|
+
if (!pendingEntry) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
clearTimeout(pendingEntry.timer);
|
|
181
|
+
this.pendingUserScopeGroupMessages.delete(key);
|
|
182
|
+
pendingEntry.resolve(true);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private deferPotentialMirroredUserScopeGroupMessage(params: {
|
|
187
|
+
messageId: string;
|
|
188
|
+
chatId: string;
|
|
189
|
+
run: () => Promise<void>;
|
|
190
|
+
}): Promise<boolean> {
|
|
191
|
+
const key = this.buildPendingUserScopeGroupMessageKey(params.messageId, params.chatId);
|
|
192
|
+
const existingEntry = this.pendingUserScopeGroupMessages.get(key);
|
|
193
|
+
if (existingEntry) {
|
|
194
|
+
return Promise.resolve(true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
const timer = setTimeout(async () => {
|
|
199
|
+
this.pendingUserScopeGroupMessages.delete(key);
|
|
200
|
+
try {
|
|
201
|
+
await params.run();
|
|
202
|
+
} finally {
|
|
203
|
+
resolve(true);
|
|
204
|
+
}
|
|
205
|
+
}, USER_SCOPE_GROUP_MIRROR_DEBOUNCE_MS);
|
|
206
|
+
|
|
207
|
+
this.pendingUserScopeGroupMessages.set(key, { timer, resolve });
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
98
211
|
// ─── V2 FETCH mode event handling ───────────────────────────────────────
|
|
99
212
|
|
|
100
213
|
/**
|
|
@@ -167,18 +280,49 @@ export class InboundHandler {
|
|
|
167
280
|
}
|
|
168
281
|
const messageId = String(rawMessageId);
|
|
169
282
|
|
|
170
|
-
const dedupKey = `${eventScope}:${messageId}`;
|
|
171
|
-
if (this.dedup.isDuplicate(dedupKey)) {
|
|
172
|
-
this.logger.debug(`Fetch: duplicate message ${messageId} for scope ${eventScope}, skipping`);
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
283
|
const dialogId = String(data.chat?.dialogId ?? data.message.chatId ?? data.user.id ?? '');
|
|
177
284
|
if (!dialogId) {
|
|
178
285
|
this.logger.warn('Fetch: message event has no dialogId, skipping');
|
|
179
286
|
return true;
|
|
180
287
|
}
|
|
288
|
+
const chatInternalId = String(data.message?.chatId ?? data.chat?.id ?? dialogId);
|
|
181
289
|
const isP2P = !dialogId.startsWith('chat');
|
|
290
|
+
const normalizedBotId = String(fetchCtx.botId);
|
|
291
|
+
const normalizedSenderId = String(data.user?.id ?? '');
|
|
292
|
+
const normalizedAuthorId = String(data.message?.authorId ?? '');
|
|
293
|
+
|
|
294
|
+
if (eventScope === 'bot' || normalizedSenderId === normalizedBotId || normalizedAuthorId === normalizedBotId) {
|
|
295
|
+
this.rememberBotConversation(dialogId, chatInternalId);
|
|
296
|
+
if (eventScope === 'bot' && this.consumePendingUserScopeGroupMessage(messageId, chatInternalId)) {
|
|
297
|
+
this.logger.debug(`Fetch: canceled pending mirrored user-scope group message ${messageId}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isBitrixBackendServiceMessage(data.message?.text ?? '', data.message?.params)) {
|
|
302
|
+
this.logger.debug(`Fetch: skipping Bitrix backend service message ${messageId}`);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (normalizedSenderId === normalizedBotId || normalizedAuthorId === normalizedBotId) {
|
|
307
|
+
this.logger.debug(`Fetch: skipping self-authored message ${messageId} for scope ${eventScope}`);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (eventScope === 'user' && this.shouldSkipMirroredUserEvent({
|
|
312
|
+
dialogId,
|
|
313
|
+
chatId: chatInternalId,
|
|
314
|
+
isP2P,
|
|
315
|
+
botId: normalizedBotId,
|
|
316
|
+
})) {
|
|
317
|
+
this.logger.debug(`Fetch: skipping mirrored user-scope message ${messageId}`);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const dedupKey = `${eventScope}:${messageId}`;
|
|
322
|
+
if (this.dedup.isDuplicate(dedupKey)) {
|
|
323
|
+
this.logger.debug(`Fetch: duplicate message ${messageId} for scope ${eventScope}, skipping`);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
182
326
|
|
|
183
327
|
// Extract file attachments from message params
|
|
184
328
|
const media = extractFilesFromParams(data.message.params);
|
|
@@ -208,7 +352,7 @@ export class InboundHandler {
|
|
|
208
352
|
senderName: data.user?.name ?? dialogId,
|
|
209
353
|
senderFirstName: data.user?.firstName,
|
|
210
354
|
chatId: dialogId,
|
|
211
|
-
chatInternalId
|
|
355
|
+
chatInternalId,
|
|
212
356
|
chatName: data.chat?.name,
|
|
213
357
|
chatType: data.chat?.type,
|
|
214
358
|
messageId,
|
|
@@ -231,6 +375,16 @@ export class InboundHandler {
|
|
|
231
375
|
memberId: '',
|
|
232
376
|
};
|
|
233
377
|
|
|
378
|
+
if (eventScope === 'user' && !isP2P && this.config.agentMode && !this.isKnownBotConversation(dialogId, chatInternalId)) {
|
|
379
|
+
return this.deferPotentialMirroredUserScopeGroupMessage({
|
|
380
|
+
messageId,
|
|
381
|
+
chatId: chatInternalId,
|
|
382
|
+
run: async () => {
|
|
383
|
+
await this.onMessage?.(ctx);
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
234
388
|
await this.onMessage?.(ctx);
|
|
235
389
|
return true;
|
|
236
390
|
}
|
|
@@ -269,14 +423,44 @@ export class InboundHandler {
|
|
|
269
423
|
this.logger.warn('Fetch: reaction event has no dialogId, skipping', { eventId: item.eventId });
|
|
270
424
|
return true;
|
|
271
425
|
}
|
|
426
|
+
const chatId = String(data.chat?.id ?? data.message.chatId ?? dialogId);
|
|
427
|
+
const isP2P = !dialogId.startsWith('chat');
|
|
428
|
+
const normalizedBotId = String(fetchCtx.botId);
|
|
429
|
+
const normalizedSenderId = String(data.user.id ?? '');
|
|
430
|
+
const normalizedMessageAuthorId = String(data.message.authorId ?? '');
|
|
431
|
+
|
|
432
|
+
if (eventScope === 'bot' || normalizedSenderId === normalizedBotId || normalizedMessageAuthorId === normalizedBotId) {
|
|
433
|
+
this.rememberBotConversation(dialogId, chatId);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (normalizedSenderId === normalizedBotId) {
|
|
437
|
+
this.logger.debug(`Fetch: skipping self-authored reaction ${messageId} for scope ${eventScope}`);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (eventScope === 'user' && this.shouldSkipMirroredUserEvent({
|
|
442
|
+
dialogId,
|
|
443
|
+
chatId,
|
|
444
|
+
isP2P,
|
|
445
|
+
botId: normalizedBotId,
|
|
446
|
+
})) {
|
|
447
|
+
this.logger.debug(`Fetch: skipping mirrored user-scope reaction ${messageId}`);
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const dedupKey = `${eventScope}:reaction:${messageId}:${normalizedSenderId}:${action}:${String(data.reaction ?? '')}`;
|
|
452
|
+
if (this.dedup.isDuplicate(dedupKey)) {
|
|
453
|
+
this.logger.debug(`Fetch: duplicate reaction ${messageId} for scope ${eventScope}, skipping`);
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
272
456
|
|
|
273
457
|
await this.onReactionChange?.({
|
|
274
458
|
eventScope,
|
|
275
|
-
senderId:
|
|
459
|
+
senderId: normalizedSenderId,
|
|
276
460
|
dialogId,
|
|
277
|
-
chatId
|
|
461
|
+
chatId,
|
|
278
462
|
messageId,
|
|
279
|
-
messageAuthorId:
|
|
463
|
+
messageAuthorId: normalizedMessageAuthorId,
|
|
280
464
|
reaction: String(data.reaction ?? ''),
|
|
281
465
|
action,
|
|
282
466
|
language: data.language,
|
|
@@ -300,6 +484,8 @@ export class InboundHandler {
|
|
|
300
484
|
return true;
|
|
301
485
|
}
|
|
302
486
|
|
|
487
|
+
this.rememberBotConversation(data.dialogId, String(data.chat.id ?? data.dialogId));
|
|
488
|
+
|
|
303
489
|
const joinCtx: FetchJoinChatContext = {
|
|
304
490
|
senderId: String(data.user?.id ?? data.dialogId),
|
|
305
491
|
dialogId: data.dialogId,
|
|
@@ -348,6 +534,7 @@ export class InboundHandler {
|
|
|
348
534
|
return true;
|
|
349
535
|
}
|
|
350
536
|
const isP2P = !dialogId.startsWith('chat');
|
|
537
|
+
this.rememberBotConversation(dialogId, String(data.chat?.id ?? data.message?.chatId ?? dialogId));
|
|
351
538
|
|
|
352
539
|
const cmdCtx: FetchCommandContext = {
|
|
353
540
|
commandId,
|
|
@@ -448,6 +635,12 @@ export class InboundHandler {
|
|
|
448
635
|
|
|
449
636
|
destroy(): void {
|
|
450
637
|
this.dedup.destroy();
|
|
638
|
+
this.knownBotConversations.clear();
|
|
639
|
+
for (const pendingEntry of this.pendingUserScopeGroupMessages.values()) {
|
|
640
|
+
clearTimeout(pendingEntry.timer);
|
|
641
|
+
pendingEntry.resolve(true);
|
|
642
|
+
}
|
|
643
|
+
this.pendingUserScopeGroupMessages.clear();
|
|
451
644
|
}
|
|
452
645
|
}
|
|
453
646
|
|
|
@@ -466,6 +659,126 @@ function normalizeMessageParams(params: B24V2MessageParams | undefined): Record<
|
|
|
466
659
|
return params;
|
|
467
660
|
}
|
|
468
661
|
|
|
662
|
+
function extractAttachBlockMessages(params: B24V2MessageParams | undefined): string[] {
|
|
663
|
+
const normalizedParams = normalizeMessageParams(params);
|
|
664
|
+
const rawAttach = normalizedParams.ATTACH;
|
|
665
|
+
if (!Array.isArray(rawAttach)) {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return rawAttach.flatMap((attachItem) => {
|
|
670
|
+
if (!attachItem || typeof attachItem !== 'object') {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const rawBlocks = (attachItem as Record<string, unknown>).BLOCKS;
|
|
675
|
+
if (!Array.isArray(rawBlocks)) {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return rawBlocks
|
|
680
|
+
.map((block) => (
|
|
681
|
+
block && typeof block === 'object'
|
|
682
|
+
? String((block as Record<string, unknown>).MESSAGE ?? '').trim().toLowerCase()
|
|
683
|
+
: ''
|
|
684
|
+
))
|
|
685
|
+
.filter(Boolean);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function isBitrixBackendServiceMessage(text: string, params: B24V2MessageParams | undefined): boolean {
|
|
690
|
+
if (text.trim().toLowerCase() !== 'backend') {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const blockMessages = extractAttachBlockMessages(params);
|
|
695
|
+
if (blockMessages.length === 0) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const hasEvent = blockMessages.some((message) => message.startsWith('event:'));
|
|
700
|
+
const hasTool = blockMessages.some((message) => message === 'tool: im');
|
|
701
|
+
const hasCategory = blockMessages.some((message) => message === 'category: chat');
|
|
702
|
+
|
|
703
|
+
return hasEvent && hasTool && hasCategory;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function normalizeFileRecords(rawFiles: unknown): Record<string, Record<string, unknown>> {
|
|
707
|
+
if (!rawFiles || typeof rawFiles !== 'object') {
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (Array.isArray(rawFiles)) {
|
|
712
|
+
return rawFiles.reduce<Record<string, Record<string, unknown>>>((acc, item) => {
|
|
713
|
+
if (!item || typeof item !== 'object') {
|
|
714
|
+
return acc;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const normalizedItem = item as Record<string, unknown>;
|
|
718
|
+
const rawId = normalizedItem.id ?? normalizedItem.ID;
|
|
719
|
+
if (rawId == null) {
|
|
720
|
+
return acc;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
acc[String(rawId)] = normalizedItem;
|
|
724
|
+
return acc;
|
|
725
|
+
}, {});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return rawFiles as Record<string, Record<string, unknown>>;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function normalizeFileExtension(rawExtension: unknown, fileName?: string): string {
|
|
732
|
+
const explicitExtension = typeof rawExtension === 'string'
|
|
733
|
+
? rawExtension.trim().replace(/^\.+/, '')
|
|
734
|
+
: '';
|
|
735
|
+
if (explicitExtension) {
|
|
736
|
+
return explicitExtension;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const normalizedName = typeof fileName === 'string' ? fileName.trim() : '';
|
|
740
|
+
const lastDotIndex = normalizedName.lastIndexOf('.');
|
|
741
|
+
if (lastDotIndex <= 0 || lastDotIndex === normalizedName.length - 1) {
|
|
742
|
+
return '';
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return normalizedName.slice(lastDotIndex + 1).trim().replace(/^\.+/, '');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function normalizeFileName(params: {
|
|
749
|
+
fileId: string;
|
|
750
|
+
rawName: unknown;
|
|
751
|
+
rawExtension: unknown;
|
|
752
|
+
}): string {
|
|
753
|
+
const normalizedName = typeof params.rawName === 'string'
|
|
754
|
+
? params.rawName.trim()
|
|
755
|
+
: '';
|
|
756
|
+
if (normalizedName) {
|
|
757
|
+
return normalizedName;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const extension = normalizeFileExtension(params.rawExtension);
|
|
761
|
+
return extension ? `file_${params.fileId}.${extension}` : `file_${params.fileId}`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function normalizeMediaType(rawType: unknown, extension: string): 'image' | 'file' {
|
|
765
|
+
const normalizedType = typeof rawType === 'string'
|
|
766
|
+
? rawType.trim().toLowerCase()
|
|
767
|
+
: '';
|
|
768
|
+
|
|
769
|
+
if (normalizedType === 'image') {
|
|
770
|
+
return 'image';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'tiff', 'tif', 'ico'].includes(
|
|
774
|
+
extension.toLowerCase(),
|
|
775
|
+
)) {
|
|
776
|
+
return 'image';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return 'file';
|
|
780
|
+
}
|
|
781
|
+
|
|
469
782
|
function extractFilesFromParams(params: B24V2MessageParams | undefined): B24MediaItem[] {
|
|
470
783
|
const normalizedParams = normalizeMessageParams(params);
|
|
471
784
|
|
|
@@ -475,13 +788,40 @@ function extractFilesFromParams(params: B24V2MessageParams | undefined): B24Medi
|
|
|
475
788
|
const fileIds = Array.isArray(rawFileIds) ? rawFileIds : [rawFileIds];
|
|
476
789
|
if (!fileIds.length) return [];
|
|
477
790
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
791
|
+
const fileRecords = normalizeFileRecords(normalizedParams.FILES);
|
|
792
|
+
|
|
793
|
+
return fileIds.map((id) => {
|
|
794
|
+
const fileId = String(id);
|
|
795
|
+
const fileRecord = fileRecords[fileId] ?? {};
|
|
796
|
+
const viewerAttrs = fileRecord.viewerAttrs;
|
|
797
|
+
const viewerTitle = viewerAttrs && typeof viewerAttrs === 'object'
|
|
798
|
+
? (viewerAttrs as Record<string, unknown>).title
|
|
799
|
+
: undefined;
|
|
800
|
+
const rawName = fileRecord.name ?? fileRecord.NAME ?? viewerTitle;
|
|
801
|
+
const name = normalizeFileName({
|
|
802
|
+
fileId,
|
|
803
|
+
rawName,
|
|
804
|
+
rawExtension: fileRecord.extension ?? fileRecord.EXTENSION,
|
|
805
|
+
});
|
|
806
|
+
const extension = normalizeFileExtension(
|
|
807
|
+
fileRecord.extension ?? fileRecord.EXTENSION,
|
|
808
|
+
name,
|
|
809
|
+
);
|
|
810
|
+
const rawSize = Number(fileRecord.size ?? fileRecord.SIZE ?? 0);
|
|
811
|
+
const size = Number.isFinite(rawSize) && rawSize > 0 ? rawSize : 0;
|
|
812
|
+
const urlDownload = typeof fileRecord.urlDownload === 'string'
|
|
813
|
+
? fileRecord.urlDownload
|
|
814
|
+
: undefined;
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
id: fileId,
|
|
818
|
+
name,
|
|
819
|
+
extension,
|
|
820
|
+
size,
|
|
821
|
+
type: normalizeMediaType(fileRecord.type ?? fileRecord.TYPE, extension),
|
|
822
|
+
...(urlDownload ? { urlDownload } : {}),
|
|
823
|
+
};
|
|
824
|
+
});
|
|
485
825
|
}
|
|
486
826
|
|
|
487
827
|
function extractReplyToMessageId(params: B24V2MessageParams | undefined): string | undefined {
|