itd-api 0.0.4 → 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.
@@ -579,6 +579,24 @@ var AttachmentType = Object.freeze({
579
579
  /** Голосовые комментарии: `audio/ogg`, с полем `duration`. */
580
580
  Audio: "audio"
581
581
  });
582
+ var SpanType = Object.freeze({
583
+ /** Хэштег. Название без решётки лежит в `tag`. */
584
+ Hashtag: "hashtag",
585
+ /** Упоминание. Имя пользователя лежит в `tag`. */
586
+ Mention: "mention",
587
+ /** Ссылка. Адрес лежит в `url`, а не в `tag`. */
588
+ Link: "link",
589
+ Bold: "bold",
590
+ Italic: "italic",
591
+ Underline: "underline",
592
+ /** Зачёркнутый. */
593
+ Strike: "strike",
594
+ /** Спойлер: текст скрыт до нажатия. */
595
+ Spoiler: "spoiler",
596
+ /** Моноширинный. */
597
+ Monospace: "monospace",
598
+ Quote: "quote"
599
+ });
582
600
  var ReportTargetType = Object.freeze({
583
601
  Post: "post",
584
602
  Comment: "comment",
@@ -1556,7 +1574,7 @@ var AuthManager = class {
1556
1574
  }
1557
1575
  if (credentials.turnstileToken) return credentials.turnstileToken;
1558
1576
  throw new ItdConfigError(
1559
- "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY."
1577
+ "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i itd-api-turnstile, \u0437\u0430\u0442\u0435\u043C getTurnstileToken: createTurnstileSolver()."
1560
1578
  );
1561
1579
  }
1562
1580
  async #performSignIn(credentials) {
@@ -1698,7 +1716,7 @@ function normalizeBaseUrl(baseUrl) {
1698
1716
  // src/core/config.ts
1699
1717
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1700
1718
  var DEFAULT_TIMEOUT = 3e4;
1701
- var LIBRARY_VERSION = "0.0.4";
1719
+ var LIBRARY_VERSION = "0.0.6";
1702
1720
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1703
1721
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1704
1722
  function requirePositive(value, name) {
@@ -2093,6 +2111,7 @@ function createApiError(context) {
2093
2111
  function sleep(ms) {
2094
2112
  return new Promise((resolve) => setTimeout(resolve, ms));
2095
2113
  }
2114
+ var EMPTY_KEYS = /* @__PURE__ */ new Set();
2096
2115
  function setHeader(headers, name, value) {
2097
2116
  try {
2098
2117
  headers.set(name, value);
@@ -2146,6 +2165,7 @@ function createAbortBundle(userSignal, timeout) {
2146
2165
  var HttpClient = class {
2147
2166
  #config;
2148
2167
  #collaborators;
2168
+ #plugins;
2149
2169
  constructor(config, collaborators = {}) {
2150
2170
  this.#config = config;
2151
2171
  this.#collaborators = collaborators;
@@ -2154,6 +2174,19 @@ var HttpClient = class {
2154
2174
  get baseUrl() {
2155
2175
  return this.#config.baseUrl;
2156
2176
  }
2177
+ /**
2178
+ * Имена опций запроса, заявленные плагинами.
2179
+ *
2180
+ * Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
2181
+ * если их никто не заявил, отсеивают.
2182
+ */
2183
+ get pluginOptionKeys() {
2184
+ return this.#plugins?.optionKeys ?? EMPTY_KEYS;
2185
+ }
2186
+ /** Подключает список плагинов. Реестр общий с клиентом и пополняется через `itd.use()`. */
2187
+ usePlugins(plugins) {
2188
+ this.#plugins = plugins;
2189
+ }
2157
2190
  /**
2158
2191
  * Подключает недостающие части конвейера.
2159
2192
  *
@@ -2173,10 +2206,22 @@ var HttpClient = class {
2173
2206
  * @throws {ItdNetworkError} если запрос не дошёл до сервера
2174
2207
  */
2175
2208
  async request(options) {
2176
- const task = () => this.#withRetries(options);
2209
+ const task = () => this.#withPlugins(options);
2177
2210
  if (!this.#collaborators.schedule || options.skipQueue) return task();
2178
2211
  return this.#collaborators.schedule(task);
2179
2212
  }
2213
+ /**
2214
+ * Прогоняет запрос через обёртки плагинов.
2215
+ *
2216
+ * Цепочка стоит **снаружи повторов и внутри очереди**: плагин должен увидеть запрос
2217
+ * и ответ по одному разу, независимо от того, сколько попыток понадобилось, — иначе,
2218
+ * например, текст поста зашифруется повторно на второй попытке.
2219
+ */
2220
+ #withPlugins(options) {
2221
+ const plugins = this.#plugins;
2222
+ if (!plugins || plugins.size === 0) return this.#withRetries(options);
2223
+ return plugins.run(options, (request) => this.#withRetries(request));
2224
+ }
2180
2225
  async #withRetries(options) {
2181
2226
  const method = options.method.toUpperCase();
2182
2227
  for (let attempt = 1; ; attempt++) {
@@ -2322,6 +2367,97 @@ var HttpClient = class {
2322
2367
  }
2323
2368
  };
2324
2369
 
2370
+ // src/core/plugins.ts
2371
+ var NO_KEYS = /* @__PURE__ */ new Set();
2372
+ var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
2373
+ "signal",
2374
+ "timeout",
2375
+ "headers",
2376
+ "retry",
2377
+ "method",
2378
+ "path",
2379
+ "query",
2380
+ "body",
2381
+ "skipAuth",
2382
+ "skipAuthRefresh",
2383
+ "skipQueue",
2384
+ "raw"
2385
+ ]);
2386
+ var PluginRegistry = class {
2387
+ #transformers = [];
2388
+ #optionKeys = /* @__PURE__ */ new Set();
2389
+ #names = /* @__PURE__ */ new Set();
2390
+ /** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
2391
+ get size() {
2392
+ return this.#transformers.length;
2393
+ }
2394
+ /** Имена опций запроса, заявленные плагинами. */
2395
+ get optionKeys() {
2396
+ return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
2397
+ }
2398
+ /**
2399
+ * Подключает плагин.
2400
+ *
2401
+ * @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
2402
+ * имя опции
2403
+ */
2404
+ add(plugin, context) {
2405
+ if (typeof plugin?.install !== "function") {
2406
+ 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()");
2407
+ }
2408
+ const name = plugin.name;
2409
+ if (typeof name !== "string" || name.trim() === "") {
2410
+ 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");
2411
+ }
2412
+ if (this.#names.has(name)) {
2413
+ throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
2414
+ }
2415
+ const keys = plugin.optionKeys ?? [];
2416
+ for (const key of keys) {
2417
+ if (typeof key !== "string" || key.trim() === "") {
2418
+ 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`);
2419
+ }
2420
+ if (RESERVED_OPTION_KEYS.has(key)) {
2421
+ throw new ItdConfigError(
2422
+ `\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(", ")}`
2423
+ );
2424
+ }
2425
+ }
2426
+ const before = this.#transformers.length;
2427
+ try {
2428
+ plugin.install({
2429
+ ...context,
2430
+ use: (transformer) => {
2431
+ if (typeof transformer !== "function") {
2432
+ 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`);
2433
+ }
2434
+ this.#transformers.push(transformer);
2435
+ }
2436
+ });
2437
+ } catch (error) {
2438
+ this.#transformers.length = before;
2439
+ throw error;
2440
+ }
2441
+ this.#names.add(name);
2442
+ for (const key of keys) this.#optionKeys.add(key);
2443
+ }
2444
+ /**
2445
+ * Прогоняет запрос через цепочку обёрток.
2446
+ *
2447
+ * Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
2448
+ * а обёрток единицы — экономить тут не на чем.
2449
+ *
2450
+ * @param execute настоящий запрос, вызывается самой внутренней обёрткой
2451
+ */
2452
+ run(request, execute) {
2453
+ const chain = this.#transformers.reduceRight(
2454
+ (next, transformer) => (current) => transformer(current, next),
2455
+ execute
2456
+ );
2457
+ return chain(request);
2458
+ }
2459
+ };
2460
+
2325
2461
  // src/core/rate-limit.ts
2326
2462
  var RequestQueue = class {
2327
2463
  #concurrency;
@@ -3242,15 +3378,31 @@ var BaseResource = class {
3242
3378
  constructor(http) {
3243
3379
  this.http = http;
3244
3380
  }
3245
- /** Переносит общие поля опций запроса в параметры транспорта. */
3381
+ /**
3382
+ * Переносит общие поля опций запроса в параметры транспорта.
3383
+ *
3384
+ * Поля перечислены поимённо, а не скопированы целиком: параметры методов наследуют
3385
+ * {@link RequestOptions} и приносят с собой `limit`, `cursor` и прочее, чему в описании
3386
+ * запроса делать нечего. Исключение — опции, заявленные плагинами: их библиотека
3387
+ * не понимает, но обязана донести до обёрток нетронутыми.
3388
+ */
3246
3389
  requestOptions(options) {
3247
3390
  if (!options) return {};
3248
- return {
3391
+ const result = {
3249
3392
  ...options.signal !== void 0 ? { signal: options.signal } : {},
3250
3393
  ...options.timeout !== void 0 ? { timeout: options.timeout } : {},
3251
3394
  ...options.headers !== void 0 ? { headers: options.headers } : {},
3252
3395
  ...options.retry !== void 0 ? { retry: options.retry } : {}
3253
3396
  };
3397
+ const pluginKeys = this.http.pluginOptionKeys;
3398
+ if (pluginKeys.size === 0) return result;
3399
+ const source = options;
3400
+ const target = result;
3401
+ for (const key of pluginKeys) {
3402
+ const value = source[key];
3403
+ if (value !== void 0) target[key] = value;
3404
+ }
3405
+ return result;
3254
3406
  }
3255
3407
  /**
3256
3408
  * Собирает перебор страниц.
@@ -4477,7 +4629,16 @@ var PostsResource = class extends BaseResource {
4477
4629
  });
4478
4630
  return pickArray(body, "posts");
4479
4631
  }
4480
- /** Загружает страницу постов пользователя (его стену). */
4632
+ /**
4633
+ * Загружает страницу стены пользователя.
4634
+ *
4635
+ * Это **не только его собственные посты**: сюда попадают и записи, которые другие
4636
+ * оставили на его стене — у них `author` чужой, а `wallRecipient` указывает на владельца
4637
+ * стены. Поэтому число записей обычно больше, чем `postsCount` из профиля; чтобы
4638
+ * получить только авторские посты, отфильтруйте по `post.author.id`.
4639
+ *
4640
+ * Принимает и UUID, и имя пользователя.
4641
+ */
4481
4642
  async byUser(user, params = {}) {
4482
4643
  const body = await this.http.request({
4483
4644
  method: "GET",
@@ -4492,7 +4653,7 @@ var PostsResource = class extends BaseResource {
4492
4653
  });
4493
4654
  return readCursorPage(body, "posts");
4494
4655
  }
4495
- /** Перебирает посты пользователя. */
4656
+ /** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
4496
4657
  iterateByUser(user, params = {}) {
4497
4658
  const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4498
4659
  return this.paginate(
@@ -4735,19 +4896,34 @@ var UsersResource = class extends BaseResource {
4735
4896
  ...this.requestOptions(options)
4736
4897
  });
4737
4898
  }
4738
- /** Загружает страницу подписчиков. */
4899
+ /**
4900
+ * Загружает подписчиков пользователя.
4901
+ *
4902
+ * ⚠️ **Сервер этот список не листает.** Возвращаются первые 20 записей и только они:
4903
+ * параметр `page` игнорируется (любая страница отдаёт те же записи и `pagination.page: 1`),
4904
+ * `limit` больше 20 молча уменьшается, а `hasMore` всегда `false`. Последнее честно —
4905
+ * получить продолжение нечем.
4906
+ *
4907
+ * Числу `total` доверять тоже не стоит: оно расходится с `followersCount` из профиля —
4908
+ * на проверенных аккаунтах занижено примерно на 1–4%.
4909
+ */
4739
4910
  followers(user, params = {}) {
4740
4911
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4741
4912
  }
4742
- /** Перебирает подписчиков. */
4913
+ /**
4914
+ * Перебирает подписчиков.
4915
+ *
4916
+ * ⚠️ Перебор закончится после первых 20 записей: сервер список не листает —
4917
+ * см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
4918
+ */
4743
4919
  iterateFollowers(user, params = {}) {
4744
4920
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4745
4921
  }
4746
- /** Загружает страницу подписок. */
4922
+ /** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
4747
4923
  following(user, params = {}) {
4748
4924
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4749
4925
  }
4750
- /** Перебирает подписки. */
4926
+ /** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
4751
4927
  iterateFollowing(user, params = {}) {
4752
4928
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4753
4929
  }
@@ -4787,11 +4963,11 @@ var UsersResource = class extends BaseResource {
4787
4963
  ...this.requestOptions(options)
4788
4964
  });
4789
4965
  }
4790
- /** Загружает страницу заблокированных пользователей. */
4966
+ /** Загружает заблокированных пользователей. Ограничения те же, что у {@link followers}. */
4791
4967
  blocked(params = {}) {
4792
4968
  return this.#userPage("/api/users/me/blocked", params);
4793
4969
  }
4794
- /** Перебирает заблокированных пользователей. */
4970
+ /** Перебирает заблокированных. Закончится после первых 20 записей — см. {@link followers}. */
4795
4971
  iterateBlocked(params = {}) {
4796
4972
  return this.#userPaginator("/api/users/me/blocked", params);
4797
4973
  }
@@ -4852,6 +5028,9 @@ var UsersResource = class extends BaseResource {
4852
5028
  * Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
4853
5029
  * под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
4854
5030
  * список по-своему.
5031
+ *
5032
+ * `page` уходит в запрос, хотя сервер его сейчас не читает (см. {@link followers}):
5033
+ * когда пагинацию починят, работать начнёт само.
4855
5034
  */
4856
5035
  async #loadUserPage(path, params, state) {
4857
5036
  const body = await this.http.request({
@@ -4884,6 +5063,7 @@ var ItdClient = class {
4884
5063
  #authManager;
4885
5064
  #jar;
4886
5065
  #queue;
5066
+ #plugins = new PluginRegistry();
4887
5067
  /** Авторизация, сессии и пароли. */
4888
5068
  auth;
4889
5069
  /** Профили, подписки, блокировки, приватность. */
@@ -4920,6 +5100,7 @@ var ItdClient = class {
4920
5100
  this.#http = new HttpClient(this.#config);
4921
5101
  this.#authManager = new AuthManager(this.#config, this.#http, this.#jar);
4922
5102
  this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
5103
+ this.#http.usePlugins(this.#plugins);
4923
5104
  this.#http.setCollaborators({
4924
5105
  getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4925
5106
  getDeviceId: () => this.#authManager.getDeviceId(),
@@ -4965,6 +5146,27 @@ var ItdClient = class {
4965
5146
  request(options) {
4966
5147
  return this.#http.request(options);
4967
5148
  }
5149
+ /**
5150
+ * Подключает плагин.
5151
+ *
5152
+ * Плагин работает на уровне транспорта: видит запрос до отправки и разобранный ответ,
5153
+ * поэтому одна обёртка охватывает сразу все методы клиента. Подключать можно в любой
5154
+ * момент, но обычно это делают сразу после создания клиента.
5155
+ *
5156
+ * @throws {ItdConfigError} если плагин задан неверно или уже подключён
5157
+ *
5158
+ * @example
5159
+ * ```ts
5160
+ * import { crypt } from 'itd-api-crypto';
5161
+ *
5162
+ * itd.use(crypt());
5163
+ * await itd.posts.create({ content: 'секрет' }, { encrypt: 'invis' });
5164
+ * ```
5165
+ */
5166
+ use(plugin) {
5167
+ this.#plugins.add(plugin, { baseUrl: this.#config.baseUrl, logger: this.#config.logger });
5168
+ return this;
5169
+ }
4968
5170
  /**
4969
5171
  * Подписывается на события авторизации.
4970
5172
  *
@@ -5244,6 +5446,7 @@ exports.ReportTargetType = ReportTargetType;
5244
5446
  exports.RuntimeMode = RuntimeMode;
5245
5447
  exports.STREAM_PATH = STREAM_PATH;
5246
5448
  exports.SignInStatus = SignInStatus;
5449
+ exports.SpanType = SpanType;
5247
5450
  exports.TURNSTILE_SITE_KEY = TURNSTILE_SITE_KEY;
5248
5451
  exports.UnauthorizedStreamError = UnauthorizedStreamError;
5249
5452
  exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
@@ -5274,5 +5477,5 @@ exports.readUnreadCountEvent = readUnreadCountEvent;
5274
5477
  exports.report = report;
5275
5478
  exports.resolveNotificationUrl = resolveNotificationUrl;
5276
5479
  exports.toDate = toDate;
5277
- //# sourceMappingURL=chunk-XG43KEYF.cjs.map
5278
- //# sourceMappingURL=chunk-XG43KEYF.cjs.map
5480
+ //# sourceMappingURL=chunk-K7NBFQEE.cjs.map
5481
+ //# sourceMappingURL=chunk-K7NBFQEE.cjs.map