@ihazz/bitrix24 0.1.4 → 0.1.6
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 +12 -3
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/access-control.ts +61 -4
- package/src/api.ts +107 -0
- package/src/channel.ts +405 -12
- package/src/commands.ts +60 -0
- package/src/config-schema.ts +1 -2
- package/src/inbound-handler.ts +1 -9
- package/src/media-service.ts +186 -0
- package/src/message-utils.ts +21 -11
- package/src/runtime.ts +23 -0
- package/src/types.ts +11 -2
- package/tests/access-control.test.ts +178 -6
- package/tests/channel.test.ts +538 -0
- package/tests/inbound-handler.test.ts +4 -2
- package/tests/media-service.test.ts +224 -0
- package/tests/message-utils.test.ts +13 -16
package/README.md
CHANGED
|
@@ -47,15 +47,25 @@ Add to your `openclaw.json`:
|
|
|
47
47
|
"webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
|
|
48
48
|
"botName": "OpenClaw",
|
|
49
49
|
"botCode": "openclaw",
|
|
50
|
-
"callbackPath": "/hooks/bitrix24",
|
|
51
50
|
"callbackUrl": "https://your-server.com/hooks/bitrix24",
|
|
52
51
|
"dmPolicy": "open",
|
|
52
|
+
"allowFrom": ["*"],
|
|
53
53
|
"showTyping": true
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Set allow in plugin section:
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"plugins": {
|
|
63
|
+
"allow": [
|
|
64
|
+
"bitrix24"
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
59
69
|
Only `webhookUrl` is required. The gateway will not start without it.
|
|
60
70
|
|
|
61
71
|
### Configuration Options
|
|
@@ -65,8 +75,7 @@ Only `webhookUrl` is required. The gateway will not start without it.
|
|
|
65
75
|
| `webhookUrl` | — | Bitrix24 REST webhook URL (**required**) |
|
|
66
76
|
| `botName` | `"OpenClaw"` | Bot display name (shown in welcome message) |
|
|
67
77
|
| `botCode` | `"openclaw"` | Unique bot code for `imbot.register` |
|
|
68
|
-
| `
|
|
69
|
-
| `callbackUrl` | — | Full public URL for bot registration (e.g. `https://your-server.com/hooks/bitrix24`).|
|
|
78
|
+
| `callbackUrl` | — | Full public URL for bot EVENT_HANDLER (e.g. `https://your-server.com/hooks/bitrix24`). Path is auto-extracted for route registration. |
|
|
70
79
|
| `dmPolicy` | `"open"` | Access policy: `"open"` / `"allowlist"` / `"pairing"` |
|
|
71
80
|
| `allowFrom` | — | Allowed B24 user IDs (when `dmPolicy: "allowlist"`) |
|
|
72
81
|
| `showTyping` | `true` | Send typing indicator before responding |
|
package/index.ts
CHANGED
|
@@ -28,9 +28,10 @@ export default {
|
|
|
28
28
|
|
|
29
29
|
api.registerChannel({ plugin: bitrix24Plugin });
|
|
30
30
|
|
|
31
|
-
// Register HTTP webhook route
|
|
31
|
+
// Register HTTP webhook route — derive path from callbackUrl
|
|
32
32
|
const channels = api.config?.channels as Record<string, Record<string, unknown>> | undefined;
|
|
33
|
-
const
|
|
33
|
+
const callbackUrl = channels?.bitrix24?.callbackUrl as string | undefined;
|
|
34
|
+
const callbackPath = callbackUrl ? new URL(callbackUrl).pathname : '/hooks/bitrix24';
|
|
34
35
|
|
|
35
36
|
api.registerHttpRoute({
|
|
36
37
|
path: callbackPath,
|
package/package.json
CHANGED
package/src/access-control.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Bitrix24AccountConfig } from './types.js';
|
|
2
|
+
import type { PluginRuntime, ChannelPairingAdapter } from './runtime.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Normalize an allowFrom entry — strip platform prefixes.
|
|
5
6
|
* "bitrix24:42" → "42", "b24:42" → "42", "bx24:42" → "42", "42" → "42"
|
|
6
7
|
*/
|
|
7
8
|
export function normalizeAllowEntry(entry: string): string {
|
|
8
|
-
return entry.trim().replace(/^(bitrix24|b24|bx24)
|
|
9
|
+
return entry.trim().replace(/^(bitrix24|b24|bx24):/i, '');
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -17,7 +18,7 @@ export function checkAccess(
|
|
|
17
18
|
senderId: string,
|
|
18
19
|
config: Bitrix24AccountConfig,
|
|
19
20
|
): boolean {
|
|
20
|
-
const policy = config.dmPolicy ?? '
|
|
21
|
+
const policy = config.dmPolicy ?? 'pairing';
|
|
21
22
|
|
|
22
23
|
switch (policy) {
|
|
23
24
|
case 'open':
|
|
@@ -31,10 +32,66 @@ export function checkAccess(
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
case 'pairing':
|
|
34
|
-
// Pairing
|
|
35
|
-
return
|
|
35
|
+
// Pairing requires runtime — use checkAccessWithPairing() instead
|
|
36
|
+
return false;
|
|
36
37
|
|
|
37
38
|
default:
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
export type AccessResult = 'allow' | 'deny' | 'pairing';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pairing-aware access check.
|
|
47
|
+
* Merges config allowFrom with file-based allowFrom store.
|
|
48
|
+
* For pairing mode, upserts a pairing request and sends the reply.
|
|
49
|
+
*/
|
|
50
|
+
export async function checkAccessWithPairing(params: {
|
|
51
|
+
senderId: string;
|
|
52
|
+
config: Bitrix24AccountConfig;
|
|
53
|
+
runtime: PluginRuntime;
|
|
54
|
+
accountId: string;
|
|
55
|
+
pairingAdapter: ChannelPairingAdapter;
|
|
56
|
+
sendReply: (text: string) => Promise<void>;
|
|
57
|
+
logger?: { debug: (...args: unknown[]) => void };
|
|
58
|
+
}): Promise<AccessResult> {
|
|
59
|
+
const { senderId, config, runtime, accountId, pairingAdapter, sendReply, logger } = params;
|
|
60
|
+
const policy = config.dmPolicy ?? 'pairing';
|
|
61
|
+
|
|
62
|
+
if (policy === 'open') return 'allow';
|
|
63
|
+
|
|
64
|
+
// Read file-based allowFrom store and merge with config
|
|
65
|
+
const storeAllowFrom = await runtime.channel.pairing.readAllowFromStore('bitrix24', '', accountId);
|
|
66
|
+
const configAllowFrom = (config.allowFrom ?? []).map(normalizeAllowEntry);
|
|
67
|
+
const merged = [...new Set([...configAllowFrom, ...storeAllowFrom])];
|
|
68
|
+
const normalizedSender = normalizeAllowEntry(String(senderId));
|
|
69
|
+
|
|
70
|
+
if (merged.includes(normalizedSender)) return 'allow';
|
|
71
|
+
|
|
72
|
+
if (policy === 'allowlist') {
|
|
73
|
+
logger?.debug('Access denied (allowlist)', { senderId });
|
|
74
|
+
return 'deny';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// policy === 'pairing'
|
|
78
|
+
const { code, created } = await runtime.channel.pairing.upsertPairingRequest({
|
|
79
|
+
channel: 'bitrix24',
|
|
80
|
+
id: senderId,
|
|
81
|
+
accountId,
|
|
82
|
+
meta: {},
|
|
83
|
+
pairingAdapter,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (created) {
|
|
87
|
+
const reply = runtime.channel.pairing.buildPairingReply({
|
|
88
|
+
code,
|
|
89
|
+
channel: 'bitrix24',
|
|
90
|
+
accountId,
|
|
91
|
+
});
|
|
92
|
+
await sendReply(reply.text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
logger?.debug('Pairing request handled', { senderId, code, created });
|
|
96
|
+
return 'pairing';
|
|
97
|
+
}
|
package/src/api.ts
CHANGED
|
@@ -253,6 +253,33 @@ export class Bitrix24Api {
|
|
|
253
253
|
return result.result;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
async registerCommand(
|
|
257
|
+
webhookUrl: string,
|
|
258
|
+
params: {
|
|
259
|
+
BOT_ID: number;
|
|
260
|
+
COMMAND: string;
|
|
261
|
+
COMMON?: 'Y' | 'N';
|
|
262
|
+
HIDDEN?: 'Y' | 'N';
|
|
263
|
+
EXTRANET_SUPPORT?: 'Y' | 'N';
|
|
264
|
+
LANG: Array<{ LANGUAGE_ID: string; TITLE: string; PARAMS?: string }>;
|
|
265
|
+
EVENT_COMMAND_ADD: string;
|
|
266
|
+
},
|
|
267
|
+
): Promise<number> {
|
|
268
|
+
const result = await this.callWebhook<number>(
|
|
269
|
+
webhookUrl,
|
|
270
|
+
'imbot.command.register',
|
|
271
|
+
params,
|
|
272
|
+
);
|
|
273
|
+
return result.result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async unregisterCommand(webhookUrl: string, commandId: number): Promise<boolean> {
|
|
277
|
+
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.command.unregister', {
|
|
278
|
+
COMMAND_ID: commandId,
|
|
279
|
+
});
|
|
280
|
+
return result.result;
|
|
281
|
+
}
|
|
282
|
+
|
|
256
283
|
async unregisterBot(webhookUrl: string, botId: number): Promise<boolean> {
|
|
257
284
|
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.unregister', {
|
|
258
285
|
BOT_ID: botId,
|
|
@@ -260,6 +287,86 @@ export class Bitrix24Api {
|
|
|
260
287
|
return result.result;
|
|
261
288
|
}
|
|
262
289
|
|
|
290
|
+
// ─── File / Disk methods ─────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get file info including DOWNLOAD_URL.
|
|
294
|
+
* Uses the user's access token (file belongs to the user's disk).
|
|
295
|
+
*/
|
|
296
|
+
async getFileInfo(
|
|
297
|
+
clientEndpoint: string,
|
|
298
|
+
accessToken: string,
|
|
299
|
+
fileId: number,
|
|
300
|
+
): Promise<{ DOWNLOAD_URL: string; [key: string]: unknown }> {
|
|
301
|
+
const result = await this.callWithToken<{ DOWNLOAD_URL: string; [key: string]: unknown }>(
|
|
302
|
+
clientEndpoint,
|
|
303
|
+
'disk.file.get',
|
|
304
|
+
accessToken,
|
|
305
|
+
{ id: fileId },
|
|
306
|
+
);
|
|
307
|
+
return result.result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get the disk folder ID for a chat (needed for file uploads).
|
|
312
|
+
*/
|
|
313
|
+
async getChatFolder(
|
|
314
|
+
clientEndpoint: string,
|
|
315
|
+
accessToken: string,
|
|
316
|
+
chatId: number,
|
|
317
|
+
): Promise<number> {
|
|
318
|
+
const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
|
|
319
|
+
clientEndpoint,
|
|
320
|
+
'im.disk.folder.get',
|
|
321
|
+
accessToken,
|
|
322
|
+
{ CHAT_ID: chatId },
|
|
323
|
+
);
|
|
324
|
+
return result.result.ID;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Upload a file to a disk folder (base64 encoded).
|
|
329
|
+
* Returns the disk file ID.
|
|
330
|
+
*/
|
|
331
|
+
async uploadFile(
|
|
332
|
+
clientEndpoint: string,
|
|
333
|
+
accessToken: string,
|
|
334
|
+
folderId: number,
|
|
335
|
+
fileName: string,
|
|
336
|
+
content: Buffer,
|
|
337
|
+
): Promise<number> {
|
|
338
|
+
const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
|
|
339
|
+
clientEndpoint,
|
|
340
|
+
'disk.folder.uploadfile',
|
|
341
|
+
accessToken,
|
|
342
|
+
{
|
|
343
|
+
id: folderId,
|
|
344
|
+
data: { NAME: fileName },
|
|
345
|
+
fileContent: [fileName, content.toString('base64')],
|
|
346
|
+
generateUniqueName: true,
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
return result.result.ID;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Publish an uploaded file to a chat.
|
|
354
|
+
*/
|
|
355
|
+
async commitFileToChat(
|
|
356
|
+
clientEndpoint: string,
|
|
357
|
+
accessToken: string,
|
|
358
|
+
chatId: number,
|
|
359
|
+
diskId: number,
|
|
360
|
+
): Promise<boolean> {
|
|
361
|
+
const result = await this.callWithToken<boolean>(
|
|
362
|
+
clientEndpoint,
|
|
363
|
+
'im.disk.file.commit',
|
|
364
|
+
accessToken,
|
|
365
|
+
{ CHAT_ID: chatId, DISK_ID: diskId },
|
|
366
|
+
);
|
|
367
|
+
return result.result;
|
|
368
|
+
}
|
|
369
|
+
|
|
263
370
|
destroy(): void {
|
|
264
371
|
this.rateLimiter.destroy();
|
|
265
372
|
}
|