@ihazz/bitrix24 1.0.3 → 1.1.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 CHANGED
@@ -5,13 +5,15 @@ OpenClaw channel plugin for Bitrix24 Messenger based on Bot Platform 2.0 (`imbot
5
5
  ## Current Status
6
6
 
7
7
  - Works with Bitrix24 Bot Platform 2.0
8
- - Supports direct messages only
8
+ - Supports direct messages and group chats
9
+ - Uses secure defaults: `dmPolicy: "webhookUser"` and `groupPolicy: "webhookUser"`
10
+ - Supports optional pairing, allowlist and open access modes
11
+ - Uses mention gating in groups by default with `requireMention: true`
12
+ - Stores all delivered messages from chats where the bot is present in fast RAM history and relies on normal OpenClaw session files for long-term transcripts
9
13
  - Supports inbound and outbound media
10
- - Uses secure default access policy: `webhookUser`
11
- - Supports optional pairing flow for controlled approval
12
14
  - Production recommendation today: one portal per plugin instance
13
15
 
14
- Group chats are intentionally not supported. If the bot is added to a group chat, it sends a short notice and leaves the chat.
16
+ If a group is disabled by policy or blocked by `groupAllowFrom`, the bot sends a short notice and leaves the chat.
15
17
 
16
18
  ## Installation
17
19
 
@@ -36,9 +38,9 @@ Create an inbound webhook in Bitrix24:
36
38
  1. Open **Apps** > **Developer resources** > **Other** > **Inbound webhook**
37
39
  2. Create a webhook for your OpenClaw bot
38
40
  3. Grant scopes:
39
- - `imbot` — the minimum scope required for full bot operation
40
- - `im` reserved for future `agentMode` support; `agentMode` is not production-ready yet and will be available in future versions
41
- - you may also grant any extra scopes beyond this set if they are needed for your Bitrix24 scenarios
41
+ - `imbot` for normal bot operation
42
+ - `im` if you enable `agentMode` to ingest user-visible events and agent-side watches
43
+ - add any extra scopes required by your Bitrix24 scenario
42
44
  4. Save the webhook and copy the URL
43
45
 
44
46
  Example webhook URL:
@@ -60,7 +62,49 @@ Minimal configuration:
60
62
  "webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
61
63
  "botName": "OpenClaw",
62
64
  "dmPolicy": "webhookUser",
63
- "showTyping": true
65
+ "groupPolicy": "webhookUser",
66
+ "requireMention": true,
67
+ "historyLimit": 100,
68
+ "showTyping": true,
69
+ "capabilities": [
70
+ "inlineButtons",
71
+ "reactions"
72
+ ]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Example with group overrides:
79
+
80
+ ```json
81
+ {
82
+ "channels": {
83
+ "bitrix24": {
84
+ "webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
85
+ "botName": "OpenClaw",
86
+ "dmPolicy": "webhookUser",
87
+ "groupPolicy": "webhookUser",
88
+ "groupAllowFrom": ["chat208", "615"],
89
+ "requireMention": true,
90
+ "historyLimit": 100,
91
+ "groups": {
92
+ "*": {
93
+ "requireMention": true
94
+ },
95
+ "chat208": {
96
+ "groupPolicy": "open",
97
+ "requireMention": false
98
+ },
99
+ "615": {
100
+ "groupPolicy": "allowlist",
101
+ "requireMention": true,
102
+ "allowFrom": ["20", "42"],
103
+ "watch": [
104
+ { "userId": "77", "topics": ["secret", "contract"] }
105
+ ]
106
+ }
107
+ }
64
108
  }
65
109
  }
66
110
  }
@@ -90,20 +134,93 @@ If `eventMode` is omitted, the plugin uses:
90
134
  | Parameter | Default | Description |
91
135
  |---|---|---|
92
136
  | `webhookUrl` | — | Bitrix24 REST webhook URL. Required. |
93
- | `callbackUrl` | — | Public HTTPS URL for webhook delivery mode. Required only for `eventMode: "webhook"`. |
94
137
  | `eventMode` | auto | `fetch` or `webhook`. Auto-selects from `callbackUrl` when omitted. |
138
+ | `callbackUrl` | — | Public HTTPS URL for webhook delivery mode. Required only for `eventMode: "webhook"`. |
95
139
  | `botName` | `"OpenClaw"` | Bot display name. |
96
- | `botCode` | auto | Optional explicit bot code. If omitted, the plugin registers the bot as `openclaw_<webhookUserId>`, and if that code is occupied it tries `openclaw_<webhookUserId>_2`, `_3`, and so on. |
97
- | `botToken` | `md5(webhookUrl)` | Bot token for `imbot.v2` authentication (32-char hex string). If omitted, derived as `md5(webhookUrl)`. **Strongly recommended to set explicitly** if your `webhookUrl` may change (e.g. ngrok, dynamic DNS) — otherwise the plugin loses control of the previously registered bot. You can find the token in the Bitrix24 admin panel or compute it as `md5` of the original webhook URL. |
140
+ | `botCode` | auto | Optional explicit bot code. If omitted, the plugin registers the bot as `openclaw_<webhookUserId>` and falls back to `_2`, `_3`, and so on if needed. |
141
+ | `botToken` | `md5(webhookUrl)` | Bot token for `imbot.v2` authentication. Strongly recommended to set explicitly if your `webhookUrl` may change. |
98
142
  | `botAvatar` | — | Optional base64 avatar override. |
99
- | `dmPolicy` | `"webhookUser"` | Access policy: `webhookUser` or `pairing`. |
143
+ | `allowFrom` | — | Sender allowlist for `dmPolicy: "allowlist"` and as an approved-sender source for pairing/group overrides. |
144
+ | `dmPolicy` | `"webhookUser"` | Direct-message access policy: `webhookUser`, `pairing`, `allowlist`, `open`. |
145
+ | `groupPolicy` | `"webhookUser"` | Group-chat access policy: `disabled`, `webhookUser`, `pairing`, `allowlist`, `open`. |
146
+ | `groupAllowFrom` | — | Allowlist for group chats themselves. Values may be `dialogId` like `"chat208"` or numeric `chatId` like `"208"`. |
147
+ | `requireMention` | `true` | When `true`, the bot answers in groups only when mentioned. Group messages are still added to RAM history. |
148
+ | `historyLimit` | `100` | Maximum number of RAM history entries kept per conversation. |
149
+ | `groups` | — | Per-group overrides keyed by `dialogId`, numeric `chatId`, or `"*"`. Each override may set `groupPolicy`, `requireMention`, `allowFrom`, and `watch`. |
100
150
  | `showTyping` | `true` | Sends typing indicator before response. |
101
151
  | `enabled` | `true` | Enables or disables the account. |
102
- | `agentMode` | `false` | Reserved for future user-event integration. Not working yet; planned for future versions. |
152
+ | `agentMode` | `false` | Enables combined polling with `imbot.v2.Event.get(withUserEvents=true)`, subscribes the webhook owner to `im.v2` event recording in fetch mode, and stores delivered user-visible messages in RAM history without answering them directly. |
153
+ | `agentWatch` | — | Watch rules for user-visible events captured through `agentMode`. Keys follow the same matching pattern as chats: direct dialog like `"77"`, group dialog like `"chat520"`, numeric chat id like `"520"`, or wildcard `"*"`. Only `mode: "notifyOwnerDm"` is supported. |
103
154
  | `pollingIntervalMs` | `3000` | Base poll interval for `fetch` mode. |
104
155
  | `pollingFastIntervalMs` | `100` | Fast follow-up poll interval when more events are pending. |
105
156
 
106
- ### Agent-Driven Reactions
157
+ Group override matching order:
158
+
159
+ 1. Exact `dialogId`, for example `"chat208"`
160
+ 2. Exact numeric `chatId`, for example `"208"`
161
+ 3. Fallback wildcard `"*"`
162
+
163
+ ## Scenario Presets
164
+
165
+ The direct-message scenario is configured independently through `dmPolicy`. Group scenarios are controlled by `groupPolicy`, `requireMention`, optional `allowFrom`, and optional per-group `watch`.
166
+
167
+ | Scenario | Behavior | Minimal settings |
168
+ |---|---|---|
169
+ | `1. One-on-one chat with the bot.` | The bot answers in a direct chat according to the selected DM access policy. | `dmPolicy: "webhookUser" \| "pairing" \| "allowlist" \| "open"` |
170
+ | `2. Add the bot to a group chat where it responds to any message from any participant.` | The bot stores all delivered group messages in RAM history and answers every participant without mention. | `groupPolicy: "open"`, `requireMention: false` |
171
+ | `3. Add the bot to a group chat where it responds only to mentions from any participant.` | The bot stores all delivered group messages in RAM history, but answers only when mentioned. | `groupPolicy: "open"`, `requireMention: true` |
172
+ | `4. Add the bot to a group chat where it responds to all of my messages in that chat.` | The bot stores all delivered group messages in RAM history, but answers only the webhook owner and does not require mention. | `groupPolicy: "webhookUser"`, `requireMention: false` |
173
+ | `5. Add the bot to a group chat where it responds only to my mentions in that chat.` | The bot stores all delivered group messages in RAM history, but answers only the webhook owner and only on mention. | `groupPolicy: "webhookUser"`, `requireMention: true` |
174
+ | `6. Add the bot to a group chat where scenarios 2 through 5 apply only to users from my allowlist.` | The bot stores all delivered group messages in RAM history, but group replies are limited to the merged allowlist. | `groupPolicy: "allowlist"`, `allowFrom: ["77", "42"]`, plus `requireMention: false` or `true` |
175
+ | `7. The bot can summarize or search messages from any direct or group chat it participates in.` | This is baseline behavior. All delivered messages are recorded in RAM history and can later be used for in-chat context or explicit cross-chat lookup. | No extra flag. Usually `historyLimit: 100`. Use explicit Bitrix24 chat mention like `[CHAT=520]Green Chat 15[/CHAT]` for cross-chat lookup. |
176
+ | `8. In a group chat, the bot can watch a specific user and answer only on selected topics.` | The bot records all delivered messages and may answer watched users without mention when a watch rule matches. | `groups.<chat>.watch: [{ "userId": "77", "topics": ["secret", "contract"] }]` |
177
+ | `9. In a group chat, the bot can watch a specific user or all users and notify the bot owner in DM with a forwarded copy of the matched message.` | The bot records all delivered messages. When a watch rule matches, it does not answer in the group and instead sends the webhook owner a DM notice with a context URL, plus a native forwarded message copy. | `groups.<chat>.watch: [{ "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }]` |
178
+ | `10. The bot can watch all accessible group chats for a phrase and notify the bot owner in DM with a forwarded copy of the matched message.` | The bot records all delivered group messages. A wildcard group watch applies across every allowed group chat, even when a specific chat also has its own overrides. | `groups["*"].watch: [{ "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }]` |
179
+
180
+ Default profile:
181
+
182
+ - `dmPolicy: "webhookUser"`
183
+ - `groupPolicy: "webhookUser"`
184
+ - `requireMention: true`
185
+ - `historyLimit: 100`
186
+
187
+ ## Agent Mode
188
+
189
+ When `agentMode: true` is enabled in `fetch` mode, the plugin:
190
+
191
+ - subscribes the webhook owner to `im.v2` event recording
192
+ - polls `imbot.v2.Event.get` with `withUserEvents: true`
193
+ - receives both normal bot events and additional user-visible message events
194
+ - stores those additional user-visible messages in RAM history
195
+ - never answers user-visible events directly as the bot
196
+
197
+ This is mainly useful for owner-side watch rules and future agent-style retrieval scenarios.
198
+
199
+ Example:
200
+
201
+ ```json
202
+ {
203
+ "channels": {
204
+ "bitrix24": {
205
+ "webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
206
+ "agentMode": true,
207
+ "agentWatch": {
208
+ "*": [
209
+ { "userId": "*", "topics": ["incident"], "mode": "notifyOwnerDm" }
210
+ ],
211
+ "77": [
212
+ { "userId": "*", "topics": ["urgent"], "mode": "notifyOwnerDm" }
213
+ ],
214
+ "chat520": [
215
+ { "userId": "*", "topics": ["release"], "mode": "notifyOwnerDm" }
216
+ ]
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ ## Agent-Driven Reactions
107
224
 
108
225
  To enable agent-driven reactions, add `reactions` to the channel capabilities in your OpenClaw config:
109
226
 
@@ -118,9 +235,9 @@ To enable agent-driven reactions, add `reactions` to the channel capabilities in
118
235
  }
119
236
  ```
120
237
 
121
- The plugin maps standard Unicode emoji (👍, 🔥, 👀, etc.) to Bitrix24 reaction codes automatically. B24 reaction codes (like `like`, `fire`, `eyes`) can also be used directly.
238
+ The plugin maps standard Unicode emoji to Bitrix24 reaction codes automatically. Native Bitrix24 reaction codes like `like`, `fire` and `eyes` also work.
122
239
 
123
- ### Inline Buttons
240
+ ## Inline Buttons
124
241
 
125
242
  To enable agent-driven inline buttons, add `inlineButtons` to the channel capabilities in your OpenClaw config:
126
243
 
@@ -143,33 +260,74 @@ The plugin supports two button input forms:
143
260
  For converted generic buttons:
144
261
 
145
262
  - `callback_data` starting with `/` becomes a native Bitrix24 slash command button
146
- - any other `callback_data` becomes `ACTION=PUT`
147
- - `primary`, `attention`, and `danger` button styles are mapped to Bitrix24 color tokens
263
+ - any other `callback_data` becomes `ACTION=SEND`
264
+ - `primary`, `attention` and `danger` button styles are mapped to Bitrix24 color tokens
148
265
 
149
266
  ## Access Policies
150
267
 
151
- ### `webhookUser`
268
+ ### Direct Messages
152
269
 
153
- Default and recommended mode for production.
270
+ - `webhookUser`: only the Bitrix24 user from the webhook URL may talk to the bot
271
+ - `pairing`: user must be approved through the OpenClaw pairing flow
272
+ - `allowlist`: user must be present in `allowFrom`
273
+ - `open`: any direct-message sender is allowed
154
274
 
155
- Only the Bitrix24 user whose ID is embedded in the webhook URL may talk to the bot. For a URL like:
275
+ ### Group Chats
156
276
 
157
- ```text
158
- https://your-portal.bitrix24.com/rest/42/secret/
159
- ```
277
+ - `disabled`: bot sends a short notice and leaves the chat
278
+ - `webhookUser`: only the webhook owner may use the bot in that group
279
+ - `pairing`: sender must be approved; the public group receives only a short notice and never a pairing code
280
+ - `allowlist`: sender must be allowed by merged `allowFrom` and per-group `groups.<key>.allowFrom`
281
+ - `open`: any sender in the allowed group may use the bot
160
282
 
161
- only user `42` is allowed to use the bot in direct messages.
283
+ `groupAllowFrom` controls whether the group itself is allowed at all. A matching explicit entry in `groups` also enables that group even if the top-level `groupAllowFrom` is set to a different list.
162
284
 
163
- ### `pairing`
285
+ ### Group Message Memory
164
286
 
165
- Pairing mode allows a user to request access and wait for approval through the OpenClaw pairing flow.
287
+ All delivered group messages are added to RAM history, even when the sender is not allowed to use the bot or when `requireMention: true` prevents an answer.
166
288
 
167
- Approval hint:
289
+ This means:
168
290
 
169
- ```bash
170
- openclaw pairing approve bitrix24 <CODE>
291
+ - `groupPolicy` controls who may receive a reply in the group
292
+ - `requireMention` controls whether an explicit Bitrix24 mention is required for that reply
293
+ - RAM-backed context and cross-chat lookup still see the delivered group history regardless of those reply rules
294
+
295
+ When `agentMode: true` is enabled, delivered user-visible messages are also written to RAM history. They are stored for context and watches only; the bot does not answer those user events directly.
296
+
297
+ ### Group Watches
298
+
299
+ Per-group `watch` rules let the bot react to a specific user and only on selected topics:
300
+
301
+ ```json
302
+ {
303
+ "channels": {
304
+ "bitrix24": {
305
+ "webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
306
+ "groupPolicy": "webhookUser",
307
+ "requireMention": true,
308
+ "groups": {
309
+ "chat615": {
310
+ "watch": [
311
+ { "userId": "77", "topics": ["secret", "contract"], "mode": "reply" }
312
+ ]
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
171
318
  ```
172
319
 
320
+ For a matching `watch` rule:
321
+
322
+ - the sender is matched by Bitrix24 user ID; use `"*"` to match any sender
323
+ - `topics` are matched by normalized text inclusion / token prefix matching
324
+ - `mode: "reply"` answers in the group even if the normal group policy would otherwise require a mention
325
+ - `mode: "notifyOwnerDm"` sends the webhook owner a DM notice with a user mention and a deep link back to the original chat context, plus a native forwarded copy of the matched message instead of replying in the group
326
+ - `groups["*"].watch` applies the same rule set to every allowed group chat
327
+ - if a specific group also defines `watch`, those chat-specific rules are checked first and the wildcard rules remain available as a fallback
328
+
329
+ If `mode` is omitted, `reply` is used.
330
+
173
331
  ## Delivery Modes
174
332
 
175
333
  ### `fetch`
@@ -187,19 +345,37 @@ openclaw pairing approve bitrix24 <CODE>
187
345
  ## Supported Behavior
188
346
 
189
347
  - Direct messages
348
+ - Group chats with policy-driven access control
349
+ - Mention gating in groups with `requireMention`
350
+ - Group-specific overrides through `groups`
190
351
  - Typing indicators
191
352
  - Text replies with BBCode conversion
192
- - Inline keyboard buttons (`channelData.bitrix24.keyboard` and converted generic button rows)
193
- - Reactions (agent can add/remove reactions on messages via `imbot.v2.Chat.Message.Reaction.*`)
194
- - File upload to chat via `imbot.v2.File.upload`
353
+ - Inline keyboard buttons
354
+ - Reactions via `imbot.v2.Chat.Message.Reaction.*`
355
+ - File upload via `imbot.v2.File.upload`
195
356
  - File download via `imbot.v2.File.download`
196
357
  - Slash command registration via `imbot.v2.Command.*`
197
358
  - Deduplication for repeated deliveries
198
359
  - Rate limiting for Bitrix24 API calls
360
+ - RAM history cache for recent chat context
361
+ - RAM history capture for all delivered group messages
362
+ - Reply-to-message lookup from RAM cache with Bitrix24 API fallback
363
+ - Forwarded-message hydration from Bitrix24 message context, including merge with buffered neighboring context when related events arrive close together
364
+ - Cross-chat lookup from RAM memory by explicit Bitrix24 chat mention like `[CHAT=520]Green Chat 15[/CHAT]`
365
+ - Topic watches for specific group users
366
+ - Owner-DM watch notifications with native forwarded messages
367
+ - Agent-mode ingestion of user-visible messages through combined polling
368
+ - Agent-mode owner-DM watch notifications through `agentWatch`
369
+
370
+ The RAM cache keeps up to `historyLimit` entries per conversation and uses an LRU cap of `1000` conversation keys per process.
199
371
 
200
372
  ## Not Supported
201
373
 
202
- - Group chat conversations
374
+ - Persistent SQLite or search-based history layer
375
+ - Persistent reply or forward search across chats beyond what Bitrix24 `Chat.Message.get` / `getContext` can return
376
+ - “Reply to me in DM because this started in a group” behavior
377
+ - Sending replies as the user through `im.v2.Chat.Message.*`
378
+ - Agent-triggered user auto-replies or one-shot autoresponder flows
203
379
  - Production-ready multi-account operation in one process
204
380
 
205
381
  There is already account-config scaffolding in the plugin, but the current production recommendation is still one active Bitrix24 portal per instance.
@@ -207,18 +383,36 @@ There is already account-config scaffolding in the plugin, but the current produ
207
383
  ## Verification
208
384
 
209
385
  1. Start OpenClaw with the plugin enabled.
210
- 2. Open a direct chat with the bot in Bitrix24.
211
- 3. Send a message.
212
- 4. Verify that the bot shows typing and returns a response.
213
- 5. If `dmPolicy` is `webhookUser`, verify that only the webhook owner can talk to the bot.
386
+ 2. Open a direct chat with the bot in Bitrix24 and send a message.
387
+ 3. Verify the bot shows typing and returns a response.
388
+ 4. If `dmPolicy` is `webhookUser`, verify that only the webhook owner can use direct messages.
389
+ 5. Add the bot to an allowed group and mention it with the Bitrix24 format `[USER=<botId>]BotName[/USER]`.
390
+ 6. Verify the bot answers when mentioned and stays silent when `requireMention: true` and no mention is present.
391
+ 7. Verify that non-mentioned group messages still appear later in RAM-backed context.
392
+ 8. Reply to a recently seen message in the same chat and verify the bot keeps reply context.
393
+ 9. Forward a recent message and verify the plugin merges context when the forwarded event pair arrives together.
394
+ 10. In DM or another group, reference a visible chat as `[CHAT=<chatId>]ChatName[/CHAT]` and verify the bot can use RAM history from that chat.
395
+
396
+ Local development checks:
397
+
398
+ ```bash
399
+ npm test
400
+ npm run build
401
+ ```
214
402
 
215
403
  ## Troubleshooting
216
404
 
217
- - Verify `webhookUrl` is valid and active.
218
- - If you use `eventMode: "webhook"`, verify `callbackUrl` is reachable from the internet over HTTPS.
219
- - Do not rely on `agentMode` yet. It is not working in the current release and is planned for future versions.
220
- - Do not expect the bot to work in group chats.
405
+ - If the bot leaves a group immediately, check `groupPolicy`, `groupAllowFrom` and `groups`. A disabled or disallowed group is expected to receive a short notice and then be left.
406
+ - If the bot stays in the group but does not answer, check `requireMention`. In live Bitrix24 payloads, mention markup arrives inside `message.text` as `[USER=<botId>]Name[/USER]`.
407
+ - If the bot does not answer a user in group chat, check `groupPolicy`, `allowFrom`, and per-group overrides in `groups`.
408
+ - If a group message seems to vanish from later context, verify that the message was actually delivered to the bot process and that it has not been evicted by `historyLimit` or the global RAM LRU.
409
+ - If a per-group override does not apply, verify the key matching order: exact `dialogId`, then exact numeric `chatId`, then `"*"`.
410
+ - If reply context is missing, the referenced message may be unavailable to the current bot type, no longer accessible through Bitrix24 `Chat.Message.get`, or absent from local RAM history.
411
+ - If cross-chat lookup does not work, verify the query contains an explicit Bitrix24 chat mention like `[CHAT=520]Green Chat 15[/CHAT]` and that this chat has already appeared in RAM history during the current process lifetime.
412
+ - If `eventMode: "webhook"` is used, verify `callbackUrl` is reachable from the internet over HTTPS.
413
+ - If `agentMode` is enabled but user-visible events never appear, verify the webhook has the `im` scope and that `im.v2.Event.subscribe` succeeds for the webhook owner.
221
414
  - If file transfer fails, verify the file size and Bitrix24-side availability of the attachment.
415
+ - If the bot still leaves every group after you changed config, make sure the deployed plugin code is current. Older builds contained unconditional “group chat is not supported, leaving” logic.
222
416
 
223
417
  ## Development
224
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihazz/bitrix24",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,4 +1,4 @@
1
- import type { Bitrix24AccountConfig } from './types.js';
1
+ import type { Bitrix24AccountConfig, Bitrix24DmPolicy } from './types.js';
2
2
  import type { PluginRuntime, ChannelPairingAdapter } from './runtime.js';
3
3
  import { stripChannelPrefix } from './utils.js';
4
4
 
@@ -22,6 +22,10 @@ export function normalizeAllowList(entries: string[] | undefined): string[] {
22
22
  )];
23
23
  }
24
24
 
25
+ export function isIdentityAllowed(identity: string, allowFrom: string[] | undefined): boolean {
26
+ return normalizeAllowList(allowFrom).includes(identity);
27
+ }
28
+
25
29
  /**
26
30
  * Extract the owner user ID from a Bitrix24 inbound webhook URL.
27
31
  * Format: https://{portal}/rest/{user_id}/{webhook_token}/
@@ -55,15 +59,22 @@ export function checkAccess(
55
59
  config: Bitrix24AccountConfig,
56
60
  _options?: { dialogId?: string; isDirect?: boolean },
57
61
  ): boolean {
58
- const dmPolicy = config.dmPolicy ?? 'webhookUser';
62
+ const dmPolicy = resolveDmPolicy(config.dmPolicy);
59
63
  const identity = resolveAccessIdentity(senderId);
60
64
 
61
- if (dmPolicy === 'webhookUser') {
62
- const webhookUserId = getWebhookUserId(config.webhookUrl);
63
- return webhookUserId === identity;
65
+ switch (dmPolicy) {
66
+ case 'webhookUser': {
67
+ const webhookUserId = getWebhookUserId(config.webhookUrl);
68
+ return webhookUserId === identity;
69
+ }
70
+ case 'open':
71
+ return true;
72
+ case 'allowlist':
73
+ return isIdentityAllowed(identity, config.allowFrom);
74
+ case 'pairing':
75
+ default:
76
+ return false;
64
77
  }
65
-
66
- return false;
67
78
  }
68
79
 
69
80
  export type AccessResult = 'allow' | 'deny' | 'pairing';
@@ -96,7 +107,15 @@ export async function checkAccessWithPairing(params: {
96
107
  logger,
97
108
  } = params;
98
109
  const identity = resolveAccessIdentity(senderId);
99
- const dmPolicy = config.dmPolicy ?? 'webhookUser';
110
+ const dmPolicy = resolveDmPolicy(config.dmPolicy);
111
+
112
+ if (dmPolicy === 'open') {
113
+ return 'allow';
114
+ }
115
+
116
+ if (dmPolicy === 'allowlist') {
117
+ return isIdentityAllowed(identity, config.allowFrom) ? 'allow' : 'deny';
118
+ }
100
119
 
101
120
  if (dmPolicy === 'webhookUser') {
102
121
  const webhookUserId = getWebhookUserId(config.webhookUrl);
@@ -151,3 +170,7 @@ export async function checkAccessWithPairing(params: {
151
170
  function resolveAccessIdentity(senderId: string): string {
152
171
  return normalizeAllowEntry(String(senderId));
153
172
  }
173
+
174
+ function resolveDmPolicy(policy: Bitrix24AccountConfig['dmPolicy']): Bitrix24DmPolicy {
175
+ return policy ?? 'webhookUser';
176
+ }
package/src/api.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import type {
2
3
  B24ApiResult,
3
4
  B24V2BotResult,
4
5
  B24V2BotListResult,
6
+ B24V2GetMessageContextResult,
7
+ B24V2GetMessageResult,
5
8
  B24V2SendMessageResult,
6
9
  B24V2ReadMessageResult,
7
10
  B24InputActionStatusCode,
@@ -261,28 +264,94 @@ export class Bitrix24Api {
261
264
  webhookUrl: string,
262
265
  bot: BotContext,
263
266
  dialogId: string,
264
- message: string,
265
- options?: { keyboard?: B24Keyboard; attach?: unknown; system?: boolean; urlPreview?: boolean },
267
+ message: string | null,
268
+ options?: {
269
+ keyboard?: B24Keyboard;
270
+ attach?: unknown;
271
+ system?: boolean;
272
+ urlPreview?: boolean;
273
+ forwardMessages?: number[];
274
+ },
266
275
  ): Promise<number> {
267
- const fields: Record<string, unknown> = { message };
276
+ const fields: Record<string, unknown> = {};
277
+ if (typeof message === 'string') {
278
+ fields.message = message;
279
+ }
268
280
  if (options?.keyboard) fields.keyboard = options.keyboard;
269
281
  if (options?.attach) fields.attach = options.attach;
270
282
  if (options?.system !== undefined) fields.system = options.system;
271
283
  if (options?.urlPreview !== undefined) fields.urlPreview = options.urlPreview;
284
+ if (options?.forwardMessages?.length) {
285
+ fields.forwardIds = Object.fromEntries(
286
+ options.forwardMessages.map((forwardMessageId) => [randomUUID(), forwardMessageId]),
287
+ );
288
+ }
289
+
290
+ const params: Record<string, unknown> = {
291
+ botId: bot.botId,
292
+ botToken: bot.botToken,
293
+ dialogId,
294
+ fields,
295
+ };
272
296
 
273
297
  const result = await this.callWebhook<B24V2SendMessageResult>(
274
298
  webhookUrl,
275
299
  'imbot.v2.Chat.Message.send',
276
- { botId: bot.botId, botToken: bot.botToken, dialogId, fields },
300
+ params,
277
301
  );
278
302
  const messageId = result.result?.id;
279
- if (!Number.isFinite(messageId) || messageId <= 0) {
303
+ if (!Number.isFinite(messageId) || Number(messageId) <= 0) {
304
+ if (options?.forwardMessages?.length && typeof message !== 'string') {
305
+ return 0;
306
+ }
280
307
  throw new Bitrix24ApiError(
281
308
  'INVALID_RESPONSE',
282
309
  'imbot.v2.Chat.Message.send returned response without valid message id',
283
310
  );
284
311
  }
285
- return messageId;
312
+ return Number(messageId);
313
+ }
314
+
315
+ /**
316
+ * Get a single message by id (imbot.v2.Chat.Message.get).
317
+ */
318
+ async getMessage(
319
+ webhookUrl: string,
320
+ bot: BotContext,
321
+ messageId: number,
322
+ ): Promise<B24V2GetMessageResult> {
323
+ const result = await this.callWebhook<B24V2GetMessageResult>(
324
+ webhookUrl,
325
+ 'imbot.v2.Chat.Message.get',
326
+ {
327
+ botId: bot.botId,
328
+ botToken: bot.botToken,
329
+ messageId,
330
+ },
331
+ );
332
+ return result.result;
333
+ }
334
+
335
+ /**
336
+ * Get message window around a message id (imbot.v2.Chat.Message.getContext).
337
+ */
338
+ async getMessageContext(
339
+ webhookUrl: string,
340
+ bot: BotContext,
341
+ messageId: number,
342
+ range?: number,
343
+ ): Promise<B24V2GetMessageContextResult> {
344
+ const result = await this.callWebhook<B24V2GetMessageContextResult>(
345
+ webhookUrl,
346
+ 'imbot.v2.Chat.Message.getContext',
347
+ {
348
+ botId: bot.botId,
349
+ botToken: bot.botToken,
350
+ messageId,
351
+ ...(range ? { range } : {}),
352
+ },
353
+ );
354
+ return result.result;
286
355
  }
287
356
 
288
357
  /**
@@ -503,7 +572,7 @@ export class Bitrix24Api {
503
572
  async fetchEvents(
504
573
  webhookUrl: string,
505
574
  bot: BotContext,
506
- params: { offset?: number; limit?: number },
575
+ params: { offset?: number; limit?: number; withUserEvents?: boolean },
507
576
  ): Promise<B24V2EventGetResult> {
508
577
  const result = await this.callWebhook<B24V2EventGetResult>(
509
578
  webhookUrl,