@ihazz/bitrix24 0.2.5 → 1.0.1
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 +138 -156
- package/index.ts +46 -11
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/bitrix24/SKILL.md +96 -0
- package/src/access-control.ts +102 -48
- package/src/api.ts +434 -232
- package/src/channel.ts +1492 -366
- package/src/commands.ts +169 -31
- package/src/config-schema.ts +8 -3
- package/src/config.ts +11 -0
- package/src/dedup.ts +4 -0
- package/src/i18n.ts +127 -0
- package/src/inbound-handler.ts +306 -110
- package/src/media-service.ts +218 -65
- package/src/message-utils.ts +252 -10
- package/src/polling-service.ts +240 -0
- package/src/rate-limiter.ts +11 -6
- package/src/send-service.ts +140 -60
- package/src/types.ts +279 -185
- package/src/utils.ts +54 -3
- package/tests/access-control.test.ts +174 -58
- package/tests/api.test.ts +95 -0
- package/tests/channel.test.ts +231 -10
- package/tests/commands.test.ts +57 -0
- package/tests/config.test.ts +5 -1
- package/tests/i18n.test.ts +47 -0
- package/tests/inbound-handler.test.ts +554 -69
- package/tests/index.test.ts +94 -0
- package/tests/media-service.test.ts +146 -51
- package/tests/message-utils.test.ts +64 -0
- package/tests/polling-service.test.ts +77 -0
- package/tests/rate-limiter.test.ts +2 -2
- package/tests/send-service.test.ts +145 -0
package/src/access-control.ts
CHANGED
|
@@ -1,43 +1,69 @@
|
|
|
1
1
|
import type { Bitrix24AccountConfig } from './types.js';
|
|
2
2
|
import type { PluginRuntime, ChannelPairingAdapter } from './runtime.js';
|
|
3
|
+
import { stripChannelPrefix } from './utils.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Normalize an allowFrom entry — strip platform prefixes.
|
|
6
7
|
* "bitrix24:42" → "42", "b24:42" → "42", "bx24:42" → "42", "42" → "42"
|
|
7
8
|
*/
|
|
8
9
|
export function normalizeAllowEntry(entry: string): string {
|
|
9
|
-
return entry.trim()
|
|
10
|
+
return stripChannelPrefix(entry.trim());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeAllowList(entries: string[] | undefined): string[] {
|
|
14
|
+
if (!Array.isArray(entries)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return [...new Set(
|
|
19
|
+
entries
|
|
20
|
+
.map((entry) => normalizeAllowEntry(String(entry)))
|
|
21
|
+
.filter(Boolean),
|
|
22
|
+
)];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract the owner user ID from a Bitrix24 inbound webhook URL.
|
|
27
|
+
* Format: https://{portal}/rest/{user_id}/{webhook_token}/
|
|
28
|
+
*/
|
|
29
|
+
export function getWebhookUserId(webhookUrl: string | undefined): string | null {
|
|
30
|
+
if (!webhookUrl) return null;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(webhookUrl);
|
|
34
|
+
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
35
|
+
const restIndex = pathParts.indexOf('rest');
|
|
36
|
+
|
|
37
|
+
if (restIndex < 0 || pathParts.length <= restIndex + 2) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const userId = pathParts[restIndex + 1];
|
|
42
|
+
return userId ? normalizeAllowEntry(userId) : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
10
46
|
}
|
|
11
47
|
|
|
12
48
|
/**
|
|
13
49
|
* Check whether a sender is allowed to communicate with the bot.
|
|
14
50
|
*
|
|
15
|
-
*
|
|
51
|
+
* Pairing requires runtime state, so the synchronous helper cannot approve access.
|
|
16
52
|
*/
|
|
17
53
|
export function checkAccess(
|
|
18
54
|
senderId: string,
|
|
19
55
|
config: Bitrix24AccountConfig,
|
|
56
|
+
_options?: { dialogId?: string; isDirect?: boolean },
|
|
20
57
|
): boolean {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
switch (policy) {
|
|
24
|
-
case 'open':
|
|
25
|
-
return true;
|
|
26
|
-
|
|
27
|
-
case 'allowlist': {
|
|
28
|
-
const allowList = config.allowFrom;
|
|
29
|
-
if (!allowList || allowList.length === 0) return false;
|
|
30
|
-
const normalized = allowList.map(normalizeAllowEntry);
|
|
31
|
-
return normalized.includes(String(senderId));
|
|
32
|
-
}
|
|
58
|
+
const dmPolicy = config.dmPolicy ?? 'webhookUser';
|
|
59
|
+
const identity = resolveAccessIdentity(senderId);
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
default:
|
|
39
|
-
return false;
|
|
61
|
+
if (dmPolicy === 'webhookUser') {
|
|
62
|
+
const webhookUserId = getWebhookUserId(config.webhookUrl);
|
|
63
|
+
return webhookUserId === identity;
|
|
40
64
|
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
export type AccessResult = 'allow' | 'deny' | 'pairing';
|
|
@@ -49,6 +75,8 @@ export type AccessResult = 'allow' | 'deny' | 'pairing';
|
|
|
49
75
|
*/
|
|
50
76
|
export async function checkAccessWithPairing(params: {
|
|
51
77
|
senderId: string;
|
|
78
|
+
dialogId?: string;
|
|
79
|
+
isDirect?: boolean;
|
|
52
80
|
config: Bitrix24AccountConfig;
|
|
53
81
|
runtime: PluginRuntime;
|
|
54
82
|
accountId: string;
|
|
@@ -56,44 +84,70 @@ export async function checkAccessWithPairing(params: {
|
|
|
56
84
|
sendReply: (text: string) => Promise<void>;
|
|
57
85
|
logger?: { debug: (...args: unknown[]) => void };
|
|
58
86
|
}): Promise<AccessResult> {
|
|
59
|
-
const {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
87
|
+
const {
|
|
88
|
+
senderId,
|
|
89
|
+
dialogId,
|
|
90
|
+
isDirect,
|
|
91
|
+
config,
|
|
92
|
+
runtime,
|
|
93
|
+
accountId,
|
|
94
|
+
pairingAdapter,
|
|
95
|
+
sendReply,
|
|
96
|
+
logger,
|
|
97
|
+
} = params;
|
|
98
|
+
const identity = resolveAccessIdentity(senderId);
|
|
99
|
+
const dmPolicy = config.dmPolicy ?? 'webhookUser';
|
|
63
100
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const configAllowFrom = (config.allowFrom ?? []).map(normalizeAllowEntry);
|
|
67
|
-
const merged = [...new Set([...configAllowFrom, ...storeAllowFrom])];
|
|
68
|
-
const normalizedSender = normalizeAllowEntry(String(senderId));
|
|
101
|
+
if (dmPolicy === 'webhookUser') {
|
|
102
|
+
const webhookUserId = getWebhookUserId(config.webhookUrl);
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
if (webhookUserId && webhookUserId === identity) {
|
|
105
|
+
return 'allow';
|
|
106
|
+
}
|
|
71
107
|
|
|
72
|
-
|
|
73
|
-
logger?.debug('Access denied (allowlist)', { senderId });
|
|
108
|
+
logger?.debug('Access denied (webhookUser)', { senderId, dialogId, webhookUserId, identity });
|
|
74
109
|
return 'deny';
|
|
75
110
|
}
|
|
76
111
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
meta: {},
|
|
83
|
-
pairingAdapter,
|
|
84
|
-
});
|
|
112
|
+
const storeAllowFrom = await runtime.channel.pairing.readAllowFromStore('bitrix24', '', accountId);
|
|
113
|
+
const approved = [...new Set([
|
|
114
|
+
...normalizeAllowList(config.allowFrom),
|
|
115
|
+
...normalizeAllowList(storeAllowFrom),
|
|
116
|
+
])];
|
|
85
117
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
if (approved.includes(identity)) return 'allow';
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const { code, created } = await runtime.channel.pairing.upsertPairingRequest({
|
|
89
122
|
channel: 'bitrix24',
|
|
123
|
+
id: identity,
|
|
90
124
|
accountId,
|
|
125
|
+
meta: {},
|
|
126
|
+
pairingAdapter,
|
|
91
127
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
128
|
+
|
|
129
|
+
if (created) {
|
|
130
|
+
const reply = runtime.channel.pairing.buildPairingReply({
|
|
131
|
+
code,
|
|
132
|
+
channel: 'bitrix24',
|
|
133
|
+
accountId,
|
|
134
|
+
});
|
|
135
|
+
const replyText = typeof reply === 'string'
|
|
136
|
+
? reply
|
|
137
|
+
: (reply && typeof reply === 'object' && 'text' in reply && typeof (reply as Record<string, unknown>).text === 'string')
|
|
138
|
+
? (reply as Record<string, unknown>).text as string
|
|
139
|
+
: String(reply);
|
|
140
|
+
await sendReply(replyText);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logger?.debug('Pairing request handled', { senderId, dialogId, identity, code, created });
|
|
144
|
+
return 'pairing';
|
|
145
|
+
} catch (err) {
|
|
146
|
+
logger?.debug('Pairing request failed, falling back to deny', { senderId, dialogId, identity, error: err });
|
|
147
|
+
return 'deny';
|
|
95
148
|
}
|
|
149
|
+
}
|
|
96
150
|
|
|
97
|
-
|
|
98
|
-
return
|
|
151
|
+
function resolveAccessIdentity(senderId: string): string {
|
|
152
|
+
return normalizeAllowEntry(String(senderId));
|
|
99
153
|
}
|