@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
import { Bitrix24Api } from './api.js';
|
|
6
|
+
import { defaultLogger } from './utils.js';
|
|
7
|
+
|
|
8
|
+
interface Logger {
|
|
9
|
+
info: (...args: unknown[]) => void;
|
|
10
|
+
warn: (...args: unknown[]) => void;
|
|
11
|
+
error: (...args: unknown[]) => void;
|
|
12
|
+
debug: (...args: unknown[]) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DownloadedMedia {
|
|
16
|
+
path: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MIME_MAP: Record<string, string> = {
|
|
22
|
+
jpg: 'image/jpeg',
|
|
23
|
+
jpeg: 'image/jpeg',
|
|
24
|
+
png: 'image/png',
|
|
25
|
+
gif: 'image/gif',
|
|
26
|
+
webp: 'image/webp',
|
|
27
|
+
svg: 'image/svg+xml',
|
|
28
|
+
bmp: 'image/bmp',
|
|
29
|
+
ico: 'image/x-icon',
|
|
30
|
+
tiff: 'image/tiff',
|
|
31
|
+
tif: 'image/tiff',
|
|
32
|
+
mp4: 'video/mp4',
|
|
33
|
+
webm: 'video/webm',
|
|
34
|
+
avi: 'video/x-msvideo',
|
|
35
|
+
mov: 'video/quicktime',
|
|
36
|
+
mkv: 'video/x-matroska',
|
|
37
|
+
mp3: 'audio/mpeg',
|
|
38
|
+
wav: 'audio/wav',
|
|
39
|
+
ogg: 'audio/ogg',
|
|
40
|
+
flac: 'audio/flac',
|
|
41
|
+
pdf: 'application/pdf',
|
|
42
|
+
doc: 'application/msword',
|
|
43
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
44
|
+
xls: 'application/vnd.ms-excel',
|
|
45
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
46
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
47
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
48
|
+
zip: 'application/zip',
|
|
49
|
+
rar: 'application/x-rar-compressed',
|
|
50
|
+
gz: 'application/gzip',
|
|
51
|
+
tar: 'application/x-tar',
|
|
52
|
+
txt: 'text/plain',
|
|
53
|
+
csv: 'text/csv',
|
|
54
|
+
html: 'text/html',
|
|
55
|
+
css: 'text/css',
|
|
56
|
+
js: 'application/javascript',
|
|
57
|
+
json: 'application/json',
|
|
58
|
+
xml: 'application/xml',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function mimeFromExtension(ext: string): string {
|
|
62
|
+
return MIME_MAP[ext.toLowerCase()] ?? 'application/octet-stream';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const MEDIA_DIR = join(tmpdir(), 'openclaw-b24-media');
|
|
66
|
+
|
|
67
|
+
export class MediaService {
|
|
68
|
+
private api: Bitrix24Api;
|
|
69
|
+
private logger: Logger;
|
|
70
|
+
private dirReady = false;
|
|
71
|
+
|
|
72
|
+
constructor(api: Bitrix24Api, logger?: Logger) {
|
|
73
|
+
this.api = api;
|
|
74
|
+
this.logger = logger ?? defaultLogger;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async ensureDir(): Promise<void> {
|
|
78
|
+
if (this.dirReady) return;
|
|
79
|
+
await mkdir(MEDIA_DIR, { recursive: true });
|
|
80
|
+
this.dirReady = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Download a file from B24 and save it locally.
|
|
85
|
+
* Uses the user's access token to call disk.file.get for the download URL.
|
|
86
|
+
*/
|
|
87
|
+
async downloadMedia(params: {
|
|
88
|
+
fileId: string;
|
|
89
|
+
fileName: string;
|
|
90
|
+
extension: string;
|
|
91
|
+
clientEndpoint: string;
|
|
92
|
+
userToken: string;
|
|
93
|
+
}): Promise<DownloadedMedia | null> {
|
|
94
|
+
const { fileId, fileName, extension, clientEndpoint, userToken } = params;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Get download URL from B24 REST API
|
|
98
|
+
const fileInfo = await this.api.getFileInfo(
|
|
99
|
+
clientEndpoint,
|
|
100
|
+
userToken,
|
|
101
|
+
Number(fileId),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const downloadUrl = fileInfo.DOWNLOAD_URL;
|
|
105
|
+
if (!downloadUrl) {
|
|
106
|
+
this.logger.warn('No DOWNLOAD_URL for file', { fileId });
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Download the file
|
|
111
|
+
const response = await fetch(downloadUrl);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
this.logger.warn('Failed to download file', {
|
|
114
|
+
fileId,
|
|
115
|
+
status: response.status,
|
|
116
|
+
});
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
121
|
+
|
|
122
|
+
// Save to temp directory with UUID prefix for uniqueness
|
|
123
|
+
await this.ensureDir();
|
|
124
|
+
const savePath = join(MEDIA_DIR, `${randomUUID()}_${fileName}`);
|
|
125
|
+
await writeFile(savePath, buffer);
|
|
126
|
+
|
|
127
|
+
const contentType = mimeFromExtension(extension);
|
|
128
|
+
this.logger.debug('Downloaded media', {
|
|
129
|
+
fileId,
|
|
130
|
+
fileName,
|
|
131
|
+
contentType,
|
|
132
|
+
size: buffer.length,
|
|
133
|
+
path: savePath,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return { path: savePath, contentType, name: fileName };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
this.logger.error('Error downloading media', { fileId, error: err });
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Upload a local file to a B24 chat.
|
|
145
|
+
* 3-step process: get folder → upload → commit.
|
|
146
|
+
*/
|
|
147
|
+
async uploadMediaToChat(params: {
|
|
148
|
+
localPath: string;
|
|
149
|
+
fileName: string;
|
|
150
|
+
chatId: number;
|
|
151
|
+
clientEndpoint: string;
|
|
152
|
+
botToken: string;
|
|
153
|
+
}): Promise<boolean> {
|
|
154
|
+
const { localPath, fileName, chatId, clientEndpoint, botToken } = params;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Read the file
|
|
158
|
+
const content = await readFile(localPath);
|
|
159
|
+
|
|
160
|
+
// Step 1: Get chat folder
|
|
161
|
+
const folderId = await this.api.getChatFolder(clientEndpoint, botToken, chatId);
|
|
162
|
+
|
|
163
|
+
// Step 2: Upload file (base64)
|
|
164
|
+
const diskId = await this.api.uploadFile(
|
|
165
|
+
clientEndpoint,
|
|
166
|
+
botToken,
|
|
167
|
+
folderId,
|
|
168
|
+
fileName,
|
|
169
|
+
content,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Step 3: Publish to chat
|
|
173
|
+
await this.api.commitFileToChat(clientEndpoint, botToken, chatId, diskId);
|
|
174
|
+
|
|
175
|
+
this.logger.debug('Uploaded media to chat', { fileName, chatId, diskId });
|
|
176
|
+
return true;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
this.logger.error('Error uploading media to chat', {
|
|
179
|
+
fileName,
|
|
180
|
+
chatId,
|
|
181
|
+
error: err,
|
|
182
|
+
});
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/message-utils.ts
CHANGED
|
@@ -90,15 +90,25 @@ export function buildKeyboard(
|
|
|
90
90
|
}>
|
|
91
91
|
>,
|
|
92
92
|
): B24Keyboard {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
const keyboard: B24Keyboard = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < rows.length; i++) {
|
|
96
|
+
for (const btn of rows[i]) {
|
|
97
|
+
keyboard.push({
|
|
98
|
+
TEXT: btn.text,
|
|
99
|
+
...(btn.command ? { COMMAND: btn.command, COMMAND_PARAMS: btn.commandParams ?? '' } : {}),
|
|
100
|
+
...(btn.link ? { LINK: btn.link } : {}),
|
|
101
|
+
BG_COLOR: btn.bgColor ?? '#29619b',
|
|
102
|
+
TEXT_COLOR: btn.textColor ?? '#fff',
|
|
103
|
+
DISPLAY: btn.fullWidth ? 'LINE' : 'BLOCK',
|
|
104
|
+
BLOCK: btn.disableAfterClick ? 'Y' : 'N',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (i < rows.length - 1) {
|
|
109
|
+
keyboard.push({ TYPE: 'NEWLINE' });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return keyboard;
|
|
104
114
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -6,6 +6,13 @@ interface ReplyPayload {
|
|
|
6
6
|
channelData?: Record<string, unknown>;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/** Adapter provided by the channel plugin for pairing support */
|
|
10
|
+
export interface ChannelPairingAdapter {
|
|
11
|
+
idLabel: string;
|
|
12
|
+
normalizeAllowEntry?: (entry: string) => string;
|
|
13
|
+
notifyApproval?: (params: { cfg: Record<string, unknown>; id: string; runtime?: unknown }) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
export interface PluginRuntime {
|
|
10
17
|
config: {
|
|
11
18
|
loadConfig: () => Record<string, unknown>;
|
|
@@ -50,6 +57,22 @@ export interface PluginRuntime {
|
|
|
50
57
|
recordInboundSession: (params: Record<string, unknown>) => Promise<void>;
|
|
51
58
|
[key: string]: unknown;
|
|
52
59
|
};
|
|
60
|
+
pairing: {
|
|
61
|
+
readAllowFromStore: (channel: string, env: string, accountId: string) => Promise<string[]>;
|
|
62
|
+
upsertPairingRequest: (params: {
|
|
63
|
+
channel: string;
|
|
64
|
+
id: string;
|
|
65
|
+
accountId: string;
|
|
66
|
+
meta?: Record<string, unknown>;
|
|
67
|
+
pairingAdapter: ChannelPairingAdapter;
|
|
68
|
+
}) => Promise<{ code: string; created: boolean }>;
|
|
69
|
+
buildPairingReply: (params: {
|
|
70
|
+
code: string;
|
|
71
|
+
channel: string;
|
|
72
|
+
accountId: string;
|
|
73
|
+
}) => { text: string };
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
};
|
|
53
76
|
[key: string]: unknown;
|
|
54
77
|
};
|
|
55
78
|
logging: {
|
package/src/types.ts
CHANGED
|
@@ -211,14 +211,23 @@ export interface KeyboardButton {
|
|
|
211
211
|
COMMAND?: string;
|
|
212
212
|
COMMAND_PARAMS?: string;
|
|
213
213
|
BG_COLOR?: string;
|
|
214
|
+
BG_COLOR_TOKEN?: 'primary' | 'secondary' | 'alert' | 'base';
|
|
214
215
|
TEXT_COLOR?: string;
|
|
215
216
|
DISPLAY?: 'LINE' | 'BLOCK';
|
|
216
217
|
DISABLED?: 'Y' | 'N';
|
|
217
218
|
BLOCK?: 'Y' | 'N';
|
|
218
219
|
LINK?: string;
|
|
220
|
+
WIDTH?: number;
|
|
221
|
+
ACTION?: 'PUT' | 'SEND' | 'COPY' | 'CALL' | 'DIALOG';
|
|
222
|
+
ACTION_VALUE?: string;
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
export
|
|
225
|
+
export interface KeyboardNewline {
|
|
226
|
+
TYPE: 'NEWLINE';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** B24 keyboard: flat array with NEWLINE separators between rows */
|
|
230
|
+
export type B24Keyboard = (KeyboardButton | KeyboardNewline)[];
|
|
222
231
|
|
|
223
232
|
export interface SendMessageOptions {
|
|
224
233
|
ATTACH?: unknown;
|
|
@@ -242,7 +251,6 @@ export interface Bitrix24AccountConfig {
|
|
|
242
251
|
botName?: string;
|
|
243
252
|
botCode?: string;
|
|
244
253
|
botAvatar?: string;
|
|
245
|
-
callbackPath?: string;
|
|
246
254
|
callbackUrl?: string;
|
|
247
255
|
dmPolicy?: 'open' | 'allowlist' | 'pairing';
|
|
248
256
|
allowFrom?: string[];
|
|
@@ -285,6 +293,7 @@ export interface B24MediaItem {
|
|
|
285
293
|
extension: string;
|
|
286
294
|
size: number;
|
|
287
295
|
type: 'image' | 'file';
|
|
296
|
+
urlDownload?: string;
|
|
288
297
|
}
|
|
289
298
|
|
|
290
299
|
// ─── Portal State ────────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { checkAccess, normalizeAllowEntry } from '../src/access-control.js';
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { checkAccess, normalizeAllowEntry, checkAccessWithPairing } from '../src/access-control.js';
|
|
3
|
+
import type { PluginRuntime, ChannelPairingAdapter } from '../src/runtime.js';
|
|
3
4
|
|
|
4
5
|
describe('normalizeAllowEntry', () => {
|
|
5
6
|
it('strips bitrix24: prefix', () => {
|
|
@@ -14,6 +15,12 @@ describe('normalizeAllowEntry', () => {
|
|
|
14
15
|
expect(normalizeAllowEntry('bx24:7')).toBe('7');
|
|
15
16
|
});
|
|
16
17
|
|
|
18
|
+
it('strips prefixes case-insensitively', () => {
|
|
19
|
+
expect(normalizeAllowEntry('Bitrix24:42')).toBe('42');
|
|
20
|
+
expect(normalizeAllowEntry('B24:100')).toBe('100');
|
|
21
|
+
expect(normalizeAllowEntry('BX24:7')).toBe('7');
|
|
22
|
+
});
|
|
23
|
+
|
|
17
24
|
it('returns plain ID as-is', () => {
|
|
18
25
|
expect(normalizeAllowEntry('42')).toBe('42');
|
|
19
26
|
});
|
|
@@ -29,8 +36,8 @@ describe('checkAccess', () => {
|
|
|
29
36
|
expect(checkAccess('999', { dmPolicy: 'open' })).toBe(true);
|
|
30
37
|
});
|
|
31
38
|
|
|
32
|
-
it('defaults to
|
|
33
|
-
expect(checkAccess('1', {})).toBe(
|
|
39
|
+
it('defaults to pairing (returns false without runtime)', () => {
|
|
40
|
+
expect(checkAccess('1', {})).toBe(false);
|
|
34
41
|
});
|
|
35
42
|
|
|
36
43
|
it('allows listed users in allowlist mode', () => {
|
|
@@ -61,7 +68,172 @@ describe('checkAccess', () => {
|
|
|
61
68
|
expect(checkAccess('1', config)).toBe(false);
|
|
62
69
|
});
|
|
63
70
|
|
|
64
|
-
it('
|
|
65
|
-
expect(checkAccess('1', { dmPolicy: 'pairing' })).toBe(
|
|
71
|
+
it('returns false for pairing mode (requires runtime)', () => {
|
|
72
|
+
expect(checkAccess('1', { dmPolicy: 'pairing' })).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── checkAccessWithPairing ─────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function makeMockRuntime(storeAllowFrom: string[] = []): PluginRuntime {
|
|
79
|
+
return {
|
|
80
|
+
config: { loadConfig: () => ({}) },
|
|
81
|
+
channel: {
|
|
82
|
+
routing: { resolveAgentRoute: vi.fn() },
|
|
83
|
+
reply: {
|
|
84
|
+
finalizeInboundContext: vi.fn(),
|
|
85
|
+
dispatchReplyWithBufferedBlockDispatcher: vi.fn(),
|
|
86
|
+
},
|
|
87
|
+
session: { recordInboundSession: vi.fn() },
|
|
88
|
+
pairing: {
|
|
89
|
+
readAllowFromStore: vi.fn().mockResolvedValue(storeAllowFrom),
|
|
90
|
+
upsertPairingRequest: vi.fn().mockResolvedValue({ code: 'ABCD1234', created: true }),
|
|
91
|
+
buildPairingReply: vi.fn().mockReturnValue({ text: 'Your pairing code: ABCD1234' }),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
logging: { getChildLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }) },
|
|
95
|
+
} as unknown as PluginRuntime;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const mockAdapter: ChannelPairingAdapter = {
|
|
99
|
+
idLabel: 'bitrix24UserId',
|
|
100
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(bitrix24|b24|bx24):/i, ''),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const silentLogger = { debug: () => {} };
|
|
104
|
+
|
|
105
|
+
describe('checkAccessWithPairing', () => {
|
|
106
|
+
it('returns allow for open policy', async () => {
|
|
107
|
+
const runtime = makeMockRuntime();
|
|
108
|
+
const result = await checkAccessWithPairing({
|
|
109
|
+
senderId: '1',
|
|
110
|
+
config: { dmPolicy: 'open' },
|
|
111
|
+
runtime,
|
|
112
|
+
accountId: 'default',
|
|
113
|
+
pairingAdapter: mockAdapter,
|
|
114
|
+
sendReply: vi.fn(),
|
|
115
|
+
logger: silentLogger,
|
|
116
|
+
});
|
|
117
|
+
expect(result).toBe('allow');
|
|
118
|
+
// Should NOT read store for open policy
|
|
119
|
+
expect(runtime.channel.pairing.readAllowFromStore).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns allow when sender is in config allowFrom', async () => {
|
|
123
|
+
const runtime = makeMockRuntime();
|
|
124
|
+
const result = await checkAccessWithPairing({
|
|
125
|
+
senderId: '42',
|
|
126
|
+
config: { dmPolicy: 'pairing', allowFrom: ['42'] },
|
|
127
|
+
runtime,
|
|
128
|
+
accountId: 'default',
|
|
129
|
+
pairingAdapter: mockAdapter,
|
|
130
|
+
sendReply: vi.fn(),
|
|
131
|
+
logger: silentLogger,
|
|
132
|
+
});
|
|
133
|
+
expect(result).toBe('allow');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns allow when sender is in file store', async () => {
|
|
137
|
+
const runtime = makeMockRuntime(['42']);
|
|
138
|
+
const result = await checkAccessWithPairing({
|
|
139
|
+
senderId: '42',
|
|
140
|
+
config: { dmPolicy: 'pairing' },
|
|
141
|
+
runtime,
|
|
142
|
+
accountId: 'default',
|
|
143
|
+
pairingAdapter: mockAdapter,
|
|
144
|
+
sendReply: vi.fn(),
|
|
145
|
+
logger: silentLogger,
|
|
146
|
+
});
|
|
147
|
+
expect(result).toBe('allow');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('merges config and store allowFrom (deduped)', async () => {
|
|
151
|
+
const runtime = makeMockRuntime(['42', '99']);
|
|
152
|
+
const result = await checkAccessWithPairing({
|
|
153
|
+
senderId: '99',
|
|
154
|
+
config: { dmPolicy: 'allowlist', allowFrom: ['42'] },
|
|
155
|
+
runtime,
|
|
156
|
+
accountId: 'default',
|
|
157
|
+
pairingAdapter: mockAdapter,
|
|
158
|
+
sendReply: vi.fn(),
|
|
159
|
+
logger: silentLogger,
|
|
160
|
+
});
|
|
161
|
+
expect(result).toBe('allow');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns deny for allowlist when sender not in merged list', async () => {
|
|
165
|
+
const runtime = makeMockRuntime(['42']);
|
|
166
|
+
const result = await checkAccessWithPairing({
|
|
167
|
+
senderId: '999',
|
|
168
|
+
config: { dmPolicy: 'allowlist', allowFrom: ['1'] },
|
|
169
|
+
runtime,
|
|
170
|
+
accountId: 'default',
|
|
171
|
+
pairingAdapter: mockAdapter,
|
|
172
|
+
sendReply: vi.fn(),
|
|
173
|
+
logger: silentLogger,
|
|
174
|
+
});
|
|
175
|
+
expect(result).toBe('deny');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('upserts pairing request and sends reply for new pairing', async () => {
|
|
179
|
+
const runtime = makeMockRuntime();
|
|
180
|
+
const sendReply = vi.fn();
|
|
181
|
+
const result = await checkAccessWithPairing({
|
|
182
|
+
senderId: '77',
|
|
183
|
+
config: { dmPolicy: 'pairing' },
|
|
184
|
+
runtime,
|
|
185
|
+
accountId: 'default',
|
|
186
|
+
pairingAdapter: mockAdapter,
|
|
187
|
+
sendReply,
|
|
188
|
+
logger: silentLogger,
|
|
189
|
+
});
|
|
190
|
+
expect(result).toBe('pairing');
|
|
191
|
+
expect(runtime.channel.pairing.upsertPairingRequest).toHaveBeenCalledWith({
|
|
192
|
+
channel: 'bitrix24',
|
|
193
|
+
id: '77',
|
|
194
|
+
accountId: 'default',
|
|
195
|
+
meta: {},
|
|
196
|
+
pairingAdapter: mockAdapter,
|
|
197
|
+
});
|
|
198
|
+
expect(runtime.channel.pairing.buildPairingReply).toHaveBeenCalledWith({
|
|
199
|
+
code: 'ABCD1234',
|
|
200
|
+
channel: 'bitrix24',
|
|
201
|
+
accountId: 'default',
|
|
202
|
+
});
|
|
203
|
+
expect(sendReply).toHaveBeenCalledWith('Your pairing code: ABCD1234');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('does not send reply for duplicate pairing request', async () => {
|
|
207
|
+
const runtime = makeMockRuntime();
|
|
208
|
+
(runtime.channel.pairing.upsertPairingRequest as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
209
|
+
code: 'ABCD1234',
|
|
210
|
+
created: false,
|
|
211
|
+
});
|
|
212
|
+
const sendReply = vi.fn();
|
|
213
|
+
const result = await checkAccessWithPairing({
|
|
214
|
+
senderId: '77',
|
|
215
|
+
config: { dmPolicy: 'pairing' },
|
|
216
|
+
runtime,
|
|
217
|
+
accountId: 'default',
|
|
218
|
+
pairingAdapter: mockAdapter,
|
|
219
|
+
sendReply,
|
|
220
|
+
logger: silentLogger,
|
|
221
|
+
});
|
|
222
|
+
expect(result).toBe('pairing');
|
|
223
|
+
expect(sendReply).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('normalizes config allowFrom entries with prefixes', async () => {
|
|
227
|
+
const runtime = makeMockRuntime();
|
|
228
|
+
const result = await checkAccessWithPairing({
|
|
229
|
+
senderId: '42',
|
|
230
|
+
config: { dmPolicy: 'pairing', allowFrom: ['b24:42'] },
|
|
231
|
+
runtime,
|
|
232
|
+
accountId: 'default',
|
|
233
|
+
pairingAdapter: mockAdapter,
|
|
234
|
+
sendReply: vi.fn(),
|
|
235
|
+
logger: silentLogger,
|
|
236
|
+
});
|
|
237
|
+
expect(result).toBe('allow');
|
|
66
238
|
});
|
|
67
239
|
});
|