@ihazz/bitrix24 0.2.5 → 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/src/api.ts CHANGED
@@ -1,29 +1,114 @@
1
- import type { B24ApiResult, SendMessageOptions } from './types.js';
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 { Bitrix24ApiError, withRetry, isRateLimitError, defaultLogger } from './utils.js';
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
- interface Logger {
6
- info: (...args: unknown[]) => void;
7
- warn: (...args: unknown[]) => void;
8
- error: (...args: unknown[]) => void;
9
- debug: (...args: unknown[]) => void;
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; clientId?: string } = {}) {
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}`, { url });
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
- const response = await fetch(url, {
47
- method: 'POST',
48
- headers: { 'Content-Type': 'application/json' },
49
- body: JSON.stringify(payload),
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 data = (await response.json()) as B24ApiResult<T>;
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: isRateLimitError,
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
- * Call B24 REST API via OAuth/event access token.
82
- * Used when processing webhook events that include access_token.
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 callWithToken<T = unknown>(
90
- clientEndpoint: string,
91
- method: string,
92
- accessToken: string,
93
- params?: Record<string, unknown>,
94
- ): Promise<B24ApiResult<T>> {
95
- await this.rateLimiter.acquire();
96
-
97
- const url = `${clientEndpoint.replace(/\/+$/, '')}/${method}.json`;
98
- this.logger.debug(`API call (token): ${method}`, { url });
99
-
100
- const result = await withRetry(
101
- async () => {
102
- const response = await fetch(url, {
103
- method: 'POST',
104
- headers: {
105
- 'Content-Type': 'application/json',
106
- Authorization: `Bearer ${accessToken}`,
107
- },
108
- body: JSON.stringify(params ?? {}),
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
- if (data.error) {
121
- throw new Bitrix24ApiError(
122
- data.error,
123
- data.error_description ?? 'Unknown error',
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
- return data;
128
- },
129
- {
130
- maxRetries: 2,
131
- shouldRetry: isRateLimitError,
132
- initialDelayMs: 1000,
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
- return result;
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
- // ─── Convenience methods ──────────────────────────────────────────────
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?: SendMessageOptions,
265
+ options?: { keyboard?: B24Keyboard; attach?: unknown; system?: boolean; urlPreview?: boolean },
146
266
  ): Promise<number> {
147
- const result = await this.callWebhook<number>(webhookUrl, 'imbot.message.add', {
148
- DIALOG_ID: dialogId,
149
- MESSAGE: message,
150
- ...options,
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
- async sendMessageWithToken(
156
- clientEndpoint: string,
157
- accessToken: string,
158
- dialogId: string,
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
- return result.result;
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 result = await this.callWebhook<boolean>(webhookUrl, 'imbot.message.update', {
181
- MESSAGE_ID: messageId,
182
- MESSAGE: message,
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
- async updateMessageWithToken(
188
- clientEndpoint: string,
189
- accessToken: string,
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 result = await this.callWithToken<boolean>(
194
- clientEndpoint,
195
- 'imbot.message.update',
196
- accessToken,
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
- MESSAGE_ID: messageId,
199
- MESSAGE: message,
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
- async sendTyping(webhookUrl: string, dialogId: string): Promise<boolean> {
206
- const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.chat.sendTyping', {
207
- DIALOG_ID: dialogId,
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
- async sendTypingWithToken(
213
- clientEndpoint: string,
214
- accessToken: string,
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
- ): Promise<boolean> {
217
- const result = await this.callWithToken<boolean>(
218
- clientEndpoint,
219
- 'imbot.chat.sendTyping',
220
- accessToken,
221
- { DIALOG_ID: dialogId },
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
- async listBots(
400
+ /**
401
+ * Bot leaves a chat (imbot.v2.Chat.leave).
402
+ */
403
+ async leaveChat(
227
404
  webhookUrl: string,
228
- ): Promise<Array<{ ID: number; NAME: string; CODE: string; OPENLINE: string }>> {
229
- // B24 returns an object keyed by bot ID, not an array
230
- const result = await this.callWebhook<
231
- Record<string, { ID: number; NAME: string; CODE: string; OPENLINE: string }>
232
- >(webhookUrl, 'imbot.bot.list', {});
233
- return Object.values(result.result ?? {});
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
- async registerBot(
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
- params: Record<string, unknown>,
239
- ): Promise<number> {
240
- const result = await this.callWebhook<number>(webhookUrl, 'imbot.register', params);
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
- async updateBot(
435
+ /**
436
+ * Remove a reaction from a message (imbot.v2.Chat.Message.Reaction.delete).
437
+ */
438
+ async deleteReaction(
245
439
  webhookUrl: string,
246
- botId: number,
247
- fields: Record<string, unknown>,
440
+ bot: BotContext,
441
+ messageId: number,
442
+ reaction: string,
248
443
  ): Promise<boolean> {
249
- const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.update', {
250
- BOT_ID: botId,
251
- FIELDS: fields,
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
- BOT_ID: number;
260
- COMMAND: string;
261
- COMMON?: 'Y' | 'N';
262
- HIDDEN?: 'Y' | 'N';
263
- EXTRANET_SUPPORT?: 'Y' | 'N';
264
- LANG: Array<{ LANGUAGE_ID: string; TITLE: string; PARAMS?: string }>;
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<number> {
268
- const result = await this.callWebhook<number>(
469
+ ): Promise<B24V2CommandResult> {
470
+ const result = await this.callWebhook<B24V2CommandResult>(
269
471
  webhookUrl,
270
- 'imbot.command.register',
271
- params,
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
- async unregisterCommand(webhookUrl: string, commandId: number): Promise<boolean> {
277
- const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.command.unregister', {
278
- COMMAND_ID: commandId,
279
- });
280
- return result.result;
281
- }
282
-
283
- async unregisterBot(webhookUrl: string, botId: number): Promise<boolean> {
284
- const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.unregister', {
285
- BOT_ID: botId,
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
- // ─── File / Disk methods ─────────────────────────────────────────────
498
+ // ─── Event methods (imbot.v2.Event.*) ───────────────────────────────────
291
499
 
292
500
  /**
293
- * Get file info including DOWNLOAD_URL.
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 getFileInfo(
297
- clientEndpoint: string,
298
- accessToken: string,
299
- fileId: number,
300
- ): Promise<{ DOWNLOAD_URL: string; [key: string]: unknown }> {
301
- const result = await this.callWithToken<{ DOWNLOAD_URL: string; [key: string]: unknown }>(
302
- clientEndpoint,
303
- 'disk.file.get',
304
- accessToken,
305
- { id: fileId },
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
- async getFileInfoViaWebhook(
311
- webhookUrl: string,
312
- fileId: number,
313
- ): Promise<{ DOWNLOAD_URL: string; [key: string]: unknown }> {
314
- const result = await this.callWebhook<{ DOWNLOAD_URL: string; [key: string]: unknown }>(
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
- 'disk.file.get',
317
- { id: fileId },
528
+ 'im.v2.Event.subscribe',
529
+ {},
318
530
  );
319
531
  return result.result;
320
532
  }
321
533
 
322
534
  /**
323
- * Get the disk folder ID for a chat (needed for file uploads).
535
+ * Unsubscribe current user from event recording (im.v2.Event.unsubscribe).
536
+ * Idempotent — safe to call even if not subscribed.
324
537
  */
325
- async getChatFolder(
326
- clientEndpoint: string,
327
- accessToken: string,
328
- chatId: number,
329
- ): Promise<number> {
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.ID;
544
+ return result.result;
337
545
  }
338
546
 
547
+ // ─── File methods (imbot.v2.File.*) ─────────────────────────────────────
548
+
339
549
  /**
340
- * Upload a file to a disk folder (base64 encoded).
341
- * Returns the disk file ID.
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
- clientEndpoint: string,
345
- accessToken: string,
346
- folderId: number,
347
- fileName: string,
348
- content: Buffer,
349
- ): Promise<number> {
350
- const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
351
- clientEndpoint,
352
- 'disk.folder.uploadfile',
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.ID;
564
+ return result.result;
362
565
  }
363
566
 
364
567
  /**
365
- * Publish an uploaded file to a chat.
568
+ * Get download URL for a file (imbot.v2.File.download).
366
569
  */
367
- async commitFileToChat(
368
- clientEndpoint: string,
369
- accessToken: string,
370
- chatId: number,
371
- diskId: number,
372
- ): Promise<boolean> {
373
- const result = await this.callWithToken<boolean>(
374
- clientEndpoint,
375
- 'im.disk.file.commit',
376
- accessToken,
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 {