@ihazz/bitrix24 0.1.5 → 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/package.json +1 -1
- package/src/access-control.ts +61 -4
- package/src/api.ts +80 -0
- package/src/channel.ts +134 -25
- package/src/commands.ts +1 -1
- package/src/config-schema.ts +1 -1
- package/src/inbound-handler.ts +1 -9
- package/src/media-service.ts +186 -0
- package/src/runtime.ts +23 -0
- package/src/types.ts +1 -0
- 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/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
|
@@ -287,6 +287,86 @@ export class Bitrix24Api {
|
|
|
287
287
|
return result.result;
|
|
288
288
|
}
|
|
289
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
|
+
|
|
290
370
|
destroy(): void {
|
|
291
371
|
this.rateLimiter.destroy();
|
|
292
372
|
}
|
package/src/channel.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { basename } from 'node:path';
|
|
2
3
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
4
|
import { listAccountIds, resolveAccount, getConfig } from './config.js';
|
|
4
5
|
import { Bitrix24Api } from './api.js';
|
|
5
6
|
import { SendService } from './send-service.js';
|
|
7
|
+
import { MediaService } from './media-service.js';
|
|
8
|
+
import type { DownloadedMedia } from './media-service.js';
|
|
6
9
|
import { InboundHandler } from './inbound-handler.js';
|
|
7
|
-
import {
|
|
10
|
+
import { normalizeAllowEntry, checkAccessWithPairing } from './access-control.js';
|
|
8
11
|
import { defaultLogger } from './utils.js';
|
|
9
12
|
import { getBitrix24Runtime } from './runtime.js';
|
|
13
|
+
import type { ChannelPairingAdapter } from './runtime.js';
|
|
10
14
|
import { OPENCLAW_COMMANDS } from './commands.js';
|
|
11
15
|
import type {
|
|
12
16
|
B24MsgContext,
|
|
@@ -28,6 +32,7 @@ interface Logger {
|
|
|
28
32
|
interface GatewayState {
|
|
29
33
|
api: Bitrix24Api;
|
|
30
34
|
sendService: SendService;
|
|
35
|
+
mediaService: MediaService;
|
|
31
36
|
inboundHandler: InboundHandler;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -35,18 +40,19 @@ let gatewayState: GatewayState | null = null;
|
|
|
35
40
|
|
|
36
41
|
// ─── Keyboard / Button conversion ────────────────────────────────────────────
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
/** Generic button format used by OpenClaw channelData. */
|
|
44
|
+
export interface ChannelButton {
|
|
39
45
|
text: string;
|
|
40
46
|
callback_data?: string;
|
|
41
47
|
style?: string;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
|
-
* Convert
|
|
46
|
-
*
|
|
47
|
-
*
|
|
51
|
+
* Convert OpenClaw button rows to B24 flat KEYBOARD array.
|
|
52
|
+
* Input: Array<Array<{ text, callback_data, style }>>
|
|
53
|
+
* Output: flat array with { TYPE: 'NEWLINE' } separators between rows.
|
|
48
54
|
*/
|
|
49
|
-
function convertButtonsToKeyboard(rows:
|
|
55
|
+
export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
|
|
50
56
|
const keyboard: B24Keyboard = [];
|
|
51
57
|
|
|
52
58
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -86,9 +92,9 @@ function convertButtonsToKeyboard(rows: TelegramButton[][]): B24Keyboard {
|
|
|
86
92
|
|
|
87
93
|
/**
|
|
88
94
|
* Extract B24 keyboard from a dispatcher payload's channelData.
|
|
89
|
-
* Checks bitrix24-specific data first, then falls back to
|
|
95
|
+
* Checks bitrix24-specific data first, then falls back to OpenClaw generic button format.
|
|
90
96
|
*/
|
|
91
|
-
function extractKeyboardFromPayload(
|
|
97
|
+
export function extractKeyboardFromPayload(
|
|
92
98
|
payload: { channelData?: Record<string, unknown> },
|
|
93
99
|
): B24Keyboard | undefined {
|
|
94
100
|
const cd = payload.channelData;
|
|
@@ -100,8 +106,8 @@ function extractKeyboardFromPayload(
|
|
|
100
106
|
return b24Data.keyboard;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
// Translate from
|
|
104
|
-
const tgData = cd.telegram as { buttons?:
|
|
109
|
+
// Translate from OpenClaw generic button format (channelData.telegram key)
|
|
110
|
+
const tgData = cd.telegram as { buttons?: ChannelButton[][] } | undefined;
|
|
105
111
|
if (tgData?.buttons?.length) {
|
|
106
112
|
return convertButtonsToKeyboard(tgData.buttons);
|
|
107
113
|
}
|
|
@@ -270,7 +276,7 @@ export const bitrix24Plugin = {
|
|
|
270
276
|
|
|
271
277
|
capabilities: {
|
|
272
278
|
chatTypes: ['direct', 'group'] as const,
|
|
273
|
-
media:
|
|
279
|
+
media: true,
|
|
274
280
|
reactions: false,
|
|
275
281
|
threads: false,
|
|
276
282
|
nativeCommands: true,
|
|
@@ -283,12 +289,33 @@ export const bitrix24Plugin = {
|
|
|
283
289
|
},
|
|
284
290
|
|
|
285
291
|
security: {
|
|
286
|
-
resolveDmPolicy: (
|
|
287
|
-
account
|
|
292
|
+
resolveDmPolicy: (params: { cfg?: Record<string, unknown>; accountId?: string; account?: { config?: Record<string, unknown> } }) => ({
|
|
293
|
+
policy: (params.account?.config?.dmPolicy as string) ?? 'pairing',
|
|
294
|
+
allowFrom: (params.account?.config?.allowFrom as string[]) ?? [],
|
|
295
|
+
policyPath: 'channels.bitrix24.dmPolicy',
|
|
296
|
+
allowFromPath: 'channels.bitrix24.',
|
|
297
|
+
approveHint: 'openclaw pairing approve bitrix24 <CODE>',
|
|
298
|
+
normalizeEntry: (raw: string) => raw.replace(/^(bitrix24|b24|bx24):/i, ''),
|
|
299
|
+
}),
|
|
288
300
|
normalizeAllowFrom: (entry: string) =>
|
|
289
|
-
entry.replace(/^(bitrix24|b24|bx24)
|
|
301
|
+
entry.replace(/^(bitrix24|b24|bx24):/i, ''),
|
|
290
302
|
},
|
|
291
303
|
|
|
304
|
+
pairing: {
|
|
305
|
+
idLabel: 'bitrix24UserId',
|
|
306
|
+
normalizeAllowEntry: (entry: string) => normalizeAllowEntry(entry),
|
|
307
|
+
notifyApproval: async (params: { cfg: Record<string, unknown>; id: string; runtime?: unknown }) => {
|
|
308
|
+
const { config: acctCfg } = resolveAccount(params.cfg);
|
|
309
|
+
if (!acctCfg.webhookUrl) return;
|
|
310
|
+
const api = new Bitrix24Api();
|
|
311
|
+
try {
|
|
312
|
+
await api.sendMessage(acctCfg.webhookUrl, params.id, '\u2705 OpenClaw access approved.');
|
|
313
|
+
} finally {
|
|
314
|
+
api.destroy();
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
} satisfies ChannelPairingAdapter,
|
|
318
|
+
|
|
292
319
|
outbound: {
|
|
293
320
|
deliveryMode: 'direct' as const,
|
|
294
321
|
|
|
@@ -409,6 +436,7 @@ export const bitrix24Plugin = {
|
|
|
409
436
|
const clientId = createHash('md5').update(config.webhookUrl).digest('hex');
|
|
410
437
|
const api = new Bitrix24Api({ logger, clientId });
|
|
411
438
|
const sendService = new SendService(api, logger);
|
|
439
|
+
const mediaService = new MediaService(api, logger);
|
|
412
440
|
|
|
413
441
|
// Register or update bot on the B24 portal
|
|
414
442
|
const botId = await ensureBotRegistered(api, config, logger);
|
|
@@ -435,6 +463,64 @@ export const bitrix24Plugin = {
|
|
|
435
463
|
const runtime = getBitrix24Runtime();
|
|
436
464
|
const cfg = runtime.config.loadConfig();
|
|
437
465
|
|
|
466
|
+
// Pairing-aware access control
|
|
467
|
+
const accessResult = await checkAccessWithPairing({
|
|
468
|
+
senderId: msgCtx.senderId,
|
|
469
|
+
config,
|
|
470
|
+
runtime,
|
|
471
|
+
accountId: ctx.accountId,
|
|
472
|
+
pairingAdapter: bitrix24Plugin.pairing,
|
|
473
|
+
sendReply: async (text: string) => {
|
|
474
|
+
const replySendCtx = {
|
|
475
|
+
webhookUrl: config.webhookUrl,
|
|
476
|
+
clientEndpoint: msgCtx.clientEndpoint,
|
|
477
|
+
botToken: msgCtx.botToken,
|
|
478
|
+
dialogId: msgCtx.chatId,
|
|
479
|
+
};
|
|
480
|
+
await sendService.sendText(replySendCtx, text, { convertMarkdown: false });
|
|
481
|
+
},
|
|
482
|
+
logger,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (accessResult !== 'allow') {
|
|
486
|
+
logger.debug(`Message blocked (${accessResult})`, { senderId: msgCtx.senderId });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Download media files if present
|
|
491
|
+
let mediaFields: Record<string, unknown> = {};
|
|
492
|
+
if (msgCtx.media.length > 0) {
|
|
493
|
+
const downloaded = (await Promise.all(
|
|
494
|
+
msgCtx.media.map((m) =>
|
|
495
|
+
mediaService.downloadMedia({
|
|
496
|
+
fileId: m.id,
|
|
497
|
+
fileName: m.name,
|
|
498
|
+
extension: m.extension,
|
|
499
|
+
clientEndpoint: msgCtx.clientEndpoint,
|
|
500
|
+
userToken: msgCtx.userToken,
|
|
501
|
+
}),
|
|
502
|
+
),
|
|
503
|
+
)).filter(Boolean) as DownloadedMedia[];
|
|
504
|
+
|
|
505
|
+
if (downloaded.length > 0) {
|
|
506
|
+
mediaFields = {
|
|
507
|
+
MediaPath: downloaded[0].path,
|
|
508
|
+
MediaType: downloaded[0].contentType,
|
|
509
|
+
MediaUrl: downloaded[0].path,
|
|
510
|
+
MediaPaths: downloaded.map((m) => m.path),
|
|
511
|
+
MediaUrls: downloaded.map((m) => m.path),
|
|
512
|
+
MediaTypes: downloaded.map((m) => m.contentType),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Use placeholder body for media-only messages
|
|
518
|
+
let body = msgCtx.text;
|
|
519
|
+
if (!body && msgCtx.media.length > 0) {
|
|
520
|
+
const hasImage = msgCtx.media.some((m) => m.type === 'image');
|
|
521
|
+
body = hasImage ? '<media:image>' : '<media:document>';
|
|
522
|
+
}
|
|
523
|
+
|
|
438
524
|
// Resolve which agent handles this conversation
|
|
439
525
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
440
526
|
cfg,
|
|
@@ -454,9 +540,9 @@ export const bitrix24Plugin = {
|
|
|
454
540
|
|
|
455
541
|
// Build and finalize inbound context for OpenClaw agent
|
|
456
542
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
457
|
-
Body:
|
|
458
|
-
BodyForAgent:
|
|
459
|
-
RawBody:
|
|
543
|
+
Body: body,
|
|
544
|
+
BodyForAgent: body,
|
|
545
|
+
RawBody: body,
|
|
460
546
|
From: `bitrix24:${msgCtx.chatId}`,
|
|
461
547
|
To: `bitrix24:${msgCtx.chatId}`,
|
|
462
548
|
SessionKey: route.sessionKey,
|
|
@@ -473,6 +559,7 @@ export const bitrix24Plugin = {
|
|
|
473
559
|
CommandAuthorized: true,
|
|
474
560
|
OriginatingChannel: 'bitrix24',
|
|
475
561
|
OriginatingTo: `bitrix24:${msgCtx.chatId}`,
|
|
562
|
+
...mediaFields,
|
|
476
563
|
});
|
|
477
564
|
|
|
478
565
|
const sendCtx = {
|
|
@@ -489,6 +576,18 @@ export const bitrix24Plugin = {
|
|
|
489
576
|
cfg,
|
|
490
577
|
dispatcherOptions: {
|
|
491
578
|
deliver: async (payload) => {
|
|
579
|
+
// Send media if present in reply
|
|
580
|
+
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
581
|
+
for (const mediaUrl of mediaUrls) {
|
|
582
|
+
await mediaService.uploadMediaToChat({
|
|
583
|
+
localPath: mediaUrl,
|
|
584
|
+
fileName: basename(mediaUrl),
|
|
585
|
+
chatId: Number(msgCtx.chatInternalId),
|
|
586
|
+
clientEndpoint: msgCtx.clientEndpoint,
|
|
587
|
+
botToken: msgCtx.botToken,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
// Send text if present
|
|
492
591
|
if (payload.text) {
|
|
493
592
|
const keyboard = extractKeyboardFromPayload(payload);
|
|
494
593
|
await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
|
|
@@ -529,15 +628,25 @@ export const bitrix24Plugin = {
|
|
|
529
628
|
|
|
530
629
|
logger.info('Inbound command', { commandName, commandParams, senderId, dialogId });
|
|
531
630
|
|
|
532
|
-
// Access control
|
|
533
|
-
if (!checkAccess(senderId, config)) {
|
|
534
|
-
logger.debug(`Access denied for command from user ${senderId}`);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
631
|
const runtime = getBitrix24Runtime();
|
|
539
632
|
const cfg = runtime.config.loadConfig();
|
|
540
633
|
|
|
634
|
+
// Pairing-aware access control (commands don't send pairing replies)
|
|
635
|
+
const accessResult = await checkAccessWithPairing({
|
|
636
|
+
senderId,
|
|
637
|
+
config,
|
|
638
|
+
runtime,
|
|
639
|
+
accountId: ctx.accountId,
|
|
640
|
+
pairingAdapter: bitrix24Plugin.pairing,
|
|
641
|
+
sendReply: async () => {},
|
|
642
|
+
logger,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (accessResult !== 'allow') {
|
|
646
|
+
logger.debug(`Command blocked (${accessResult})`, { senderId });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
541
650
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
542
651
|
cfg,
|
|
543
652
|
channel: 'bitrix24',
|
|
@@ -545,7 +654,7 @@ export const bitrix24Plugin = {
|
|
|
545
654
|
peer: { kind: isDm ? 'direct' : 'group', id: dialogId },
|
|
546
655
|
});
|
|
547
656
|
|
|
548
|
-
// Native commands use a separate slash-command session
|
|
657
|
+
// Native commands use a separate slash-command session
|
|
549
658
|
const slashSessionKey = `bitrix24:slash:${senderId}`;
|
|
550
659
|
|
|
551
660
|
const inboundCtx = runtime.channel.reply.finalizeInboundContext({
|
|
@@ -628,7 +737,7 @@ export const bitrix24Plugin = {
|
|
|
628
737
|
},
|
|
629
738
|
});
|
|
630
739
|
|
|
631
|
-
gatewayState = { api, sendService, inboundHandler };
|
|
740
|
+
gatewayState = { api, sendService, mediaService, inboundHandler };
|
|
632
741
|
|
|
633
742
|
logger.info(`[${ctx.accountId}] Bitrix24 channel started`);
|
|
634
743
|
|
package/src/commands.ts
CHANGED
package/src/config-schema.ts
CHANGED
|
@@ -7,7 +7,7 @@ const AccountSchema = z.object({
|
|
|
7
7
|
botCode: z.string().optional().default('openclaw'),
|
|
8
8
|
botAvatar: z.string().optional(),
|
|
9
9
|
callbackUrl: z.string().url().optional(),
|
|
10
|
-
dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('
|
|
10
|
+
dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('pairing'),
|
|
11
11
|
allowFrom: z.array(z.string()).optional(),
|
|
12
12
|
showTyping: z.boolean().optional().default(true),
|
|
13
13
|
streamUpdates: z.boolean().optional().default(false),
|
package/src/inbound-handler.ts
CHANGED
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
B24MediaItem,
|
|
12
12
|
} from './types.js';
|
|
13
13
|
import { Dedup } from './dedup.js';
|
|
14
|
-
import { checkAccess } from './access-control.js';
|
|
15
14
|
import type { Bitrix24AccountConfig } from './types.js';
|
|
16
15
|
import { defaultLogger } from './utils.js';
|
|
17
16
|
|
|
@@ -109,14 +108,6 @@ export class InboundHandler {
|
|
|
109
108
|
return true;
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
const senderId = String(params.FROM_USER_ID);
|
|
113
|
-
|
|
114
|
-
// Access control
|
|
115
|
-
if (!checkAccess(senderId, this.config)) {
|
|
116
|
-
this.logger.debug(`Access denied for user ${senderId}`);
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
111
|
// Extract bot entry
|
|
121
112
|
const botEntry = extractBotEntry(event.data.BOT);
|
|
122
113
|
if (!botEntry) {
|
|
@@ -183,5 +174,6 @@ function normalizeFiles(files?: Record<string, B24File>): B24MediaItem[] {
|
|
|
183
174
|
extension: file.extension,
|
|
184
175
|
size: file.size,
|
|
185
176
|
type: file.image ? 'image' as const : 'file' as const,
|
|
177
|
+
urlDownload: file.urlDownload || undefined,
|
|
186
178
|
}));
|
|
187
179
|
}
|