@ihazz/bitrix24 0.2.4 → 1.0.0
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 +118 -164
- package/index.ts +46 -11
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/bitrix24/SKILL.md +70 -0
- package/src/access-control.ts +102 -46
- package/src/api.ts +434 -232
- package/src/channel.ts +1486 -393
- 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 +279 -61
- 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/api.ts
CHANGED
|
@@ -1,29 +1,114 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
B24ApiResult,
|
|
3
|
+
B24V2BotResult,
|
|
4
|
+
B24V2BotListResult,
|
|
5
|
+
B24V2SendMessageResult,
|
|
6
|
+
B24V2ReadMessageResult,
|
|
7
|
+
B24InputActionStatusCode,
|
|
8
|
+
B24V2CommandResult,
|
|
9
|
+
B24V2EventGetResult,
|
|
10
|
+
B24V2FileUploadResult,
|
|
11
|
+
B24V2FileDownloadResult,
|
|
12
|
+
B24Keyboard,
|
|
13
|
+
Logger,
|
|
14
|
+
} from './types.js';
|
|
2
15
|
import { RateLimiter } from './rate-limiter.js';
|
|
3
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Bitrix24ApiError,
|
|
18
|
+
withRetry,
|
|
19
|
+
isRateLimitError,
|
|
20
|
+
defaultLogger,
|
|
21
|
+
maskWebhookUrl,
|
|
22
|
+
} from './utils.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* V2 bot context passed to every API call.
|
|
26
|
+
* In webhook auth mode, botToken is required for all calls.
|
|
27
|
+
*/
|
|
28
|
+
export interface BotContext {
|
|
29
|
+
botId: number;
|
|
30
|
+
botToken: string;
|
|
31
|
+
}
|
|
4
32
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
33
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
34
|
+
const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
35
|
+
const RETRYABLE_NETWORK_CODES = new Set([
|
|
36
|
+
'ECONNABORTED',
|
|
37
|
+
'ECONNREFUSED',
|
|
38
|
+
'ECONNRESET',
|
|
39
|
+
'EAI_AGAIN',
|
|
40
|
+
'EHOSTUNREACH',
|
|
41
|
+
'ENETUNREACH',
|
|
42
|
+
'ETIMEDOUT',
|
|
43
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
44
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
45
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
46
|
+
'UND_ERR_SOCKET',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function isRequestTimeoutError(error: unknown): boolean {
|
|
50
|
+
return error instanceof Error
|
|
51
|
+
&& (error.name === 'AbortError' || error.name === 'TimeoutError');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getErrorCode(error: unknown): string | null {
|
|
55
|
+
if (!error || typeof error !== 'object') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ('code' in error && typeof (error as { code?: unknown }).code === 'string') {
|
|
60
|
+
return (error as { code: string }).code;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ('cause' in error) {
|
|
64
|
+
const cause = (error as { cause?: unknown }).cause;
|
|
65
|
+
if (cause && typeof cause === 'object' && 'code' in cause && typeof (cause as { code?: unknown }).code === 'string') {
|
|
66
|
+
return (cause as { code: string }).code;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isRetryableWebhookError(error: unknown): boolean {
|
|
74
|
+
if (isRateLimitError(error)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (error instanceof Bitrix24ApiError) {
|
|
79
|
+
if (error.code === 'TIMEOUT') {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error.code.startsWith('HTTP_')) {
|
|
84
|
+
const status = Number(error.code.slice(5));
|
|
85
|
+
return Number.isFinite(status) && RETRYABLE_HTTP_STATUSES.has(status);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const errorCode = getErrorCode(error);
|
|
92
|
+
if (errorCode && RETRYABLE_NETWORK_CODES.has(errorCode)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return error instanceof TypeError
|
|
97
|
+
&& /fetch failed|network|socket|connect timeout|headers timeout|body timeout|terminated|timeout/i.test(error.message);
|
|
10
98
|
}
|
|
11
99
|
|
|
12
100
|
export class Bitrix24Api {
|
|
13
101
|
private rateLimiter: RateLimiter;
|
|
14
102
|
private logger: Logger;
|
|
15
|
-
private clientId?: string;
|
|
16
103
|
|
|
17
|
-
constructor(opts: { maxPerSecond?: number; logger?: Logger
|
|
104
|
+
constructor(opts: { maxPerSecond?: number; logger?: Logger } = {}) {
|
|
18
105
|
this.rateLimiter = new RateLimiter({ maxPerSecond: opts.maxPerSecond ?? 2 });
|
|
19
106
|
this.logger = opts.logger ?? defaultLogger;
|
|
20
|
-
this.clientId = opts.clientId;
|
|
21
107
|
}
|
|
22
108
|
|
|
23
109
|
/**
|
|
24
110
|
* Call B24 REST API via webhook URL.
|
|
25
111
|
* Webhook URL format: https://domain.bitrix24.com/rest/{user_id}/{token}/
|
|
26
|
-
* The URL itself contains authentication.
|
|
27
112
|
*/
|
|
28
113
|
async callWebhook<T = unknown>(
|
|
29
114
|
webhookUrl: string,
|
|
@@ -33,21 +118,27 @@ export class Bitrix24Api {
|
|
|
33
118
|
await this.rateLimiter.acquire();
|
|
34
119
|
|
|
35
120
|
const url = `${webhookUrl.replace(/\/+$/, '')}/${method}.json`;
|
|
36
|
-
this.logger.debug(`API call: ${method}
|
|
37
|
-
|
|
38
|
-
// In webhook mode, CLIENT_ID identifies the "app" that owns the bot
|
|
39
|
-
const payload = { ...(params ?? {}) };
|
|
40
|
-
if (this.clientId && !payload.CLIENT_ID) {
|
|
41
|
-
payload.CLIENT_ID = this.clientId;
|
|
42
|
-
}
|
|
121
|
+
this.logger.debug({ url: maskWebhookUrl(url) }, `API call: ${method}`);
|
|
43
122
|
|
|
44
123
|
const result = await withRetry(
|
|
45
124
|
async () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
125
|
+
let response: Response;
|
|
126
|
+
try {
|
|
127
|
+
response = await fetch(url, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify(params ?? {}),
|
|
131
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (isRequestTimeoutError(error)) {
|
|
135
|
+
throw new Bitrix24ApiError(
|
|
136
|
+
'TIMEOUT',
|
|
137
|
+
`${method} timed out after ${REQUEST_TIMEOUT_MS}ms`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
51
142
|
|
|
52
143
|
if (!response.ok) {
|
|
53
144
|
throw new Bitrix24ApiError(
|
|
@@ -56,7 +147,16 @@ export class Bitrix24Api {
|
|
|
56
147
|
);
|
|
57
148
|
}
|
|
58
149
|
|
|
59
|
-
const
|
|
150
|
+
const responseText = await response.text();
|
|
151
|
+
let data: B24ApiResult<T>;
|
|
152
|
+
try {
|
|
153
|
+
data = JSON.parse(responseText) as B24ApiResult<T>;
|
|
154
|
+
} catch {
|
|
155
|
+
throw new Bitrix24ApiError(
|
|
156
|
+
'INVALID_RESPONSE',
|
|
157
|
+
`Expected JSON from ${method}, got: ${responseText.slice(0, 200)}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
60
160
|
|
|
61
161
|
if (data.error) {
|
|
62
162
|
throw new Bitrix24ApiError(
|
|
@@ -69,7 +169,7 @@ export class Bitrix24Api {
|
|
|
69
169
|
},
|
|
70
170
|
{
|
|
71
171
|
maxRetries: 2,
|
|
72
|
-
shouldRetry:
|
|
172
|
+
shouldRetry: isRetryableWebhookError,
|
|
73
173
|
initialDelayMs: 1000,
|
|
74
174
|
},
|
|
75
175
|
);
|
|
@@ -77,306 +177,408 @@ export class Bitrix24Api {
|
|
|
77
177
|
return result;
|
|
78
178
|
}
|
|
79
179
|
|
|
180
|
+
// ─── Bot methods (imbot.v2.Bot.*) ───────────────────────────────────────
|
|
181
|
+
|
|
80
182
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @param clientEndpoint - e.g. "https://up.bitrix24.com/rest/"
|
|
85
|
-
* @param method - e.g. "imbot.message.add"
|
|
86
|
-
* @param accessToken - from event auth block
|
|
87
|
-
* @param params - method parameters
|
|
183
|
+
* Register a new bot (imbot.v2.Bot.register).
|
|
184
|
+
* Idempotent: same code returns existing bot.
|
|
88
185
|
*/
|
|
89
|
-
async
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
throw new Bitrix24ApiError(
|
|
113
|
-
`HTTP_${response.status}`,
|
|
114
|
-
`HTTP ${response.status}: ${response.statusText}`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const data = (await response.json()) as B24ApiResult<T>;
|
|
186
|
+
async registerBot(
|
|
187
|
+
webhookUrl: string,
|
|
188
|
+
botToken: string,
|
|
189
|
+
fields: {
|
|
190
|
+
code: string;
|
|
191
|
+
properties: { name: string; lastName?: string; workPosition?: string; color?: string; gender?: string; avatar?: string };
|
|
192
|
+
type?: string;
|
|
193
|
+
eventMode?: 'fetch' | 'webhook';
|
|
194
|
+
webhookUrl?: string;
|
|
195
|
+
isHidden?: boolean;
|
|
196
|
+
isReactionsEnabled?: boolean;
|
|
197
|
+
isSupportOpenline?: boolean;
|
|
198
|
+
backgroundId?: string | null;
|
|
199
|
+
},
|
|
200
|
+
): Promise<B24V2BotResult> {
|
|
201
|
+
const result = await this.callWebhook<B24V2BotResult>(
|
|
202
|
+
webhookUrl,
|
|
203
|
+
'imbot.v2.Bot.register',
|
|
204
|
+
{ botToken, fields },
|
|
205
|
+
);
|
|
206
|
+
return result.result;
|
|
207
|
+
}
|
|
119
208
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Update bot properties (imbot.v2.Bot.update).
|
|
211
|
+
*/
|
|
212
|
+
async updateBot(
|
|
213
|
+
webhookUrl: string,
|
|
214
|
+
bot: BotContext,
|
|
215
|
+
fields: Record<string, unknown>,
|
|
216
|
+
): Promise<B24V2BotResult> {
|
|
217
|
+
const result = await this.callWebhook<B24V2BotResult>(
|
|
218
|
+
webhookUrl,
|
|
219
|
+
'imbot.v2.Bot.update',
|
|
220
|
+
{ botId: bot.botId, botToken: bot.botToken, fields },
|
|
221
|
+
);
|
|
222
|
+
return result.result;
|
|
223
|
+
}
|
|
126
224
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
225
|
+
/**
|
|
226
|
+
* List bots of the current application (imbot.v2.Bot.list).
|
|
227
|
+
*/
|
|
228
|
+
async listBots(
|
|
229
|
+
webhookUrl: string,
|
|
230
|
+
botToken: string,
|
|
231
|
+
): Promise<B24V2BotListResult> {
|
|
232
|
+
const result = await this.callWebhook<B24V2BotListResult>(
|
|
233
|
+
webhookUrl,
|
|
234
|
+
'imbot.v2.Bot.list',
|
|
235
|
+
{ botToken },
|
|
134
236
|
);
|
|
237
|
+
return result.result;
|
|
238
|
+
}
|
|
135
239
|
|
|
136
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Unregister a bot (imbot.v2.Bot.unregister).
|
|
242
|
+
*/
|
|
243
|
+
async unregisterBot(
|
|
244
|
+
webhookUrl: string,
|
|
245
|
+
bot: BotContext,
|
|
246
|
+
): Promise<boolean> {
|
|
247
|
+
const result = await this.callWebhook<boolean>(
|
|
248
|
+
webhookUrl,
|
|
249
|
+
'imbot.v2.Bot.unregister',
|
|
250
|
+
{ botId: bot.botId, botToken: bot.botToken },
|
|
251
|
+
);
|
|
252
|
+
return result.result;
|
|
137
253
|
}
|
|
138
254
|
|
|
139
|
-
// ───
|
|
255
|
+
// ─── Message methods (imbot.v2.Chat.Message.*) ──────────────────────────
|
|
140
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Send a message (imbot.v2.Chat.Message.send).
|
|
259
|
+
*/
|
|
141
260
|
async sendMessage(
|
|
142
261
|
webhookUrl: string,
|
|
262
|
+
bot: BotContext,
|
|
143
263
|
dialogId: string,
|
|
144
264
|
message: string,
|
|
145
|
-
options?:
|
|
265
|
+
options?: { keyboard?: B24Keyboard; attach?: unknown; system?: boolean; urlPreview?: boolean },
|
|
146
266
|
): Promise<number> {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return result.result;
|
|
153
|
-
}
|
|
267
|
+
const fields: Record<string, unknown> = { message };
|
|
268
|
+
if (options?.keyboard) fields.keyboard = options.keyboard;
|
|
269
|
+
if (options?.attach) fields.attach = options.attach;
|
|
270
|
+
if (options?.system !== undefined) fields.system = options.system;
|
|
271
|
+
if (options?.urlPreview !== undefined) fields.urlPreview = options.urlPreview;
|
|
154
272
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
message: string,
|
|
160
|
-
options?: SendMessageOptions,
|
|
161
|
-
): Promise<number> {
|
|
162
|
-
const result = await this.callWithToken<number>(
|
|
163
|
-
clientEndpoint,
|
|
164
|
-
'imbot.message.add',
|
|
165
|
-
accessToken,
|
|
166
|
-
{
|
|
167
|
-
DIALOG_ID: dialogId,
|
|
168
|
-
MESSAGE: message,
|
|
169
|
-
...options,
|
|
170
|
-
},
|
|
273
|
+
const result = await this.callWebhook<B24V2SendMessageResult>(
|
|
274
|
+
webhookUrl,
|
|
275
|
+
'imbot.v2.Chat.Message.send',
|
|
276
|
+
{ botId: bot.botId, botToken: bot.botToken, dialogId, fields },
|
|
171
277
|
);
|
|
172
|
-
|
|
278
|
+
const messageId = result.result?.id;
|
|
279
|
+
if (!Number.isFinite(messageId) || messageId <= 0) {
|
|
280
|
+
throw new Bitrix24ApiError(
|
|
281
|
+
'INVALID_RESPONSE',
|
|
282
|
+
'imbot.v2.Chat.Message.send returned response without valid message id',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return messageId;
|
|
173
286
|
}
|
|
174
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Update a message (imbot.v2.Chat.Message.update).
|
|
290
|
+
*/
|
|
175
291
|
async updateMessage(
|
|
176
292
|
webhookUrl: string,
|
|
293
|
+
bot: BotContext,
|
|
177
294
|
messageId: number,
|
|
178
295
|
message: string,
|
|
296
|
+
options?: { keyboard?: B24Keyboard | 'N'; attach?: unknown },
|
|
179
297
|
): Promise<boolean> {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
298
|
+
const fields: Record<string, unknown> = { message };
|
|
299
|
+
if (options?.keyboard) fields.keyboard = options.keyboard;
|
|
300
|
+
if (options?.attach) fields.attach = options.attach;
|
|
301
|
+
|
|
302
|
+
const result = await this.callWebhook<boolean>(
|
|
303
|
+
webhookUrl,
|
|
304
|
+
'imbot.v2.Chat.Message.update',
|
|
305
|
+
{ botId: bot.botId, botToken: bot.botToken, messageId, fields },
|
|
306
|
+
);
|
|
184
307
|
return result.result;
|
|
185
308
|
}
|
|
186
309
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Answer a native bot command (imbot.v2.Command.answer).
|
|
312
|
+
*/
|
|
313
|
+
async answerCommand(
|
|
314
|
+
webhookUrl: string,
|
|
315
|
+
bot: BotContext,
|
|
316
|
+
commandId: number,
|
|
190
317
|
messageId: number,
|
|
318
|
+
dialogId: string,
|
|
191
319
|
message: string,
|
|
320
|
+
options?: { keyboard?: B24Keyboard; attach?: unknown; system?: boolean; urlPreview?: boolean },
|
|
192
321
|
): Promise<boolean> {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
322
|
+
const fields: Record<string, unknown> = { message };
|
|
323
|
+
if (options?.keyboard) fields.keyboard = options.keyboard;
|
|
324
|
+
if (options?.attach) fields.attach = options.attach;
|
|
325
|
+
if (options?.system !== undefined) fields.system = options.system;
|
|
326
|
+
if (options?.urlPreview !== undefined) fields.urlPreview = options.urlPreview;
|
|
327
|
+
|
|
328
|
+
const result = await this.callWebhook<boolean>(
|
|
329
|
+
webhookUrl,
|
|
330
|
+
'imbot.v2.Command.answer',
|
|
197
331
|
{
|
|
198
|
-
|
|
199
|
-
|
|
332
|
+
botId: bot.botId,
|
|
333
|
+
botToken: bot.botToken,
|
|
334
|
+
commandId,
|
|
335
|
+
messageId,
|
|
336
|
+
dialogId,
|
|
337
|
+
fields,
|
|
200
338
|
},
|
|
201
339
|
);
|
|
202
340
|
return result.result;
|
|
203
341
|
}
|
|
204
342
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
343
|
+
// ─── Chat methods (imbot.v2.Chat.*) ─────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Send typing indicator (imbot.v2.Chat.InputAction.notify).
|
|
347
|
+
*/
|
|
348
|
+
async notifyInputAction(
|
|
349
|
+
webhookUrl: string,
|
|
350
|
+
bot: BotContext,
|
|
351
|
+
dialogId: string,
|
|
352
|
+
options?: {
|
|
353
|
+
statusMessageCode?: B24InputActionStatusCode;
|
|
354
|
+
duration?: number;
|
|
355
|
+
},
|
|
356
|
+
): Promise<boolean> {
|
|
357
|
+
const params: Record<string, unknown> = {
|
|
358
|
+
botId: bot.botId,
|
|
359
|
+
botToken: bot.botToken,
|
|
360
|
+
dialogId,
|
|
361
|
+
};
|
|
362
|
+
if (options?.statusMessageCode) params.statusMessageCode = options.statusMessageCode;
|
|
363
|
+
if (options?.duration !== undefined) params.duration = options.duration;
|
|
364
|
+
|
|
365
|
+
const result = await this.callWebhook<boolean>(
|
|
366
|
+
webhookUrl,
|
|
367
|
+
'imbot.v2.Chat.InputAction.notify',
|
|
368
|
+
params,
|
|
369
|
+
);
|
|
209
370
|
return result.result;
|
|
210
371
|
}
|
|
211
372
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Backward-compatible wrapper for typing indicator.
|
|
375
|
+
*/
|
|
376
|
+
/**
|
|
377
|
+
* Mark messages as read (imbot.v2.Chat.Message.read).
|
|
378
|
+
*/
|
|
379
|
+
async readMessage(
|
|
380
|
+
webhookUrl: string,
|
|
381
|
+
bot: BotContext,
|
|
215
382
|
dialogId: string,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
383
|
+
messageId?: number,
|
|
384
|
+
): Promise<B24V2ReadMessageResult> {
|
|
385
|
+
const params: Record<string, unknown> = {
|
|
386
|
+
botId: bot.botId,
|
|
387
|
+
botToken: bot.botToken,
|
|
388
|
+
dialogId,
|
|
389
|
+
};
|
|
390
|
+
if (messageId !== undefined) params.messageId = messageId;
|
|
391
|
+
|
|
392
|
+
const result = await this.callWebhook<B24V2ReadMessageResult>(
|
|
393
|
+
webhookUrl,
|
|
394
|
+
'imbot.v2.Chat.Message.read',
|
|
395
|
+
params,
|
|
222
396
|
);
|
|
223
397
|
return result.result;
|
|
224
398
|
}
|
|
225
399
|
|
|
226
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Bot leaves a chat (imbot.v2.Chat.leave).
|
|
402
|
+
*/
|
|
403
|
+
async leaveChat(
|
|
227
404
|
webhookUrl: string,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
405
|
+
bot: BotContext,
|
|
406
|
+
dialogId: string,
|
|
407
|
+
): Promise<boolean> {
|
|
408
|
+
const result = await this.callWebhook<boolean>(
|
|
409
|
+
webhookUrl,
|
|
410
|
+
'imbot.v2.Chat.leave',
|
|
411
|
+
{ botId: bot.botId, botToken: bot.botToken, dialogId },
|
|
412
|
+
);
|
|
413
|
+
return result.result;
|
|
234
414
|
}
|
|
235
415
|
|
|
236
|
-
|
|
416
|
+
// ─── Reaction methods (imbot.v2.Chat.Message.Reaction.*) ────────────────
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Add a reaction to a message (imbot.v2.Chat.Message.Reaction.add).
|
|
420
|
+
*/
|
|
421
|
+
async addReaction(
|
|
237
422
|
webhookUrl: string,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
423
|
+
bot: BotContext,
|
|
424
|
+
messageId: number,
|
|
425
|
+
reaction: string,
|
|
426
|
+
): Promise<boolean> {
|
|
427
|
+
const result = await this.callWebhook<boolean>(
|
|
428
|
+
webhookUrl,
|
|
429
|
+
'imbot.v2.Chat.Message.Reaction.add',
|
|
430
|
+
{ botId: bot.botId, botToken: bot.botToken, messageId, reaction },
|
|
431
|
+
);
|
|
241
432
|
return result.result;
|
|
242
433
|
}
|
|
243
434
|
|
|
244
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Remove a reaction from a message (imbot.v2.Chat.Message.Reaction.delete).
|
|
437
|
+
*/
|
|
438
|
+
async deleteReaction(
|
|
245
439
|
webhookUrl: string,
|
|
246
|
-
|
|
247
|
-
|
|
440
|
+
bot: BotContext,
|
|
441
|
+
messageId: number,
|
|
442
|
+
reaction: string,
|
|
248
443
|
): Promise<boolean> {
|
|
249
|
-
const result = await this.callWebhook<boolean>(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
444
|
+
const result = await this.callWebhook<boolean>(
|
|
445
|
+
webhookUrl,
|
|
446
|
+
'imbot.v2.Chat.Message.Reaction.delete',
|
|
447
|
+
{ botId: bot.botId, botToken: bot.botToken, messageId, reaction },
|
|
448
|
+
);
|
|
253
449
|
return result.result;
|
|
254
450
|
}
|
|
255
451
|
|
|
452
|
+
// ─── Command methods (imbot.v2.Command.*) ───────────────────────────────
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Register a slash command (imbot.v2.Command.register).
|
|
456
|
+
* Idempotent: same command for same bot returns existing.
|
|
457
|
+
*/
|
|
256
458
|
async registerCommand(
|
|
257
459
|
webhookUrl: string,
|
|
460
|
+
bot: BotContext,
|
|
258
461
|
params: {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
EVENT_COMMAND_ADD: string;
|
|
462
|
+
command: string;
|
|
463
|
+
title?: Record<string, string>;
|
|
464
|
+
params?: Record<string, string>;
|
|
465
|
+
common?: boolean;
|
|
466
|
+
hidden?: boolean;
|
|
467
|
+
extranetSupport?: boolean;
|
|
266
468
|
},
|
|
267
|
-
): Promise<
|
|
268
|
-
const result = await this.callWebhook<
|
|
469
|
+
): Promise<B24V2CommandResult> {
|
|
470
|
+
const result = await this.callWebhook<B24V2CommandResult>(
|
|
269
471
|
webhookUrl,
|
|
270
|
-
'imbot.
|
|
271
|
-
|
|
472
|
+
'imbot.v2.Command.register',
|
|
473
|
+
{
|
|
474
|
+
botId: bot.botId,
|
|
475
|
+
botToken: bot.botToken,
|
|
476
|
+
...params,
|
|
477
|
+
},
|
|
272
478
|
);
|
|
273
479
|
return result.result;
|
|
274
480
|
}
|
|
275
481
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const result = await this.callWebhook<boolean>(
|
|
285
|
-
|
|
286
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Unregister a slash command (imbot.v2.Command.unregister).
|
|
484
|
+
*/
|
|
485
|
+
async unregisterCommand(
|
|
486
|
+
webhookUrl: string,
|
|
487
|
+
bot: BotContext,
|
|
488
|
+
commandId: number,
|
|
489
|
+
): Promise<boolean> {
|
|
490
|
+
const result = await this.callWebhook<boolean>(
|
|
491
|
+
webhookUrl,
|
|
492
|
+
'imbot.v2.Command.unregister',
|
|
493
|
+
{ botId: bot.botId, botToken: bot.botToken, commandId },
|
|
494
|
+
);
|
|
287
495
|
return result.result;
|
|
288
496
|
}
|
|
289
497
|
|
|
290
|
-
// ───
|
|
498
|
+
// ─── Event methods (imbot.v2.Event.*) ───────────────────────────────────
|
|
291
499
|
|
|
292
500
|
/**
|
|
293
|
-
*
|
|
294
|
-
* Uses the user's access token (file belongs to the user's disk).
|
|
501
|
+
* Fetch events via polling (imbot.v2.Event.get).
|
|
295
502
|
*/
|
|
296
|
-
async
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
): Promise<
|
|
301
|
-
const result = await this.
|
|
302
|
-
|
|
303
|
-
'
|
|
304
|
-
|
|
305
|
-
|
|
503
|
+
async fetchEvents(
|
|
504
|
+
webhookUrl: string,
|
|
505
|
+
bot: BotContext,
|
|
506
|
+
params: { offset?: number; limit?: number },
|
|
507
|
+
): Promise<B24V2EventGetResult> {
|
|
508
|
+
const result = await this.callWebhook<B24V2EventGetResult>(
|
|
509
|
+
webhookUrl,
|
|
510
|
+
'imbot.v2.Event.get',
|
|
511
|
+
{
|
|
512
|
+
botId: bot.botId,
|
|
513
|
+
botToken: bot.botToken,
|
|
514
|
+
...params,
|
|
515
|
+
},
|
|
306
516
|
);
|
|
307
517
|
return result.result;
|
|
308
518
|
}
|
|
309
519
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
520
|
+
// ─── User event subscription (im.v2.Event.*) ───────────────────────────
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Subscribe current user to event recording (im.v2.Event.subscribe).
|
|
524
|
+
*/
|
|
525
|
+
async subscribeUserEvents(webhookUrl: string): Promise<boolean> {
|
|
526
|
+
const result = await this.callWebhook<boolean>(
|
|
315
527
|
webhookUrl,
|
|
316
|
-
'
|
|
317
|
-
{
|
|
528
|
+
'im.v2.Event.subscribe',
|
|
529
|
+
{},
|
|
318
530
|
);
|
|
319
531
|
return result.result;
|
|
320
532
|
}
|
|
321
533
|
|
|
322
534
|
/**
|
|
323
|
-
*
|
|
535
|
+
* Unsubscribe current user from event recording (im.v2.Event.unsubscribe).
|
|
536
|
+
* Idempotent — safe to call even if not subscribed.
|
|
324
537
|
*/
|
|
325
|
-
async
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
|
|
331
|
-
clientEndpoint,
|
|
332
|
-
'im.disk.folder.get',
|
|
333
|
-
accessToken,
|
|
334
|
-
{ CHAT_ID: chatId },
|
|
538
|
+
async unsubscribeUserEvents(webhookUrl: string): Promise<boolean> {
|
|
539
|
+
const result = await this.callWebhook<boolean>(
|
|
540
|
+
webhookUrl,
|
|
541
|
+
'im.v2.Event.unsubscribe',
|
|
542
|
+
{},
|
|
335
543
|
);
|
|
336
|
-
return result.result
|
|
544
|
+
return result.result;
|
|
337
545
|
}
|
|
338
546
|
|
|
547
|
+
// ─── File methods (imbot.v2.File.*) ─────────────────────────────────────
|
|
548
|
+
|
|
339
549
|
/**
|
|
340
|
-
* Upload a file to a
|
|
341
|
-
*
|
|
550
|
+
* Upload a file to a chat (imbot.v2.File.upload).
|
|
551
|
+
* Single call replaces the old 3-step process (folder.get → upload → commit).
|
|
342
552
|
*/
|
|
343
553
|
async uploadFile(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
accessToken,
|
|
354
|
-
{
|
|
355
|
-
id: folderId,
|
|
356
|
-
data: { NAME: fileName },
|
|
357
|
-
fileContent: [fileName, content.toString('base64')],
|
|
358
|
-
generateUniqueName: true,
|
|
359
|
-
},
|
|
554
|
+
webhookUrl: string,
|
|
555
|
+
bot: BotContext,
|
|
556
|
+
dialogId: string,
|
|
557
|
+
fields: { name: string; content: string; message?: string },
|
|
558
|
+
): Promise<B24V2FileUploadResult> {
|
|
559
|
+
const result = await this.callWebhook<B24V2FileUploadResult>(
|
|
560
|
+
webhookUrl,
|
|
561
|
+
'imbot.v2.File.upload',
|
|
562
|
+
{ botId: bot.botId, botToken: bot.botToken, dialogId, fields },
|
|
360
563
|
);
|
|
361
|
-
return result.result
|
|
564
|
+
return result.result;
|
|
362
565
|
}
|
|
363
566
|
|
|
364
567
|
/**
|
|
365
|
-
*
|
|
568
|
+
* Get download URL for a file (imbot.v2.File.download).
|
|
366
569
|
*/
|
|
367
|
-
async
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
): Promise<
|
|
373
|
-
const result = await this.
|
|
374
|
-
|
|
375
|
-
'
|
|
376
|
-
|
|
377
|
-
{ CHAT_ID: chatId, DISK_ID: diskId },
|
|
570
|
+
async getFileDownloadUrl(
|
|
571
|
+
webhookUrl: string,
|
|
572
|
+
bot: BotContext,
|
|
573
|
+
dialogId: string,
|
|
574
|
+
fileId: number,
|
|
575
|
+
): Promise<string> {
|
|
576
|
+
const result = await this.callWebhook<B24V2FileDownloadResult>(
|
|
577
|
+
webhookUrl,
|
|
578
|
+
'imbot.v2.File.download',
|
|
579
|
+
{ botId: bot.botId, botToken: bot.botToken, dialogId, fileId },
|
|
378
580
|
);
|
|
379
|
-
return result.result;
|
|
581
|
+
return result.result.downloadUrl;
|
|
380
582
|
}
|
|
381
583
|
|
|
382
584
|
destroy(): void {
|