@ihazz/bitrix24 0.2.4 → 1.0.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 +118 -164
- package/index.ts +46 -11
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/bitrix24/SKILL.md +70 -0
- package/src/access-control.ts +102 -46
- package/src/api.ts +434 -232
- package/src/channel.ts +1486 -393
- package/src/commands.ts +169 -31
- package/src/config-schema.ts +8 -3
- package/src/config.ts +11 -0
- package/src/dedup.ts +4 -0
- package/src/i18n.ts +127 -0
- package/src/inbound-handler.ts +306 -110
- package/src/media-service.ts +218 -65
- package/src/message-utils.ts +252 -10
- package/src/polling-service.ts +240 -0
- package/src/rate-limiter.ts +11 -6
- package/src/send-service.ts +140 -60
- package/src/types.ts +279 -185
- package/src/utils.ts +54 -3
- package/tests/access-control.test.ts +174 -58
- package/tests/api.test.ts +95 -0
- package/tests/channel.test.ts +279 -61
- package/tests/commands.test.ts +57 -0
- package/tests/config.test.ts +5 -1
- package/tests/i18n.test.ts +47 -0
- package/tests/inbound-handler.test.ts +554 -69
- package/tests/index.test.ts +94 -0
- package/tests/media-service.test.ts +146 -51
- package/tests/message-utils.test.ts +64 -0
- package/tests/polling-service.test.ts +77 -0
- package/tests/rate-limiter.test.ts +2 -2
- package/tests/send-service.test.ts +145 -0
package/src/inbound-handler.ts
CHANGED
|
@@ -1,34 +1,53 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parse as parseQueryString } from 'qs';
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
B24V2FetchEventItem,
|
|
4
|
+
B24V2WebhookEvent,
|
|
5
|
+
B24V2MessageEventData,
|
|
6
|
+
B24V2JoinChatEventData,
|
|
7
|
+
B24V2CommandEventData,
|
|
8
|
+
B24V2DeleteEventData,
|
|
9
|
+
B24V2Message,
|
|
10
10
|
B24MsgContext,
|
|
11
11
|
B24MediaItem,
|
|
12
|
+
FetchContext,
|
|
13
|
+
Bitrix24AccountConfig,
|
|
14
|
+
Logger,
|
|
12
15
|
} from './types.js';
|
|
13
16
|
import { Dedup } from './dedup.js';
|
|
14
|
-
import type { Bitrix24AccountConfig } from './types.js';
|
|
15
17
|
import { defaultLogger } from './utils.js';
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/** Normalized fetch command context passed to onFetchCommand callback. */
|
|
20
|
+
export interface FetchCommandContext {
|
|
21
|
+
commandId: number;
|
|
22
|
+
commandName: string;
|
|
23
|
+
commandParams: string;
|
|
24
|
+
commandText: string;
|
|
25
|
+
senderId: string;
|
|
26
|
+
dialogId: string;
|
|
27
|
+
chatType: string;
|
|
28
|
+
messageId: string;
|
|
29
|
+
language?: string;
|
|
30
|
+
fetchCtx: FetchContext;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Normalized fetch join chat context */
|
|
34
|
+
export interface FetchJoinChatContext {
|
|
35
|
+
dialogId: string;
|
|
36
|
+
chatType: string;
|
|
37
|
+
language?: string;
|
|
38
|
+
fetchCtx: FetchContext;
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
export interface InboundHandlerOptions {
|
|
25
42
|
config: Bitrix24AccountConfig;
|
|
26
43
|
logger?: Logger;
|
|
27
44
|
onMessage?: (ctx: B24MsgContext) => void | Promise<void>;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
/** Called when bot is invited to a chat (FETCH or webhook). */
|
|
46
|
+
onJoinChat?: (ctx: FetchJoinChatContext) => void | Promise<void>;
|
|
47
|
+
/** Called for a slash command. */
|
|
48
|
+
onCommand?: (cmdCtx: FetchCommandContext) => void | Promise<void>;
|
|
49
|
+
/** Called when bot is deleted. */
|
|
50
|
+
onBotDelete?: (data: B24V2DeleteEventData) => void | Promise<void>;
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
export class InboundHandler {
|
|
@@ -36,10 +55,9 @@ export class InboundHandler {
|
|
|
36
55
|
private config: Bitrix24AccountConfig;
|
|
37
56
|
private logger: Logger;
|
|
38
57
|
private onMessage?: (ctx: B24MsgContext) => void | Promise<void>;
|
|
39
|
-
private onJoinChat?: (
|
|
40
|
-
private onCommand?: (
|
|
41
|
-
private
|
|
42
|
-
private onBotDelete?: (event: B24Event) => void | Promise<void>;
|
|
58
|
+
private onJoinChat?: (ctx: FetchJoinChatContext) => void | Promise<void>;
|
|
59
|
+
private onCommand?: (cmdCtx: FetchCommandContext) => void | Promise<void>;
|
|
60
|
+
private onBotDelete?: (data: B24V2DeleteEventData) => void | Promise<void>;
|
|
43
61
|
|
|
44
62
|
constructor(opts: InboundHandlerOptions) {
|
|
45
63
|
this.dedup = new Dedup();
|
|
@@ -48,132 +66,310 @@ export class InboundHandler {
|
|
|
48
66
|
this.onMessage = opts.onMessage;
|
|
49
67
|
this.onJoinChat = opts.onJoinChat;
|
|
50
68
|
this.onCommand = opts.onCommand;
|
|
51
|
-
this.onAppInstall = opts.onAppInstall;
|
|
52
69
|
this.onBotDelete = opts.onBotDelete;
|
|
53
70
|
}
|
|
54
71
|
|
|
72
|
+
// ─── V2 FETCH mode event handling ───────────────────────────────────────
|
|
73
|
+
|
|
55
74
|
/**
|
|
56
|
-
* Handle a
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* @param rawBody - Either a parsed object or a URL-encoded string
|
|
60
|
-
* @returns true if the event was handled
|
|
75
|
+
* Handle a single event item from imbot.v2.Event.get (FETCH mode).
|
|
76
|
+
* V2 events have camelCase fields and nested bot/message/chat/user objects.
|
|
61
77
|
*/
|
|
62
|
-
async
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
: rawBody;
|
|
78
|
+
async handleFetchEvent(item: B24V2FetchEventItem, fetchCtx: FetchContext): Promise<boolean> {
|
|
79
|
+
const eventType = item.type;
|
|
80
|
+
this.logger.debug(`Fetch event: ${eventType} (id=${item.eventId})`);
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
switch (eventType) {
|
|
83
|
+
case 'ONIMBOTV2MESSAGEADD':
|
|
84
|
+
return this.handleV2Message(item, fetchCtx);
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
86
|
+
case 'ONIMBOTV2JOINCHAT':
|
|
87
|
+
return this.handleV2JoinChat(item, fetchCtx);
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
case 'ONIMBOTV2COMMANDADD':
|
|
90
|
+
return this.handleV2Command(item, fetchCtx);
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return this.handleMessage(event as B24MessageEvent);
|
|
80
|
-
case 'ONIMBOTJOINCHAT':
|
|
81
|
-
await this.onJoinChat?.(event as B24JoinChatEvent);
|
|
82
|
-
return true;
|
|
83
|
-
case 'ONIMCOMMANDADD':
|
|
84
|
-
await this.onCommand?.(event as B24CommandEvent);
|
|
92
|
+
case 'ONIMBOTV2DELETE':
|
|
93
|
+
await this.onBotDelete?.(item.data as B24V2DeleteEventData);
|
|
85
94
|
return true;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
case '
|
|
90
|
-
|
|
95
|
+
|
|
96
|
+
case 'ONIMBOTV2MESSAGEUPDATE':
|
|
97
|
+
case 'ONIMBOTV2MESSAGEDELETE':
|
|
98
|
+
case 'ONIMBOTV2CONTEXTGET':
|
|
99
|
+
case 'ONIMBOTV2REACTIONCHANGE':
|
|
100
|
+
this.logger.debug(`Fetch: skipping ${eventType} (not handled)`);
|
|
91
101
|
return true;
|
|
102
|
+
|
|
92
103
|
default:
|
|
93
|
-
this.logger.debug(`
|
|
94
|
-
return
|
|
104
|
+
this.logger.debug(`Fetch: unhandled event type ${eventType}`);
|
|
105
|
+
return true;
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
/**
|
|
99
|
-
* Handle
|
|
110
|
+
* Handle V2 ONIMBOTV2MESSAGEADD event.
|
|
111
|
+
* V2 data: { bot, message, chat, user, language }
|
|
100
112
|
*/
|
|
101
|
-
private async
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
private async handleV2Message(
|
|
114
|
+
item: B24V2FetchEventItem,
|
|
115
|
+
fetchCtx: FetchContext,
|
|
116
|
+
): Promise<boolean> {
|
|
117
|
+
const data = item.data as B24V2MessageEventData;
|
|
118
|
+
|
|
119
|
+
// Runtime guard: ensure essential V2 data fields exist
|
|
120
|
+
if (!data?.message || !data?.user) {
|
|
121
|
+
this.logger.warn('Fetch: MESSAGEADD event missing message or user data, skipping', { eventId: item.eventId });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const rawMessageId = data.message.id ?? item.eventId;
|
|
126
|
+
if (rawMessageId == null) {
|
|
127
|
+
this.logger.warn('Fetch: MESSAGEADD event missing both message.id and eventId, skipping');
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const messageId = String(rawMessageId);
|
|
104
131
|
|
|
105
|
-
// Dedup check
|
|
106
132
|
if (this.dedup.isDuplicate(messageId)) {
|
|
107
|
-
this.logger.debug(`
|
|
133
|
+
this.logger.debug(`Fetch: duplicate message ${messageId}, skipping`);
|
|
108
134
|
return true;
|
|
109
135
|
}
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return false;
|
|
137
|
+
const dialogId = String(data.chat?.dialogId ?? data.message.chatId ?? data.user.id ?? '');
|
|
138
|
+
if (!dialogId) {
|
|
139
|
+
this.logger.warn('Fetch: message event has no dialogId, skipping');
|
|
140
|
+
return true;
|
|
116
141
|
}
|
|
142
|
+
const isP2P = !dialogId.startsWith('chat');
|
|
143
|
+
|
|
144
|
+
// Extract file attachments from message params
|
|
145
|
+
const media = extractFilesFromParams(data.message.params);
|
|
117
146
|
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
this.logger.info('Fetch: message payload', {
|
|
148
|
+
eventId: item.eventId,
|
|
149
|
+
messageId,
|
|
150
|
+
dialogId,
|
|
151
|
+
chatId: data.message.chatId,
|
|
152
|
+
forward: data.message.forward,
|
|
153
|
+
params: data.message.params,
|
|
154
|
+
text: data.message.text,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const ctx: B24MsgContext = {
|
|
158
|
+
channel: 'bitrix24',
|
|
159
|
+
senderId: String(data.user?.id ?? dialogId),
|
|
160
|
+
senderName: data.user?.name ?? dialogId,
|
|
161
|
+
senderFirstName: data.user?.firstName,
|
|
162
|
+
chatId: dialogId,
|
|
163
|
+
chatInternalId: String(data.message?.chatId ?? dialogId),
|
|
164
|
+
messageId,
|
|
165
|
+
replyToMessageId: extractReplyToMessageId(data.message.params),
|
|
166
|
+
isForwarded: isForwardedMessage(data.message.forward),
|
|
167
|
+
text: data.message?.text ?? '',
|
|
168
|
+
isDm: isP2P,
|
|
169
|
+
isGroup: !isP2P,
|
|
170
|
+
media,
|
|
171
|
+
language: data.language,
|
|
172
|
+
raw: item,
|
|
173
|
+
botId: fetchCtx.botId,
|
|
174
|
+
memberId: '',
|
|
175
|
+
};
|
|
120
176
|
|
|
121
|
-
// Dispatch to handler
|
|
122
177
|
await this.onMessage?.(ctx);
|
|
123
178
|
return true;
|
|
124
179
|
}
|
|
125
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Handle V2 ONIMBOTV2JOINCHAT event.
|
|
183
|
+
*/
|
|
184
|
+
private async handleV2JoinChat(
|
|
185
|
+
item: B24V2FetchEventItem,
|
|
186
|
+
fetchCtx: FetchContext,
|
|
187
|
+
): Promise<boolean> {
|
|
188
|
+
const data = item.data as B24V2JoinChatEventData;
|
|
189
|
+
|
|
190
|
+
if (!data?.chat || !data?.dialogId) {
|
|
191
|
+
this.logger.warn('Fetch: JOINCHAT event missing chat or dialogId, skipping', { eventId: item.eventId });
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const joinCtx: FetchJoinChatContext = {
|
|
196
|
+
dialogId: data.dialogId,
|
|
197
|
+
chatType: data.chat.type,
|
|
198
|
+
language: data.language,
|
|
199
|
+
fetchCtx,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
await this.onJoinChat?.(joinCtx);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Handle V2 ONIMBOTV2COMMANDADD event.
|
|
208
|
+
* V2 data: { bot, command, message, chat, user, language }
|
|
209
|
+
*/
|
|
210
|
+
private async handleV2Command(
|
|
211
|
+
item: B24V2FetchEventItem,
|
|
212
|
+
fetchCtx: FetchContext,
|
|
213
|
+
): Promise<boolean> {
|
|
214
|
+
const data = item.data as B24V2CommandEventData;
|
|
215
|
+
|
|
216
|
+
const rawCommand = typeof data.command?.command === 'string' ? data.command.command : '';
|
|
217
|
+
const commandName = rawCommand.replace(/^\//, '');
|
|
218
|
+
if (!commandName) {
|
|
219
|
+
this.logger.warn('Fetch: command event has no command name, skipping');
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const commandId = Number(data.command?.id ?? 0);
|
|
223
|
+
if (!Number.isFinite(commandId) || commandId <= 0) {
|
|
224
|
+
this.logger.warn('Fetch: command event has no commandId, skipping');
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const commandParams = typeof data.command?.params === 'string'
|
|
229
|
+
? data.command.params.trim()
|
|
230
|
+
: '';
|
|
231
|
+
const commandText = commandParams
|
|
232
|
+
? `/${commandName} ${commandParams}`
|
|
233
|
+
: `/${commandName}`;
|
|
234
|
+
|
|
235
|
+
const dialogId = String(data.chat?.dialogId ?? data.message?.chatId ?? data.user?.id ?? '');
|
|
236
|
+
if (!dialogId) {
|
|
237
|
+
this.logger.warn('Fetch: command event has no dialogId, skipping');
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const isP2P = !dialogId.startsWith('chat');
|
|
241
|
+
|
|
242
|
+
const cmdCtx: FetchCommandContext = {
|
|
243
|
+
commandId,
|
|
244
|
+
commandName,
|
|
245
|
+
commandParams,
|
|
246
|
+
commandText,
|
|
247
|
+
senderId: String(data.user?.id ?? dialogId),
|
|
248
|
+
dialogId,
|
|
249
|
+
chatType: isP2P ? 'P' : String(data.chat?.type ?? ''),
|
|
250
|
+
messageId: String(data.message?.id ?? item.eventId),
|
|
251
|
+
language: data.language,
|
|
252
|
+
fetchCtx,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
await this.onCommand?.(cmdCtx);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── V2 Webhook mode event handling ─────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Handle an incoming V2 webhook POST body.
|
|
263
|
+
* V2 webhooks deliver JSON with { event, data } structure.
|
|
264
|
+
*/
|
|
265
|
+
async handleWebhook(rawBody: string | Record<string, unknown>): Promise<boolean> {
|
|
266
|
+
let payload: B24V2WebhookEvent;
|
|
267
|
+
|
|
268
|
+
if (typeof rawBody === 'string') {
|
|
269
|
+
try {
|
|
270
|
+
payload = JSON.parse(rawBody) as B24V2WebhookEvent;
|
|
271
|
+
} catch {
|
|
272
|
+
const parsedBody = parseQueryString(rawBody, {
|
|
273
|
+
allowDots: true,
|
|
274
|
+
depth: 10,
|
|
275
|
+
parseArrays: true,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (!parsedBody || typeof parsedBody !== 'object' || !('event' in parsedBody)) {
|
|
279
|
+
this.logger.warn('Failed to parse V2 webhook body');
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
payload = parsedBody as unknown as B24V2WebhookEvent;
|
|
284
|
+
}
|
|
285
|
+
} else if (rawBody && typeof rawBody === 'object' && 'event' in rawBody) {
|
|
286
|
+
payload = rawBody as unknown as B24V2WebhookEvent;
|
|
287
|
+
} else {
|
|
288
|
+
this.logger.warn('Webhook body missing "event" field', { type: typeof rawBody });
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const eventType = payload.event;
|
|
293
|
+
if (!eventType) {
|
|
294
|
+
this.logger.warn('Received webhook without event type', payload);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.logger.debug(`Received V2 webhook event: ${eventType}`);
|
|
299
|
+
|
|
300
|
+
// Convert webhook event to fetch event format and reuse handlers
|
|
301
|
+
// V2 webhook data has same structure as fetch events
|
|
302
|
+
const syntheticItem: B24V2FetchEventItem = {
|
|
303
|
+
eventId: 0,
|
|
304
|
+
type: eventType,
|
|
305
|
+
date: new Date().toISOString(),
|
|
306
|
+
data: payload.data,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// For webhook mode we need botId from the bot object in the event
|
|
310
|
+
const botData = payload.data as { bot?: { id?: number } };
|
|
311
|
+
const botId = botData?.bot?.id ?? 0;
|
|
312
|
+
|
|
313
|
+
const fetchCtx: FetchContext = {
|
|
314
|
+
webhookUrl: this.config.webhookUrl ?? '',
|
|
315
|
+
botId,
|
|
316
|
+
botToken: this.config.botToken ?? '',
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return this.handleFetchEvent(syntheticItem, fetchCtx);
|
|
320
|
+
}
|
|
321
|
+
|
|
126
322
|
destroy(): void {
|
|
127
323
|
this.dedup.destroy();
|
|
128
324
|
}
|
|
129
325
|
}
|
|
130
326
|
|
|
131
|
-
// ───
|
|
327
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
132
328
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Extract media items from V2 message params.
|
|
331
|
+
* In V2, file info may come in params.FILE_ID array.
|
|
332
|
+
* File details are available via imbot.v2.File.download.
|
|
333
|
+
*/
|
|
334
|
+
function extractFilesFromParams(params: Record<string, unknown>): B24MediaItem[] {
|
|
335
|
+
// V2 events may include file IDs in params (array or single value)
|
|
336
|
+
const rawFileIds = params?.FILE_ID;
|
|
337
|
+
if (rawFileIds == null) return [];
|
|
338
|
+
const fileIds = Array.isArray(rawFileIds) ? rawFileIds : [rawFileIds];
|
|
339
|
+
if (!fileIds.length) return [];
|
|
340
|
+
|
|
341
|
+
return fileIds.map((id) => ({
|
|
342
|
+
id: String(id),
|
|
343
|
+
name: `file_${id}`,
|
|
344
|
+
extension: '',
|
|
345
|
+
size: 0,
|
|
346
|
+
type: 'file' as const,
|
|
347
|
+
}));
|
|
136
348
|
}
|
|
137
349
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
chatInternalId: String(params.TO_CHAT_ID),
|
|
152
|
-
messageId: String(params.MESSAGE_ID),
|
|
153
|
-
text: params.MESSAGE || '',
|
|
154
|
-
isDm: params.CHAT_TYPE === 'P',
|
|
155
|
-
isGroup: params.CHAT_TYPE !== 'P',
|
|
156
|
-
media: normalizeFiles(params.FILES),
|
|
157
|
-
platform: params.PLATFORM_CONTEXT,
|
|
158
|
-
language: params.LANGUAGE,
|
|
159
|
-
raw: event,
|
|
160
|
-
botToken: botEntry.access_token,
|
|
161
|
-
userToken: event.auth.access_token,
|
|
162
|
-
clientEndpoint: botEntry.client_endpoint,
|
|
163
|
-
botId: botEntry.BOT_ID,
|
|
164
|
-
memberId: event.auth.member_id,
|
|
165
|
-
};
|
|
350
|
+
function extractReplyToMessageId(params: Record<string, unknown>): string | undefined {
|
|
351
|
+
const rawReplyId = params?.REPLY_ID;
|
|
352
|
+
if (rawReplyId == null) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (Array.isArray(rawReplyId)) {
|
|
357
|
+
const firstReplyId = rawReplyId.find((value) => value != null && String(value).trim() !== '');
|
|
358
|
+
return firstReplyId == null ? undefined : String(firstReplyId).trim();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const normalizedReplyId = String(rawReplyId).trim();
|
|
362
|
+
return normalizedReplyId.length > 0 ? normalizedReplyId : undefined;
|
|
166
363
|
}
|
|
167
364
|
|
|
168
|
-
function
|
|
169
|
-
if (
|
|
365
|
+
function isForwardedMessage(forward: unknown): boolean {
|
|
366
|
+
if (forward == null) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
170
369
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
type: file.image ? 'image' as const : 'file' as const,
|
|
177
|
-
urlDownload: file.urlDownload || undefined,
|
|
178
|
-
}));
|
|
370
|
+
if (typeof forward === 'string') {
|
|
371
|
+
return forward.trim().length > 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return true;
|
|
179
375
|
}
|