@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
|
@@ -1,12 +1,7 @@
|
|
|
1
|
+
import { stringify as stringifyQueryString } from 'qs';
|
|
1
2
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
import { InboundHandler
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
// Import fixtures
|
|
6
|
-
import textFixture from './fixtures/onimbotmessageadd-text.json';
|
|
7
|
-
import fileFixture from './fixtures/onimbotmessageadd-file.json';
|
|
8
|
-
import joinFixture from './fixtures/onimbotjoinchat.json';
|
|
9
|
-
import commandFixture from './fixtures/onimcommandadd.json';
|
|
3
|
+
import { InboundHandler } from '../src/inbound-handler.js';
|
|
4
|
+
import type { B24MsgContext, FetchContext } from '../src/types.js';
|
|
10
5
|
|
|
11
6
|
const silentLogger = {
|
|
12
7
|
info: () => {},
|
|
@@ -15,6 +10,91 @@ const silentLogger = {
|
|
|
15
10
|
debug: () => {},
|
|
16
11
|
};
|
|
17
12
|
|
|
13
|
+
const fetchCtx: FetchContext = {
|
|
14
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
15
|
+
botId: 2081,
|
|
16
|
+
botToken: 'bot_token_test_abc123',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const baseBot = {
|
|
20
|
+
id: 2081,
|
|
21
|
+
code: 'openclaw',
|
|
22
|
+
type: 'bot',
|
|
23
|
+
isHidden: false,
|
|
24
|
+
isSupportOpenline: false,
|
|
25
|
+
isReactionsEnabled: true,
|
|
26
|
+
backgroundId: null,
|
|
27
|
+
language: 'ru',
|
|
28
|
+
eventMode: 'fetch',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const baseUser = {
|
|
32
|
+
id: 1,
|
|
33
|
+
active: true,
|
|
34
|
+
name: 'Test User',
|
|
35
|
+
firstName: 'Test',
|
|
36
|
+
lastName: 'User',
|
|
37
|
+
workPosition: 'Developer',
|
|
38
|
+
color: '#df532d',
|
|
39
|
+
avatar: '',
|
|
40
|
+
gender: 'M',
|
|
41
|
+
birthday: '',
|
|
42
|
+
extranet: false,
|
|
43
|
+
bot: false,
|
|
44
|
+
connector: false,
|
|
45
|
+
externalAuthId: 'default',
|
|
46
|
+
status: 'online',
|
|
47
|
+
idle: false,
|
|
48
|
+
lastActivityDate: false,
|
|
49
|
+
absent: false,
|
|
50
|
+
departments: [1],
|
|
51
|
+
phones: false,
|
|
52
|
+
type: 'employee',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function createFetchMessageEvent(overrides: Record<string, unknown> = {}) {
|
|
56
|
+
return {
|
|
57
|
+
eventId: 1001,
|
|
58
|
+
type: 'ONIMBOTV2MESSAGEADD',
|
|
59
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
60
|
+
data: {
|
|
61
|
+
bot: baseBot,
|
|
62
|
+
message: {
|
|
63
|
+
id: 490659,
|
|
64
|
+
chatId: 40985,
|
|
65
|
+
authorId: 1,
|
|
66
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
67
|
+
text: 'Hello, how are you?',
|
|
68
|
+
isSystem: false,
|
|
69
|
+
uuid: '',
|
|
70
|
+
forward: null,
|
|
71
|
+
params: {},
|
|
72
|
+
viewedByOthers: false,
|
|
73
|
+
},
|
|
74
|
+
chat: {
|
|
75
|
+
id: 40985,
|
|
76
|
+
dialogId: '1',
|
|
77
|
+
type: 'private',
|
|
78
|
+
name: 'Test User',
|
|
79
|
+
entityType: '',
|
|
80
|
+
owner: 1,
|
|
81
|
+
avatar: '',
|
|
82
|
+
color: '#ab7761',
|
|
83
|
+
},
|
|
84
|
+
user: baseUser,
|
|
85
|
+
language: 'ru',
|
|
86
|
+
...overrides,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createWebhookEvent(eventType: string, data: Record<string, unknown>) {
|
|
92
|
+
return {
|
|
93
|
+
event: eventType,
|
|
94
|
+
data,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
18
98
|
describe('InboundHandler', () => {
|
|
19
99
|
let handler: InboundHandler;
|
|
20
100
|
|
|
@@ -22,15 +102,15 @@ describe('InboundHandler', () => {
|
|
|
22
102
|
handler?.destroy();
|
|
23
103
|
});
|
|
24
104
|
|
|
25
|
-
it('dispatches
|
|
105
|
+
it('dispatches fetch message events', async () => {
|
|
26
106
|
const onMessage = vi.fn();
|
|
27
107
|
handler = new InboundHandler({
|
|
28
|
-
config: { dmPolicy: '
|
|
108
|
+
config: { dmPolicy: 'pairing' },
|
|
29
109
|
logger: silentLogger,
|
|
30
110
|
onMessage,
|
|
31
111
|
});
|
|
32
112
|
|
|
33
|
-
const result = await handler.
|
|
113
|
+
const result = await handler.handleFetchEvent(createFetchMessageEvent(), fetchCtx);
|
|
34
114
|
expect(result).toBe(true);
|
|
35
115
|
expect(onMessage).toHaveBeenCalledOnce();
|
|
36
116
|
|
|
@@ -39,125 +119,530 @@ describe('InboundHandler', () => {
|
|
|
39
119
|
expect(ctx.text).toBe('Hello, how are you?');
|
|
40
120
|
expect(ctx.senderId).toBe('1');
|
|
41
121
|
expect(ctx.senderName).toBe('Test User');
|
|
122
|
+
expect(ctx.senderFirstName).toBe('Test');
|
|
42
123
|
expect(ctx.chatId).toBe('1');
|
|
124
|
+
expect(ctx.chatInternalId).toBe('40985');
|
|
125
|
+
expect(ctx.messageId).toBe('490659');
|
|
126
|
+
expect(ctx.replyToMessageId).toBeUndefined();
|
|
127
|
+
expect(ctx.isForwarded).toBe(false);
|
|
43
128
|
expect(ctx.isDm).toBe(true);
|
|
44
129
|
expect(ctx.isGroup).toBe(false);
|
|
45
|
-
expect(ctx.
|
|
46
|
-
expect(ctx.
|
|
130
|
+
expect(ctx.media).toEqual([]);
|
|
131
|
+
expect(ctx.language).toBe('ru');
|
|
132
|
+
expect(ctx.botId).toBe(2081);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('keeps incoming dialogId for direct chats without replacing it by user id', async () => {
|
|
136
|
+
const onMessage = vi.fn();
|
|
137
|
+
handler = new InboundHandler({
|
|
138
|
+
config: { dmPolicy: 'pairing' },
|
|
139
|
+
logger: silentLogger,
|
|
140
|
+
onMessage,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const event = createFetchMessageEvent({
|
|
144
|
+
chat: {
|
|
145
|
+
id: 40985,
|
|
146
|
+
dialogId: '123',
|
|
147
|
+
type: 'private',
|
|
148
|
+
name: 'Test User',
|
|
149
|
+
entityType: '',
|
|
150
|
+
owner: 55,
|
|
151
|
+
avatar: '',
|
|
152
|
+
color: '#ab7761',
|
|
153
|
+
},
|
|
154
|
+
user: {
|
|
155
|
+
...baseUser,
|
|
156
|
+
id: 55,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
161
|
+
|
|
162
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
163
|
+
expect(ctx.chatId).toBe('123');
|
|
164
|
+
expect(ctx.senderId).toBe('55');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('deduplicates fetch messages by message id', async () => {
|
|
168
|
+
const onMessage = vi.fn();
|
|
169
|
+
handler = new InboundHandler({
|
|
170
|
+
config: { dmPolicy: 'pairing' },
|
|
171
|
+
logger: silentLogger,
|
|
172
|
+
onMessage,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const event = createFetchMessageEvent();
|
|
176
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
177
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
178
|
+
|
|
179
|
+
expect(onMessage).toHaveBeenCalledOnce();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('extracts media ids from V2 message params', async () => {
|
|
183
|
+
const onMessage = vi.fn();
|
|
184
|
+
handler = new InboundHandler({
|
|
185
|
+
config: { dmPolicy: 'pairing' },
|
|
186
|
+
logger: silentLogger,
|
|
187
|
+
onMessage,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const event = createFetchMessageEvent({
|
|
191
|
+
message: {
|
|
192
|
+
id: 492035,
|
|
193
|
+
chatId: 40985,
|
|
194
|
+
authorId: 1,
|
|
195
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
196
|
+
text: '',
|
|
197
|
+
isSystem: false,
|
|
198
|
+
uuid: '',
|
|
199
|
+
forward: null,
|
|
200
|
+
params: { FILE_ID: [94611, '94612'] },
|
|
201
|
+
viewedByOthers: false,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
206
|
+
|
|
207
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
208
|
+
expect(ctx.media).toEqual([
|
|
209
|
+
{
|
|
210
|
+
id: '94611',
|
|
211
|
+
name: 'file_94611',
|
|
212
|
+
extension: '',
|
|
213
|
+
size: 0,
|
|
214
|
+
type: 'file',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: '94612',
|
|
218
|
+
name: 'file_94612',
|
|
219
|
+
extension: '',
|
|
220
|
+
size: 0,
|
|
221
|
+
type: 'file',
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('extracts reply target id from V2 message params', async () => {
|
|
227
|
+
const onMessage = vi.fn();
|
|
228
|
+
handler = new InboundHandler({
|
|
229
|
+
config: { dmPolicy: 'pairing' },
|
|
230
|
+
logger: silentLogger,
|
|
231
|
+
onMessage,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const event = createFetchMessageEvent({
|
|
235
|
+
message: {
|
|
236
|
+
id: 492036,
|
|
237
|
+
chatId: 40985,
|
|
238
|
+
authorId: 1,
|
|
239
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
240
|
+
text: 'reply text',
|
|
241
|
+
isSystem: false,
|
|
242
|
+
uuid: '',
|
|
243
|
+
forward: null,
|
|
244
|
+
params: { REPLY_ID: '490600' },
|
|
245
|
+
viewedByOthers: false,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
250
|
+
|
|
251
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
252
|
+
expect(ctx.replyToMessageId).toBe('490600');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('marks forwarded V2 messages', async () => {
|
|
256
|
+
const onMessage = vi.fn();
|
|
257
|
+
handler = new InboundHandler({
|
|
258
|
+
config: { dmPolicy: 'pairing' },
|
|
259
|
+
logger: silentLogger,
|
|
260
|
+
onMessage,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const event = createFetchMessageEvent({
|
|
264
|
+
message: {
|
|
265
|
+
id: 492037,
|
|
266
|
+
chatId: 40985,
|
|
267
|
+
authorId: 1,
|
|
268
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
269
|
+
text: 'forwarded body',
|
|
270
|
+
isSystem: false,
|
|
271
|
+
uuid: '',
|
|
272
|
+
forward: { id: 'chat208/6717', userId: 1, chatType: 'chat' },
|
|
273
|
+
params: {},
|
|
274
|
+
viewedByOthers: false,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
279
|
+
|
|
280
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
281
|
+
expect(ctx.isForwarded).toBe(true);
|
|
47
282
|
});
|
|
48
283
|
|
|
49
|
-
it('
|
|
284
|
+
it('dispatches webhook message events from JSON body', async () => {
|
|
50
285
|
const onMessage = vi.fn();
|
|
51
286
|
handler = new InboundHandler({
|
|
52
|
-
config: { dmPolicy: '
|
|
287
|
+
config: { webhookUrl: fetchCtx.webhookUrl, botToken: fetchCtx.botToken, dmPolicy: 'pairing' },
|
|
53
288
|
logger: silentLogger,
|
|
54
289
|
onMessage,
|
|
55
290
|
});
|
|
56
291
|
|
|
57
|
-
|
|
58
|
-
|
|
292
|
+
const payload = createWebhookEvent('ONIMBOTV2MESSAGEADD', {
|
|
293
|
+
bot: {
|
|
294
|
+
id: '2081',
|
|
295
|
+
code: 'openclaw',
|
|
296
|
+
auth: {
|
|
297
|
+
access_token: 'bot_token_test_abc123',
|
|
298
|
+
refresh_token: 'refresh_token_test',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
message: {
|
|
302
|
+
id: '490659',
|
|
303
|
+
chatId: '40985',
|
|
304
|
+
authorId: '1',
|
|
305
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
306
|
+
text: 'Hello from webhook',
|
|
307
|
+
isSystem: '0',
|
|
308
|
+
uuid: '',
|
|
309
|
+
forward: '',
|
|
310
|
+
params: {},
|
|
311
|
+
viewedByOthers: '0',
|
|
312
|
+
},
|
|
313
|
+
chat: {
|
|
314
|
+
id: '40985',
|
|
315
|
+
dialogId: '1',
|
|
316
|
+
type: 'private',
|
|
317
|
+
name: 'Test User',
|
|
318
|
+
entityType: '',
|
|
319
|
+
owner: '1',
|
|
320
|
+
avatar: '',
|
|
321
|
+
color: '#ab7761',
|
|
322
|
+
},
|
|
323
|
+
user: {
|
|
324
|
+
...baseUser,
|
|
325
|
+
id: '1',
|
|
326
|
+
active: '1',
|
|
327
|
+
extranet: '0',
|
|
328
|
+
bot: '0',
|
|
329
|
+
connector: '0',
|
|
330
|
+
idle: '0',
|
|
331
|
+
lastActivityDate: '0',
|
|
332
|
+
absent: '0',
|
|
333
|
+
departments: ['1'],
|
|
334
|
+
phones: '0',
|
|
335
|
+
},
|
|
336
|
+
language: 'ru',
|
|
337
|
+
});
|
|
59
338
|
|
|
339
|
+
const result = await handler.handleWebhook(JSON.stringify(payload));
|
|
340
|
+
expect(result).toBe(true);
|
|
60
341
|
expect(onMessage).toHaveBeenCalledOnce();
|
|
342
|
+
|
|
343
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
344
|
+
expect(ctx.text).toBe('Hello from webhook');
|
|
345
|
+
expect(ctx.chatId).toBe('1');
|
|
346
|
+
expect(ctx.messageId).toBe('490659');
|
|
61
347
|
});
|
|
62
348
|
|
|
63
|
-
it('dispatches
|
|
349
|
+
it('dispatches webhook message events from query string body', async () => {
|
|
64
350
|
const onMessage = vi.fn();
|
|
65
351
|
handler = new InboundHandler({
|
|
66
|
-
config: {
|
|
352
|
+
config: { webhookUrl: fetchCtx.webhookUrl, botToken: fetchCtx.botToken, dmPolicy: 'pairing' },
|
|
67
353
|
logger: silentLogger,
|
|
68
354
|
onMessage,
|
|
69
355
|
});
|
|
70
356
|
|
|
71
|
-
|
|
72
|
-
|
|
357
|
+
const payload = createWebhookEvent('ONIMBOTV2MESSAGEADD', {
|
|
358
|
+
bot: {
|
|
359
|
+
id: '2081',
|
|
360
|
+
code: 'openclaw',
|
|
361
|
+
auth: {
|
|
362
|
+
access_token: 'bot_token_test_abc123',
|
|
363
|
+
refresh_token: 'refresh_token_test',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
message: {
|
|
367
|
+
id: '490660',
|
|
368
|
+
chatId: '40985',
|
|
369
|
+
authorId: '1',
|
|
370
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
371
|
+
text: 'Hello from qs',
|
|
372
|
+
isSystem: '0',
|
|
373
|
+
uuid: '',
|
|
374
|
+
forward: '',
|
|
375
|
+
params: {},
|
|
376
|
+
viewedByOthers: '0',
|
|
377
|
+
},
|
|
378
|
+
chat: {
|
|
379
|
+
id: '40985',
|
|
380
|
+
dialogId: '1',
|
|
381
|
+
type: 'private',
|
|
382
|
+
name: 'Test User',
|
|
383
|
+
entityType: '',
|
|
384
|
+
owner: '1',
|
|
385
|
+
avatar: '',
|
|
386
|
+
color: '#ab7761',
|
|
387
|
+
},
|
|
388
|
+
user: {
|
|
389
|
+
...baseUser,
|
|
390
|
+
id: '1',
|
|
391
|
+
active: '1',
|
|
392
|
+
extranet: '0',
|
|
393
|
+
bot: '0',
|
|
394
|
+
connector: '0',
|
|
395
|
+
idle: '0',
|
|
396
|
+
lastActivityDate: '0',
|
|
397
|
+
absent: '0',
|
|
398
|
+
departments: ['1'],
|
|
399
|
+
phones: '0',
|
|
400
|
+
},
|
|
401
|
+
language: 'ru',
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const result = await handler.handleWebhook(stringifyQueryString(payload));
|
|
405
|
+
expect(result).toBe(true);
|
|
73
406
|
expect(onMessage).toHaveBeenCalledOnce();
|
|
74
407
|
});
|
|
75
408
|
|
|
76
409
|
it('dispatches join chat events', async () => {
|
|
77
410
|
const onJoinChat = vi.fn();
|
|
78
411
|
handler = new InboundHandler({
|
|
79
|
-
config: { dmPolicy: '
|
|
412
|
+
config: { dmPolicy: 'pairing' },
|
|
80
413
|
logger: silentLogger,
|
|
81
414
|
onJoinChat,
|
|
82
415
|
});
|
|
83
416
|
|
|
84
|
-
const
|
|
417
|
+
const event = {
|
|
418
|
+
eventId: 1002,
|
|
419
|
+
type: 'ONIMBOTV2JOINCHAT',
|
|
420
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
421
|
+
data: {
|
|
422
|
+
bot: baseBot,
|
|
423
|
+
dialogId: '1',
|
|
424
|
+
chat: {
|
|
425
|
+
id: 40985,
|
|
426
|
+
dialogId: '1',
|
|
427
|
+
type: 'private',
|
|
428
|
+
name: 'Test User',
|
|
429
|
+
entityType: '',
|
|
430
|
+
owner: 1,
|
|
431
|
+
avatar: '',
|
|
432
|
+
color: '#ab7761',
|
|
433
|
+
},
|
|
434
|
+
user: baseUser,
|
|
435
|
+
language: 'ru',
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const result = await handler.handleFetchEvent(event, fetchCtx);
|
|
85
440
|
expect(result).toBe(true);
|
|
86
441
|
expect(onJoinChat).toHaveBeenCalledOnce();
|
|
442
|
+
expect(onJoinChat).toHaveBeenCalledWith({
|
|
443
|
+
dialogId: '1',
|
|
444
|
+
chatType: 'private',
|
|
445
|
+
language: 'ru',
|
|
446
|
+
fetchCtx,
|
|
447
|
+
});
|
|
87
448
|
});
|
|
88
449
|
|
|
89
450
|
it('dispatches command events', async () => {
|
|
90
451
|
const onCommand = vi.fn();
|
|
91
452
|
handler = new InboundHandler({
|
|
92
|
-
config: { dmPolicy: '
|
|
453
|
+
config: { dmPolicy: 'pairing' },
|
|
93
454
|
logger: silentLogger,
|
|
94
455
|
onCommand,
|
|
95
456
|
});
|
|
96
457
|
|
|
97
|
-
const
|
|
458
|
+
const event = {
|
|
459
|
+
eventId: 1003,
|
|
460
|
+
type: 'ONIMBOTV2COMMANDADD',
|
|
461
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
462
|
+
data: {
|
|
463
|
+
bot: baseBot,
|
|
464
|
+
command: {
|
|
465
|
+
id: 53,
|
|
466
|
+
command: '/help',
|
|
467
|
+
params: 'topic',
|
|
468
|
+
context: 'textarea',
|
|
469
|
+
},
|
|
470
|
+
message: {
|
|
471
|
+
id: 490667,
|
|
472
|
+
chatId: 40985,
|
|
473
|
+
authorId: 1,
|
|
474
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
475
|
+
text: '/help topic',
|
|
476
|
+
isSystem: false,
|
|
477
|
+
uuid: '',
|
|
478
|
+
forward: null,
|
|
479
|
+
params: {},
|
|
480
|
+
viewedByOthers: false,
|
|
481
|
+
},
|
|
482
|
+
chat: {
|
|
483
|
+
id: 40985,
|
|
484
|
+
dialogId: '1',
|
|
485
|
+
type: 'private',
|
|
486
|
+
name: 'Test User',
|
|
487
|
+
entityType: '',
|
|
488
|
+
owner: 1,
|
|
489
|
+
avatar: '',
|
|
490
|
+
color: '#ab7761',
|
|
491
|
+
},
|
|
492
|
+
user: baseUser,
|
|
493
|
+
language: 'ru',
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const result = await handler.handleFetchEvent(event, fetchCtx);
|
|
98
498
|
expect(result).toBe(true);
|
|
99
499
|
expect(onCommand).toHaveBeenCalledOnce();
|
|
500
|
+
expect(onCommand).toHaveBeenCalledWith({
|
|
501
|
+
commandId: 53,
|
|
502
|
+
commandName: 'help',
|
|
503
|
+
commandParams: 'topic',
|
|
504
|
+
commandText: '/help topic',
|
|
505
|
+
senderId: '1',
|
|
506
|
+
dialogId: '1',
|
|
507
|
+
chatType: 'P',
|
|
508
|
+
messageId: '490667',
|
|
509
|
+
language: 'ru',
|
|
510
|
+
fetchCtx,
|
|
511
|
+
});
|
|
100
512
|
});
|
|
101
513
|
|
|
102
|
-
it('
|
|
514
|
+
it('dispatches keyboard command events even when message payload is incomplete', async () => {
|
|
515
|
+
const onCommand = vi.fn();
|
|
103
516
|
handler = new InboundHandler({
|
|
104
|
-
config: { dmPolicy: '
|
|
517
|
+
config: { dmPolicy: 'pairing' },
|
|
105
518
|
logger: silentLogger,
|
|
519
|
+
onCommand,
|
|
106
520
|
});
|
|
107
521
|
|
|
108
|
-
const
|
|
109
|
-
|
|
522
|
+
const event = {
|
|
523
|
+
eventId: 1004,
|
|
524
|
+
type: 'ONIMBOTV2COMMANDADD',
|
|
525
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
526
|
+
data: {
|
|
527
|
+
bot: baseBot,
|
|
528
|
+
command: {
|
|
529
|
+
id: 54,
|
|
530
|
+
command: '/status',
|
|
531
|
+
context: 'keyboard',
|
|
532
|
+
},
|
|
533
|
+
chat: {
|
|
534
|
+
id: 40985,
|
|
535
|
+
dialogId: 1,
|
|
536
|
+
type: 'private',
|
|
537
|
+
name: 'Test User',
|
|
538
|
+
entityType: '',
|
|
539
|
+
owner: 1,
|
|
540
|
+
avatar: '',
|
|
541
|
+
color: '#ab7761',
|
|
542
|
+
},
|
|
543
|
+
user: baseUser,
|
|
544
|
+
language: 'ru',
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
549
|
+
expect(result).toBe(true);
|
|
550
|
+
expect(onCommand).toHaveBeenCalledOnce();
|
|
551
|
+
expect(onCommand).toHaveBeenCalledWith({
|
|
552
|
+
commandId: 54,
|
|
553
|
+
commandName: 'status',
|
|
554
|
+
commandParams: '',
|
|
555
|
+
commandText: '/status',
|
|
556
|
+
senderId: '1',
|
|
557
|
+
dialogId: '1',
|
|
558
|
+
chatType: 'P',
|
|
559
|
+
messageId: '1004',
|
|
560
|
+
language: 'ru',
|
|
561
|
+
fetchCtx,
|
|
562
|
+
});
|
|
110
563
|
});
|
|
111
564
|
|
|
112
|
-
it('
|
|
565
|
+
it('builds command text from command payload instead of message text', async () => {
|
|
566
|
+
const onCommand = vi.fn();
|
|
113
567
|
handler = new InboundHandler({
|
|
114
|
-
config: { dmPolicy: '
|
|
568
|
+
config: { dmPolicy: 'pairing' },
|
|
115
569
|
logger: silentLogger,
|
|
570
|
+
onCommand,
|
|
116
571
|
});
|
|
117
572
|
|
|
118
|
-
const
|
|
119
|
-
|
|
573
|
+
const event = {
|
|
574
|
+
eventId: 1005,
|
|
575
|
+
type: 'ONIMBOTV2COMMANDADD',
|
|
576
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
577
|
+
data: {
|
|
578
|
+
bot: baseBot,
|
|
579
|
+
command: {
|
|
580
|
+
id: 55,
|
|
581
|
+
command: '/status',
|
|
582
|
+
params: '',
|
|
583
|
+
context: 'keyboard',
|
|
584
|
+
},
|
|
585
|
+
message: {
|
|
586
|
+
id: 490668,
|
|
587
|
+
chatId: 40985,
|
|
588
|
+
authorId: 1,
|
|
589
|
+
date: '2026-03-18T08:00:00+02:00',
|
|
590
|
+
text: '/commands',
|
|
591
|
+
isSystem: false,
|
|
592
|
+
uuid: '',
|
|
593
|
+
forward: null,
|
|
594
|
+
params: {},
|
|
595
|
+
viewedByOthers: false,
|
|
596
|
+
},
|
|
597
|
+
chat: {
|
|
598
|
+
id: 40985,
|
|
599
|
+
dialogId: '1',
|
|
600
|
+
type: 'private',
|
|
601
|
+
name: 'Test User',
|
|
602
|
+
entityType: '',
|
|
603
|
+
owner: 1,
|
|
604
|
+
avatar: '',
|
|
605
|
+
color: '#ab7761',
|
|
606
|
+
},
|
|
607
|
+
user: baseUser,
|
|
608
|
+
language: 'ru',
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
613
|
+
expect(result).toBe(true);
|
|
614
|
+
expect(onCommand).toHaveBeenCalledOnce();
|
|
615
|
+
expect(onCommand).toHaveBeenCalledWith({
|
|
616
|
+
commandId: 55,
|
|
617
|
+
commandName: 'status',
|
|
618
|
+
commandParams: '',
|
|
619
|
+
commandText: '/status',
|
|
620
|
+
senderId: '1',
|
|
621
|
+
dialogId: '1',
|
|
622
|
+
chatType: 'P',
|
|
623
|
+
messageId: '490668',
|
|
624
|
+
language: 'ru',
|
|
625
|
+
fetchCtx,
|
|
626
|
+
});
|
|
120
627
|
});
|
|
121
|
-
});
|
|
122
628
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
629
|
+
it('returns true for unknown event types to acknowledge delivery', async () => {
|
|
630
|
+
handler = new InboundHandler({
|
|
631
|
+
config: { dmPolicy: 'pairing' },
|
|
632
|
+
logger: silentLogger,
|
|
633
|
+
});
|
|
128
634
|
|
|
129
|
-
|
|
130
|
-
expect(
|
|
131
|
-
expect(ctx.senderName).toBe('Test User');
|
|
132
|
-
expect(ctx.senderFirstName).toBe('Test');
|
|
133
|
-
expect(ctx.chatId).toBe('1');
|
|
134
|
-
expect(ctx.chatInternalId).toBe('40985');
|
|
135
|
-
expect(ctx.messageId).toBe('490659');
|
|
136
|
-
expect(ctx.text).toBe('Hello, how are you?');
|
|
137
|
-
expect(ctx.isDm).toBe(true);
|
|
138
|
-
expect(ctx.isGroup).toBe(false);
|
|
139
|
-
expect(ctx.media).toEqual([]);
|
|
140
|
-
expect(ctx.platform).toBe('web');
|
|
141
|
-
expect(ctx.language).toBe('ru');
|
|
142
|
-
expect(ctx.botId).toBe(2081);
|
|
143
|
-
expect(ctx.memberId).toBe('test_member_id_123');
|
|
144
|
-
expect(ctx.clientEndpoint).toBe('https://test.bitrix24.com/rest/');
|
|
635
|
+
const result = await handler.handleWebhook({ event: 'UNKNOWN_EVENT', data: {} });
|
|
636
|
+
expect(result).toBe(true);
|
|
145
637
|
});
|
|
146
638
|
|
|
147
|
-
it('
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
expect(ctx.text).toBe('');
|
|
153
|
-
expect(ctx.media).toHaveLength(1);
|
|
154
|
-
expect(ctx.media[0]).toEqual({
|
|
155
|
-
id: '94611',
|
|
156
|
-
name: 'document.txt',
|
|
157
|
-
extension: 'txt',
|
|
158
|
-
size: 101,
|
|
159
|
-
type: 'file',
|
|
160
|
-
urlDownload: 'https://test.bitrix24.com/disk/downloadFile/94611/',
|
|
639
|
+
it('returns false for missing event type', async () => {
|
|
640
|
+
handler = new InboundHandler({
|
|
641
|
+
config: { dmPolicy: 'pairing' },
|
|
642
|
+
logger: silentLogger,
|
|
161
643
|
});
|
|
644
|
+
|
|
645
|
+
const result = await handler.handleWebhook({});
|
|
646
|
+
expect(result).toBe(false);
|
|
162
647
|
});
|
|
163
648
|
});
|