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.
- package/README.md +152 -9
- package/dist/{chunk-JV75JWOX.js → chunk-CCRQI3ON.js} +218 -16
- package/dist/chunk-CCRQI3ON.js.map +1 -0
- package/dist/{chunk-XG43KEYF.cjs → chunk-K7NBFQEE.cjs} +218 -15
- package/dist/chunk-K7NBFQEE.cjs.map +1 -0
- package/dist/{index-olL_q_yu.d.cts → index-CDGXKy8r.d.cts} +218 -16
- package/dist/{index-olL_q_yu.d.ts → index-CDGXKy8r.d.ts} +218 -16
- package/dist/index.cjs +88 -84
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +87 -83
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/package.json +8 -3
- package/dist/chunk-JV75JWOX.js.map +0 -1
- package/dist/chunk-XG43KEYF.cjs.map +0 -1
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
|
-
// по страницам — когда
|
|
209
|
-
for await (const page of itd.
|
|
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
|
## Публикация
|
|
@@ -252,6 +287,24 @@ await itd.posts.create(draft.content('второй')); // заготовка
|
|
|
252
287
|
Файлы из `attach()` загружаются автоматически, порядок вложений сохраняется, MIME-тип
|
|
253
288
|
проверяется до отправки.
|
|
254
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
|
+
|
|
255
308
|
Билдеры есть у поста, комментария, опроса и жалобы. Все они неизменяемые, а `build()`
|
|
256
309
|
проверяет данные и бросает `ItdConfigError` **до** обращения к сети:
|
|
257
310
|
|
|
@@ -361,8 +414,8 @@ const itd = new ItdClient({
|
|
|
361
414
|
по модулям, и темп будет общим.
|
|
362
415
|
|
|
363
416
|
`concurrency` (по умолчанию 6) ограничивает только одновременность. От ограничения частоты
|
|
364
|
-
он почти не спасает: десять запросов подряд при `concurrency: 1` уходят за ~150 мс,
|
|
365
|
-
|
|
417
|
+
он почти не спасает: десять запросов подряд при `concurrency: 1` уходят за ~150 мс,
|
|
418
|
+
а окно сервера измеряется десятками секунд. Темп задаёт `rps`:
|
|
366
419
|
|
|
367
420
|
```ts
|
|
368
421
|
rateLimit: { concurrency: 2, rps: 0.5 } // не чаще одного запроса в 2 секунды
|
|
@@ -371,7 +424,9 @@ rateLimit: { concurrency: 2, rps: 0.5 } // не чаще одного запр
|
|
|
371
424
|
Ставить `concurrency: 1` без нужды не стоит: загрузка видео с таймаутом в 300 секунд
|
|
372
425
|
заблокирует на это время вообще всё остальное.
|
|
373
426
|
|
|
374
|
-
**Ограничение частоты — отдельный механизм.**
|
|
427
|
+
**Ограничение частоты — отдельный механизм.** Лимит у каждого эндпоинта свой: замеры по
|
|
428
|
+
`x-ratelimit-limit` дали 90 у `/api/posts`, 40 у `/api/users/me` и `/api/notifications/`,
|
|
429
|
+
25 у `/api/v1/auth/refresh` и всего 15 у `/api/files/upload`. Сервер
|
|
375
430
|
не присылает `Retry-After` и не сообщает, когда окно сбросится: есть только заголовки
|
|
376
431
|
`x-ratelimit-limit` и `x-ratelimit-remaining` (доступны на `ItdRateLimitError` как
|
|
377
432
|
`rateLimit` и `rateLimitRemaining`).
|
|
@@ -403,6 +458,92 @@ Deno и React Native ограничение не действует.
|
|
|
403
458
|
|
|
404
459
|
---
|
|
405
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
|
+
|
|
406
547
|
## Что доступно
|
|
407
548
|
|
|
408
549
|
| Раздел | Методы |
|
|
@@ -417,6 +558,7 @@ Deno и React Native ограничение не действует.
|
|
|
417
558
|
| `itd.reports` · `itd.verification` | жалобы, заявка на верификацию |
|
|
418
559
|
| `itd.subscription` · `itd.platform` | подписка, способы оплаты, анонсы |
|
|
419
560
|
| `itd.realtime()` | поток уведомлений |
|
|
561
|
+
| `itd.use()` | плагины: обёртки вокруг запроса и ответа |
|
|
420
562
|
| `itd.request()` | произвольный запрос, если метода ещё нет |
|
|
421
563
|
|
|
422
564
|
Метода не хватает или ответ разошёлся с документацией — есть запасной путь:
|
|
@@ -445,7 +587,8 @@ TypeScript 5.0+. Пакет собран в ESM и CommonJS, типы корре
|
|
|
445
587
|
|
|
446
588
|
```bash
|
|
447
589
|
npm install
|
|
448
|
-
npm test #
|
|
590
|
+
npm test # 417 тестов
|
|
591
|
+
npm run test:all # вместе с пакетами workspace
|
|
449
592
|
npm run typecheck
|
|
450
593
|
npm run lint
|
|
451
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",
|
|
@@ -1554,7 +1572,7 @@ var AuthManager = class {
|
|
|
1554
1572
|
}
|
|
1555
1573
|
if (credentials.turnstileToken) return credentials.turnstileToken;
|
|
1556
1574
|
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."
|
|
1575
|
+
"\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
1576
|
);
|
|
1559
1577
|
}
|
|
1560
1578
|
async #performSignIn(credentials) {
|
|
@@ -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.
|
|
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.#
|
|
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
|
-
|
|
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
|
* Собирает перебор страниц.
|
|
@@ -4475,7 +4627,16 @@ var PostsResource = class extends BaseResource {
|
|
|
4475
4627
|
});
|
|
4476
4628
|
return pickArray(body, "posts");
|
|
4477
4629
|
}
|
|
4478
|
-
/**
|
|
4630
|
+
/**
|
|
4631
|
+
* Загружает страницу стены пользователя.
|
|
4632
|
+
*
|
|
4633
|
+
* Это **не только его собственные посты**: сюда попадают и записи, которые другие
|
|
4634
|
+
* оставили на его стене — у них `author` чужой, а `wallRecipient` указывает на владельца
|
|
4635
|
+
* стены. Поэтому число записей обычно больше, чем `postsCount` из профиля; чтобы
|
|
4636
|
+
* получить только авторские посты, отфильтруйте по `post.author.id`.
|
|
4637
|
+
*
|
|
4638
|
+
* Принимает и UUID, и имя пользователя.
|
|
4639
|
+
*/
|
|
4479
4640
|
async byUser(user, params = {}) {
|
|
4480
4641
|
const body = await this.http.request({
|
|
4481
4642
|
method: "GET",
|
|
@@ -4490,7 +4651,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4490
4651
|
});
|
|
4491
4652
|
return readCursorPage(body, "posts");
|
|
4492
4653
|
}
|
|
4493
|
-
/** Перебирает
|
|
4654
|
+
/** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
|
|
4494
4655
|
iterateByUser(user, params = {}) {
|
|
4495
4656
|
const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
|
|
4496
4657
|
return this.paginate(
|
|
@@ -4733,19 +4894,34 @@ var UsersResource = class extends BaseResource {
|
|
|
4733
4894
|
...this.requestOptions(options)
|
|
4734
4895
|
});
|
|
4735
4896
|
}
|
|
4736
|
-
/**
|
|
4897
|
+
/**
|
|
4898
|
+
* Загружает подписчиков пользователя.
|
|
4899
|
+
*
|
|
4900
|
+
* ⚠️ **Сервер этот список не листает.** Возвращаются первые 20 записей и только они:
|
|
4901
|
+
* параметр `page` игнорируется (любая страница отдаёт те же записи и `pagination.page: 1`),
|
|
4902
|
+
* `limit` больше 20 молча уменьшается, а `hasMore` всегда `false`. Последнее честно —
|
|
4903
|
+
* получить продолжение нечем.
|
|
4904
|
+
*
|
|
4905
|
+
* Числу `total` доверять тоже не стоит: оно расходится с `followersCount` из профиля —
|
|
4906
|
+
* на проверенных аккаунтах занижено примерно на 1–4%.
|
|
4907
|
+
*/
|
|
4737
4908
|
followers(user, params = {}) {
|
|
4738
4909
|
return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
|
|
4739
4910
|
}
|
|
4740
|
-
/**
|
|
4911
|
+
/**
|
|
4912
|
+
* Перебирает подписчиков.
|
|
4913
|
+
*
|
|
4914
|
+
* ⚠️ Перебор закончится после первых 20 записей: сервер список не листает —
|
|
4915
|
+
* см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
|
|
4916
|
+
*/
|
|
4741
4917
|
iterateFollowers(user, params = {}) {
|
|
4742
4918
|
return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
|
|
4743
4919
|
}
|
|
4744
|
-
/** Загружает
|
|
4920
|
+
/** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
|
|
4745
4921
|
following(user, params = {}) {
|
|
4746
4922
|
return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/following`, params);
|
|
4747
4923
|
}
|
|
4748
|
-
/** Перебирает подписки. */
|
|
4924
|
+
/** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
|
|
4749
4925
|
iterateFollowing(user, params = {}) {
|
|
4750
4926
|
return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/following`, params);
|
|
4751
4927
|
}
|
|
@@ -4785,11 +4961,11 @@ var UsersResource = class extends BaseResource {
|
|
|
4785
4961
|
...this.requestOptions(options)
|
|
4786
4962
|
});
|
|
4787
4963
|
}
|
|
4788
|
-
/** Загружает
|
|
4964
|
+
/** Загружает заблокированных пользователей. Ограничения те же, что у {@link followers}. */
|
|
4789
4965
|
blocked(params = {}) {
|
|
4790
4966
|
return this.#userPage("/api/users/me/blocked", params);
|
|
4791
4967
|
}
|
|
4792
|
-
/** Перебирает
|
|
4968
|
+
/** Перебирает заблокированных. Закончится после первых 20 записей — см. {@link followers}. */
|
|
4793
4969
|
iterateBlocked(params = {}) {
|
|
4794
4970
|
return this.#userPaginator("/api/users/me/blocked", params);
|
|
4795
4971
|
}
|
|
@@ -4850,6 +5026,9 @@ var UsersResource = class extends BaseResource {
|
|
|
4850
5026
|
* Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
|
|
4851
5027
|
* под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
|
|
4852
5028
|
* список по-своему.
|
|
5029
|
+
*
|
|
5030
|
+
* `page` уходит в запрос, хотя сервер его сейчас не читает (см. {@link followers}):
|
|
5031
|
+
* когда пагинацию починят, работать начнёт само.
|
|
4853
5032
|
*/
|
|
4854
5033
|
async #loadUserPage(path, params, state) {
|
|
4855
5034
|
const body = await this.http.request({
|
|
@@ -4882,6 +5061,7 @@ var ItdClient = class {
|
|
|
4882
5061
|
#authManager;
|
|
4883
5062
|
#jar;
|
|
4884
5063
|
#queue;
|
|
5064
|
+
#plugins = new PluginRegistry();
|
|
4885
5065
|
/** Авторизация, сессии и пароли. */
|
|
4886
5066
|
auth;
|
|
4887
5067
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -4918,6 +5098,7 @@ var ItdClient = class {
|
|
|
4918
5098
|
this.#http = new HttpClient(this.#config);
|
|
4919
5099
|
this.#authManager = new AuthManager(this.#config, this.#http, this.#jar);
|
|
4920
5100
|
this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
|
|
5101
|
+
this.#http.usePlugins(this.#plugins);
|
|
4921
5102
|
this.#http.setCollaborators({
|
|
4922
5103
|
getAuthHeaders: () => this.#authManager.getAuthHeaders(),
|
|
4923
5104
|
getDeviceId: () => this.#authManager.getDeviceId(),
|
|
@@ -4963,6 +5144,27 @@ var ItdClient = class {
|
|
|
4963
5144
|
request(options) {
|
|
4964
5145
|
return this.#http.request(options);
|
|
4965
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
|
+
}
|
|
4966
5168
|
/**
|
|
4967
5169
|
* Подписывается на события авторизации.
|
|
4968
5170
|
*
|
|
@@ -5189,6 +5391,6 @@ function toDate(value) {
|
|
|
5189
5391
|
return Number.isFinite(date.getTime()) ? date : null;
|
|
5190
5392
|
}
|
|
5191
5393
|
|
|
5192
|
-
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-
|
|
5194
|
-
//# sourceMappingURL=chunk-
|
|
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
|