@ihazz/bitrix24 1.0.3 → 1.1.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 +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 +88 -6
- 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 +341 -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,147 @@ 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('deduplicates bot and user message events that 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(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
135
275
|
it('keeps incoming dialogId for direct chats without replacing it by user id', async () => {
|
|
136
276
|
const onMessage = vi.fn();
|
|
137
277
|
handler = new InboundHandler({
|
|
@@ -157,7 +297,7 @@ describe('InboundHandler', () => {
|
|
|
157
297
|
},
|
|
158
298
|
});
|
|
159
299
|
|
|
160
|
-
await handler.handleFetchEvent(event, fetchCtx);
|
|
300
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
161
301
|
|
|
162
302
|
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
163
303
|
expect(ctx.chatId).toBe('123');
|
|
@@ -173,8 +313,8 @@ describe('InboundHandler', () => {
|
|
|
173
313
|
});
|
|
174
314
|
|
|
175
315
|
const event = createFetchMessageEvent();
|
|
176
|
-
await handler.handleFetchEvent(event, fetchCtx);
|
|
177
|
-
await handler.handleFetchEvent(event, fetchCtx);
|
|
316
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
317
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
178
318
|
|
|
179
319
|
expect(onMessage).toHaveBeenCalledOnce();
|
|
180
320
|
});
|
|
@@ -281,6 +421,91 @@ describe('InboundHandler', () => {
|
|
|
281
421
|
expect(ctx.isForwarded).toBe(true);
|
|
282
422
|
});
|
|
283
423
|
|
|
424
|
+
it('detects bot mention in a group message from real Bitrix payload format', async () => {
|
|
425
|
+
const onMessage = vi.fn();
|
|
426
|
+
handler = new InboundHandler({
|
|
427
|
+
config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
|
|
428
|
+
logger: silentLogger,
|
|
429
|
+
onMessage,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const event = createFetchMessageEvent({
|
|
433
|
+
eventId: 9745,
|
|
434
|
+
message: {
|
|
435
|
+
id: 30857,
|
|
436
|
+
chatId: 520,
|
|
437
|
+
authorId: 1,
|
|
438
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
439
|
+
text: '[USER=6809]OpenClaw[/USER] привет',
|
|
440
|
+
isSystem: false,
|
|
441
|
+
uuid: '',
|
|
442
|
+
forward: null,
|
|
443
|
+
params: [],
|
|
444
|
+
viewedByOthers: false,
|
|
445
|
+
},
|
|
446
|
+
chat: {
|
|
447
|
+
id: 520,
|
|
448
|
+
dialogId: 'chat520',
|
|
449
|
+
type: 'chat',
|
|
450
|
+
name: 'Group Chat',
|
|
451
|
+
entityType: '',
|
|
452
|
+
owner: 1,
|
|
453
|
+
avatar: '',
|
|
454
|
+
color: '#ab7761',
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
|
|
459
|
+
|
|
460
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
461
|
+
expect(ctx.chatId).toBe('chat520');
|
|
462
|
+
expect(ctx.chatInternalId).toBe('520');
|
|
463
|
+
expect(ctx.chatName).toBe('Group Chat');
|
|
464
|
+
expect(ctx.chatType).toBe('chat');
|
|
465
|
+
expect(ctx.isDm).toBe(false);
|
|
466
|
+
expect(ctx.isGroup).toBe(true);
|
|
467
|
+
expect(ctx.wasMentioned).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('does not mark plain group messages as mentions', async () => {
|
|
471
|
+
const onMessage = vi.fn();
|
|
472
|
+
handler = new InboundHandler({
|
|
473
|
+
config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
|
|
474
|
+
logger: silentLogger,
|
|
475
|
+
onMessage,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const event = createFetchMessageEvent({
|
|
479
|
+
message: {
|
|
480
|
+
id: 30858,
|
|
481
|
+
chatId: 520,
|
|
482
|
+
authorId: 1,
|
|
483
|
+
date: '2026-03-19T14:36:10+02:00',
|
|
484
|
+
text: 'привет',
|
|
485
|
+
isSystem: false,
|
|
486
|
+
uuid: '',
|
|
487
|
+
forward: null,
|
|
488
|
+
params: [],
|
|
489
|
+
viewedByOthers: false,
|
|
490
|
+
},
|
|
491
|
+
chat: {
|
|
492
|
+
id: 520,
|
|
493
|
+
dialogId: 'chat520',
|
|
494
|
+
type: 'chat',
|
|
495
|
+
name: 'Group Chat',
|
|
496
|
+
entityType: '',
|
|
497
|
+
owner: 1,
|
|
498
|
+
avatar: '',
|
|
499
|
+
color: '#ab7761',
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
await handler.handleFetchEvent(event, fetchCtx);
|
|
504
|
+
|
|
505
|
+
const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
|
|
506
|
+
expect(ctx.wasMentioned).toBe(false);
|
|
507
|
+
});
|
|
508
|
+
|
|
284
509
|
it('dispatches webhook message events from JSON body', async () => {
|
|
285
510
|
const onMessage = vi.fn();
|
|
286
511
|
handler = new InboundHandler({
|
|
@@ -440,13 +665,58 @@ describe('InboundHandler', () => {
|
|
|
440
665
|
expect(result).toBe(true);
|
|
441
666
|
expect(onJoinChat).toHaveBeenCalledOnce();
|
|
442
667
|
expect(onJoinChat).toHaveBeenCalledWith({
|
|
668
|
+
senderId: '1',
|
|
443
669
|
dialogId: '1',
|
|
670
|
+
chatId: '40985',
|
|
444
671
|
chatType: 'private',
|
|
445
672
|
language: 'ru',
|
|
446
673
|
fetchCtx,
|
|
447
674
|
});
|
|
448
675
|
});
|
|
449
676
|
|
|
677
|
+
it('dispatches group join chat events with numeric chat id', async () => {
|
|
678
|
+
const onJoinChat = vi.fn();
|
|
679
|
+
handler = new InboundHandler({
|
|
680
|
+
config: { dmPolicy: 'pairing' },
|
|
681
|
+
logger: silentLogger,
|
|
682
|
+
onJoinChat,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const event = {
|
|
686
|
+
eventId: 1006,
|
|
687
|
+
type: 'ONIMBOTV2JOINCHAT',
|
|
688
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
689
|
+
data: {
|
|
690
|
+
bot: baseBot,
|
|
691
|
+
dialogId: 'chat520',
|
|
692
|
+
chat: {
|
|
693
|
+
id: 520,
|
|
694
|
+
dialogId: 'chat520',
|
|
695
|
+
type: 'chat',
|
|
696
|
+
name: 'Group Chat',
|
|
697
|
+
entityType: '',
|
|
698
|
+
owner: 1,
|
|
699
|
+
avatar: '',
|
|
700
|
+
color: '#ab7761',
|
|
701
|
+
},
|
|
702
|
+
user: baseUser,
|
|
703
|
+
language: 'ru',
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
708
|
+
expect(result).toBe(true);
|
|
709
|
+
expect(onJoinChat).toHaveBeenCalledOnce();
|
|
710
|
+
expect(onJoinChat).toHaveBeenCalledWith({
|
|
711
|
+
senderId: '1',
|
|
712
|
+
dialogId: 'chat520',
|
|
713
|
+
chatId: '520',
|
|
714
|
+
chatType: 'chat',
|
|
715
|
+
language: 'ru',
|
|
716
|
+
fetchCtx,
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
450
720
|
it('dispatches command events', async () => {
|
|
451
721
|
const onCommand = vi.fn();
|
|
452
722
|
handler = new InboundHandler({
|
|
@@ -504,6 +774,7 @@ describe('InboundHandler', () => {
|
|
|
504
774
|
commandText: '/help topic',
|
|
505
775
|
senderId: '1',
|
|
506
776
|
dialogId: '1',
|
|
777
|
+
chatId: '40985',
|
|
507
778
|
chatType: 'P',
|
|
508
779
|
messageId: '490667',
|
|
509
780
|
language: 'ru',
|
|
@@ -555,6 +826,7 @@ describe('InboundHandler', () => {
|
|
|
555
826
|
commandText: '/status',
|
|
556
827
|
senderId: '1',
|
|
557
828
|
dialogId: '1',
|
|
829
|
+
chatId: '40985',
|
|
558
830
|
chatType: 'P',
|
|
559
831
|
messageId: '1004',
|
|
560
832
|
language: 'ru',
|
|
@@ -562,6 +834,71 @@ describe('InboundHandler', () => {
|
|
|
562
834
|
});
|
|
563
835
|
});
|
|
564
836
|
|
|
837
|
+
it('dispatches group command events with numeric chat id', async () => {
|
|
838
|
+
const onCommand = vi.fn();
|
|
839
|
+
handler = new InboundHandler({
|
|
840
|
+
config: { dmPolicy: 'pairing' },
|
|
841
|
+
logger: silentLogger,
|
|
842
|
+
onCommand,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const event = {
|
|
846
|
+
eventId: 1007,
|
|
847
|
+
type: 'ONIMBOTV2COMMANDADD',
|
|
848
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
849
|
+
data: {
|
|
850
|
+
bot: baseBot,
|
|
851
|
+
command: {
|
|
852
|
+
id: 56,
|
|
853
|
+
command: '/help',
|
|
854
|
+
params: '',
|
|
855
|
+
context: 'textarea',
|
|
856
|
+
},
|
|
857
|
+
message: {
|
|
858
|
+
id: 30857,
|
|
859
|
+
chatId: 520,
|
|
860
|
+
authorId: 1,
|
|
861
|
+
date: '2026-03-19T14:36:03+02:00',
|
|
862
|
+
text: '/help',
|
|
863
|
+
isSystem: false,
|
|
864
|
+
uuid: '',
|
|
865
|
+
forward: null,
|
|
866
|
+
params: [],
|
|
867
|
+
viewedByOthers: false,
|
|
868
|
+
},
|
|
869
|
+
chat: {
|
|
870
|
+
id: 520,
|
|
871
|
+
dialogId: 'chat520',
|
|
872
|
+
type: 'chat',
|
|
873
|
+
name: 'Group Chat',
|
|
874
|
+
entityType: '',
|
|
875
|
+
owner: 1,
|
|
876
|
+
avatar: '',
|
|
877
|
+
color: '#ab7761',
|
|
878
|
+
},
|
|
879
|
+
user: baseUser,
|
|
880
|
+
language: 'ru',
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const result = await handler.handleFetchEvent(event as never, fetchCtx);
|
|
885
|
+
expect(result).toBe(true);
|
|
886
|
+
expect(onCommand).toHaveBeenCalledOnce();
|
|
887
|
+
expect(onCommand).toHaveBeenCalledWith({
|
|
888
|
+
commandId: 56,
|
|
889
|
+
commandName: 'help',
|
|
890
|
+
commandParams: '',
|
|
891
|
+
commandText: '/help',
|
|
892
|
+
senderId: '1',
|
|
893
|
+
dialogId: 'chat520',
|
|
894
|
+
chatId: '520',
|
|
895
|
+
chatType: 'chat',
|
|
896
|
+
messageId: '30857',
|
|
897
|
+
language: 'ru',
|
|
898
|
+
fetchCtx,
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
|
|
565
902
|
it('builds command text from command payload instead of message text', async () => {
|
|
566
903
|
const onCommand = vi.fn();
|
|
567
904
|
handler = new InboundHandler({
|
|
@@ -619,6 +956,7 @@ describe('InboundHandler', () => {
|
|
|
619
956
|
commandText: '/status',
|
|
620
957
|
senderId: '1',
|
|
621
958
|
dialogId: '1',
|
|
959
|
+
chatId: '40985',
|
|
622
960
|
chatType: 'P',
|
|
623
961
|
messageId: '490668',
|
|
624
962
|
language: 'ru',
|
|
@@ -74,4 +74,42 @@ describe('PollingService', () => {
|
|
|
74
74
|
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
75
75
|
expect(state.offset).toBe(11);
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
it('passes withUserEvents through to fetch polling', async () => {
|
|
79
|
+
tempHome = mkdtempSync(join(tmpdir(), 'b24-polling-agent-'));
|
|
80
|
+
process.env.HOME = tempHome;
|
|
81
|
+
|
|
82
|
+
const abortController = new AbortController();
|
|
83
|
+
const fetchEvents = vi.fn().mockImplementation(async () => {
|
|
84
|
+
abortController.abort();
|
|
85
|
+
return {
|
|
86
|
+
events: [],
|
|
87
|
+
lastEventId: 0,
|
|
88
|
+
hasMore: false,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const api = { fetchEvents } as unknown as Bitrix24Api;
|
|
93
|
+
const service = new PollingService({
|
|
94
|
+
api,
|
|
95
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
96
|
+
bot: { botId: 7, botToken: 'bot_token' },
|
|
97
|
+
accountId: 'default',
|
|
98
|
+
pollingIntervalMs: 1,
|
|
99
|
+
pollingFastIntervalMs: 1,
|
|
100
|
+
withUserEvents: true,
|
|
101
|
+
onEvent: vi.fn(),
|
|
102
|
+
abortSignal: abortController.signal,
|
|
103
|
+
logger: silentLogger,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await service.start();
|
|
107
|
+
|
|
108
|
+
expect(fetchEvents).toHaveBeenCalledOnce();
|
|
109
|
+
expect(fetchEvents).toHaveBeenCalledWith(
|
|
110
|
+
'https://test.bitrix24.com/rest/1/token/',
|
|
111
|
+
{ botId: 7, botToken: 'bot_token' },
|
|
112
|
+
{ offset: 0, limit: 100, withUserEvents: true },
|
|
113
|
+
);
|
|
114
|
+
});
|
|
77
115
|
});
|