@ihazz/bitrix24 1.0.3 → 1.1.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 +236 -42
- package/package.json +1 -1
- package/src/access-control.ts +31 -8
- package/src/api.ts +76 -7
- package/src/channel.ts +1025 -137
- package/src/commands.ts +7 -7
- package/src/config-schema.ts +24 -1
- package/src/config.ts +4 -2
- package/src/group-access.ts +279 -0
- package/src/history-cache.ts +122 -0
- package/src/i18n.ts +140 -50
- package/src/inbound-handler.ts +91 -8
- package/src/polling-service.ts +4 -0
- package/src/send-service.ts +14 -5
- package/src/types.ts +67 -3
- package/tests/access-control.test.ts +43 -0
- package/tests/api.test.ts +131 -0
- package/tests/channel-flow.test.ts +1692 -0
- package/tests/channel.test.ts +88 -2
- package/tests/config.test.ts +120 -0
- package/tests/group-access.test.ts +340 -0
- package/tests/history-cache.test.ts +117 -0
- package/tests/i18n.test.ts +55 -12
- package/tests/inbound-handler.test.ts +388 -3
- package/tests/polling-service.test.ts +38 -0
- package/tests/send-service.test.ts +17 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { HistoryCache } from '../src/history-cache.js';
|
|
3
|
+
|
|
4
|
+
describe('HistoryCache', () => {
|
|
5
|
+
it('keeps only the newest entries up to the per-key limit', () => {
|
|
6
|
+
const cache = new HistoryCache();
|
|
7
|
+
|
|
8
|
+
cache.append({
|
|
9
|
+
key: 'chat:1',
|
|
10
|
+
limit: 2,
|
|
11
|
+
entry: { messageId: '1', sender: 'A', body: 'first' },
|
|
12
|
+
});
|
|
13
|
+
cache.append({
|
|
14
|
+
key: 'chat:1',
|
|
15
|
+
limit: 2,
|
|
16
|
+
entry: { messageId: '2', sender: 'A', body: 'second' },
|
|
17
|
+
});
|
|
18
|
+
cache.append({
|
|
19
|
+
key: 'chat:1',
|
|
20
|
+
limit: 2,
|
|
21
|
+
entry: { messageId: '3', sender: 'A', body: 'third' },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(cache.get('chat:1')).toEqual([
|
|
25
|
+
{ messageId: '2', sender: 'A', body: 'second' },
|
|
26
|
+
{ messageId: '3', sender: 'A', body: 'third' },
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('finds entries by message id', () => {
|
|
31
|
+
const cache = new HistoryCache();
|
|
32
|
+
cache.append({
|
|
33
|
+
key: 'chat:1',
|
|
34
|
+
limit: 10,
|
|
35
|
+
entry: { messageId: '55', sender: 'A', body: 'hello' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(cache.findByMessageId('chat:1', '55')).toMatchObject({
|
|
39
|
+
messageId: '55',
|
|
40
|
+
body: 'hello',
|
|
41
|
+
});
|
|
42
|
+
expect(cache.findByMessageId('chat:1', '99')).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('evicts oldest keys when cache exceeds maxKeys', () => {
|
|
46
|
+
const cache = new HistoryCache({ maxKeys: 2 });
|
|
47
|
+
cache.append({
|
|
48
|
+
key: 'chat:1',
|
|
49
|
+
limit: 10,
|
|
50
|
+
entry: { messageId: '1', sender: 'A', body: 'one' },
|
|
51
|
+
});
|
|
52
|
+
cache.append({
|
|
53
|
+
key: 'chat:2',
|
|
54
|
+
limit: 10,
|
|
55
|
+
entry: { messageId: '2', sender: 'B', body: 'two' },
|
|
56
|
+
});
|
|
57
|
+
cache.append({
|
|
58
|
+
key: 'chat:3',
|
|
59
|
+
limit: 10,
|
|
60
|
+
entry: { messageId: '3', sender: 'C', body: 'three' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(cache.size()).toBe(2);
|
|
64
|
+
expect(cache.get('chat:1')).toEqual([]);
|
|
65
|
+
expect(cache.get('chat:2')).toHaveLength(1);
|
|
66
|
+
expect(cache.get('chat:3')).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('stores and lists conversation metadata for history keys', () => {
|
|
70
|
+
const cache = new HistoryCache();
|
|
71
|
+
|
|
72
|
+
cache.append({
|
|
73
|
+
key: 'default:chat520',
|
|
74
|
+
limit: 10,
|
|
75
|
+
entry: { messageId: '1', sender: 'Alice', body: 'hello' },
|
|
76
|
+
meta: {
|
|
77
|
+
dialogId: 'chat520',
|
|
78
|
+
chatId: '520',
|
|
79
|
+
chatName: 'Чат зеленый 17',
|
|
80
|
+
chatType: 'chat',
|
|
81
|
+
isGroup: true,
|
|
82
|
+
lastActivityAt: 1,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(cache.listConversations()).toEqual([
|
|
87
|
+
{
|
|
88
|
+
key: 'default:chat520',
|
|
89
|
+
dialogId: 'chat520',
|
|
90
|
+
chatId: '520',
|
|
91
|
+
chatName: 'Чат зеленый 17',
|
|
92
|
+
chatType: 'chat',
|
|
93
|
+
isGroup: true,
|
|
94
|
+
lastActivityAt: 1,
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('does not append duplicate entries with the same message id to one history key', () => {
|
|
100
|
+
const cache = new HistoryCache();
|
|
101
|
+
|
|
102
|
+
cache.append({
|
|
103
|
+
key: 'chat:1',
|
|
104
|
+
limit: 10,
|
|
105
|
+
entry: { messageId: '55', sender: 'A', body: 'hello', eventScope: 'bot' },
|
|
106
|
+
});
|
|
107
|
+
cache.append({
|
|
108
|
+
key: 'chat:1',
|
|
109
|
+
limit: 10,
|
|
110
|
+
entry: { messageId: '55', sender: 'A', body: 'hello', eventScope: 'user' },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(cache.get('chat:1')).toEqual([
|
|
114
|
+
{ messageId: '55', sender: 'A', body: 'hello', eventScope: 'bot' },
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/tests/i18n.test.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
+
accessApproved,
|
|
4
|
+
accessDenied,
|
|
5
|
+
groupPairingPending,
|
|
3
6
|
resolveLocale,
|
|
4
7
|
welcomeMessage,
|
|
5
8
|
onboardingMessage,
|
|
9
|
+
ownerAndAllowedUsersOnly,
|
|
6
10
|
personalBotOwnerOnly,
|
|
7
|
-
|
|
8
|
-
replyMessageUnsupported,
|
|
11
|
+
watchOwnerDmNotice,
|
|
9
12
|
} from '../src/i18n.js';
|
|
10
13
|
|
|
11
14
|
describe('i18n welcome messages', () => {
|
|
@@ -16,12 +19,12 @@ describe('i18n welcome messages', () => {
|
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
it('returns localized Russian welcome message', () => {
|
|
19
|
-
expect(welcomeMessage('ru', 'OpenClaw')).toContain('OpenClaw готов');
|
|
22
|
+
expect(welcomeMessage('ru', 'OpenClaw')).toContain('OpenClaw готов к работе');
|
|
20
23
|
});
|
|
21
24
|
|
|
22
25
|
it('falls back to English welcome message for unknown locale', () => {
|
|
23
26
|
expect(welcomeMessage('zz', 'OpenClaw')).toBe(
|
|
24
|
-
'OpenClaw ready. Send
|
|
27
|
+
'OpenClaw is ready. Send a message or choose a command below.',
|
|
25
28
|
);
|
|
26
29
|
});
|
|
27
30
|
|
|
@@ -31,17 +34,57 @@ describe('i18n welcome messages', () => {
|
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
it('returns localized personal bot access denied message', () => {
|
|
34
|
-
expect(personalBotOwnerOnly('ru')).toContain('
|
|
35
|
-
expect(personalBotOwnerOnly('en')).toContain('
|
|
37
|
+
expect(personalBotOwnerOnly('ru')).toContain('Писать ему может только владелец');
|
|
38
|
+
expect(personalBotOwnerOnly('en')).toContain('Only the bot owner can message it');
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
it('returns localized
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
41
|
+
it('returns localized access approved message', () => {
|
|
42
|
+
expect(accessApproved('ru')).toBe('Доступ к боту подтвержден.');
|
|
43
|
+
expect(accessApproved('en')).toBe('Access to the bot has been approved.');
|
|
41
44
|
});
|
|
42
45
|
|
|
43
|
-
it('returns localized
|
|
44
|
-
expect(
|
|
45
|
-
expect(
|
|
46
|
+
it('returns localized owner and allowlist access denied message', () => {
|
|
47
|
+
expect(ownerAndAllowedUsersOnly('ru')).toContain('пользователям с подтвержденным доступом');
|
|
48
|
+
expect(ownerAndAllowedUsersOnly('en')).toContain('approved access');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns localized generic access denied message', () => {
|
|
52
|
+
expect(accessDenied('ru')).toContain('нет доступа');
|
|
53
|
+
expect(accessDenied('en')).toContain('do not have access');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns localized group pairing pending message', () => {
|
|
57
|
+
expect(groupPairingPending('ru')).toContain('сначала нужно подтвердить доступ');
|
|
58
|
+
expect(groupPairingPending('en')).toContain('access must be approved first');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns localized owner watch DM notification message', () => {
|
|
62
|
+
expect(watchOwnerDmNotice('ru', {
|
|
63
|
+
chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
|
|
64
|
+
topicsRef: '[b]секрет[/b]',
|
|
65
|
+
})).toBe('Сработало правило отслеживания в чате [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]. Совпавшие темы: [b]секрет[/b]');
|
|
66
|
+
expect(watchOwnerDmNotice('en', {
|
|
67
|
+
chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
|
|
68
|
+
topicsRef: '[b]secret[/b]',
|
|
69
|
+
})).toBe('A watch rule was triggered in chat [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]. Matched topics: [b]secret[/b]');
|
|
70
|
+
expect(watchOwnerDmNotice('de', {
|
|
71
|
+
chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
|
|
72
|
+
topicsRef: '[b]geheim[/b]',
|
|
73
|
+
})).toBe('Eine Beobachtungsregel wurde im Chat [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL] ausgeloest. Zugeordnete Themen: [b]geheim[/b]');
|
|
74
|
+
expect(watchOwnerDmNotice('zz', {
|
|
75
|
+
chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
|
|
76
|
+
topicsRef: '[b]secret[/b]',
|
|
77
|
+
})).toBe('A watch rule was triggered in chat [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]. Matched topics: [b]secret[/b]');
|
|
78
|
+
|
|
79
|
+
expect(watchOwnerDmNotice('ru', {
|
|
80
|
+
chatRef: '[URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Сергей Рыжиков[/URL]',
|
|
81
|
+
topicsRef: '[b]авария[/b]',
|
|
82
|
+
sourceKind: 'dm',
|
|
83
|
+
})).toBe('Сработало правило отслеживания в личном чате с [URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Сергей Рыжиков[/URL]. Совпавшие темы: [b]авария[/b]');
|
|
84
|
+
expect(watchOwnerDmNotice('en', {
|
|
85
|
+
chatRef: '[URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Sergey Ryzhikov[/URL]',
|
|
86
|
+
topicsRef: '[b]incident[/b]',
|
|
87
|
+
sourceKind: 'dm',
|
|
88
|
+
})).toBe('A watch rule was triggered in a direct chat with [URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Sergey Ryzhikov[/URL]. Matched topics: [b]incident[/b]');
|
|
46
89
|
});
|
|
47
90
|
});
|
|
@@ -116,12 +116,14 @@ describe('InboundHandler', () => {
|
|
|
116
116
|
|
|
117
117
|
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
118
118
|
expect(ctx.channel).toBe('bitrix24');
|
|
119
|
+
expect(ctx.eventScope).toBe('bot');
|
|
119
120
|
expect(ctx.text).toBe('Hello, how are you?');
|
|
120
121
|
expect(ctx.senderId).toBe('1');
|
|
121
122
|
expect(ctx.senderName).toBe('Test User');
|
|
122
123
|
expect(ctx.senderFirstName).toBe('Test');
|
|
123
124
|
expect(ctx.chatId).toBe('1');
|
|
124
125
|
expect(ctx.chatInternalId).toBe('40985');
|
|
126
|
+
expect(ctx.chatName).toBe('Test User');
|
|
125
127
|
expect(ctx.messageId).toBe('490659');
|
|
126
128
|
expect(ctx.replyToMessageId).toBeUndefined();
|
|
127
129
|
expect(ctx.isForwarded).toBe(false);
|
|
@@ -129,9 +131,194 @@ describe('InboundHandler', () => {
|
|
|
129
131
|
expect(ctx.isGroup).toBe(false);
|
|
130
132
|
expect(ctx.media).toEqual([]);
|
|
131
133
|
expect(ctx.language).toBe('ru');
|
|
134
|
+
expect(ctx.timestamp).toBe(Date.parse('2026-03-18T08:00:00+02:00'));
|
|
132
135
|
expect(ctx.botId).toBe(2081);
|
|
133
136
|
});
|
|
134
137
|
|
|
138
|
+
it('dispatches user message events from combined fetch in agent mode', async () => {
|
|
139
|
+
const onMessage = vi.fn();
|
|
140
|
+
handler = new InboundHandler({
|
|
141
|
+
config: { dmPolicy: 'pairing', agentMode: true },
|
|
142
|
+
logger: silentLogger,
|
|
143
|
+
onMessage,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const event = {
|
|
147
|
+
eventId: 1007,
|
|
148
|
+
type: 'ONIMV2MESSAGEADD',
|
|
149
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
150
|
+
data: {
|
|
151
|
+
message: {
|
|
152
|
+
id: 490660,
|
|
153
|
+
chatId: 520,
|
|
154
|
+
authorId: 77,
|
|
155
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
156
|
+
text: 'Авария в личке',
|
|
157
|
+
isSystem: false,
|
|
158
|
+
uuid: '',
|
|
159
|
+
forward: null,
|
|
160
|
+
params: {},
|
|
161
|
+
viewedByOthers: false,
|
|
162
|
+
},
|
|
163
|
+
chat: {
|
|
164
|
+
id: 520,
|
|
165
|
+
dialogId: '77',
|
|
166
|
+
type: 'private',
|
|
167
|
+
name: 'Sergey',
|
|
168
|
+
entityType: '',
|
|
169
|
+
owner: 1,
|
|
170
|
+
avatar: '',
|
|
171
|
+
color: '#ab7761',
|
|
172
|
+
},
|
|
173
|
+
user: {
|
|
174
|
+
...baseUser,
|
|
175
|
+
id: 77,
|
|
176
|
+
name: 'Sergey',
|
|
177
|
+
firstName: 'Sergey',
|
|
178
|
+
},
|
|
179
|
+
language: 'ru',
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
184
|
+
expect(result).toBe(true);
|
|
185
|
+
expect(onMessage).toHaveBeenCalledOnce();
|
|
186
|
+
expect(onMessage.mock.calls[0][0]).toMatchObject({
|
|
187
|
+
eventScope: 'user',
|
|
188
|
+
chatId: '77',
|
|
189
|
+
senderId: '77',
|
|
190
|
+
text: 'Авария в личке',
|
|
191
|
+
wasMentioned: false,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('processes bot and user message events separately when they share one message id', async () => {
|
|
196
|
+
const onMessage = vi.fn();
|
|
197
|
+
handler = new InboundHandler({
|
|
198
|
+
config: { dmPolicy: 'pairing', agentMode: true },
|
|
199
|
+
logger: silentLogger,
|
|
200
|
+
onMessage,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const botEvent = createFetchMessageEvent({
|
|
204
|
+
message: {
|
|
205
|
+
id: 555001,
|
|
206
|
+
chatId: 520,
|
|
207
|
+
authorId: 77,
|
|
208
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
209
|
+
text: 'shared message',
|
|
210
|
+
isSystem: false,
|
|
211
|
+
uuid: '',
|
|
212
|
+
forward: null,
|
|
213
|
+
params: {},
|
|
214
|
+
viewedByOthers: false,
|
|
215
|
+
},
|
|
216
|
+
chat: {
|
|
217
|
+
id: 520,
|
|
218
|
+
dialogId: 'chat520',
|
|
219
|
+
type: 'chat',
|
|
220
|
+
name: 'Group Chat',
|
|
221
|
+
entityType: '',
|
|
222
|
+
owner: 1,
|
|
223
|
+
avatar: '',
|
|
224
|
+
color: '#ab7761',
|
|
225
|
+
},
|
|
226
|
+
user: {
|
|
227
|
+
...baseUser,
|
|
228
|
+
id: 77,
|
|
229
|
+
name: 'Sergey',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const userEvent = {
|
|
234
|
+
eventId: 1008,
|
|
235
|
+
type: 'ONIMV2MESSAGEADD',
|
|
236
|
+
date: '2026-03-19T14:36:04+02:00',
|
|
237
|
+
data: {
|
|
238
|
+
message: {
|
|
239
|
+
id: 555001,
|
|
240
|
+
chatId: 520,
|
|
241
|
+
authorId: 77,
|
|
242
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
243
|
+
text: 'shared message',
|
|
244
|
+
isSystem: false,
|
|
245
|
+
uuid: '',
|
|
246
|
+
forward: null,
|
|
247
|
+
params: {},
|
|
248
|
+
viewedByOthers: false,
|
|
249
|
+
},
|
|
250
|
+
chat: {
|
|
251
|
+
id: 520,
|
|
252
|
+
dialogId: 'chat520',
|
|
253
|
+
type: 'chat',
|
|
254
|
+
name: 'Group Chat',
|
|
255
|
+
entityType: '',
|
|
256
|
+
owner: 1,
|
|
257
|
+
avatar: '',
|
|
258
|
+
color: '#ab7761',
|
|
259
|
+
},
|
|
260
|
+
user: {
|
|
261
|
+
...baseUser,
|
|
262
|
+
id: 77,
|
|
263
|
+
name: 'Sergey',
|
|
264
|
+
},
|
|
265
|
+
language: 'ru',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
await handler.handleFetchEvent(botEvent as never, fetchCtx);
|
|
270
|
+
await handler.handleFetchEvent(userEvent as never, fetchCtx);
|
|
271
|
+
|
|
272
|
+
expect(onMessage).toHaveBeenCalledTimes(2);
|
|
273
|
+
expect((onMessage.mock.calls[0][0] as B24MsgContext).eventScope).toBe('bot');
|
|
274
|
+
expect((onMessage.mock.calls[1][0] as B24MsgContext).eventScope).toBe('user');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('still deduplicates repeated events within the same scope', async () => {
|
|
278
|
+
const onMessage = vi.fn();
|
|
279
|
+
handler = new InboundHandler({
|
|
280
|
+
config: { dmPolicy: 'pairing', agentMode: true },
|
|
281
|
+
logger: silentLogger,
|
|
282
|
+
onMessage,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const botEvent = createFetchMessageEvent({
|
|
286
|
+
message: {
|
|
287
|
+
id: 555002,
|
|
288
|
+
chatId: 520,
|
|
289
|
+
authorId: 77,
|
|
290
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
291
|
+
text: 'same scope duplicate',
|
|
292
|
+
isSystem: false,
|
|
293
|
+
uuid: '',
|
|
294
|
+
forward: null,
|
|
295
|
+
params: {},
|
|
296
|
+
viewedByOthers: false,
|
|
297
|
+
},
|
|
298
|
+
chat: {
|
|
299
|
+
id: 520,
|
|
300
|
+
dialogId: 'chat520',
|
|
301
|
+
type: 'chat',
|
|
302
|
+
name: 'Group Chat',
|
|
303
|
+
entityType: '',
|
|
304
|
+
owner: 1,
|
|
305
|
+
avatar: '',
|
|
306
|
+
color: '#ab7761',
|
|
307
|
+
},
|
|
308
|
+
user: {
|
|
309
|
+
...baseUser,
|
|
310
|
+
id: 77,
|
|
311
|
+
name: 'Sergey',
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await handler.handleFetchEvent(botEvent as never, fetchCtx);
|
|
316
|
+
await handler.handleFetchEvent(botEvent as never, fetchCtx);
|
|
317
|
+
|
|
318
|
+
expect(onMessage).toHaveBeenCalledTimes(1);
|
|
319
|
+
expect((onMessage.mock.calls[0][0] as B24MsgContext).eventScope).toBe('bot');
|
|
320
|
+
});
|
|
321
|
+
|
|
135
322
|
it('keeps incoming dialogId for direct chats without replacing it by user id', async () => {
|
|
136
323
|
const onMessage = vi.fn();
|
|
137
324
|
handler = new InboundHandler({
|
|
@@ -157,7 +344,7 @@ describe('InboundHandler', () => {
|
|
|
157
344
|
},
|
|
158
345
|
});
|
|
159
346
|
|
|
160
|
-
await handler.handleFetchEvent(event, fetchCtx);
|
|
347
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
161
348
|
|
|
162
349
|
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
163
350
|
expect(ctx.chatId).toBe('123');
|
|
@@ -173,8 +360,8 @@ describe('InboundHandler', () => {
|
|
|
173
360
|
});
|
|
174
361
|
|
|
175
362
|
const event = createFetchMessageEvent();
|
|
176
|
-
await handler.handleFetchEvent(event, fetchCtx);
|
|
177
|
-
await handler.handleFetchEvent(event, fetchCtx);
|
|
363
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
364
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
178
365
|
|
|
179
366
|
expect(onMessage).toHaveBeenCalledOnce();
|
|
180
367
|
});
|
|
@@ -281,6 +468,91 @@ describe('InboundHandler', () => {
|
|
|
281
468
|
expect(ctx.isForwarded).toBe(true);
|
|
282
469
|
});
|
|
283
470
|
|
|
471
|
+
it('detects bot mention in a group message from real Bitrix payload format', async () => {
|
|
472
|
+
const onMessage = vi.fn();
|
|
473
|
+
handler = new InboundHandler({
|
|
474
|
+
config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
|
|
475
|
+
logger: silentLogger,
|
|
476
|
+
onMessage,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const event = createFetchMessageEvent({
|
|
480
|
+
eventId: 9745,
|
|
481
|
+
message: {
|
|
482
|
+
id: 30857,
|
|
483
|
+
chatId: 520,
|
|
484
|
+
authorId: 1,
|
|
485
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
486
|
+
text: '[USER=6809]OpenClaw[/USER] привет',
|
|
487
|
+
isSystem: false,
|
|
488
|
+
uuid: '',
|
|
489
|
+
forward: null,
|
|
490
|
+
params: [],
|
|
491
|
+
viewedByOthers: false,
|
|
492
|
+
},
|
|
493
|
+
chat: {
|
|
494
|
+
id: 520,
|
|
495
|
+
dialogId: 'chat520',
|
|
496
|
+
type: 'chat',
|
|
497
|
+
name: 'Group Chat',
|
|
498
|
+
entityType: '',
|
|
499
|
+
owner: 1,
|
|
500
|
+
avatar: '',
|
|
501
|
+
color: '#ab7761',
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
506
|
+
|
|
507
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
508
|
+
expect(ctx.chatId).toBe('chat520');
|
|
509
|
+
expect(ctx.chatInternalId).toBe('520');
|
|
510
|
+
expect(ctx.chatName).toBe('Group Chat');
|
|
511
|
+
expect(ctx.chatType).toBe('chat');
|
|
512
|
+
expect(ctx.isDm).toBe(false);
|
|
513
|
+
expect(ctx.isGroup).toBe(true);
|
|
514
|
+
expect(ctx.wasMentioned).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('does not mark plain group messages as mentions', async () => {
|
|
518
|
+
const onMessage = vi.fn();
|
|
519
|
+
handler = new InboundHandler({
|
|
520
|
+
config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
|
|
521
|
+
logger: silentLogger,
|
|
522
|
+
onMessage,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const event = createFetchMessageEvent({
|
|
526
|
+
message: {
|
|
527
|
+
id: 30858,
|
|
528
|
+
chatId: 520,
|
|
529
|
+
authorId: 1,
|
|
530
|
+
date: '2026-03-19T14:36:10+02:00',
|
|
531
|
+
text: 'привет',
|
|
532
|
+
isSystem: false,
|
|
533
|
+
uuid: '',
|
|
534
|
+
forward: null,
|
|
535
|
+
params: [],
|
|
536
|
+
viewedByOthers: false,
|
|
537
|
+
},
|
|
538
|
+
chat: {
|
|
539
|
+
id: 520,
|
|
540
|
+
dialogId: 'chat520',
|
|
541
|
+
type: 'chat',
|
|
542
|
+
name: 'Group Chat',
|
|
543
|
+
entityType: '',
|
|
544
|
+
owner: 1,
|
|
545
|
+
avatar: '',
|
|
546
|
+
color: '#ab7761',
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
551
|
+
|
|
552
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
553
|
+
expect(ctx.wasMentioned).toBe(false);
|
|
554
|
+
});
|
|
555
|
+
|
|
284
556
|
it('dispatches webhook message events from JSON body', async () => {
|
|
285
557
|
const onMessage = vi.fn();
|
|
286
558
|
handler = new InboundHandler({
|
|
@@ -440,13 +712,58 @@ describe('InboundHandler', () => {
|
|
|
440
712
|
expect(result).toBe(true);
|
|
441
713
|
expect(onJoinChat).toHaveBeenCalledOnce();
|
|
442
714
|
expect(onJoinChat).toHaveBeenCalledWith({
|
|
715
|
+
senderId: '1',
|
|
443
716
|
dialogId: '1',
|
|
717
|
+
chatId: '40985',
|
|
444
718
|
chatType: 'private',
|
|
445
719
|
language: 'ru',
|
|
446
720
|
fetchCtx,
|
|
447
721
|
});
|
|
448
722
|
});
|
|
449
723
|
|
|
724
|
+
it('dispatches group join chat events with numeric chat id', async () => {
|
|
725
|
+
const onJoinChat = vi.fn();
|
|
726
|
+
handler = new InboundHandler({
|
|
727
|
+
config: { dmPolicy: 'pairing' },
|
|
728
|
+
logger: silentLogger,
|
|
729
|
+
onJoinChat,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const event = {
|
|
733
|
+
eventId: 1006,
|
|
734
|
+
type: 'ONIMBOTV2JOINCHAT',
|
|
735
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
736
|
+
data: {
|
|
737
|
+
bot: baseBot,
|
|
738
|
+
dialogId: 'chat520',
|
|
739
|
+
chat: {
|
|
740
|
+
id: 520,
|
|
741
|
+
dialogId: 'chat520',
|
|
742
|
+
type: 'chat',
|
|
743
|
+
name: 'Group Chat',
|
|
744
|
+
entityType: '',
|
|
745
|
+
owner: 1,
|
|
746
|
+
avatar: '',
|
|
747
|
+
color: '#ab7761',
|
|
748
|
+
},
|
|
749
|
+
user: baseUser,
|
|
750
|
+
language: 'ru',
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
755
|
+
expect(result).toBe(true);
|
|
756
|
+
expect(onJoinChat).toHaveBeenCalledOnce();
|
|
757
|
+
expect(onJoinChat).toHaveBeenCalledWith({
|
|
758
|
+
senderId: '1',
|
|
759
|
+
dialogId: 'chat520',
|
|
760
|
+
chatId: '520',
|
|
761
|
+
chatType: 'chat',
|
|
762
|
+
language: 'ru',
|
|
763
|
+
fetchCtx,
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
450
767
|
it('dispatches command events', async () => {
|
|
451
768
|
const onCommand = vi.fn();
|
|
452
769
|
handler = new InboundHandler({
|
|
@@ -504,6 +821,7 @@ describe('InboundHandler', () => {
|
|
|
504
821
|
commandText: '/help topic',
|
|
505
822
|
senderId: '1',
|
|
506
823
|
dialogId: '1',
|
|
824
|
+
chatId: '40985',
|
|
507
825
|
chatType: 'P',
|
|
508
826
|
messageId: '490667',
|
|
509
827
|
language: 'ru',
|
|
@@ -555,6 +873,7 @@ describe('InboundHandler', () => {
|
|
|
555
873
|
commandText: '/status',
|
|
556
874
|
senderId: '1',
|
|
557
875
|
dialogId: '1',
|
|
876
|
+
chatId: '40985',
|
|
558
877
|
chatType: 'P',
|
|
559
878
|
messageId: '1004',
|
|
560
879
|
language: 'ru',
|
|
@@ -562,6 +881,71 @@ describe('InboundHandler', () => {
|
|
|
562
881
|
});
|
|
563
882
|
});
|
|
564
883
|
|
|
884
|
+
it('dispatches group command events with numeric chat id', async () => {
|
|
885
|
+
const onCommand = vi.fn();
|
|
886
|
+
handler = new InboundHandler({
|
|
887
|
+
config: { dmPolicy: 'pairing' },
|
|
888
|
+
logger: silentLogger,
|
|
889
|
+
onCommand,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
const event = {
|
|
893
|
+
eventId: 1007,
|
|
894
|
+
type: 'ONIMBOTV2COMMANDADD',
|
|
895
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
896
|
+
data: {
|
|
897
|
+
bot: baseBot,
|
|
898
|
+
command: {
|
|
899
|
+
id: 56,
|
|
900
|
+
command: '/help',
|
|
901
|
+
params: '',
|
|
902
|
+
context: 'textarea',
|
|
903
|
+
},
|
|
904
|
+
message: {
|
|
905
|
+
id: 30857,
|
|
906
|
+
chatId: 520,
|
|
907
|
+
authorId: 1,
|
|
908
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
909
|
+
text: '/help',
|
|
910
|
+
isSystem: false,
|
|
911
|
+
uuid: '',
|
|
912
|
+
forward: null,
|
|
913
|
+
params: [],
|
|
914
|
+
viewedByOthers: false,
|
|
915
|
+
},
|
|
916
|
+
chat: {
|
|
917
|
+
id: 520,
|
|
918
|
+
dialogId: 'chat520',
|
|
919
|
+
type: 'chat',
|
|
920
|
+
name: 'Group Chat',
|
|
921
|
+
entityType: '',
|
|
922
|
+
owner: 1,
|
|
923
|
+
avatar: '',
|
|
924
|
+
color: '#ab7761',
|
|
925
|
+
},
|
|
926
|
+
user: baseUser,
|
|
927
|
+
language: 'ru',
|
|
928
|
+
},
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
932
|
+
expect(result).toBe(true);
|
|
933
|
+
expect(onCommand).toHaveBeenCalledOnce();
|
|
934
|
+
expect(onCommand).toHaveBeenCalledWith({
|
|
935
|
+
commandId: 56,
|
|
936
|
+
commandName: 'help',
|
|
937
|
+
commandParams: '',
|
|
938
|
+
commandText: '/help',
|
|
939
|
+
senderId: '1',
|
|
940
|
+
dialogId: 'chat520',
|
|
941
|
+
chatId: '520',
|
|
942
|
+
chatType: 'chat',
|
|
943
|
+
messageId: '30857',
|
|
944
|
+
language: 'ru',
|
|
945
|
+
fetchCtx,
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
|
|
565
949
|
it('builds command text from command payload instead of message text', async () => {
|
|
566
950
|
const onCommand = vi.fn();
|
|
567
951
|
handler = new InboundHandler({
|
|
@@ -619,6 +1003,7 @@ describe('InboundHandler', () => {
|
|
|
619
1003
|
commandText: '/status',
|
|
620
1004
|
senderId: '1',
|
|
621
1005
|
dialogId: '1',
|
|
1006
|
+
chatId: '40985',
|
|
622
1007
|
chatType: 'P',
|
|
623
1008
|
messageId: '490668',
|
|
624
1009
|
language: 'ru',
|