@ihazz/bitrix24 1.1.1 → 1.1.2
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/package.json +9 -1
- package/src/media-service.ts +78 -25
- package/src/state-paths.ts +24 -0
- package/tests/access-control.test.ts +0 -398
- package/tests/api.test.ts +0 -226
- package/tests/channel-flow.test.ts +0 -1692
- package/tests/channel.test.ts +0 -842
- package/tests/commands.test.ts +0 -57
- package/tests/config.test.ts +0 -210
- package/tests/dedup.test.ts +0 -50
- package/tests/fixtures/onimbotjoinchat.json +0 -48
- package/tests/fixtures/onimbotmessageadd-file.json +0 -86
- package/tests/fixtures/onimbotmessageadd-text.json +0 -59
- package/tests/fixtures/onimcommandadd.json +0 -45
- package/tests/group-access.test.ts +0 -340
- package/tests/history-cache.test.ts +0 -117
- package/tests/i18n.test.ts +0 -90
- package/tests/inbound-handler.test.ts +0 -1033
- package/tests/index.test.ts +0 -94
- package/tests/media-service.test.ts +0 -319
- package/tests/message-utils.test.ts +0 -184
- package/tests/polling-service.test.ts +0 -115
- package/tests/rate-limiter.test.ts +0 -52
- package/tests/send-service.test.ts +0 -162
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -9
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { PollingService } from '../src/polling-service.js';
|
|
6
|
-
import type { Bitrix24Api } from '../src/api.js';
|
|
7
|
-
|
|
8
|
-
const silentLogger = {
|
|
9
|
-
info: vi.fn(),
|
|
10
|
-
warn: vi.fn(),
|
|
11
|
-
error: vi.fn(),
|
|
12
|
-
debug: vi.fn(),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
describe('PollingService', () => {
|
|
16
|
-
let tempHome = '';
|
|
17
|
-
const originalHome = process.env.HOME;
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
if (tempHome) {
|
|
21
|
-
rmSync(tempHome, { recursive: true, force: true });
|
|
22
|
-
tempHome = '';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (originalHome === undefined) {
|
|
26
|
-
delete process.env.HOME;
|
|
27
|
-
} else {
|
|
28
|
-
process.env.HOME = originalHome;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
vi.clearAllMocks();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('persists offset only up to the last successfully processed event', async () => {
|
|
35
|
-
tempHome = mkdtempSync(join(tmpdir(), 'b24-polling-'));
|
|
36
|
-
process.env.HOME = tempHome;
|
|
37
|
-
|
|
38
|
-
const abortController = new AbortController();
|
|
39
|
-
const fetchEvents = vi.fn().mockResolvedValue({
|
|
40
|
-
events: [
|
|
41
|
-
{ eventId: 10, type: 'ONE', date: '', data: {} },
|
|
42
|
-
{ eventId: 11, type: 'TWO', date: '', data: {} },
|
|
43
|
-
],
|
|
44
|
-
lastEventId: 11,
|
|
45
|
-
hasMore: false,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const api = { fetchEvents } as unknown as Bitrix24Api;
|
|
49
|
-
const onEvent = vi.fn(async (event: { eventId: number }) => {
|
|
50
|
-
if (event.eventId === 11) {
|
|
51
|
-
abortController.abort();
|
|
52
|
-
throw new Error('boom');
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const service = new PollingService({
|
|
57
|
-
api,
|
|
58
|
-
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
59
|
-
bot: { botId: 7, botToken: 'bot_token' },
|
|
60
|
-
accountId: 'default',
|
|
61
|
-
pollingIntervalMs: 10,
|
|
62
|
-
pollingFastIntervalMs: 10,
|
|
63
|
-
onEvent: onEvent as never,
|
|
64
|
-
abortSignal: abortController.signal,
|
|
65
|
-
logger: silentLogger,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await service.start();
|
|
69
|
-
|
|
70
|
-
const statePath = join(tempHome, '.openclaw', 'state', 'bitrix24', 'poll-offset-default.json');
|
|
71
|
-
const state = JSON.parse(readFileSync(statePath, 'utf-8')) as { offset: number };
|
|
72
|
-
|
|
73
|
-
expect(fetchEvents).toHaveBeenCalledOnce();
|
|
74
|
-
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
75
|
-
expect(state.offset).toBe(11);
|
|
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
|
-
});
|
|
115
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { RateLimiter } from '../src/rate-limiter.js';
|
|
3
|
-
|
|
4
|
-
describe('RateLimiter', () => {
|
|
5
|
-
let limiter: RateLimiter;
|
|
6
|
-
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
limiter?.destroy();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('allows immediate requests within budget', async () => {
|
|
12
|
-
limiter = new RateLimiter({ maxPerSecond: 2 });
|
|
13
|
-
// First two should be immediate
|
|
14
|
-
const start = Date.now();
|
|
15
|
-
await limiter.acquire();
|
|
16
|
-
await limiter.acquire();
|
|
17
|
-
const elapsed = Date.now() - start;
|
|
18
|
-
expect(elapsed).toBeLessThan(50);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('queues requests when budget exhausted', async () => {
|
|
22
|
-
limiter = new RateLimiter({ maxPerSecond: 2 });
|
|
23
|
-
// Exhaust budget
|
|
24
|
-
await limiter.acquire();
|
|
25
|
-
await limiter.acquire();
|
|
26
|
-
// Third should be delayed
|
|
27
|
-
const start = Date.now();
|
|
28
|
-
await limiter.acquire();
|
|
29
|
-
const elapsed = Date.now() - start;
|
|
30
|
-
expect(elapsed).toBeGreaterThanOrEqual(400); // ~500ms for 1 token at 2/sec
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('tracks pending count', async () => {
|
|
34
|
-
limiter = new RateLimiter({ maxPerSecond: 1 });
|
|
35
|
-
await limiter.acquire();
|
|
36
|
-
expect(limiter.pending).toBe(0);
|
|
37
|
-
|
|
38
|
-
// Start a pending acquire
|
|
39
|
-
const p = limiter.acquire();
|
|
40
|
-
expect(limiter.pending).toBe(1);
|
|
41
|
-
await p;
|
|
42
|
-
expect(limiter.pending).toBe(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('rejects pending on destroy', async () => {
|
|
46
|
-
limiter = new RateLimiter({ maxPerSecond: 1 });
|
|
47
|
-
await limiter.acquire();
|
|
48
|
-
const p = limiter.acquire();
|
|
49
|
-
limiter.destroy();
|
|
50
|
-
await expect(p).rejects.toThrow('RateLimiter destroyed');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { SendService } from '../src/send-service.js';
|
|
3
|
-
import type { Bitrix24Api } from '../src/api.js';
|
|
4
|
-
|
|
5
|
-
const silentLogger = {
|
|
6
|
-
info: vi.fn(),
|
|
7
|
-
warn: vi.fn(),
|
|
8
|
-
error: vi.fn(),
|
|
9
|
-
debug: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function mockApi(): Bitrix24Api {
|
|
13
|
-
return {
|
|
14
|
-
answerCommand: vi.fn().mockResolvedValue(true),
|
|
15
|
-
notifyInputAction: vi.fn().mockResolvedValue(true),
|
|
16
|
-
sendTyping: vi.fn().mockResolvedValue(true),
|
|
17
|
-
readMessage: vi.fn().mockResolvedValue({
|
|
18
|
-
chatId: 1,
|
|
19
|
-
lastId: 123,
|
|
20
|
-
counter: 0,
|
|
21
|
-
viewedMessages: [123],
|
|
22
|
-
}),
|
|
23
|
-
sendMessage: vi.fn(),
|
|
24
|
-
updateMessage: vi.fn(),
|
|
25
|
-
} as unknown as Bitrix24Api;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('SendService', () => {
|
|
29
|
-
let api: Bitrix24Api;
|
|
30
|
-
let service: SendService;
|
|
31
|
-
|
|
32
|
-
const ctx = {
|
|
33
|
-
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
34
|
-
bot: { botId: 7, botToken: 'bot_token' },
|
|
35
|
-
dialogId: '42',
|
|
36
|
-
};
|
|
37
|
-
const commandCtx = {
|
|
38
|
-
...ctx,
|
|
39
|
-
commandId: 55,
|
|
40
|
-
messageId: 123,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
api = mockApi();
|
|
45
|
-
service = new SendService(api, silentLogger);
|
|
46
|
-
vi.clearAllMocks();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('sends explicit input action status', async () => {
|
|
50
|
-
await service.sendStatus(ctx, 'IMBOT_AGENT_ACTION_COMPOSING', 45);
|
|
51
|
-
|
|
52
|
-
expect(api.notifyInputAction).toHaveBeenCalledWith(
|
|
53
|
-
ctx.webhookUrl,
|
|
54
|
-
ctx.bot,
|
|
55
|
-
ctx.dialogId,
|
|
56
|
-
{
|
|
57
|
-
statusMessageCode: 'IMBOT_AGENT_ACTION_COMPOSING',
|
|
58
|
-
duration: 45,
|
|
59
|
-
},
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('uses notifyInputAction for sendTyping', async () => {
|
|
64
|
-
await service.sendTyping(ctx);
|
|
65
|
-
|
|
66
|
-
expect(api.notifyInputAction).toHaveBeenCalledWith(
|
|
67
|
-
ctx.webhookUrl,
|
|
68
|
-
ctx.bot,
|
|
69
|
-
ctx.dialogId,
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('marks a specific message as read', async () => {
|
|
74
|
-
await service.markRead(ctx, 123);
|
|
75
|
-
|
|
76
|
-
expect(api.readMessage).toHaveBeenCalledWith(
|
|
77
|
-
ctx.webhookUrl,
|
|
78
|
-
ctx.bot,
|
|
79
|
-
ctx.dialogId,
|
|
80
|
-
123,
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('swallows read failures', async () => {
|
|
85
|
-
(api.readMessage as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('read failed'));
|
|
86
|
-
|
|
87
|
-
await expect(service.markRead(ctx, 123)).resolves.toBeUndefined();
|
|
88
|
-
expect(silentLogger.debug).toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('answers native commands via imbot.v2.Command.answer', async () => {
|
|
92
|
-
await service.answerCommandText(commandCtx, 'Status ok');
|
|
93
|
-
|
|
94
|
-
expect(api.answerCommand).toHaveBeenCalledWith(
|
|
95
|
-
ctx.webhookUrl,
|
|
96
|
-
ctx.bot,
|
|
97
|
-
55,
|
|
98
|
-
123,
|
|
99
|
-
ctx.dialogId,
|
|
100
|
-
'Status ok',
|
|
101
|
-
undefined,
|
|
102
|
-
);
|
|
103
|
-
expect(api.sendMessage).not.toHaveBeenCalled();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('sends command follow-up chunks as regular chat messages', async () => {
|
|
107
|
-
(api.sendMessage as ReturnType<typeof vi.fn>).mockResolvedValueOnce(777);
|
|
108
|
-
const longText = `${'A'.repeat(20000)}${'B'.repeat(50)}`;
|
|
109
|
-
|
|
110
|
-
const result = await service.answerCommandText(commandCtx, longText);
|
|
111
|
-
|
|
112
|
-
expect(result.ok).toBe(true);
|
|
113
|
-
expect(api.answerCommand).toHaveBeenCalledOnce();
|
|
114
|
-
expect(api.sendMessage).toHaveBeenCalledOnce();
|
|
115
|
-
expect(api.sendMessage).toHaveBeenCalledWith(
|
|
116
|
-
ctx.webhookUrl,
|
|
117
|
-
ctx.bot,
|
|
118
|
-
ctx.dialogId,
|
|
119
|
-
'B'.repeat(50),
|
|
120
|
-
undefined,
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('throws when sendText cannot deliver a chunk', async () => {
|
|
125
|
-
(api.sendMessage as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('send failed'));
|
|
126
|
-
|
|
127
|
-
await expect(service.sendText(ctx, 'Hello')).rejects.toThrow('send failed');
|
|
128
|
-
expect(silentLogger.error).toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('throws when command answer fails on the first chunk', async () => {
|
|
132
|
-
(api.answerCommand as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('answer failed'));
|
|
133
|
-
|
|
134
|
-
await expect(service.answerCommandText(commandCtx, 'Status ok')).rejects.toThrow('answer failed');
|
|
135
|
-
expect(silentLogger.error).toHaveBeenCalled();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('throws when command follow-up chunk cannot be sent', async () => {
|
|
139
|
-
(api.sendMessage as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('follow-up failed'));
|
|
140
|
-
const longText = `${'A'.repeat(20000)}${'B'.repeat(50)}`;
|
|
141
|
-
|
|
142
|
-
await expect(service.answerCommandText(commandCtx, longText)).rejects.toThrow('follow-up failed');
|
|
143
|
-
expect(silentLogger.error).toHaveBeenCalled();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('passes forwardMessages to the first outbound chunk', async () => {
|
|
147
|
-
(api.sendMessage as ReturnType<typeof vi.fn>).mockResolvedValueOnce(777);
|
|
148
|
-
|
|
149
|
-
await service.sendText(ctx, 'Forward this', {
|
|
150
|
-
convertMarkdown: false,
|
|
151
|
-
forwardMessages: [101, 102],
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
expect(api.sendMessage).toHaveBeenCalledWith(
|
|
155
|
-
ctx.webhookUrl,
|
|
156
|
-
ctx.bot,
|
|
157
|
-
ctx.dialogId,
|
|
158
|
-
'Forward this',
|
|
159
|
-
{ forwardMessages: [101, 102] },
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"outDir": "./dist",
|
|
9
|
-
"rootDir": ".",
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"strict": true,
|
|
14
|
-
"esModuleInterop": true,
|
|
15
|
-
"skipLibCheck": true,
|
|
16
|
-
"forceConsistentCasingInFileNames": true,
|
|
17
|
-
"resolveJsonModule": true,
|
|
18
|
-
"isolatedModules": true
|
|
19
|
-
},
|
|
20
|
-
"include": ["index.ts", "src/**/*.ts"],
|
|
21
|
-
"exclude": ["node_modules", "dist", "tests"]
|
|
22
|
-
}
|