itd-api 0.0.3 → 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
@@ -36,10 +36,9 @@ for await (const post of itd.posts.iterate({ tab: FeedTab.Following })) {
36
36
  ```ts
37
37
  import { ItdClient, FileTokenStorage } from 'itd-api/node';
38
38
 
39
- const itd = new ItdClient({
40
- auth: { email, password, getTurnstileToken }, // вход требует капчи, см. ниже
41
- storage: new FileTokenStorage('./.itd-session.json'),
42
- });
39
+ // Сессия из файла: продлевается сама, `auth` не нужен. Как её туда положить —
40
+ // в разделе «Авторизация»: вход требует капчи, поэтому делается один раз.
41
+ const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });
43
42
 
44
43
  await itd.posts.create((p) => p.content('привет').attach('./photo.jpg'));
45
44
  ```
@@ -50,26 +49,70 @@ await itd.posts.create((p) => p.content('привет').attach('./photo.jpg'));
50
49
 
51
50
  ## Авторизация
52
51
 
53
- Четыре формы на выбор:
52
+ Откуда клиент берёт доступ к API — либо из опции `auth`, либо из `storage`, либо
53
+ из явного вызова входа. Всё это взаимозаменяемо, и **обязательного варианта нет**.
54
54
 
55
55
  ```ts
56
56
  new ItdClient({ auth: '<accessToken>' }); // разовый вызов
57
- new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию
57
+ new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию строками
58
58
  new ItdClient({ auth: { email, password, getTurnstileToken } }); // войти самому
59
59
  new ItdClient({ auth: { getToken: () => vault.read() } }); // токен извне
60
+
61
+ new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') }); // сессия с прошлого раза
62
+ new ItdClient(); // войти позже, через itd.auth
63
+ ```
64
+
65
+ `auth` и `storage` не конкурируют, а дополняют друг друга: **хранилище главнее** — оно
66
+ отражает текущее состояние сессии, — а недостающие поля берутся из `auth`. Типичный случай:
67
+ в хранилище лежит только `accessToken`, а `refreshToken` приходит из настроек приложения.
68
+
69
+ ### Сессия из хранилища — `auth` не нужен
70
+
71
+ Если предыдущий запуск сохранил сессию, для работы достаточно одного `storage`:
72
+
73
+ ```ts
74
+ import { ItdClient, FileTokenStorage } from 'itd-api/node';
75
+
76
+ const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });
77
+
78
+ const me = await itd.users.me(); // токен подставится сам
60
79
  ```
61
80
 
81
+ Более того, сохранённого `accessToken` не требуется вовсе. Хватает cookie: первый запрос
82
+ получит `401`, библиотека продлит сессию и повторит его — вызывающий код ничего не заметит.
83
+
84
+ Refresh-токен живёт 30 суток и обновляется при каждом продлении, поэтому регулярно
85
+ работающему боту капчу достаточно решить один раз, при первом запуске.
86
+
87
+ Проверить, есть ли что продлевать, можно заранее:
88
+
89
+ ```ts
90
+ if (await itd.auth.hasRefreshSession()) await itd.auth.refresh();
91
+ else redirectToLogin();
92
+ ```
93
+
94
+ ### Продление токена
95
+
62
96
  При ответе `401` библиотека продлевает сессию и повторяет запрос. Параллельные запросы,
63
97
  одновременно получившие `401`, ждут **одного** обновления, а не запускают своё — иначе сервер
64
98
  увидел бы десяток одновременных `refresh` и отверг бы все, кроме первого.
65
99
 
66
100
  Отключить автоматику: `autoRefresh: false`, дальше `await itd.auth.refresh()` вручную.
67
101
 
102
+ Когда продлить не удалось, `refresh()` бросает ошибку **сервера** — по её коду видно, что
103
+ именно случилось: `SESSION_NOT_FOUND` (сессия отозвана или истекла), `SESSION_REVOKED`,
104
+ `REFRESH_TOKEN_MISSING` (продлевать нечем). Та же ошибка приходит в событии `authError`:
105
+
106
+ ```ts
107
+ itd.on('authError', ({ error }) => {
108
+ if (isItdApiError(error)) console.error('Сессия потеряна:', error.code);
109
+ });
110
+ ```
111
+
68
112
  ### Капча обязательна при входе
69
113
 
70
- `signIn`, `signUp` и `forgotPassword` требуют токен Cloudflare Turnstile. Полностью
71
- автоматического входа по логину и паролю поэтому не бывает: капчу должен решить кто-то
72
- снаружи, а библиотека принимает готовый токен.
114
+ `signIn`, `signUp` и `forgotPassword` требуют токен Cloudflare Turnstile. Сам клиент капчу
115
+ не решает он принимает готовый токен, а решает его кто-то снаружи.
73
116
 
74
117
  ```ts
75
118
  import { TURNSTILE_SITE_KEY } from 'itd-api';
@@ -90,6 +133,24 @@ new ItdClient({
90
133
  });
91
134
  ```
92
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
+
93
154
  ### Вход с кодом из письма
94
155
 
95
156
  ```ts
@@ -123,24 +184,33 @@ await itd.auth.resetPasswordWithOtp({
123
184
  | `FileTokenStorage` | `itd-api/node` | Node, Bun, Deno |
124
185
  | `createTokenStorage({ get, set, clear })` | `itd-api` | своё: Redis, БД, AsyncStorage |
125
186
 
126
- Refresh-токен приходит в cookie `refresh_token`, а `fetch` вне браузера их не хранит
127
- библиотека ведёт собственный cookie-jar и сохраняет его вместе с сессией. В браузере
128
- используется `credentials: 'include'`, в React Native cookie ведёт нативный слой.
187
+ По умолчанию сессия живёт в памяти процесса и теряется при перезапуске. Укажите `storage`
188
+ и библиотека сама запишет туда всё нужное после входа и после каждого продления; отдельно
189
+ сохранять ничего не надо.
190
+
191
+ В сессию попадают `accessToken`, `refreshToken`, cookie и `deviceId`. Сохранять её целиком
192
+ важно: refresh-токен приходит в cookie `refresh_token`, а `fetch` вне браузера их не хранит,
193
+ поэтому библиотека ведёт собственный cookie-jar. В браузере используется
194
+ `credentials: 'include'`, в React Native cookie ведёт нативный слой.
129
195
 
130
- Токен можно передать и строкой (`auth: { accessToken, refreshToken }`) — вне браузера
131
- библиотека сама подставит его нужной cookie. В браузере так не выйдет: настоящая cookie
132
- помечена `HttpOnly`, и из JS её не выставить.
196
+ `deviceId` идентификатор устройства из заголовка `X-Device-Id`. Сервер различает по нему
197
+ записи в списке сессий (`itd.auth.sessions()`), поэтому при постоянном хранилище он переживает
198
+ перезапуск и бот не плодит по новой сессии на каждый старт. Своё значение — опцией `deviceId`.
133
199
 
134
- Сервер выдаёт при каждом обновлении **новый** refresh-токен взамен прежнего, поэтому
135
- сохранять сессию нужно после обновления, а не один раз при входе:
200
+ Refresh-токен можно передать и строкой (`auth: { accessToken, refreshToken }`) — вне браузера
201
+ библиотека сама подставит его нужной cookie. В браузере так не выйдет: cookie помечена
202
+ `HttpOnly`, и из JS её не выставить.
203
+
204
+ **Сервер выдаёт при каждом продлении новый refresh-токен взамен прежнего.** Со штатным
205
+ `storage` это происходит само. Если же вы храните сессию сами, снимайте её после каждого
206
+ обновления, а не один раз при входе, — иначе сохранённое значение протухнет:
136
207
 
137
208
  ```ts
138
209
  itd.on('tokens', async () => saveSomewhere(await itd.getSession()));
139
- ```
140
210
 
141
- Вместе с сессией хранится `deviceId` — идентификатор устройства из заголовка `X-Device-Id`.
142
- Сервер различает по нему записи в списке сессий, поэтому при постоянном хранилище он
143
- переживает перезапуск, а бот не плодит по новой сессии на каждый старт.
211
+ // при следующем запуске
212
+ await itd.setSession(await loadFromSomewhere());
213
+ ```
144
214
 
145
215
  ---
146
216
 
@@ -152,8 +222,8 @@ itd.on('tokens', async () => saveSomewhere(await itd.getSession()));
152
222
  // по элементам
153
223
  for await (const post of itd.posts.iterate({ tab: 'popular' })) { … }
154
224
 
155
- // по страницам — когда нужен, например, total
156
- for await (const page of itd.users.iterateFollowers('durov').pages()) {
225
+ // по страницам — когда нужны сведения о самой странице
226
+ for await (const page of itd.posts.iterateComments(postId).pages()) {
157
227
  console.log(page.items.length, 'из', page.total);
158
228
  }
159
229
 
@@ -171,6 +241,24 @@ const next = await itd.posts.list({ tab: 'popular', cursor: page.nextCursor ?? u
171
241
  Курсор непрозрачен: у вкладки `popular` это номер страницы, у `following` — отметка времени.
172
242
  Передавайте его обратно как есть.
173
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
+
174
262
  ---
175
263
 
176
264
  ## Публикация
@@ -283,11 +371,14 @@ try {
283
371
  ```ts
284
372
  const itd = new ItdClient({
285
373
  baseUrl: 'https://xn--d1ah4a.com', // свой прокси, если работаете из браузера
286
- auth: { email, password },
374
+ auth: { email, password, getTurnstileToken },
287
375
  storage: new FileTokenStorage('./.itd-session.json'),
288
376
  timeout: 30_000,
289
377
  retry: { attempts: 3, retryWrites: false },
290
378
  rateLimit: { concurrency: 4, rps: 8 },
379
+ // Заголовки латиницей: кириллица в них запрещена самим HTTP.
380
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36', // по умолчанию itd-api/<версия>; false — не слать
381
+ deviceId: '3f2a…-uuid', // по умолчанию заводится сам и живёт в сессии
291
382
  logger: true, // токены и пароли в логах маскируются
292
383
  hooks: {
293
384
  onRequest: (ctx) => console.log(ctx.method, ctx.path),
@@ -300,7 +391,24 @@ const itd = new ItdClient({
300
391
  по умолчанию не повторяется (`retryWrites: false`): повтор мог бы создать дубль поста.
301
392
  Чтения повторяются с экспоненциальным откатом.
302
393
 
303
- **Ограничение частоты отдельный механизм.** Сервер разрешает около 5 запросов в окно,
394
+ **Очередь: `concurrency` и `rps` решают разные задачи.** Все запросы идут через одну очередь
395
+ клиента, поэтому достаточно **одного экземпляра `ItdClient` на приложение** — разложите его
396
+ по модулям, и темп будет общим.
397
+
398
+ `concurrency` (по умолчанию 6) ограничивает только одновременность. От ограничения частоты
399
+ он почти не спасает: десять запросов подряд при `concurrency: 1` уходят за ~150 мс,
400
+ а окно сервера измеряется десятками секунд. Темп задаёт `rps`:
401
+
402
+ ```ts
403
+ rateLimit: { concurrency: 2, rps: 0.5 } // не чаще одного запроса в 2 секунды
404
+ ```
405
+
406
+ Ставить `concurrency: 1` без нужды не стоит: загрузка видео с таймаутом в 300 секунд
407
+ заблокирует на это время вообще всё остальное.
408
+
409
+ **Ограничение частоты — отдельный механизм.** Лимит у каждого эндпоинта свой: замеры по
410
+ `x-ratelimit-limit` дали 90 у `/api/posts`, 40 у `/api/users/me` и `/api/notifications/`,
411
+ 25 у `/api/v1/auth/refresh` и всего 15 у `/api/files/upload`. Сервер
304
412
  не присылает `Retry-After` и не сообщает, когда окно сбросится: есть только заголовки
305
413
  `x-ratelimit-limit` и `x-ratelimit-remaining` (доступны на `ItdRateLimitError` как
306
414
  `rateLimit` и `rateLimitRemaining`).