itd-api 0.0.4 → 0.0.5

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
@@ -111,9 +111,8 @@ itd.on('authError', ({ error }) => {
111
111
 
112
112
  ### Капча обязательна при входе
113
113
 
114
- `signIn`, `signUp` и `forgotPassword` требуют токен Cloudflare Turnstile. Полностью
115
- автоматического входа по логину и паролю поэтому не бывает: капчу должен решить кто-то
116
- снаружи, а библиотека принимает готовый токен.
114
+ `signIn`, `signUp` и `forgotPassword` требуют токен Cloudflare Turnstile. Сам клиент капчу
115
+ не решает он принимает готовый токен, а решает его кто-то снаружи.
117
116
 
118
117
  ```ts
119
118
  import { TURNSTILE_SITE_KEY } from 'itd-api';
@@ -134,6 +133,24 @@ new ItdClient({
134
133
  });
135
134
  ```
136
135
 
136
+ В Node таким источником может быть [`itd-api-turnstile`](./turnstile) — отдельный пакет,
137
+ который поднимает браузер и приносит токен. Отдельный он намеренно: тянет за собой Playwright
138
+ и требует графической оболочки, а нужен далеко не всем — с сохранённой сессией до входа
139
+ по паролю дело обычно вообще не доходит.
140
+
141
+ ```sh
142
+ npm i itd-api-turnstile playwright
143
+ ```
144
+
145
+ ```ts
146
+ import { createTurnstileSolver } from 'itd-api-turnstile';
147
+
148
+ new ItdClient({
149
+ storage: new FileTokenStorage('./.itd-session.json'),
150
+ auth: { email, password, getTurnstileToken: createTurnstileSolver() },
151
+ });
152
+ ```
153
+
137
154
  ### Вход с кодом из письма
138
155
 
139
156
  ```ts
@@ -205,8 +222,8 @@ await itd.setSession(await loadFromSomewhere());
205
222
  // по элементам
206
223
  for await (const post of itd.posts.iterate({ tab: 'popular' })) { … }
207
224
 
208
- // по страницам — когда нужен, например, total
209
- for await (const page of itd.users.iterateFollowers('durov').pages()) {
225
+ // по страницам — когда нужны сведения о самой странице
226
+ for await (const page of itd.posts.iterateComments(postId).pages()) {
210
227
  console.log(page.items.length, 'из', page.total);
211
228
  }
212
229
 
@@ -224,6 +241,24 @@ const next = await itd.posts.list({ tab: 'popular', cursor: page.nextCursor ?? u
224
241
  Курсор непрозрачен: у вкладки `popular` это номер страницы, у `following` — отметка времени.
225
242
  Передавайте его обратно как есть.
226
243
 
244
+ Перебор одноразовый: позиция хранится внутри, поэтому второй `for await` по тому же объекту
245
+ ничего не выдаст. Нужен ещё проход — возьмите новый перебор у того же метода.
246
+
247
+ ### Чего API не умеет
248
+
249
+ **Подписчики, подписки и заблокированные не листаются.** Сервер отдаёт первые 20 записей
250
+ и на этом всё: `page` он игнорирует, `limit` больше 20 молча уменьшает, а `hasMore` всегда
251
+ `false`. Числу `total` там тоже верить нельзя: оно расходится с `followersCount` из профиля.
252
+
253
+ ```ts
254
+ // вернёт 20 записей и остановится — это предел API, а не библиотеки
255
+ const all = await itd.users.iterateFollowers('durov').collect();
256
+ ```
257
+
258
+ **`posts.byUser()` — это стена, а не авторские посты.** В неё входят и записи, которые
259
+ другие оставили на странице пользователя, поэтому записей обычно больше, чем `postsCount`
260
+ в профиле. Нужны только свои — отфильтруйте по `post.author.id`.
261
+
227
262
  ---
228
263
 
229
264
  ## Публикация
@@ -361,8 +396,8 @@ const itd = new ItdClient({
361
396
  по модулям, и темп будет общим.
362
397
 
363
398
  `concurrency` (по умолчанию 6) ограничивает только одновременность. От ограничения частоты
364
- он почти не спасает: десять запросов подряд при `concurrency: 1` уходят за ~150 мс, а окно
365
- сервера около 5 запросов на минуту с лишним. Темп задаёт `rps`:
399
+ он почти не спасает: десять запросов подряд при `concurrency: 1` уходят за ~150 мс,
400
+ а окно сервера измеряется десятками секунд. Темп задаёт `rps`:
366
401
 
367
402
  ```ts
368
403
  rateLimit: { concurrency: 2, rps: 0.5 } // не чаще одного запроса в 2 секунды
@@ -371,7 +406,9 @@ rateLimit: { concurrency: 2, rps: 0.5 } // не чаще одного запр
371
406
  Ставить `concurrency: 1` без нужды не стоит: загрузка видео с таймаутом в 300 секунд
372
407
  заблокирует на это время вообще всё остальное.
373
408
 
374
- **Ограничение частоты — отдельный механизм.** Сервер разрешает около 5 запросов в окно,
409
+ **Ограничение частоты — отдельный механизм.** Лимит у каждого эндпоинта свой: замеры по
410
+ `x-ratelimit-limit` дали 90 у `/api/posts`, 40 у `/api/users/me` и `/api/notifications/`,
411
+ 25 у `/api/v1/auth/refresh` и всего 15 у `/api/files/upload`. Сервер
375
412
  не присылает `Retry-After` и не сообщает, когда окно сбросится: есть только заголовки
376
413
  `x-ratelimit-limit` и `x-ratelimit-remaining` (доступны на `ItdRateLimitError` как
377
414
  `rateLimit` и `rateLimitRemaining`).
@@ -1554,7 +1554,7 @@ var AuthManager = class {
1554
1554
  }
1555
1555
  if (credentials.turnstileToken) return credentials.turnstileToken;
1556
1556
  throw new ItdConfigError(
1557
- "\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."
1557
+ "\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()."
1558
1558
  );
1559
1559
  }
1560
1560
  async #performSignIn(credentials) {
@@ -1696,7 +1696,7 @@ function normalizeBaseUrl(baseUrl) {
1696
1696
  // src/core/config.ts
1697
1697
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1698
1698
  var DEFAULT_TIMEOUT = 3e4;
1699
- var LIBRARY_VERSION = "0.0.4";
1699
+ var LIBRARY_VERSION = "0.0.5";
1700
1700
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1701
1701
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1702
1702
  function requirePositive(value, name) {
@@ -4475,7 +4475,16 @@ var PostsResource = class extends BaseResource {
4475
4475
  });
4476
4476
  return pickArray(body, "posts");
4477
4477
  }
4478
- /** Загружает страницу постов пользователя (его стену). */
4478
+ /**
4479
+ * Загружает страницу стены пользователя.
4480
+ *
4481
+ * Это **не только его собственные посты**: сюда попадают и записи, которые другие
4482
+ * оставили на его стене — у них `author` чужой, а `wallRecipient` указывает на владельца
4483
+ * стены. Поэтому число записей обычно больше, чем `postsCount` из профиля; чтобы
4484
+ * получить только авторские посты, отфильтруйте по `post.author.id`.
4485
+ *
4486
+ * Принимает и UUID, и имя пользователя.
4487
+ */
4479
4488
  async byUser(user, params = {}) {
4480
4489
  const body = await this.http.request({
4481
4490
  method: "GET",
@@ -4490,7 +4499,7 @@ var PostsResource = class extends BaseResource {
4490
4499
  });
4491
4500
  return readCursorPage(body, "posts");
4492
4501
  }
4493
- /** Перебирает посты пользователя. */
4502
+ /** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
4494
4503
  iterateByUser(user, params = {}) {
4495
4504
  const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4496
4505
  return this.paginate(
@@ -4733,19 +4742,34 @@ var UsersResource = class extends BaseResource {
4733
4742
  ...this.requestOptions(options)
4734
4743
  });
4735
4744
  }
4736
- /** Загружает страницу подписчиков. */
4745
+ /**
4746
+ * Загружает подписчиков пользователя.
4747
+ *
4748
+ * ⚠️ **Сервер этот список не листает.** Возвращаются первые 20 записей и только они:
4749
+ * параметр `page` игнорируется (любая страница отдаёт те же записи и `pagination.page: 1`),
4750
+ * `limit` больше 20 молча уменьшается, а `hasMore` всегда `false`. Последнее честно —
4751
+ * получить продолжение нечем.
4752
+ *
4753
+ * Числу `total` доверять тоже не стоит: оно расходится с `followersCount` из профиля —
4754
+ * на проверенных аккаунтах занижено примерно на 1–4%.
4755
+ */
4737
4756
  followers(user, params = {}) {
4738
4757
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4739
4758
  }
4740
- /** Перебирает подписчиков. */
4759
+ /**
4760
+ * Перебирает подписчиков.
4761
+ *
4762
+ * ⚠️ Перебор закончится после первых 20 записей: сервер список не листает —
4763
+ * см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
4764
+ */
4741
4765
  iterateFollowers(user, params = {}) {
4742
4766
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4743
4767
  }
4744
- /** Загружает страницу подписок. */
4768
+ /** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
4745
4769
  following(user, params = {}) {
4746
4770
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4747
4771
  }
4748
- /** Перебирает подписки. */
4772
+ /** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
4749
4773
  iterateFollowing(user, params = {}) {
4750
4774
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4751
4775
  }
@@ -4785,11 +4809,11 @@ var UsersResource = class extends BaseResource {
4785
4809
  ...this.requestOptions(options)
4786
4810
  });
4787
4811
  }
4788
- /** Загружает страницу заблокированных пользователей. */
4812
+ /** Загружает заблокированных пользователей. Ограничения те же, что у {@link followers}. */
4789
4813
  blocked(params = {}) {
4790
4814
  return this.#userPage("/api/users/me/blocked", params);
4791
4815
  }
4792
- /** Перебирает заблокированных пользователей. */
4816
+ /** Перебирает заблокированных. Закончится после первых 20 записей — см. {@link followers}. */
4793
4817
  iterateBlocked(params = {}) {
4794
4818
  return this.#userPaginator("/api/users/me/blocked", params);
4795
4819
  }
@@ -4850,6 +4874,9 @@ var UsersResource = class extends BaseResource {
4850
4874
  * Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
4851
4875
  * под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
4852
4876
  * список по-своему.
4877
+ *
4878
+ * `page` уходит в запрос, хотя сервер его сейчас не читает (см. {@link followers}):
4879
+ * когда пагинацию починят, работать начнёт само.
4853
4880
  */
4854
4881
  async #loadUserPage(path, params, state) {
4855
4882
  const body = await this.http.request({
@@ -5190,5 +5217,5 @@ function toDate(value) {
5190
5217
  }
5191
5218
 
5192
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 };
5193
- //# sourceMappingURL=chunk-JV75JWOX.js.map
5194
- //# sourceMappingURL=chunk-JV75JWOX.js.map
5220
+ //# sourceMappingURL=chunk-RUPF4X5L.js.map
5221
+ //# sourceMappingURL=chunk-RUPF4X5L.js.map