itd-api 0.0.5 → 0.0.6

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
@@ -287,6 +287,24 @@ await itd.posts.create(draft.content('второй')); // заготовка
287
287
  Файлы из `attach()` загружаются автоматически, порядок вложений сохраняется, MIME-тип
288
288
  проверяется до отправки.
289
289
 
290
+ Разметка текста передаётся полем `spans` — библиотека её не генерирует и не пересчитывает.
291
+ Известные типы собраны в `SpanType`: `hashtag`, `mention`, `link`, `bold`, `italic`,
292
+ `underline`, `strike`, `spoiler`, `monospace`, `quote`.
293
+
294
+ ```ts
295
+ import { SpanType } from 'itd-api';
296
+
297
+ await itd.posts.create({
298
+ content: 'жирное слово и ссылка',
299
+ spans: [
300
+ { type: SpanType.Bold, offset: 0, length: 6 },
301
+ { type: SpanType.Link, offset: 15, length: 6, url: 'https://example.com' },
302
+ ],
303
+ });
304
+ ```
305
+
306
+ У `link` адрес лежит в `url`, у `hashtag` и `mention` — имя в `tag`.
307
+
290
308
  Билдеры есть у поста, комментария, опроса и жалобы. Все они неизменяемые, а `build()`
291
309
  проверяет данные и бросает `ItdConfigError` **до** обращения к сети:
292
310
 
@@ -440,6 +458,92 @@ Deno и React Native ограничение не действует.
440
458
 
441
459
  ---
442
460
 
461
+ ## Плагины
462
+
463
+ Плагин — обёртка вокруг запроса: она видит тело до отправки и разобранный ответ, поэтому
464
+ одна обёртка охватывает сразу все методы клиента. Подключается через `itd.use()`:
465
+
466
+ ```ts
467
+ import { ItdClient } from 'itd-api';
468
+ import { crypt } from 'itd-api-crypto';
469
+
470
+ const itd = new ItdClient({ auth: token });
471
+ itd.use(crypt());
472
+ ```
473
+
474
+ ### `itd-api-crypto` — скрытые сообщения
475
+
476
+ [Отдельный пакет](./crypto): прячет текст в невидимых символах внутри обычного поста.
477
+ Читатель видит обложку, а тот, у кого подключён плагин, получает спрятанное отдельным полем.
478
+
479
+ ```sh
480
+ npm i itd-api-crypto
481
+ ```
482
+
483
+ ```ts
484
+ // отправка: текст прогоняется через шифр, обложка остаётся видимой
485
+ const created = await itd.posts.create(
486
+ { content: 'секретный текст' },
487
+ { encrypt: { cipher: 'invisible', cover: 'обычный пост' } },
488
+ );
489
+
490
+ // чтение: content не меняется, расшифровка приезжает рядом
491
+ const post = await itd.posts.get(created.id);
492
+ post.secret?.text; // 'секретный текст'
493
+ ```
494
+
495
+ Работает для постов, комментариев, ответов, имени и подписи профиля. Расшифровка идёт сама
496
+ и вглубь: находки появляются и у постов ленты, и у исходного поста репоста, и у авторов.
497
+
498
+ Шифра два: `invisible` — невидимые символы с обложкой, `beecrypt` — видимый текст из букв
499
+ `жъЖЪ`. Подробности, ограничения и то, как подключить свой шифр, — в
500
+ [README пакета](./crypto).
501
+
502
+ ### Свой плагин
503
+
504
+ ```ts
505
+ import type { ItdPlugin } from 'itd-api';
506
+
507
+ const timing: ItdPlugin = {
508
+ name: 'timing',
509
+ install({ use, logger }) {
510
+ use(async (request, next) => {
511
+ const started = Date.now();
512
+ try {
513
+ return await next(request);
514
+ } finally {
515
+ logger?.info(`${request.method} ${request.path}: ${Date.now() - started} мс`);
516
+ }
517
+ });
518
+ },
519
+ };
520
+ ```
521
+
522
+ Обёртка может изменить запрос (передайте в `next` копию), подменить ответ или вернуть своё,
523
+ не обращаясь к сети. Подключённая раньше оказывается снаружи. Выполняется она один раз
524
+ на запрос, независимо от числа повторов.
525
+
526
+ Свои опции запроса плагин объявляет сам — библиотека их не понимает, но доносит до обёртки
527
+ нетронутыми:
528
+
529
+ ```ts
530
+ const plugin: ItdPlugin = {
531
+ name: 'мой',
532
+ optionKeys: ['мояОпция'],
533
+ install({ use }) { /* … */ },
534
+ };
535
+
536
+ declare module 'itd-api' {
537
+ interface RequestOptions { мояОпция?: string | undefined }
538
+ }
539
+ ```
540
+
541
+ Имена полей самого запроса (`path`, `body`, `headers`, `signal` и прочие) заявить нельзя:
542
+ подключение такого плагина завершится `ItdConfigError`. Иначе опечатка в `optionKeys`
543
+ молча подменяла бы путь или тело любого вызова.
544
+
545
+ ---
546
+
443
547
  ## Что доступно
444
548
 
445
549
  | Раздел | Методы |
@@ -454,6 +558,7 @@ Deno и React Native ограничение не действует.
454
558
  | `itd.reports` · `itd.verification` | жалобы, заявка на верификацию |
455
559
  | `itd.subscription` · `itd.platform` | подписка, способы оплаты, анонсы |
456
560
  | `itd.realtime()` | поток уведомлений |
561
+ | `itd.use()` | плагины: обёртки вокруг запроса и ответа |
457
562
  | `itd.request()` | произвольный запрос, если метода ещё нет |
458
563
 
459
564
  Метода не хватает или ответ разошёлся с документацией — есть запасной путь:
@@ -482,7 +587,8 @@ TypeScript 5.0+. Пакет собран в ESM и CommonJS, типы корре
482
587
 
483
588
  ```bash
484
589
  npm install
485
- npm test # 342 теста
590
+ npm test # 417 тестов
591
+ npm run test:all # вместе с пакетами workspace
486
592
  npm run typecheck
487
593
  npm run lint
488
594
  npm run build
@@ -577,6 +577,24 @@ var AttachmentType = Object.freeze({
577
577
  /** Голосовые комментарии: `audio/ogg`, с полем `duration`. */
578
578
  Audio: "audio"
579
579
  });
580
+ var SpanType = Object.freeze({
581
+ /** Хэштег. Название без решётки лежит в `tag`. */
582
+ Hashtag: "hashtag",
583
+ /** Упоминание. Имя пользователя лежит в `tag`. */
584
+ Mention: "mention",
585
+ /** Ссылка. Адрес лежит в `url`, а не в `tag`. */
586
+ Link: "link",
587
+ Bold: "bold",
588
+ Italic: "italic",
589
+ Underline: "underline",
590
+ /** Зачёркнутый. */
591
+ Strike: "strike",
592
+ /** Спойлер: текст скрыт до нажатия. */
593
+ Spoiler: "spoiler",
594
+ /** Моноширинный. */
595
+ Monospace: "monospace",
596
+ Quote: "quote"
597
+ });
580
598
  var ReportTargetType = Object.freeze({
581
599
  Post: "post",
582
600
  Comment: "comment",
@@ -1696,7 +1714,7 @@ function normalizeBaseUrl(baseUrl) {
1696
1714
  // src/core/config.ts
1697
1715
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1698
1716
  var DEFAULT_TIMEOUT = 3e4;
1699
- var LIBRARY_VERSION = "0.0.5";
1717
+ var LIBRARY_VERSION = "0.0.6";
1700
1718
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1701
1719
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1702
1720
  function requirePositive(value, name) {
@@ -2091,6 +2109,7 @@ function createApiError(context) {
2091
2109
  function sleep(ms) {
2092
2110
  return new Promise((resolve) => setTimeout(resolve, ms));
2093
2111
  }
2112
+ var EMPTY_KEYS = /* @__PURE__ */ new Set();
2094
2113
  function setHeader(headers, name, value) {
2095
2114
  try {
2096
2115
  headers.set(name, value);
@@ -2144,6 +2163,7 @@ function createAbortBundle(userSignal, timeout) {
2144
2163
  var HttpClient = class {
2145
2164
  #config;
2146
2165
  #collaborators;
2166
+ #plugins;
2147
2167
  constructor(config, collaborators = {}) {
2148
2168
  this.#config = config;
2149
2169
  this.#collaborators = collaborators;
@@ -2152,6 +2172,19 @@ var HttpClient = class {
2152
2172
  get baseUrl() {
2153
2173
  return this.#config.baseUrl;
2154
2174
  }
2175
+ /**
2176
+ * Имена опций запроса, заявленные плагинами.
2177
+ *
2178
+ * Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
2179
+ * если их никто не заявил, отсеивают.
2180
+ */
2181
+ get pluginOptionKeys() {
2182
+ return this.#plugins?.optionKeys ?? EMPTY_KEYS;
2183
+ }
2184
+ /** Подключает список плагинов. Реестр общий с клиентом и пополняется через `itd.use()`. */
2185
+ usePlugins(plugins) {
2186
+ this.#plugins = plugins;
2187
+ }
2155
2188
  /**
2156
2189
  * Подключает недостающие части конвейера.
2157
2190
  *
@@ -2171,10 +2204,22 @@ var HttpClient = class {
2171
2204
  * @throws {ItdNetworkError} если запрос не дошёл до сервера
2172
2205
  */
2173
2206
  async request(options) {
2174
- const task = () => this.#withRetries(options);
2207
+ const task = () => this.#withPlugins(options);
2175
2208
  if (!this.#collaborators.schedule || options.skipQueue) return task();
2176
2209
  return this.#collaborators.schedule(task);
2177
2210
  }
2211
+ /**
2212
+ * Прогоняет запрос через обёртки плагинов.
2213
+ *
2214
+ * Цепочка стоит **снаружи повторов и внутри очереди**: плагин должен увидеть запрос
2215
+ * и ответ по одному разу, независимо от того, сколько попыток понадобилось, — иначе,
2216
+ * например, текст поста зашифруется повторно на второй попытке.
2217
+ */
2218
+ #withPlugins(options) {
2219
+ const plugins = this.#plugins;
2220
+ if (!plugins || plugins.size === 0) return this.#withRetries(options);
2221
+ return plugins.run(options, (request) => this.#withRetries(request));
2222
+ }
2178
2223
  async #withRetries(options) {
2179
2224
  const method = options.method.toUpperCase();
2180
2225
  for (let attempt = 1; ; attempt++) {
@@ -2320,6 +2365,97 @@ var HttpClient = class {
2320
2365
  }
2321
2366
  };
2322
2367
 
2368
+ // src/core/plugins.ts
2369
+ var NO_KEYS = /* @__PURE__ */ new Set();
2370
+ var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
2371
+ "signal",
2372
+ "timeout",
2373
+ "headers",
2374
+ "retry",
2375
+ "method",
2376
+ "path",
2377
+ "query",
2378
+ "body",
2379
+ "skipAuth",
2380
+ "skipAuthRefresh",
2381
+ "skipQueue",
2382
+ "raw"
2383
+ ]);
2384
+ var PluginRegistry = class {
2385
+ #transformers = [];
2386
+ #optionKeys = /* @__PURE__ */ new Set();
2387
+ #names = /* @__PURE__ */ new Set();
2388
+ /** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
2389
+ get size() {
2390
+ return this.#transformers.length;
2391
+ }
2392
+ /** Имена опций запроса, заявленные плагинами. */
2393
+ get optionKeys() {
2394
+ return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
2395
+ }
2396
+ /**
2397
+ * Подключает плагин.
2398
+ *
2399
+ * @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
2400
+ * имя опции
2401
+ */
2402
+ add(plugin, context) {
2403
+ if (typeof plugin?.install !== "function") {
2404
+ throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
2405
+ }
2406
+ const name = plugin.name;
2407
+ if (typeof name !== "string" || name.trim() === "") {
2408
+ throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
2409
+ }
2410
+ if (this.#names.has(name)) {
2411
+ throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
2412
+ }
2413
+ const keys = plugin.optionKeys ?? [];
2414
+ for (const key of keys) {
2415
+ if (typeof key !== "string" || key.trim() === "") {
2416
+ throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
2417
+ }
2418
+ if (RESERVED_OPTION_KEYS.has(key)) {
2419
+ throw new ItdConfigError(
2420
+ `\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
2421
+ );
2422
+ }
2423
+ }
2424
+ const before = this.#transformers.length;
2425
+ try {
2426
+ plugin.install({
2427
+ ...context,
2428
+ use: (transformer) => {
2429
+ if (typeof transformer !== "function") {
2430
+ throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
2431
+ }
2432
+ this.#transformers.push(transformer);
2433
+ }
2434
+ });
2435
+ } catch (error) {
2436
+ this.#transformers.length = before;
2437
+ throw error;
2438
+ }
2439
+ this.#names.add(name);
2440
+ for (const key of keys) this.#optionKeys.add(key);
2441
+ }
2442
+ /**
2443
+ * Прогоняет запрос через цепочку обёрток.
2444
+ *
2445
+ * Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
2446
+ * а обёрток единицы — экономить тут не на чем.
2447
+ *
2448
+ * @param execute настоящий запрос, вызывается самой внутренней обёрткой
2449
+ */
2450
+ run(request, execute) {
2451
+ const chain = this.#transformers.reduceRight(
2452
+ (next, transformer) => (current) => transformer(current, next),
2453
+ execute
2454
+ );
2455
+ return chain(request);
2456
+ }
2457
+ };
2458
+
2323
2459
  // src/core/rate-limit.ts
2324
2460
  var RequestQueue = class {
2325
2461
  #concurrency;
@@ -3240,15 +3376,31 @@ var BaseResource = class {
3240
3376
  constructor(http) {
3241
3377
  this.http = http;
3242
3378
  }
3243
- /** Переносит общие поля опций запроса в параметры транспорта. */
3379
+ /**
3380
+ * Переносит общие поля опций запроса в параметры транспорта.
3381
+ *
3382
+ * Поля перечислены поимённо, а не скопированы целиком: параметры методов наследуют
3383
+ * {@link RequestOptions} и приносят с собой `limit`, `cursor` и прочее, чему в описании
3384
+ * запроса делать нечего. Исключение — опции, заявленные плагинами: их библиотека
3385
+ * не понимает, но обязана донести до обёрток нетронутыми.
3386
+ */
3244
3387
  requestOptions(options) {
3245
3388
  if (!options) return {};
3246
- return {
3389
+ const result = {
3247
3390
  ...options.signal !== void 0 ? { signal: options.signal } : {},
3248
3391
  ...options.timeout !== void 0 ? { timeout: options.timeout } : {},
3249
3392
  ...options.headers !== void 0 ? { headers: options.headers } : {},
3250
3393
  ...options.retry !== void 0 ? { retry: options.retry } : {}
3251
3394
  };
3395
+ const pluginKeys = this.http.pluginOptionKeys;
3396
+ if (pluginKeys.size === 0) return result;
3397
+ const source = options;
3398
+ const target = result;
3399
+ for (const key of pluginKeys) {
3400
+ const value = source[key];
3401
+ if (value !== void 0) target[key] = value;
3402
+ }
3403
+ return result;
3252
3404
  }
3253
3405
  /**
3254
3406
  * Собирает перебор страниц.
@@ -4909,6 +5061,7 @@ var ItdClient = class {
4909
5061
  #authManager;
4910
5062
  #jar;
4911
5063
  #queue;
5064
+ #plugins = new PluginRegistry();
4912
5065
  /** Авторизация, сессии и пароли. */
4913
5066
  auth;
4914
5067
  /** Профили, подписки, блокировки, приватность. */
@@ -4945,6 +5098,7 @@ var ItdClient = class {
4945
5098
  this.#http = new HttpClient(this.#config);
4946
5099
  this.#authManager = new AuthManager(this.#config, this.#http, this.#jar);
4947
5100
  this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
5101
+ this.#http.usePlugins(this.#plugins);
4948
5102
  this.#http.setCollaborators({
4949
5103
  getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4950
5104
  getDeviceId: () => this.#authManager.getDeviceId(),
@@ -4990,6 +5144,27 @@ var ItdClient = class {
4990
5144
  request(options) {
4991
5145
  return this.#http.request(options);
4992
5146
  }
5147
+ /**
5148
+ * Подключает плагин.
5149
+ *
5150
+ * Плагин работает на уровне транспорта: видит запрос до отправки и разобранный ответ,
5151
+ * поэтому одна обёртка охватывает сразу все методы клиента. Подключать можно в любой
5152
+ * момент, но обычно это делают сразу после создания клиента.
5153
+ *
5154
+ * @throws {ItdConfigError} если плагин задан неверно или уже подключён
5155
+ *
5156
+ * @example
5157
+ * ```ts
5158
+ * import { crypt } from 'itd-api-crypto';
5159
+ *
5160
+ * itd.use(crypt());
5161
+ * await itd.posts.create({ content: 'секрет' }, { encrypt: 'invis' });
5162
+ * ```
5163
+ */
5164
+ use(plugin) {
5165
+ this.#plugins.add(plugin, { baseUrl: this.#config.baseUrl, logger: this.#config.logger });
5166
+ return this;
5167
+ }
4993
5168
  /**
4994
5169
  * Подписывается на события авторизации.
4995
5170
  *
@@ -5216,6 +5391,6 @@ function toDate(value) {
5216
5391
  return Number.isFinite(date.getTime()) ? date : null;
5217
5392
  }
5218
5393
 
5219
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
5220
- //# sourceMappingURL=chunk-RUPF4X5L.js.map
5221
- //# sourceMappingURL=chunk-RUPF4X5L.js.map
5394
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
5395
+ //# sourceMappingURL=chunk-CCRQI3ON.js.map
5396
+ //# sourceMappingURL=chunk-CCRQI3ON.js.map