@ihazz/bitrix24 0.1.3
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/LICENSE +21 -0
- package/README.md +206 -0
- package/index.ts +42 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +50 -0
- package/src/access-control.ts +40 -0
- package/src/api.ts +236 -0
- package/src/channel.ts +213 -0
- package/src/config-schema.ts +21 -0
- package/src/config.ts +60 -0
- package/src/dedup.ts +49 -0
- package/src/inbound-handler.ts +187 -0
- package/src/message-utils.ts +104 -0
- package/src/rate-limiter.ts +76 -0
- package/src/runtime.ts +22 -0
- package/src/send-service.ts +173 -0
- package/src/types.ts +297 -0
- package/src/utils.ts +74 -0
- package/tests/access-control.test.ts +67 -0
- package/tests/config.test.ts +86 -0
- package/tests/dedup.test.ts +50 -0
- package/tests/fixtures/onimbotjoinchat.json +48 -0
- package/tests/fixtures/onimbotmessageadd-file.json +86 -0
- package/tests/fixtures/onimbotmessageadd-text.json +59 -0
- package/tests/fixtures/onimcommandadd.json +45 -0
- package/tests/inbound-handler.test.ts +161 -0
- package/tests/message-utils.test.ts +123 -0
- package/tests/rate-limiter.test.ts +52 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shelenkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @ihazz/bitrix24
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for Bitrix24 Messenger. Allows using a Bitrix24 chatbot as a communication interface for OpenClaw AI agents.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Receive messages from Bitrix24 users via webhook
|
|
8
|
+
- Send responses through the Bitrix24 REST API (`imbot.message.add`)
|
|
9
|
+
- Typing indicator (`imbot.chat.sendTyping`)
|
|
10
|
+
- Markdown to BB-code conversion
|
|
11
|
+
- Interactive keyboard buttons
|
|
12
|
+
- Rate limiting (2 req/sec token-bucket)
|
|
13
|
+
- Webhook deduplication (MESSAGE_ID, 5 min TTL)
|
|
14
|
+
- Access control: open, allowlist, pairing modes
|
|
15
|
+
- Multi-account support (multiple portals)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
openclaw plugins install @ihazz/bitrix24
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Bitrix24 Setup
|
|
24
|
+
|
|
25
|
+
1. Go to your Bitrix24 portal: **Apps** > **Developer resources** > **Other** > **Inbound webhook**
|
|
26
|
+
2. Name it, e.g. "OpenClaw Bot"
|
|
27
|
+
3. Under **Access permissions**, select scopes:
|
|
28
|
+
- **`imbot`** — chatbot management (register, send/update messages)
|
|
29
|
+
- **`im`** — messenger (chats, typing indicator)
|
|
30
|
+
- **`disk`** — file operations (optional, for future support)
|
|
31
|
+
4. Click **Save**
|
|
32
|
+
5. Copy the **Webhook URL** — it will look like:
|
|
33
|
+
```
|
|
34
|
+
https://your-portal.bitrix24.com/rest/1/abc123xyz456/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> The webhook URL contains authentication (user_id + token) — do not publish or commit it to a repository.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
Add to your `openclaw.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"channels": {
|
|
46
|
+
"bitrix24": {
|
|
47
|
+
"webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
|
|
48
|
+
"botName": "OpenClaw",
|
|
49
|
+
"botCode": "openclaw",
|
|
50
|
+
"callbackPath": "/hooks/bitrix24",
|
|
51
|
+
"dmPolicy": "open",
|
|
52
|
+
"showTyping": true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Only `webhookUrl` is required. The gateway will not start without it.
|
|
59
|
+
|
|
60
|
+
### Configuration Options
|
|
61
|
+
|
|
62
|
+
| Parameter | Default | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `webhookUrl` | — | Bitrix24 REST webhook URL (**required**) |
|
|
65
|
+
| `botName` | `"OpenClaw"` | Bot display name (shown in welcome message) |
|
|
66
|
+
| `botCode` | `"openclaw"` | Unique bot code for `imbot.register` |
|
|
67
|
+
| `callbackPath` | `"/hooks/bitrix24"` | Webhook endpoint path for incoming B24 events |
|
|
68
|
+
| `dmPolicy` | `"open"` | Access policy: `"open"` / `"allowlist"` / `"pairing"` |
|
|
69
|
+
| `allowFrom` | — | Allowed B24 user IDs (when `dmPolicy: "allowlist"`) |
|
|
70
|
+
| `showTyping` | `true` | Send typing indicator before responding |
|
|
71
|
+
| `streamUpdates` | `false` | Stream response via message updates |
|
|
72
|
+
| `updateIntervalMs` | `10000` | Throttle interval for streaming updates (ms, min 500) |
|
|
73
|
+
| `enabled` | `true` | Whether this account is enabled |
|
|
74
|
+
|
|
75
|
+
### Access Policies
|
|
76
|
+
|
|
77
|
+
- **`"open"`** — any Bitrix24 portal user can message the bot
|
|
78
|
+
- **`"allowlist"`** — only users listed in `allowFrom`:
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"dmPolicy": "allowlist",
|
|
82
|
+
"allowFrom": ["42", "b24:108", "bitrix24:256"]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
Prefixes `b24:`, `bx24:`, `bitrix24:` are stripped automatically.
|
|
86
|
+
- **`"pairing"`** — pairing mode (work in progress)
|
|
87
|
+
|
|
88
|
+
### Multi-Account (Multiple Portals)
|
|
89
|
+
|
|
90
|
+
Connect multiple Bitrix24 portals to a single OpenClaw server:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"channels": {
|
|
95
|
+
"bitrix24": {
|
|
96
|
+
"webhookUrl": "https://main-portal.bitrix24.com/rest/1/token1/",
|
|
97
|
+
"botName": "Main Bot",
|
|
98
|
+
"accounts": {
|
|
99
|
+
"sales": {
|
|
100
|
+
"webhookUrl": "https://sales.bitrix24.com/rest/1/token2/",
|
|
101
|
+
"botName": "Sales Bot",
|
|
102
|
+
"dmPolicy": "allowlist",
|
|
103
|
+
"allowFrom": ["10", "20", "30"]
|
|
104
|
+
},
|
|
105
|
+
"support": {
|
|
106
|
+
"webhookUrl": "https://support.bitrix24.com/rest/1/token3/",
|
|
107
|
+
"botName": "Support Bot"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The root config acts as the `"default"` account. Each entry in `accounts` inherits all root settings and can override any of them.
|
|
116
|
+
|
|
117
|
+
## Network Access
|
|
118
|
+
|
|
119
|
+
The OpenClaw gateway must be reachable over HTTPS for POST requests from Bitrix24 servers.
|
|
120
|
+
|
|
121
|
+
Default callback URL: `https://your-server.com/hooks/bitrix24`
|
|
122
|
+
|
|
123
|
+
For local development, use ngrok or a similar tool:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
ngrok http 3000
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use the HTTPS URL from ngrok as the event handler URL in Bitrix24 bot settings.
|
|
130
|
+
|
|
131
|
+
## Running
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
openclaw start
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
On successful start, logs will show: `Bitrix24 gateway started, webhook at /hooks/bitrix24`
|
|
138
|
+
|
|
139
|
+
### Verification
|
|
140
|
+
|
|
141
|
+
1. Open a chat with the bot on your Bitrix24 portal
|
|
142
|
+
2. Send any message
|
|
143
|
+
3. The bot should show a typing indicator and respond
|
|
144
|
+
|
|
145
|
+
### Troubleshooting
|
|
146
|
+
|
|
147
|
+
- Webhook URL is correct and accessible (`webhookUrl`)
|
|
148
|
+
- OpenClaw server is reachable from the internet over HTTPS
|
|
149
|
+
- Scopes `imbot`, `im`, `disk` are enabled in the B24 webhook settings
|
|
150
|
+
- No `QUERY_LIMIT_EXCEEDED` errors in logs (rate limit)
|
|
151
|
+
|
|
152
|
+
## How It Works
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
B24 user sends a message to the bot
|
|
156
|
+
|
|
|
157
|
+
B24 POSTs to /hooks/bitrix24 (application/x-www-form-urlencoded)
|
|
158
|
+
|
|
|
159
|
+
InboundHandler parses the request body
|
|
160
|
+
|
|
|
161
|
+
Event routing:
|
|
162
|
+
ONIMBOTMESSAGEADD -> message processing
|
|
163
|
+
ONIMBOTJOINCHAT -> welcome message
|
|
164
|
+
ONIMCOMMANDADD -> slash command (WIP)
|
|
165
|
+
ONIMBOTDELETE -> cleanup
|
|
166
|
+
|
|
|
167
|
+
Deduplication (MESSAGE_ID, 5 min TTL)
|
|
168
|
+
|
|
|
169
|
+
Access control (senderId vs. dmPolicy + allowFrom)
|
|
170
|
+
|
|
|
171
|
+
Normalize to MsgContext -> pass to OpenClaw AI agent
|
|
172
|
+
|
|
|
173
|
+
OpenClaw processes -> calls outbound.sendText()
|
|
174
|
+
|
|
|
175
|
+
SendService:
|
|
176
|
+
imbot.chat.sendTyping -> typing indicator
|
|
177
|
+
imbot.message.add -> response (Markdown -> BB-code)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Handled Events
|
|
181
|
+
|
|
182
|
+
| Event | Plugin Action |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `ONIMBOTMESSAGEADD` | Incoming message -> normalize -> AI agent |
|
|
185
|
+
| `ONIMBOTJOINCHAT` | Bot added to chat -> welcome message |
|
|
186
|
+
| `ONIMCOMMANDADD` | Slash command (stub) |
|
|
187
|
+
| `ONAPPINSTALL` | App installed (stub) |
|
|
188
|
+
| `ONIMBOTDELETE` | Bot removed -> cleanup |
|
|
189
|
+
|
|
190
|
+
### Built-in Safeguards
|
|
191
|
+
|
|
192
|
+
- **Rate limiter**: token-bucket, 2 requests/sec (B24 webhook limit)
|
|
193
|
+
- **Deduplication**: B24 retries webhooks if it doesn't get a 200 in time — the plugin stores MESSAGE_IDs for 5 minutes
|
|
194
|
+
- **Text conversion**: B24 uses BB-code, not Markdown — automatic conversion (`**bold**` -> `[B]bold[/B]`, etc.)
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm install
|
|
200
|
+
npm test # run tests
|
|
201
|
+
npm run build # compile TypeScript
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { bitrix24Plugin, handleWebhookRequest } from './src/channel.js';
|
|
2
|
+
import { setBitrix24Runtime } from './src/runtime.js';
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
|
|
5
|
+
interface OpenClawPluginApi {
|
|
6
|
+
config: Record<string, unknown>;
|
|
7
|
+
runtime: unknown;
|
|
8
|
+
registerChannel: (opts: { plugin: typeof bitrix24Plugin }) => void;
|
|
9
|
+
registerHttpRoute: (params: {
|
|
10
|
+
path: string;
|
|
11
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OpenClaw Bitrix24 Channel Plugin
|
|
17
|
+
*
|
|
18
|
+
* Connects Bitrix24 Messenger as a communication channel for OpenClaw agents.
|
|
19
|
+
* Users can chat with the AI assistant through Bitrix24's built-in messenger.
|
|
20
|
+
*/
|
|
21
|
+
export default {
|
|
22
|
+
id: 'bitrix24',
|
|
23
|
+
name: 'Bitrix24 Channel',
|
|
24
|
+
description: 'Bitrix24 Messenger channel for OpenClaw',
|
|
25
|
+
|
|
26
|
+
register(api: OpenClawPluginApi) {
|
|
27
|
+
setBitrix24Runtime(api.runtime as { logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; debug: (...args: unknown[]) => void }; [key: string]: unknown });
|
|
28
|
+
|
|
29
|
+
api.registerChannel({ plugin: bitrix24Plugin });
|
|
30
|
+
|
|
31
|
+
// Register HTTP webhook route on the OpenClaw gateway
|
|
32
|
+
const channels = api.config?.channels as Record<string, Record<string, unknown>> | undefined;
|
|
33
|
+
const callbackPath = (channels?.bitrix24?.callbackPath as string) ?? '/hooks/bitrix24';
|
|
34
|
+
|
|
35
|
+
api.registerHttpRoute({
|
|
36
|
+
path: callbackPath,
|
|
37
|
+
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
38
|
+
await handleWebhookRequest(req, res);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ihazz/bitrix24",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Bitrix24 Messenger channel for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"openclaw": {
|
|
9
|
+
"extensions": [
|
|
10
|
+
"./index.ts"
|
|
11
|
+
],
|
|
12
|
+
"channel": {
|
|
13
|
+
"id": "bitrix24",
|
|
14
|
+
"label": "Bitrix24",
|
|
15
|
+
"selectionLabel": "Bitrix24 (Messenger)",
|
|
16
|
+
"docsPath": "/channels/bitrix24",
|
|
17
|
+
"docsLabel": "bitrix24",
|
|
18
|
+
"blurb": "Connect to Bitrix24 Messenger via chat bot REST API.",
|
|
19
|
+
"order": 70,
|
|
20
|
+
"aliases": [
|
|
21
|
+
"b24",
|
|
22
|
+
"bx24"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"qs": "^6.13.0",
|
|
33
|
+
"zod": "^3.23.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.3.0",
|
|
37
|
+
"@types/qs": "^6.9.0",
|
|
38
|
+
"typescript": "^5.5.0",
|
|
39
|
+
"vitest": "^2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"openclaw": "*"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"openclaw": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT"
|
|
50
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Bitrix24AccountConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize an allowFrom entry — strip platform prefixes.
|
|
5
|
+
* "bitrix24:42" → "42", "b24:42" → "42", "bx24:42" → "42", "42" → "42"
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeAllowEntry(entry: string): string {
|
|
8
|
+
return entry.trim().replace(/^(bitrix24|b24|bx24):/, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check whether a sender is allowed to communicate with the bot.
|
|
13
|
+
*
|
|
14
|
+
* @returns `true` if the sender is allowed, `false` otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export function checkAccess(
|
|
17
|
+
senderId: string,
|
|
18
|
+
config: Bitrix24AccountConfig,
|
|
19
|
+
): boolean {
|
|
20
|
+
const policy = config.dmPolicy ?? 'open';
|
|
21
|
+
|
|
22
|
+
switch (policy) {
|
|
23
|
+
case 'open':
|
|
24
|
+
return true;
|
|
25
|
+
|
|
26
|
+
case 'allowlist': {
|
|
27
|
+
const allowList = config.allowFrom;
|
|
28
|
+
if (!allowList || allowList.length === 0) return false;
|
|
29
|
+
const normalized = allowList.map(normalizeAllowEntry);
|
|
30
|
+
return normalized.includes(String(senderId));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case 'pairing':
|
|
34
|
+
// Pairing mode is post-MVP — for now, treat as open
|
|
35
|
+
return true;
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { B24ApiResult, SendMessageOptions } from './types.js';
|
|
2
|
+
import { RateLimiter } from './rate-limiter.js';
|
|
3
|
+
import { Bitrix24ApiError, withRetry, isRateLimitError, defaultLogger } from './utils.js';
|
|
4
|
+
|
|
5
|
+
interface Logger {
|
|
6
|
+
info: (...args: unknown[]) => void;
|
|
7
|
+
warn: (...args: unknown[]) => void;
|
|
8
|
+
error: (...args: unknown[]) => void;
|
|
9
|
+
debug: (...args: unknown[]) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Bitrix24Api {
|
|
13
|
+
private rateLimiter: RateLimiter;
|
|
14
|
+
private logger: Logger;
|
|
15
|
+
|
|
16
|
+
constructor(opts: { maxPerSecond?: number; logger?: Logger } = {}) {
|
|
17
|
+
this.rateLimiter = new RateLimiter({ maxPerSecond: opts.maxPerSecond ?? 2 });
|
|
18
|
+
this.logger = opts.logger ?? defaultLogger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Call B24 REST API via webhook URL.
|
|
23
|
+
* Webhook URL format: https://domain.bitrix24.com/rest/{user_id}/{token}/
|
|
24
|
+
* The URL itself contains authentication.
|
|
25
|
+
*/
|
|
26
|
+
async callWebhook<T = unknown>(
|
|
27
|
+
webhookUrl: string,
|
|
28
|
+
method: string,
|
|
29
|
+
params?: Record<string, unknown>,
|
|
30
|
+
): Promise<B24ApiResult<T>> {
|
|
31
|
+
await this.rateLimiter.acquire();
|
|
32
|
+
|
|
33
|
+
const url = `${webhookUrl.replace(/\/+$/, '')}/${method}.json`;
|
|
34
|
+
this.logger.debug(`API call: ${method}`, { url });
|
|
35
|
+
|
|
36
|
+
const result = await withRetry(
|
|
37
|
+
async () => {
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(params ?? {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Bitrix24ApiError(
|
|
46
|
+
`HTTP_${response.status}`,
|
|
47
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = (await response.json()) as B24ApiResult<T>;
|
|
52
|
+
|
|
53
|
+
if (data.error) {
|
|
54
|
+
throw new Bitrix24ApiError(
|
|
55
|
+
data.error,
|
|
56
|
+
data.error_description ?? 'Unknown error',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return data;
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
maxRetries: 2,
|
|
64
|
+
shouldRetry: isRateLimitError,
|
|
65
|
+
initialDelayMs: 1000,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Call B24 REST API via OAuth/event access token.
|
|
74
|
+
* Used when processing webhook events that include access_token.
|
|
75
|
+
*
|
|
76
|
+
* @param clientEndpoint - e.g. "https://up.bitrix24.com/rest/"
|
|
77
|
+
* @param method - e.g. "imbot.message.add"
|
|
78
|
+
* @param accessToken - from event auth block
|
|
79
|
+
* @param params - method parameters
|
|
80
|
+
*/
|
|
81
|
+
async callWithToken<T = unknown>(
|
|
82
|
+
clientEndpoint: string,
|
|
83
|
+
method: string,
|
|
84
|
+
accessToken: string,
|
|
85
|
+
params?: Record<string, unknown>,
|
|
86
|
+
): Promise<B24ApiResult<T>> {
|
|
87
|
+
await this.rateLimiter.acquire();
|
|
88
|
+
|
|
89
|
+
const url = `${clientEndpoint.replace(/\/+$/, '')}/${method}.json`;
|
|
90
|
+
this.logger.debug(`API call (token): ${method}`, { url });
|
|
91
|
+
|
|
92
|
+
const result = await withRetry(
|
|
93
|
+
async () => {
|
|
94
|
+
const response = await fetch(url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
Authorization: `Bearer ${accessToken}`,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(params ?? {}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new Bitrix24ApiError(
|
|
105
|
+
`HTTP_${response.status}`,
|
|
106
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = (await response.json()) as B24ApiResult<T>;
|
|
111
|
+
|
|
112
|
+
if (data.error) {
|
|
113
|
+
throw new Bitrix24ApiError(
|
|
114
|
+
data.error,
|
|
115
|
+
data.error_description ?? 'Unknown error',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return data;
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
maxRetries: 2,
|
|
123
|
+
shouldRetry: isRateLimitError,
|
|
124
|
+
initialDelayMs: 1000,
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Convenience methods ──────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async sendMessage(
|
|
134
|
+
webhookUrl: string,
|
|
135
|
+
dialogId: string,
|
|
136
|
+
message: string,
|
|
137
|
+
options?: SendMessageOptions,
|
|
138
|
+
): Promise<number> {
|
|
139
|
+
const result = await this.callWebhook<number>(webhookUrl, 'imbot.message.add', {
|
|
140
|
+
DIALOG_ID: dialogId,
|
|
141
|
+
MESSAGE: message,
|
|
142
|
+
...options,
|
|
143
|
+
});
|
|
144
|
+
return result.result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async sendMessageWithToken(
|
|
148
|
+
clientEndpoint: string,
|
|
149
|
+
accessToken: string,
|
|
150
|
+
dialogId: string,
|
|
151
|
+
message: string,
|
|
152
|
+
options?: SendMessageOptions,
|
|
153
|
+
): Promise<number> {
|
|
154
|
+
const result = await this.callWithToken<number>(
|
|
155
|
+
clientEndpoint,
|
|
156
|
+
'imbot.message.add',
|
|
157
|
+
accessToken,
|
|
158
|
+
{
|
|
159
|
+
DIALOG_ID: dialogId,
|
|
160
|
+
MESSAGE: message,
|
|
161
|
+
...options,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
return result.result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async updateMessage(
|
|
168
|
+
webhookUrl: string,
|
|
169
|
+
messageId: number,
|
|
170
|
+
message: string,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.message.update', {
|
|
173
|
+
MESSAGE_ID: messageId,
|
|
174
|
+
MESSAGE: message,
|
|
175
|
+
});
|
|
176
|
+
return result.result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async updateMessageWithToken(
|
|
180
|
+
clientEndpoint: string,
|
|
181
|
+
accessToken: string,
|
|
182
|
+
messageId: number,
|
|
183
|
+
message: string,
|
|
184
|
+
): Promise<boolean> {
|
|
185
|
+
const result = await this.callWithToken<boolean>(
|
|
186
|
+
clientEndpoint,
|
|
187
|
+
'imbot.message.update',
|
|
188
|
+
accessToken,
|
|
189
|
+
{
|
|
190
|
+
MESSAGE_ID: messageId,
|
|
191
|
+
MESSAGE: message,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
return result.result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async sendTyping(webhookUrl: string, dialogId: string): Promise<boolean> {
|
|
198
|
+
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.chat.sendTyping', {
|
|
199
|
+
DIALOG_ID: dialogId,
|
|
200
|
+
});
|
|
201
|
+
return result.result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async sendTypingWithToken(
|
|
205
|
+
clientEndpoint: string,
|
|
206
|
+
accessToken: string,
|
|
207
|
+
dialogId: string,
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
const result = await this.callWithToken<boolean>(
|
|
210
|
+
clientEndpoint,
|
|
211
|
+
'imbot.chat.sendTyping',
|
|
212
|
+
accessToken,
|
|
213
|
+
{ DIALOG_ID: dialogId },
|
|
214
|
+
);
|
|
215
|
+
return result.result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async registerBot(
|
|
219
|
+
webhookUrl: string,
|
|
220
|
+
params: Record<string, unknown>,
|
|
221
|
+
): Promise<number> {
|
|
222
|
+
const result = await this.callWebhook<number>(webhookUrl, 'imbot.register', params);
|
|
223
|
+
return result.result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async unregisterBot(webhookUrl: string, botId: number): Promise<boolean> {
|
|
227
|
+
const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.unregister', {
|
|
228
|
+
BOT_ID: botId,
|
|
229
|
+
});
|
|
230
|
+
return result.result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
destroy(): void {
|
|
234
|
+
this.rateLimiter.destroy();
|
|
235
|
+
}
|
|
236
|
+
}
|