itd-api 0.2.0 → 0.4.0

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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as createTokenStorage, n as MemoryTokenStorage, r as TokenStorage, t as ItdSession } from "./storage-BjNRlkbE.cjs";
2
- import { _ as StreamFile, a as createRecordMultiStorage, b as fromStream, c as DEFAULT_URL_FILE_MAX_BYTES, d as FileInput, f as FileStreamContent, g as LazyFile, h as FromStreamOptions, i as createMultiTokenStorage, l as FileContent, m as FileTransferMode, n as MultiTokenStorage, o as scopedTokenStorage, p as FileStreamOptions, r as RecordStorageSource, s as DEFAULT_FILE_STREAM_BUFFER_BYTES, t as MemoryMultiTokenStorage, u as FileContext, v as UrlFile, x as fromUrl, y as UrlFileOptions } from "./multi-storage-NDqzRQcD.cjs";
2
+ import { _ as StreamFile, a as createRecordMultiStorage, c as DEFAULT_URL_FILE_MAX_BYTES, d as FileInput, f as FileStreamContent, g as LazyFile, h as FromStreamOptions, i as createMultiTokenStorage, l as FileContent, m as FileTransferMode, n as MultiTokenStorage, o as scopedTokenStorage, p as FileStreamOptions, r as RecordStorageSource, s as DEFAULT_FILE_STREAM_BUFFER_BYTES, t as MemoryMultiTokenStorage, u as FileContext, v as UrlFile, y as UrlFileOptions } from "./multi-storage-DP_ujJw1.cjs";
3
3
  //#region src/types/enums.d.ts
4
4
  /**
5
5
  * Перечисления API итд.com.
@@ -326,12 +326,12 @@ declare const ItdErrorCode: Readonly<{
326
326
  }>;
327
327
  type ItdErrorCode = Loose<(typeof ItdErrorCode)[keyof typeof ItdErrorCode]>;
328
328
  //#endregion
329
- //#region src/types/models.d.ts
329
+ //#region src/models/common.d.ts
330
330
  /**
331
331
  * Дата и время в формате ISO-8601, например `2026-07-21T14:30:00.000Z`.
332
332
  *
333
333
  * Библиотека не превращает такие поля в `Date`: строку проще сравнивать, логировать
334
- * и передавать дальше без потерь. Для разбора есть {@link toDate}.
334
+ * и передавать дальше без потерь. Для разбора есть `toDate()`.
335
335
  */
336
336
  type IsoDate = string;
337
337
  /**
@@ -371,705 +371,148 @@ interface Span {
371
371
  /** Идентификатор пользователя у некоторых ответов API с `mention`. */
372
372
  id?: string;
373
373
  }
374
+ //#endregion
375
+ //#region src/core/clock.d.ts
374
376
  /**
375
- * Значок-«пин» в профиле награда или отметка платформы.
377
+ * Часы, которыми клиент измеряет время и планирует отложенную работу.
378
+ *
379
+ * Своя реализация нужна прежде всего в тестах: она позволяет проверять тайм-ауты,
380
+ * повторы и переподключение без ожидания в реальном времени.
376
381
  */
377
- interface Pin {
378
- /** Постоянный идентификатор, например `epepuy_202605_59`. */
379
- slug: string;
380
- /** Отображаемое название. */
381
- name: string;
382
- /** Описание, за что выдан. */
383
- description: string;
384
- /** Адрес изображения. */
385
- url: string;
386
- /** Когда выдан. Приходит только в списке своих пинов. */
387
- grantedAt?: IsoDate;
382
+ interface ItdClock {
383
+ /** Текущее время в миллисекундах с начала эпохи Unix. */
384
+ now(): number;
385
+ /** Планирует вызов после завершения текущего стека и возвращает функцию отмены. */
386
+ schedule(callback: () => void, delay: number): () => void;
388
387
  }
388
+ /** Системные часы, используемые клиентом по умолчанию. */
389
+ declare const systemClock: ItdClock;
390
+ //#endregion
391
+ //#region src/core/runtime.d.ts
389
392
  /**
390
- * Автор поста или комментария.
393
+ * Как библиотека обращается с cookie.
391
394
  *
392
- * Встречается внутри `post.author` и `comment.author`.
395
+ * - `browser` cookie ведёт браузер, запросы уходят с `credentials: 'include'`;
396
+ * - `server` — cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную;
397
+ * - `auto` — определяется по среде исполнения (значение по умолчанию).
393
398
  */
394
- interface Author {
395
- id: UserId;
396
- username: string;
397
- displayName: string;
399
+ declare const RuntimeMode: Readonly<{
400
+ /** Определяется по среде исполнения. Значение по умолчанию. */
401
+ readonly Auto: "auto";
402
+ /** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
403
+ readonly Browser: "browser";
404
+ /** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
405
+ readonly Server: "server";
406
+ }>;
407
+ type RuntimeMode = (typeof RuntimeMode)[keyof typeof RuntimeMode];
408
+ /** Распознанная среда исполнения. */
409
+ declare const DetectedRuntime: Readonly<{
410
+ readonly Browser: "browser";
411
+ /** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
412
+ readonly ReactNative: "react-native";
413
+ readonly Server: "server";
414
+ }>;
415
+ type DetectedRuntime = (typeof DetectedRuntime)[keyof typeof DetectedRuntime];
416
+ //#endregion
417
+ //#region src/core/services.d.ts
418
+ /** Сервис платформы на отдельном домене. */
419
+ interface ServiceDefinition {
420
+ /** Имя, по которому запрос выбирает сервис: `{ service: 'status' }`. */
421
+ name: string;
422
+ /** Базовый URL сервиса. */
423
+ baseUrl: string;
424
+ /** Заголовки, добавляемые к каждому запросу сервиса. Заголовки вызова важнее. */
425
+ headers?: Record<string, string> | undefined;
398
426
  /**
399
- * **Эмодзи, а не картинка.**
427
+ * Слать ли заголовок авторизации.
400
428
  *
401
- * На итд.com аватар это символ клана (`🩵`, `🦎`), а не адрес изображения.
402
- * Отрисовывать его нужно как текст.
429
+ * По умолчанию включено для основного хоста и его поддоменов. Для остальных хостов
430
+ * авторизацию нужно разрешить явно: `auth: true`.
403
431
  */
404
- avatar: string;
405
- /** Пройдена ли верификация. */
406
- verified: boolean;
407
- /** Активный значок профиля. Может отсутствовать. */
408
- pin?: Pin | null;
409
- /** Есть ли премиум-подписка (значок NUKSTA). */
410
- hasNuksta?: boolean;
411
- }
412
- /**
413
- * Участник события в уведомлении.
414
- *
415
- * Отличается от {@link Author} набором полей: вместо значков приходит связь с вами.
416
- */
417
- interface Actor {
418
- id: UserId;
419
- username: string;
420
- displayName: string;
421
- /** Эмодзи-аватар, см. {@link Author.avatar}. */
422
- avatar: string;
423
- /** Подписаны ли вы на этого пользователя. */
424
- isFollowing?: boolean;
425
- /** Подписан ли он на вас. */
426
- isFollowedBy?: boolean;
432
+ auth?: boolean | undefined;
427
433
  }
428
434
  /**
429
- * Пользователь в списках.
435
+ * Именованные сервисы клиента.
430
436
  *
431
- * Набор полей зависит от эндпоинта: подписчики и подписки приносят `isFollowing`,
432
- * поиск и рекомендации — `followersCount` и `hasNuksta`. Необязательные поля отражают
433
- * это различие.
437
+ * @internal
434
438
  */
435
- interface UserSummary {
436
- id: UserId;
437
- username: string;
438
- displayName: string;
439
- /** Эмодзи-аватар, см. {@link Author.avatar}. */
440
- avatar: string;
441
- verified: boolean;
442
- /** Подписаны ли вы. Приходит в списках подписчиков и подписок. */
443
- isFollowing?: boolean;
444
- /** Есть ли премиум. Приходит в поиске и рекомендациях. */
445
- hasNuksta?: boolean;
446
- /** Число подписчиков. Приходит в поиске и рекомендациях. */
447
- followersCount?: number;
448
- }
449
- /** Поля профиля, общие для своего и чужого. */
450
- interface ProfileBase {
451
- id: UserId;
452
- username: string;
453
- displayName: string;
454
- /** Эмодзи-аватар, см. {@link Author.avatar}. */
455
- avatar: string;
456
- /** URL изображения баннера либо `null`. */
457
- banner: string | null;
458
- /** Описание профиля. */
459
- bio: string;
460
- verified: boolean;
461
- pin?: Pin | null;
462
- /** Кто может писать на стену. */
463
- wallAccess: WallAccess;
464
- /** Кто видит реакции. */
465
- likesVisibility: LikesVisibility;
466
- followersCount: number;
467
- followingCount: number;
468
- postsCount: number;
469
- createdAt: IsoDate;
470
- }
471
- /** Состояние подписки на премиум. */
472
- interface SubscriptionState {
473
- isActive: boolean;
474
- expiresAt: IsoDate | null;
475
- autoRenewal: boolean;
439
+ declare class ServiceRegistry {
440
+ #private;
441
+ /** @param primaryBaseUrl базовый URL клиента */
442
+ constructor(primaryBaseUrl?: string);
443
+ /**
444
+ * Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
445
+ * к каноничному виду, а незаданный `auth` выводится из хоста.
446
+ *
447
+ * @throws {ItdConfigError} если имя пустое, имя занято или `baseUrl` не абсолютный URL
448
+ */
449
+ define(definition: ServiceDefinition): void;
450
+ /** Определение сервиса либо `undefined`, если такого нет. */
451
+ get(name: string): ServiceDefinition | undefined;
452
+ /** Зарегистрирован ли сервис с таким именем. */
453
+ has(name: string): boolean;
454
+ /**
455
+ * Определение сервиса.
456
+ *
457
+ * @throws {ItdConfigError} если сервис не зарегистрирован
458
+ */
459
+ require(name: string): ServiceDefinition;
460
+ /**
461
+ * Базовый URL сервиса.
462
+ *
463
+ * @throws {ItdConfigError} если сервис не зарегистрирован
464
+ */
465
+ resolveBaseUrl(name: string): string;
466
+ /**
467
+ * Принадлежит ли URL основному хосту клиента или его поддомену.
468
+ *
469
+ * Используется для безопасного значения по умолчанию у разового `baseUrl`: Bearer-токен
470
+ * не должен уходить на посторонний хост без явного `skipAuth: false`.
471
+ *
472
+ * @internal
473
+ */
474
+ isPrimarySite(baseUrl: string): boolean;
476
475
  }
476
+ //#endregion
477
+ //#region src/core/url.d.ts
478
+ /** Значение параметра запроса. `undefined` и `null` в строку не попадают. */
479
+ type QueryValue = string | number | boolean | null | undefined | readonly (string | number | boolean)[];
480
+ /** Параметры строки запроса. */
481
+ type QueryParams = Record<string, QueryValue>;
482
+ //#endregion
483
+ //#region src/types/options.d.ts
477
484
  /**
478
- * Свой профиль ответ `GET /api/users/me`.
485
+ * Вход по логину и паролю.
479
486
  *
480
- * Отличается от чужого наличием {@link subscription} и {@link isPhoneVerified}
481
- * и отсутствием полей связи (`isFollowing`, `online`).
487
+ * Вход требует токен капчи Cloudflare Turnstile, поэтому полностью автоматическим он быть
488
+ * не может: капчу должен решить кто-то снаружи. Токен одноразовый и живёт несколько минут,
489
+ * так что долгоживущему клиенту нужен `getTurnstileToken` — он спрашивается заново перед
490
+ * каждой попыткой входа. Одиночный `turnstileToken` годится для разового скрипта.
482
491
  */
483
- interface MyProfile extends ProfileBase {
484
- /** Закрыт ли профиль. */
485
- isPrivate: boolean;
486
- /** Подтверждён ли телефон. Без него часть действий недоступна. */
487
- isPhoneVerified: boolean;
488
- /** Своя премиум-подписка. */
489
- subscription: SubscriptionState;
492
+ interface CredentialsAuth {
493
+ email: string;
494
+ password: string;
495
+ /** Разовый токен капчи. Для повторного входа после истечения сессии не подойдёт. */
496
+ turnstileToken?: string | undefined;
497
+ /** Источник свежего токена капчи. Спрашивается перед каждым входом. */
498
+ getTurnstileToken?: (() => string | Promise<string>) | undefined;
490
499
  }
491
500
  /**
492
- * Состояние авторизации ответ `GET /api/profile`.
501
+ * Как клиент получает доступ к API.
493
502
  *
494
- * Endpoint доступен без сессии: в этом случае `authenticated` равен `false`,
495
- * а `user` `null`.
496
- */
497
- interface AuthState {
498
- /** Есть ли действующая сессия. */
499
- authenticated: boolean;
500
- /** Заблокирован ли текущий аккаунт. */
501
- banned: boolean;
502
- /** Текущий пользователь либо `null` без действующей сессии. */
503
- user: MyProfile | null;
504
- }
505
- /**
506
- * Чужой профиль — ответ `GET /api/users/{id|username}`.
503
+ * Четыре формы от разового вызова с готовым токеном до полноценной сессии, которую
504
+ * библиотека заводит и продлевает сама.
507
505
  *
508
- * Вместо своей подписки содержит связь с вами и присутствие.
509
- */
510
- interface PublicProfile extends ProfileBase {
511
- hasNuksta?: boolean;
512
- /** Закреплённый пост, если он есть. */
513
- pinnedPostId: string | null;
514
- /** Подписаны ли вы на него. */
515
- isFollowing: boolean;
516
- /** Подписан ли он на вас. */
517
- isFollowedBy: boolean;
518
- /** Сейчас ли пользователь в сети. */
519
- online: boolean;
520
- /** Когда был в сети. `null`, если скрыто настройками приватности. */
521
- lastSeen: IsoDate | null;
522
- }
523
- /** Профиль: свой либо чужой. Различаются функцией {@link isMyProfile}. */
524
- type Profile = MyProfile | PublicProfile;
525
- /**
526
- * Свой ли это профиль.
506
+ * Опция необязательна: если {@link ItdClientOptions.storage} уже содержит сессию, доступ
507
+ * берётся оттуда. Когда заданы обе, хранилище главнее — оно отражает текущее состояние
508
+ * сессии, а недостающие поля берутся отсюда.
527
509
  *
528
510
  * @example
529
511
  * ```ts
530
- * if (isMyProfile(profile)) console.log(profile.subscription.isActive);
531
- * ```
532
- */
533
- declare function isMyProfile(profile: Profile): profile is MyProfile;
534
- /** Вложение поста или комментария. */
535
- interface Attachment {
536
- id: string;
537
- type: AttachmentType;
538
- /** Адрес файла на CDN. */
539
- url: string;
540
- /** Ширина изображения или видео в пикселях. */
541
- width?: number;
542
- /** Высота изображения или видео в пикселях. */
543
- height?: number;
544
- mimeType: string;
545
- /** Исходное имя файла. Приходит не всегда. */
546
- filename?: string;
547
- /** Размер в байтах. Приходит не всегда. */
548
- size?: number;
549
- /** Длительность аудио или видео в секундах. */
550
- duration?: number | null;
551
- /** Порядковый номер во вложениях поста. */
552
- order?: number;
553
- }
554
- /** Вариант ответа в опросе. */
555
- interface PollOption {
556
- id: string;
557
- text: string;
558
- /** Сколько голосов отдано за этот вариант. */
559
- votesCount: number;
560
- /** Порядковый номер варианта, начиная с нуля. */
561
- position: number;
562
- }
563
- /** Опрос внутри поста. */
564
- interface Poll {
565
- id: string;
566
- /** Пост, которому принадлежит опрос. */
567
- postId: string;
568
- question: string;
569
- /** Можно ли выбрать несколько вариантов. */
570
- multipleChoice: boolean;
571
- options: PollOption[];
572
- totalVotes: number;
573
- /** Голосовали ли вы. */
574
- hasVoted: boolean;
575
- /** За что проголосовали вы. Пустой массив, если голоса не было. */
576
- votedOptionIds: string[];
577
- createdAt: IsoDate;
578
- }
579
- /** Пост ленты, стены или профиля. */
580
- interface Post {
581
- id: string;
582
- content: string;
583
- /** Разметка текста. Передаётся без изменений, см. {@link Span}. */
584
- spans: Span[];
585
- author: Author;
586
- attachments: Attachment[];
587
- likesCount: number;
588
- commentsCount: number;
589
- repostsCount: number;
590
- viewsCount: number;
591
- /** Чья это стена, если пост опубликован не у себя. */
592
- wallRecipientId: UserId | null;
593
- /** Владелец стены. Приходит не во всех ответах. */
594
- wallRecipient?: Author | null;
595
- /** Поставили ли вы реакцию. */
596
- isLiked: boolean;
597
- /** Делали ли вы репост. */
598
- isReposted: boolean;
599
- /** Засчитан ли просмотр. */
600
- isViewed: boolean;
601
- /** Ваш ли это пост. */
602
- isOwner: boolean;
603
- /** Исходный пост, если это репост. */
604
- originalPost?: Post | null;
605
- poll?: Poll | null;
606
- /** Преобладающая реакция — эмодзи либо `null`. */
607
- dominantEmoji?: string | null;
608
- /** Когда пост отредактировали. `null`, если не редактировали. */
609
- editedAt: IsoDate | null;
610
- createdAt: IsoDate;
611
- /**
612
- * Служебная метка показа для телеметрии.
613
- *
614
- * Нужна только эндпоинтам `itd.telemetry.*`. В остальных случаях игнорируйте.
615
- */
616
- vs?: string;
617
- /**
618
- * Топовые комментарии. Приходят только в ответе `GET /api/posts/{id}`.
619
- *
620
- * В списках постов поле отсутствует.
621
- */
622
- comments?: Comment[];
623
- }
624
- /** На чей комментарий дан ответ. */
625
- interface CommentReplyTo {
626
- id: string;
627
- username: string;
628
- displayName: string;
629
- }
630
- /** Комментарий к посту или ответ на комментарий. */
631
- interface Comment {
632
- id: string;
633
- /** Текст. У голосового комментария пустой. */
634
- content: string;
635
- /**
636
- * Разметка текста, включая автоматически найденные сервером хэштеги и упоминания.
637
- *
638
- * Методы создания и редактирования комментария принимают только `content`, поэтому
639
- * библиотека не отправляет ручные spans в этих операциях.
640
- * Поле необязательно: отдельные ответы сервера могут его не содержать.
641
- */
642
- spans?: Span[];
643
- author: Author;
644
- likesCount: number;
645
- repliesCount: number;
646
- isLiked: boolean;
647
- createdAt: IsoDate;
648
- /** Вложения. У голосового — одно аудио с `mimeType: 'audio/ogg'`. */
649
- attachments?: Attachment[];
650
- /** Вложенные ответы. В списках приходит превью, полный список — через `itd.comments.replies()`. */
651
- replies?: Comment[];
652
- /** Заполнено только у ответов. */
653
- replyTo?: CommentReplyTo;
654
- }
655
- /**
656
- * Уведомление в единой форме.
657
- *
658
- * REST-список и SSE-поток отдают уведомления по-разному — разные имена типов, разные имена
659
- * полей, один участник против массива. Библиотека приводит оба вида к этой структуре,
660
- * поэтому объекты из `itd.notifications.list()` и из потока можно складывать в один список.
661
- *
662
- * Исходные данные не теряются: сервeрное имя типа остаётся в {@link rawType},
663
- * а весь необработанный объект — в {@link raw}.
664
- */
665
- interface Notification {
666
- id: string;
667
- /** Канонический тип. Старые имена (`like`, `comment`) приведены к новым. */
668
- type: NotificationType;
669
- /** Имя типа в том виде, в каком его прислал сервер. */
670
- rawType: string;
671
- /** Объект события: пост, комментарий, пользователь. */
672
- entityId: string | null;
673
- /** Пост, которому принадлежит комментарий, если событие о комментарии. */
674
- parentEntityId: string | null;
675
- /** Прочитано ли уведомление. */
676
- isRead: boolean;
677
- /** Кто совершил действие. Для схлопнутых уведомлений — несколько человек. */
678
- actors: Actor[];
679
- /** Сколько участников схлопнуто в одно уведомление. Минимум 1. */
680
- count: number;
681
- /** Текст или заголовок объекта события. */
682
- preview: string | null;
683
- /** Ссылка перехода, предложенная сервером. Обычно точнее её `resolveNotificationUrl()`. */
684
- clickUrl?: string;
685
- createdAt: IsoDate;
686
- /** Когда уведомление изменилось — например было прочитано. */
687
- updatedAt: IsoDate;
688
- /** Исходный объект как он пришёл от сервера. */
689
- raw: unknown;
690
- }
691
- /** Настройки приватности профиля. */
692
- interface PrivacySettings {
693
- /** Закрыт ли профиль: подписка требует одобрения. */
694
- isPrivate: boolean;
695
- wallAccess: WallAccess;
696
- likesVisibility: LikesVisibility;
697
- /** Показывать ли время последнего посещения. */
698
- showLastSeen: boolean;
699
- }
700
- /**
701
- * Настройки уведомлений.
702
- *
703
- * Сервер отдаёт плоский объект, но исторически знает два набора имён для одних и тех же
704
- * настроек (`likes` и `reactions`, `comments` и `replies`). При сохранении библиотека
705
- * отправляет оба, при чтении принимает любой.
706
- */
707
- interface NotificationSettings {
708
- /** Общий выключатель доставки. */
709
- enabled: boolean;
710
- /** Звук уведомления. */
711
- sound: boolean;
712
- /** Новые подписчики. */
713
- follows: boolean;
714
- /** Записи на вашей стене. */
715
- wallPosts: boolean;
716
- /** Реакции на ваши записи. */
717
- likes: boolean;
718
- /** Комментарии и ответы. */
719
- comments: boolean;
720
- /** Упоминания. */
721
- mentions: boolean;
722
- }
723
- /** Активная сессия входа. */
724
- interface Session {
725
- id: string;
726
- /** Та ли это сессия, из которой выполнен запрос. */
727
- isCurrent: boolean;
728
- createdAt: IsoDate;
729
- lastUsedAt: IsoDate;
730
- expiresAt: IsoDate;
731
- ipAddress: string;
732
- /** Код страны по IP, например `RU`. */
733
- ipCountry: string | null;
734
- ipCity: string | null;
735
- deviceType: Loose<'desktop' | 'mobile'>;
736
- osName: string | null;
737
- osVersion: string | null;
738
- /** Название браузера или приложения. */
739
- clientName: string | null;
740
- clientVersion: string | null;
741
- deviceModel: string | null;
742
- }
743
- /** Состояние платной подписки и её цена. */
744
- interface Subscription {
745
- /** Активна ли подписка сейчас. */
746
- active: boolean;
747
- /** Включено ли автопродление. */
748
- recurringEnabled: boolean;
749
- /** Цена в рублях. */
750
- price: number;
751
- }
752
- /** Сохранённый способ оплаты. */
753
- interface PaymentMethod {
754
- id: string;
755
- /** Последние четыре цифры карты. */
756
- last4?: string;
757
- /** Платёжная система: `visa`, `mastercard`, `mir`. */
758
- brand?: string;
759
- /** Основной ли это способ оплаты. */
760
- isDefault?: boolean;
761
- expiresAt?: IsoDate | null;
762
- }
763
- /** Хэштег. */
764
- interface Hashtag {
765
- id: string;
766
- /** Название без решётки. */
767
- name: string;
768
- /** Сколько постов с этим хэштегом. */
769
- postsCount: number;
770
- }
771
- /** Клан в рейтинге. */
772
- interface Clan {
773
- /** Эмодзи клана — оно же аватар его участников. */
774
- avatar: string;
775
- memberCount: number;
776
- }
777
- /**
778
- * Результат подписки на пользователя.
779
- *
780
- * @example
781
- * ```ts
782
- * const result = await itd.users.follow('nowkie');
783
- * // { following: true, followersCount: 11 }
784
- * ```
785
- */
786
- interface FollowResult {
787
- /** Подписка оформлена. У закрытого профиля отправляется заявка, и здесь будет `false`. */
788
- following: boolean;
789
- /** Сколько подписчиков стало у пользователя после действия. */
790
- followersCount?: number;
791
- /** Статус заявки, если профиль закрыт. */
792
- status?: Loose<'following' | 'requested'>;
793
- }
794
- /** Запись журнала изменений платформы. */
795
- interface ChangelogEntry {
796
- version: string;
797
- date: string;
798
- changes: string[];
799
- }
800
- /** Кнопка в анонсе платформы. */
801
- interface AnnouncementButton {
802
- title: string;
803
- /** Оформление: `primary`, `secondary` и другие. */
804
- style: string;
805
- action: {
806
- type: string;
807
- [key: string]: unknown;
808
- };
809
- }
810
- /** Анонс на главной странице платформы. */
811
- interface Announcement {
812
- id: string;
813
- image: {
814
- url: string;
815
- width: number;
816
- height: number;
817
- };
818
- title: string;
819
- description: string;
820
- /** Дополнительный текст мелким шрифтом. */
821
- additional_text?: string;
822
- buttons: AnnouncementButton[];
823
- }
824
- /** Баннер текущего события — виджет «портал». */
825
- interface Portal {
826
- active: boolean;
827
- title: string;
828
- url: string;
829
- }
830
- /** Происшествие в истории сервиса. */
831
- interface StatusIncidentLine {
832
- /** Вид происшествия. */
833
- t: IncidentKind;
834
- /**
835
- * Готовая строка для показа: `недоступен 6 мин (12:00–12:06)`. Время московское.
836
- * Длительность и границы интервала отдельными полями не приходят.
837
- */
838
- text: string;
839
- }
840
- /** Одни сутки в истории сервиса. */
841
- interface StatusDay {
842
- /** Худшее состояние за сутки. */
843
- type: ServiceState;
844
- /** Дата суток, `YYYY-MM-DD`. Сутки нарезаны по UTC. */
845
- date_key: string;
846
- /** Доступность за сутки в процентах. */
847
- uptime: number;
848
- /** Происшествия за сутки. */
849
- lines: StatusIncidentLine[];
850
- }
851
- /** Сервис платформы и его история доступности. */
852
- interface ServiceStatus {
853
- /** Идентификатор: `auth`, `main`, `media` и прочие. */
854
- id: string;
855
- /** Отображаемое название. */
856
- name: string;
857
- current_status: ServiceState;
858
- /** Пояснение к текущему состоянию, например `No downtime`. */
859
- current_message: string;
860
- /** Задержка последней проверки в миллисекундах. */
861
- latency_ms: number;
862
- /**
863
- * Момент последней проверки. Сервер отдаёт `YYYY-MM-DD HH:mm:ss` в UTC, библиотека
864
- * приводит значение к ISO.
865
- */
866
- last_checked: IsoDate;
867
- /** Доступность за 90 суток в процентах. */
868
- uptime_90d: number;
869
- /**
870
- * История по суткам. Ключ — сколько суток назад, `'0'` — сегодня.
871
- *
872
- * Объект разреженный: сутки без данных сервер пропускает. Ровный массив даёт
873
- * {@link statusDays}.
874
- */
875
- days: Record<string, StatusDay | undefined>;
876
- }
877
- /** Состояние платформы — ответ `itd.platform.status()`. */
878
- interface PlatformStatus {
879
- /** Худшее состояние среди сервисов. */
880
- overall_status: ServiceState;
881
- /** Когда данные последний раз пересчитаны. */
882
- updated_at: IsoDate;
883
- services: ServiceStatus[];
884
- }
885
- /** Статус заявки на верификацию. `none` означает, что заявка не подавалась. */
886
- interface VerificationStatus {
887
- status: Loose<'none' | 'pending' | 'approved' | 'rejected'>;
888
- }
889
- /** Созданная жалоба. */
890
- interface Report {
891
- id: string;
892
- createdAt: IsoDate;
893
- }
894
- /** Счётчики поста из `itd.posts.stats()`. */
895
- interface PostStats {
896
- id: string;
897
- likesCount: number;
898
- commentsCount: number;
899
- repostsCount: number;
900
- viewsCount: number;
901
- /** Преобладающая реакция — эмодзи либо `null`. */
902
- dominantEmoji: string | null;
903
- }
904
- /** Результат реакции на пост. */
905
- interface LikeResult {
906
- liked: boolean;
907
- likesCount: number;
908
- }
909
- /** Результат закрепления поста в профиле. */
910
- interface PinPostResult {
911
- success: boolean;
912
- pinnedPostId: string | null;
913
- }
914
- /** Закреплённые значки профиля и выбранный из них. */
915
- interface PinsResult {
916
- pins: Pin[];
917
- /** Идентификатор активного значка — строка, а не объект. */
918
- activePin: string | null;
919
- }
920
- /**
921
- * Разбирает дату API в объект `Date`.
922
- *
923
- * @returns `null`, если строки нет или она не разбирается
924
- *
925
- * @example
926
- * ```ts
927
- * const created = toDate(post.createdAt);
928
- * ```
929
- */
930
- declare function toDate(value: IsoDate | null | undefined): Date | null;
931
- /**
932
- * Разворачивает историю сервиса в массив на 90 суток.
933
- * Сутки без данных становятся `null`.
934
- *
935
- * @returns массив, где индекс — сколько суток назад: `[0]` — сегодня
936
- *
937
- * @example
938
- * ```ts
939
- * const status = await itd.platform.status();
940
- * const days = statusDays(status.services[0]);
941
- *
942
- * days[0]?.uptime; // доступность за сегодня
943
- * days.filter((day) => day === null).length; // за сколько суток данных нет
944
- * ```
945
- */
946
- declare function statusDays(service: ServiceStatus): (StatusDay | null)[];
947
- //#endregion
948
- //#region src/core/runtime.d.ts
949
- /**
950
- * Как библиотека обращается с cookie.
951
- *
952
- * - `browser` — cookie ведёт браузер, запросы уходят с `credentials: 'include'`;
953
- * - `server` — cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную;
954
- * - `auto` — определяется по среде исполнения (значение по умолчанию).
955
- */
956
- declare const RuntimeMode: Readonly<{
957
- /** Определяется по среде исполнения. Значение по умолчанию. */
958
- readonly Auto: "auto";
959
- /** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
960
- readonly Browser: "browser";
961
- /** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
962
- readonly Server: "server";
963
- }>;
964
- type RuntimeMode = (typeof RuntimeMode)[keyof typeof RuntimeMode];
965
- /** Распознанная среда исполнения. */
966
- declare const DetectedRuntime: Readonly<{
967
- readonly Browser: "browser";
968
- /** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
969
- readonly ReactNative: "react-native";
970
- readonly Server: "server";
971
- }>;
972
- type DetectedRuntime = (typeof DetectedRuntime)[keyof typeof DetectedRuntime];
973
- //#endregion
974
- //#region src/core/services.d.ts
975
- /** Сервис платформы на отдельном домене. */
976
- interface ServiceDefinition {
977
- /** Имя, по которому запрос выбирает сервис: `{ service: 'status' }`. */
978
- name: string;
979
- /** Базовый URL сервиса. */
980
- baseUrl: string;
981
- /** Заголовки, добавляемые к каждому запросу сервиса. Заголовки вызова важнее. */
982
- headers?: Record<string, string> | undefined;
983
- /**
984
- * Слать ли заголовок авторизации.
985
- *
986
- * По умолчанию включено для основного хоста и его поддоменов. Для остальных хостов
987
- * авторизацию нужно разрешить явно: `auth: true`.
988
- */
989
- auth?: boolean | undefined;
990
- }
991
- /**
992
- * Именованные сервисы клиента.
993
- *
994
- * @internal
995
- */
996
- declare class ServiceRegistry {
997
- #private;
998
- /** @param primaryBaseUrl базовый URL клиента */
999
- constructor(primaryBaseUrl?: string);
1000
- /**
1001
- * Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
1002
- * к каноничному виду, а незаданный `auth` выводится из хоста.
1003
- *
1004
- * @throws {ItdConfigError} если имя пустое, имя занято или `baseUrl` не абсолютный URL
1005
- */
1006
- define(definition: ServiceDefinition): void;
1007
- /** Определение сервиса либо `undefined`, если такого нет. */
1008
- get(name: string): ServiceDefinition | undefined;
1009
- /** Зарегистрирован ли сервис с таким именем. */
1010
- has(name: string): boolean;
1011
- /**
1012
- * Определение сервиса.
1013
- *
1014
- * @throws {ItdConfigError} если сервис не зарегистрирован
1015
- */
1016
- require(name: string): ServiceDefinition;
1017
- /**
1018
- * Базовый URL сервиса.
1019
- *
1020
- * @throws {ItdConfigError} если сервис не зарегистрирован
1021
- */
1022
- resolveBaseUrl(name: string): string;
1023
- /**
1024
- * Принадлежит ли URL основному хосту клиента или его поддомену.
1025
- *
1026
- * Используется для безопасного значения по умолчанию у разового `baseUrl`: Bearer-токен
1027
- * не должен уходить на посторонний хост без явного `skipAuth: false`.
1028
- *
1029
- * @internal
1030
- */
1031
- isPrimarySite(baseUrl: string): boolean;
1032
- }
1033
- //#endregion
1034
- //#region src/core/url.d.ts
1035
- /** Значение параметра запроса. `undefined` и `null` в строку не попадают. */
1036
- type QueryValue = string | number | boolean | null | undefined | readonly (string | number | boolean)[];
1037
- /** Параметры строки запроса. */
1038
- type QueryParams = Record<string, QueryValue>;
1039
- //#endregion
1040
- //#region src/types/options.d.ts
1041
- /**
1042
- * Вход по логину и паролю.
1043
- *
1044
- * Вход требует токен капчи Cloudflare Turnstile, поэтому полностью автоматическим он быть
1045
- * не может: капчу должен решить кто-то снаружи. Токен одноразовый и живёт несколько минут,
1046
- * так что долгоживущему клиенту нужен `getTurnstileToken` — он спрашивается заново перед
1047
- * каждой попыткой входа. Одиночный `turnstileToken` годится для разового скрипта.
1048
- */
1049
- interface CredentialsAuth {
1050
- email: string;
1051
- password: string;
1052
- /** Разовый токен капчи. Для повторного входа после истечения сессии не подойдёт. */
1053
- turnstileToken?: string | undefined;
1054
- /** Источник свежего токена капчи. Спрашивается перед каждым входом. */
1055
- getTurnstileToken?: (() => string | Promise<string>) | undefined;
1056
- }
1057
- /**
1058
- * Как клиент получает доступ к API.
1059
- *
1060
- * Четыре формы — от разового вызова с готовым токеном до полноценной сессии, которую
1061
- * библиотека заводит и продлевает сама.
1062
- *
1063
- * Опция необязательна: если {@link ItdClientOptions.storage} уже содержит сессию, доступ
1064
- * берётся оттуда. Когда заданы обе, хранилище главнее — оно отражает текущее состояние
1065
- * сессии, — а недостающие поля берутся отсюда.
1066
- *
1067
- * @example
1068
- * ```ts
1069
- * new ItdClient({ auth: '<accessToken>' }); // разовый вызов
1070
- * new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию
1071
- * new ItdClient({ auth: { email, password, getTurnstileToken } }); // залогиниться самому
1072
- * new ItdClient({ auth: { getToken: () => vault.read() } }); // токен из внешнего источника
512
+ * new ItdClient({ auth: '<accessToken>' }); // разовый вызов
513
+ * new ItdClient({ auth: { accessToken, refreshToken } }); // восстановить сессию
514
+ * new ItdClient({ auth: { email, password, getTurnstileToken } }); // залогиниться самому
515
+ * new ItdClient({ auth: { getToken: () => vault.read() } }); // токен из внешнего источника
1073
516
  * ```
1074
517
  */
1075
518
  type AuthInput = string | {
@@ -1227,6 +670,8 @@ interface ItdClientOptions {
1227
670
  reloginOnRefreshFailure?: boolean | undefined;
1228
671
  /** Своя реализация `fetch`: для Deno, React Native, тестов или прокси. */
1229
672
  fetch?: typeof fetch | undefined;
673
+ /** Часы для тайм-аутов, повторов и очередей. Обычно подменяются только в тестах. */
674
+ clock?: ItdClock | undefined;
1230
675
  /** Таймаут запроса в мс. По умолчанию 30000 — столько же использует сайт итд.com. `0` снимает ограничение. */
1231
676
  timeout?: number | undefined;
1232
677
  /** Повторные попытки. `false` отключает их полностью. */
@@ -1326,7 +771,7 @@ interface RawRequestOptions extends RequestOptions {
1326
771
  //#endregion
1327
772
  //#region src/core/version.d.ts
1328
773
  /** Версия библиотеки. Попадает в `User-Agent`. */
1329
- declare const LIBRARY_VERSION = "0.2.0";
774
+ declare const LIBRARY_VERSION = "0.4.0";
1330
775
  //#endregion
1331
776
  //#region src/core/config.d.ts
1332
777
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -1349,7 +794,7 @@ declare const DEFAULT_TIMEOUT = 30000;
1349
794
  * В браузере заголовок не выставляется — `User-Agent` там запрещён к изменению, и среда
1350
795
  * молча его игнорирует.
1351
796
  */
1352
- declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; itd-api/0.2.0; +https://github.com/KiowDev/itd-api)";
797
+ declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; itd-api/0.4.0; +https://github.com/KiowDev/itd-api)";
1353
798
  /** Настройки очереди со всеми значениями по умолчанию. */
1354
799
  interface ResolvedRateLimitOptions {
1355
800
  concurrency: number;
@@ -1366,6 +811,7 @@ interface ResolvedRateLimitOptions {
1366
811
  */
1367
812
  interface AuthConfig {
1368
813
  baseUrl: string;
814
+ clock: ItdClock;
1369
815
  auth: AuthInput | undefined;
1370
816
  storage: TokenStorage;
1371
817
  useCookieJar: boolean;
@@ -1672,242 +1118,427 @@ declare class AuthManager {
1672
1118
  /** Текущая сессия целиком. Полезно, чтобы сохранить её самому. */
1673
1119
  getSession(): Promise<ItdSession | null>;
1674
1120
  /**
1675
- * Идентификатор владельца сессии.
1121
+ * Идентификатор владельца сессии.
1122
+ *
1123
+ * Считается непосредственно из текущего токена и отдельно не сохраняется: после замены
1124
+ * токена идентификатор прежнего владельца остаться не может.
1125
+ */
1126
+ getUserId(): Promise<UserId | undefined>;
1127
+ /** Заменяет сессию и связанные с ней cookie целиком. */
1128
+ setSession(session: ItdSession): Promise<void>;
1129
+ /**
1130
+ * Забывает сессию и cookie. Сетевой запрос не выполняется.
1131
+ *
1132
+ * Идентификатор устройства выход переживает: иначе каждая пара «выход — вход» плодила бы
1133
+ * новую запись в списке сессий.
1134
+ */
1135
+ clear(): Promise<void>;
1136
+ }
1137
+ //#endregion
1138
+ //#region src/core/plugins/contracts.d.ts
1139
+ /**
1140
+ * Обёртка вокруг запроса.
1141
+ *
1142
+ * Получает описание запроса и продолжение цепочки. Может изменить запрос перед отправкой,
1143
+ * посмотреть и подменить разобранный ответ или вовсе не вызывать `next` и вернуть своё.
1144
+ *
1145
+ * @param request что уходит на сервер; изменять сам объект не нужно — передайте копию в `next`
1146
+ * @param next продолжение: либо следующая обёртка, либо настоящий запрос
1147
+ * @returns тело ответа в том виде, в каком его получит вызывающий код
1148
+ *
1149
+ * @example Дописать заголовок ко всем запросам
1150
+ * ```ts
1151
+ * const transformer: Transformer = (request, next) =>
1152
+ * next({ ...request, headers: { ...request.headers, 'X-Trace': trace() } });
1153
+ * ```
1154
+ */
1155
+ type Transformer = (request: RawRequestOptions, next: (request: RawRequestOptions) => Promise<unknown>) => Promise<unknown>;
1156
+ /** Освобождение ресурсов, заведённых плагином при установке. */
1157
+ type PluginTeardown = () => void | Promise<void>;
1158
+ /** Что плагин получает при подключении. */
1159
+ interface PluginContext {
1160
+ /** Базовый URL клиента — например чтобы разобрать абсолютные ссылки из ответа. */
1161
+ baseUrl: string;
1162
+ /** Отладочный вывод клиента, если он включён. */
1163
+ logger: Logger | undefined;
1164
+ /**
1165
+ * Непрозрачная fallback-область текущей авторизации.
1166
+ *
1167
+ * Нужна плагинам, которые обязаны безопасно изолировать непрозрачный токен. Для объединения
1168
+ * состояния копий одного аккаунта используйте {@link getAuthIdentity}.
1169
+ */
1170
+ getAuthScope?: (() => string) | undefined;
1171
+ /**
1172
+ * Загружает сессию и возвращает идентификаторы аккаунта и конкретной сессии из JWT.
1173
+ *
1174
+ * Предпочтительнее {@link getAuthScope} для состояния, которое должно объединяться между
1175
+ * несколькими экземплярами клиента одного аккаунта.
1176
+ */
1177
+ getAuthIdentity?: (() => Promise<AuthIdentity>) | undefined;
1178
+ /** Добавляет обёртку запроса. Подключённые раньше оказываются снаружи. */
1179
+ use(transformer: Transformer): void;
1180
+ /**
1181
+ * Добавляет перехватчики отдельных сетевых попыток.
1182
+ *
1183
+ * В отличие от {@link use}, они видят каждый retry и сырой `Response` до чтения тела.
1184
+ * Несколько наборов хуков одного плагина вызываются в порядке регистрации.
1185
+ */
1186
+ useHooks(hooks: ClientHooks): void;
1187
+ }
1188
+ /**
1189
+ * Плагин клиента.
1190
+ *
1191
+ * Подключается через `itd.use(plugin)` и работает на уровне транспорта: видит запрос
1192
+ * до отправки и разобранный ответ. Библиотека не знает, что именно делает плагин, —
1193
+ * ей достаточно списка обёрток и имён опций, которые он читает.
1194
+ *
1195
+ * @example
1196
+ * ```ts
1197
+ * const logging: ItdPlugin = {
1198
+ * name: 'logging',
1199
+ * install({ use, logger }) {
1200
+ * use(async (request, next) => {
1201
+ * logger?.info(`${request.method} ${request.path}`);
1202
+ * return next(request);
1203
+ * });
1204
+ * },
1205
+ * };
1206
+ *
1207
+ * itd.use(logging);
1208
+ * ```
1209
+ */
1210
+ interface ItdPlugin {
1211
+ /** Имя плагина. Должно быть уникальным: повторное подключение — ошибка. */
1212
+ name: string;
1213
+ /**
1214
+ * Имена опций запроса, которые плагин читает у методов ресурсов.
1676
1215
  *
1677
- * Считается непосредственно из текущего токена и отдельно не сохраняется: после замены
1678
- * токена идентификатор прежнего владельца остаться не может.
1216
+ * Библиотека этих опций не понимает и ничего с ними не делает только доносит
1217
+ * от вызова метода до обёртки нетронутыми. Без такого списка чужие поля отсеиваются,
1218
+ * чтобы случайная опечатка в параметрах не уезжала на сервер.
1219
+ *
1220
+ * Имена полей самого запроса (`path`, `body`, `headers`, `signal` и прочие из
1221
+ * `RawRequestOptions`) заявить нельзя: подключение такого плагина завершится ошибкой.
1222
+ *
1223
+ * Типы для них плагин объявляет сам, дополняя `RequestOptions`:
1224
+ * ```ts
1225
+ * declare module 'itd-api' {
1226
+ * interface RequestOptions { encrypt?: string | undefined }
1227
+ * }
1228
+ * ```
1679
1229
  */
1680
- getUserId(): Promise<UserId | undefined>;
1681
- /** Заменяет сессию и связанные с ней cookie целиком. */
1682
- setSession(session: ItdSession): Promise<void>;
1230
+ optionKeys?: readonly string[];
1231
+ /** Плагины, которые обязаны быть подключены раньше этого. */
1232
+ requires?: readonly string[];
1233
+ /** Несовместимые плагины. Достаточно объявить конфликт с одной стороны. */
1234
+ conflicts?: readonly string[];
1235
+ /** Имена плагинов, снаружи которых должна стоять эта обёртка. */
1236
+ before?: readonly string[];
1237
+ /** Имена плагинов, внутри которых должна стоять эта обёртка. */
1238
+ after?: readonly string[];
1683
1239
  /**
1684
- * Забывает сессию и cookie. Сетевой запрос не выполняется.
1240
+ * Устанавливает плагин.
1685
1241
  *
1686
- * Идентификатор устройства выход переживает: иначе каждая пара «выход вход» плодила бы
1687
- * новую запись в списке сессий.
1242
+ * Может вернуть функцию освобождения ресурсов. Она вызывается при `unuse()` или
1243
+ * окончательном `dispose()` клиента и может быть асинхронной.
1688
1244
  */
1689
- clear(): Promise<void>;
1245
+ install(context: PluginContext): unknown;
1690
1246
  }
1691
1247
  //#endregion
1692
- //#region src/core/plugins.d.ts
1248
+ //#region src/core/rate-limit.d.ts
1693
1249
  /**
1694
- * Обёртка вокруг запроса.
1250
+ * Очередь запросов: ограничивает одновременность и частоту.
1695
1251
  *
1696
- * Получает описание запроса и продолжение цепочки. Может изменить запрос перед отправкой,
1697
- * посмотреть и подменить разобранный ответ или вовсе не вызывать `next` и вернуть своё.
1252
+ * Нужна прежде всего ботам: без неё цикл по сотне постов уходит в API одним залпом
1253
+ * и упирается в `RATE_LIMIT_EXCEEDED`.
1698
1254
  *
1699
- * @param request что уходит на сервер; изменять сам объект не нужно — передайте копию в `next`
1700
- * @param next продолжение: либо следующая обёртка, либо настоящий запрос
1701
- * @returns тело ответа в том виде, в каком его получит вызывающий код
1255
+ * Частота выдерживается равномерным разносом стартов (`1000 / rps` между запросами),
1256
+ * а не окном со счётчиком: так нагрузка ровная, без всплеска в начале каждой секунды.
1702
1257
  *
1703
- * @example Дописать заголовок ко всем запросам
1704
- * ```ts
1705
- * const transformer: Transformer = (request, next) =>
1706
- * next({ ...request, headers: { ...request.headers, 'X-Trace': trace() } });
1707
- * ```
1258
+ * @internal
1708
1259
  */
1709
- type Transformer = (request: RawRequestOptions, next: (request: RawRequestOptions) => Promise<unknown>) => Promise<unknown>;
1710
- /** Освобождение ресурсов, заведённых плагином при установке. */
1711
- type PluginTeardown = () => void | Promise<void>;
1712
- /** Что плагин получает при подключении. */
1713
- interface PluginContext {
1714
- /** Базовый URL клиента например чтобы разобрать абсолютные ссылки из ответа. */
1715
- baseUrl: string;
1716
- /** Отладочный вывод клиента, если он включён. */
1717
- logger: Logger | undefined;
1260
+ declare class RequestQueue {
1261
+ #private;
1262
+ constructor(options: ResolvedRateLimitOptions, clock?: ItdClock);
1263
+ /** Сколько задач выполняется прямо сейчас. */
1264
+ get active(): number;
1265
+ /** Сколько задач ждёт очереди. */
1266
+ get pending(): number;
1718
1267
  /**
1719
- * Непрозрачная fallback-область текущей авторизации.
1268
+ * Ставит задачу в очередь.
1720
1269
  *
1721
- * Нужна плагинам, которые обязаны безопасно изолировать непрозрачный токен. Для объединения
1722
- * состояния копий одного аккаунта используйте {@link getAuthIdentity}.
1270
+ * @returns результат задачи; ошибка задачи пробрасывается без изменений
1723
1271
  */
1724
- getAuthScope?: (() => string) | undefined;
1272
+ schedule<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T>;
1725
1273
  /**
1726
- * Загружает сессию и возвращает идентификаторы аккаунта и конкретной сессии из JWT.
1274
+ * Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
1275
+ * ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
1276
+ */
1277
+ stop(): void;
1278
+ /**
1279
+ * Придерживает всю очередь на заданное время.
1727
1280
  *
1728
- * Предпочтительнее {@link getAuthScope} для состояния, которое должно объединяться между
1729
- * несколькими экземплярами клиента одного аккаунта.
1281
+ * Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
1282
+ * а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
1730
1283
  */
1731
- getAuthIdentity?: (() => Promise<AuthIdentity>) | undefined;
1732
- /** Добавляет обёртку запроса. Подключённые раньше оказываются снаружи. */
1733
- use(transformer: Transformer): void;
1284
+ pause(ms: number): void;
1285
+ }
1286
+ /**
1287
+ * Очереди по хостам: основная и по одной на каждый сервис платформы.
1288
+ *
1289
+ * @internal
1290
+ */
1291
+ declare class RequestQueuePool {
1292
+ #private;
1293
+ constructor(options: ResolvedRateLimitOptions, clock?: ItdClock);
1294
+ /** Очередь хоста. */
1295
+ for(service: string | undefined): RequestQueue;
1296
+ /** Останавливает все очереди. */
1297
+ stop(): void;
1298
+ }
1299
+ //#endregion
1300
+ //#region src/models/users.d.ts
1301
+ /** Значок-«пин» в профиле — награда или отметка платформы. */
1302
+ interface Pin {
1303
+ /** Постоянный идентификатор, например `epepuy_202605_59`. */
1304
+ slug: string;
1305
+ /** Отображаемое название. */
1306
+ name: string;
1307
+ /** Описание, за что выдан. */
1308
+ description: string;
1309
+ /** Адрес изображения. */
1310
+ url: string;
1311
+ /** Когда выдан. Приходит только в списке своих пинов. */
1312
+ grantedAt?: IsoDate;
1313
+ }
1314
+ /**
1315
+ * Автор поста или комментария.
1316
+ *
1317
+ * Встречается внутри `post.author` и `comment.author`.
1318
+ */
1319
+ interface Author {
1320
+ id: UserId;
1321
+ username: string;
1322
+ displayName: string;
1734
1323
  /**
1735
- * Добавляет перехватчики отдельных сетевых попыток.
1324
+ * **Эмодзи, а не картинка.**
1736
1325
  *
1737
- * В отличие от {@link use}, они видят каждый retry и сырой `Response` до чтения тела.
1738
- * Несколько наборов хуков одного плагина вызываются в порядке регистрации.
1326
+ * На итд.com аватар это символ клана (`🩵`, `🦎`), а не адрес изображения.
1327
+ * Отрисовывать его нужно как текст.
1739
1328
  */
1740
- useHooks(hooks: ClientHooks): void;
1329
+ avatar: string;
1330
+ /** Пройдена ли верификация. */
1331
+ verified: boolean;
1332
+ /** Активный значок профиля. Может отсутствовать. */
1333
+ pin?: Pin | null;
1334
+ /** Есть ли премиум-подписка (значок NUKSTA). */
1335
+ hasNuksta?: boolean;
1336
+ }
1337
+ /**
1338
+ * Участник события в уведомлении.
1339
+ *
1340
+ * Отличается от {@link Author} набором полей: вместо значков приходит связь с вами.
1341
+ */
1342
+ interface Actor {
1343
+ id: UserId;
1344
+ username: string;
1345
+ displayName: string;
1346
+ /** Эмодзи-аватар, см. {@link Author.avatar}. */
1347
+ avatar: string;
1348
+ /** Подписаны ли вы на этого пользователя. */
1349
+ isFollowing?: boolean;
1350
+ /** Подписан ли он на вас. */
1351
+ isFollowedBy?: boolean;
1352
+ }
1353
+ /**
1354
+ * Пользователь в списках.
1355
+ *
1356
+ * Набор полей зависит от эндпоинта: подписчики и подписки приносят `isFollowing`,
1357
+ * поиск и рекомендации — `followersCount` и `hasNuksta`. Необязательные поля отражают
1358
+ * это различие.
1359
+ */
1360
+ interface UserSummary {
1361
+ id: UserId;
1362
+ username: string;
1363
+ displayName: string;
1364
+ /** Эмодзи-аватар, см. {@link Author.avatar}. */
1365
+ avatar: string;
1366
+ verified: boolean;
1367
+ /** Подписаны ли вы. Приходит в списках подписчиков и подписок. */
1368
+ isFollowing?: boolean;
1369
+ /** Есть ли премиум. Приходит в поиске и рекомендациях. */
1370
+ hasNuksta?: boolean;
1371
+ /** Число подписчиков. Приходит в поиске и рекомендациях. */
1372
+ followersCount?: number;
1373
+ }
1374
+ /** Поля профиля, общие для своего и чужого. */
1375
+ interface ProfileBase {
1376
+ id: UserId;
1377
+ username: string;
1378
+ displayName: string;
1379
+ /** Эмодзи-аватар, см. {@link Author.avatar}. */
1380
+ avatar: string;
1381
+ /** URL изображения баннера либо `null`. */
1382
+ banner: string | null;
1383
+ /** Описание профиля. */
1384
+ bio: string;
1385
+ verified: boolean;
1386
+ pin?: Pin | null;
1387
+ /** Кто может писать на стену. */
1388
+ wallAccess: WallAccess;
1389
+ /** Кто видит реакции. */
1390
+ likesVisibility: LikesVisibility;
1391
+ followersCount: number;
1392
+ followingCount: number;
1393
+ postsCount: number;
1394
+ createdAt: IsoDate;
1395
+ }
1396
+ /** Состояние подписки на премиум. */
1397
+ interface SubscriptionState {
1398
+ isActive: boolean;
1399
+ expiresAt: IsoDate | null;
1400
+ autoRenewal: boolean;
1401
+ }
1402
+ /**
1403
+ * Свой профиль — ответ `GET /api/users/me`.
1404
+ *
1405
+ * Отличается от чужого наличием {@link subscription} и {@link isPhoneVerified}
1406
+ * и отсутствием полей связи (`isFollowing`, `online`).
1407
+ */
1408
+ interface MyProfile extends ProfileBase {
1409
+ /** Закрыт ли профиль. */
1410
+ isPrivate: boolean;
1411
+ /** Подтверждён ли телефон. Без него часть действий недоступна. */
1412
+ isPhoneVerified: boolean;
1413
+ /** Своя премиум-подписка. */
1414
+ subscription: SubscriptionState;
1415
+ }
1416
+ /**
1417
+ * Состояние авторизации — ответ `GET /api/profile`.
1418
+ *
1419
+ * Endpoint доступен без сессии: в этом случае `authenticated` равен `false`,
1420
+ * а `user` — `null`.
1421
+ */
1422
+ interface AuthState {
1423
+ /** Есть ли действующая сессия. */
1424
+ authenticated: boolean;
1425
+ /** Заблокирован ли текущий аккаунт. */
1426
+ banned: boolean;
1427
+ /** Текущий пользователь либо `null` без действующей сессии. */
1428
+ user: MyProfile | null;
1429
+ }
1430
+ /**
1431
+ * Чужой профиль — ответ `GET /api/users/{id|username}`.
1432
+ *
1433
+ * Вместо своей подписки содержит связь с вами и присутствие.
1434
+ */
1435
+ interface PublicProfile extends ProfileBase {
1436
+ hasNuksta?: boolean;
1437
+ /** Закреплённый пост, если он есть. */
1438
+ pinnedPostId: string | null;
1439
+ /** Подписаны ли вы на него. */
1440
+ isFollowing: boolean;
1441
+ /** Подписан ли он на вас. */
1442
+ isFollowedBy: boolean;
1443
+ /** Сейчас ли пользователь в сети. */
1444
+ online: boolean;
1445
+ /** Когда был в сети. `null`, если скрыто настройками приватности. */
1446
+ lastSeen: IsoDate | null;
1447
+ }
1448
+ /** Профиль: свой либо чужой. Различаются функцией `isMyProfile()`. */
1449
+ type Profile = MyProfile | PublicProfile;
1450
+ /** Настройки приватности профиля. */
1451
+ interface PrivacySettings {
1452
+ /** Закрыт ли профиль: подписка требует одобрения. */
1453
+ isPrivate: boolean;
1454
+ wallAccess: WallAccess;
1455
+ likesVisibility: LikesVisibility;
1456
+ /** Показывать ли время последнего посещения. */
1457
+ showLastSeen: boolean;
1741
1458
  }
1742
1459
  /**
1743
- * Плагин клиента.
1744
- *
1745
- * Подключается через `itd.use(plugin)` и работает на уровне транспорта: видит запрос
1746
- * до отправки и разобранный ответ. Библиотека не знает, что именно делает плагин, —
1747
- * ей достаточно списка обёрток и имён опций, которые он читает.
1460
+ * Результат подписки на пользователя.
1748
1461
  *
1749
1462
  * @example
1750
1463
  * ```ts
1751
- * const logging: ItdPlugin = {
1752
- * name: 'logging',
1753
- * install({ use, logger }) {
1754
- * use(async (request, next) => {
1755
- * logger?.info(`${request.method} ${request.path}`);
1756
- * return next(request);
1757
- * });
1758
- * },
1759
- * };
1760
- *
1761
- * itd.use(logging);
1464
+ * const result = await itd.users.follow('nowkie');
1465
+ * // { following: true, followersCount: 11 }
1762
1466
  * ```
1763
1467
  */
1764
- interface ItdPlugin {
1765
- /** Имя плагина. Должно быть уникальным: повторное подключение ошибка. */
1766
- name: string;
1767
- /**
1768
- * Имена опций запроса, которые плагин читает у методов ресурсов.
1769
- *
1770
- * Библиотека этих опций не понимает и ничего с ними не делает — только доносит
1771
- * от вызова метода до обёртки нетронутыми. Без такого списка чужие поля отсеиваются,
1772
- * чтобы случайная опечатка в параметрах не уезжала на сервер.
1773
- *
1774
- * Имена полей самого запроса (`path`, `body`, `headers`, `signal` и прочие из
1775
- * `RawRequestOptions`) заявить нельзя: подключение такого плагина завершится ошибкой.
1776
- *
1777
- * Типы для них плагин объявляет сам, дополняя `RequestOptions`:
1778
- * ```ts
1779
- * declare module 'itd-api' {
1780
- * interface RequestOptions { encrypt?: string | undefined }
1781
- * }
1782
- * ```
1783
- */
1784
- optionKeys?: readonly string[];
1785
- /** Плагины, которые обязаны быть подключены раньше этого. */
1786
- requires?: readonly string[];
1787
- /** Несовместимые плагины. Достаточно объявить конфликт с одной стороны. */
1788
- conflicts?: readonly string[];
1789
- /** Имена плагинов, снаружи которых должна стоять эта обёртка. */
1790
- before?: readonly string[];
1791
- /** Имена плагинов, внутри которых должна стоять эта обёртка. */
1792
- after?: readonly string[];
1793
- /**
1794
- * Устанавливает плагин.
1795
- *
1796
- * Может вернуть функцию освобождения ресурсов. Она вызывается при `unuse()` или
1797
- * окончательном `dispose()` клиента и может быть асинхронной.
1798
- */
1799
- install(context: PluginContext): unknown;
1468
+ interface FollowResult {
1469
+ /** Подписка оформлена. У закрытого профиля отправляется заявка, и здесь будет `false`. */
1470
+ following: boolean;
1471
+ /** Сколько подписчиков стало у пользователя после действия. */
1472
+ followersCount?: number;
1473
+ /** Статус заявки, если профиль закрыт. */
1474
+ status?: Loose<'following' | 'requested'>;
1800
1475
  }
1801
- /**
1802
- * Список подключённых плагинов и собранная из них цепочка обёрток.
1803
- *
1804
- * Живёт в клиенте, а работает в транспорте: {@link HttpClient} прогоняет через `run`
1805
- * каждый запрос, если плагины есть.
1806
- */
1807
- declare class PluginRegistry {
1808
- #private;
1809
- /** Сколько плагинов подключено. */
1810
- get size(): number;
1811
- /** Имена опций активных плагинов. */
1812
- get optionKeys(): ReadonlySet<string>;
1813
- /** Имена плагинов в фактическом порядке выполнения. */
1814
- names(): string[];
1815
- /** Подключён ли плагин с таким именем. */
1816
- has(name: string): boolean;
1817
- /** Проверяет добавление без вызова `install()`. @internal */
1818
- assertCanAdd(plugin: ItdPlugin): void;
1819
- /** Проверяет удаление без изменения реестра. @internal */
1820
- assertCanRemove(name: string): void;
1821
- /**
1822
- * Подключает плагин.
1823
- *
1824
- * @throws {ItdConfigError} если плагин задан неверно, уже подключён, нарушает зависимости
1825
- * или заявил занятое имя опции
1826
- */
1827
- add(plugin: ItdPlugin, context: Omit<PluginContext, 'use' | 'useHooks'>): void;
1828
- /**
1829
- * Отключает плагин и вызывает его функцию очистки.
1830
- *
1831
- * Новые запросы перестают видеть плагин сразу. Если его обёртка уже выполняется,
1832
- * очистка дождётся завершения этого логического запроса.
1833
- *
1834
- * @returns `false`, если такого плагина не было
1835
- */
1836
- remove(name: string): Promise<boolean>;
1837
- /**
1838
- * Отключает все плагины окончательно.
1839
- *
1840
- * Очистка идёт изнутри наружу — в порядке, обратном выполнению обёрток.
1841
- */
1842
- dispose(): Promise<void>;
1843
- /**
1844
- * Объединяет конструкторские хуки с хуками подключаемых плагинов.
1845
- *
1846
- * Возвращённый объект динамический: подключение и отключение плагина начинает действовать
1847
- * со следующего логического запроса без пересоздания транспорта.
1848
- */
1849
- hooks(base: ClientHooks): ClientHooks;
1850
- /**
1851
- * Прогоняет запрос через цепочку обёрток.
1852
- *
1853
- * Снимок цепочки берётся в начале: `unuse()` влияет на новые запросы, но не обрывает
1854
- * уже выполняющийся посередине.
1855
- *
1856
- * @param execute настоящий запрос, вызывается самой внутренней обёрткой
1857
- */
1858
- run(request: RawRequestOptions, execute: (request: RawRequestOptions) => Promise<unknown>): Promise<unknown>;
1476
+ /** Закреплённые значки профиля и выбранный из них. */
1477
+ interface PinsResult {
1478
+ pins: Pin[];
1479
+ /** Идентификатор активного значка строка, а не объект. */
1480
+ activePin: string | null;
1859
1481
  }
1860
1482
  //#endregion
1861
- //#region src/core/rate-limit.d.ts
1483
+ //#region src/models/notifications.d.ts
1862
1484
  /**
1863
- * Очередь запросов: ограничивает одновременность и частоту.
1864
- *
1865
- * Нужна прежде всего ботам: без неё цикл по сотне постов уходит в API одним залпом
1866
- * и упирается в `RATE_LIMIT_EXCEEDED`.
1485
+ * Уведомление в единой форме.
1867
1486
  *
1868
- * Частота выдерживается равномерным разносом стартов (`1000 / rps` между запросами),
1869
- * а не окном со счётчиком: так нагрузка ровная, без всплеска в начале каждой секунды.
1487
+ * REST-список и SSE-поток отдают уведомления по-разному разные имена типов, разные имена
1488
+ * полей, один участник против массива. Библиотека приводит оба вида к этой структуре,
1489
+ * поэтому объекты из `itd.notifications.list()` и из потока можно складывать в один список.
1870
1490
  *
1871
- * @internal
1491
+ * Исходные данные не теряются: серверное имя типа остаётся в {@link rawType},
1492
+ * а весь необработанный объект — в {@link raw}.
1872
1493
  */
1873
- declare class RequestQueue {
1874
- #private;
1875
- constructor(options: ResolvedRateLimitOptions);
1876
- /** Сколько задач выполняется прямо сейчас. */
1877
- get active(): number;
1878
- /** Сколько задач ждёт очереди. */
1879
- get pending(): number;
1880
- /**
1881
- * Ставит задачу в очередь.
1882
- *
1883
- * @returns результат задачи; ошибка задачи пробрасывается без изменений
1884
- */
1885
- schedule<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T>;
1886
- /**
1887
- * Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
1888
- * ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
1889
- */
1890
- stop(): void;
1891
- /**
1892
- * Придерживает всю очередь на заданное время.
1893
- *
1894
- * Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
1895
- * а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
1896
- */
1897
- pause(ms: number): void;
1494
+ interface Notification {
1495
+ id: string;
1496
+ /** Канонический тип. Старые имена (`like`, `comment`) приведены к новым. */
1497
+ type: NotificationType;
1498
+ /** Имя типа в том виде, в каком его прислал сервер. */
1499
+ rawType: string;
1500
+ /** Объект события: пост, комментарий, пользователь. */
1501
+ entityId: string | null;
1502
+ /** Пост, которому принадлежит комментарий, если событие о комментарии. */
1503
+ parentEntityId: string | null;
1504
+ /** Прочитано ли уведомление. */
1505
+ isRead: boolean;
1506
+ /** Кто совершил действие. Для схлопнутых уведомлений — несколько человек. */
1507
+ actors: Actor[];
1508
+ /** Сколько участников схлопнуто в одно уведомление. Минимум 1. */
1509
+ count: number;
1510
+ /** Текст или заголовок объекта события. */
1511
+ preview: string | null;
1512
+ /** Ссылка перехода, предложенная сервером. Обычно точнее её `resolveNotificationUrl()`. */
1513
+ clickUrl?: string;
1514
+ createdAt: IsoDate;
1515
+ /** Когда уведомление изменилось например было прочитано. */
1516
+ updatedAt: IsoDate;
1517
+ /** Исходный объект как он пришёл от сервера. */
1518
+ raw: unknown;
1898
1519
  }
1899
1520
  /**
1900
- * Очереди по хостам: основная и по одной на каждый сервис платформы.
1521
+ * Настройки уведомлений.
1901
1522
  *
1902
- * @internal
1523
+ * Сервер отдаёт плоский объект, но исторически знает два набора имён для одних и тех же
1524
+ * настроек (`likes` и `reactions`, `comments` и `replies`). При сохранении библиотека
1525
+ * отправляет оба, при чтении принимает любой.
1903
1526
  */
1904
- declare class RequestQueuePool {
1905
- #private;
1906
- constructor(options: ResolvedRateLimitOptions);
1907
- /** Очередь хоста. */
1908
- for(service: string | undefined): RequestQueue;
1909
- /** Останавливает все очереди. */
1910
- stop(): void;
1527
+ interface NotificationSettings {
1528
+ /** Общий выключатель доставки. */
1529
+ enabled: boolean;
1530
+ /** Звук уведомления. */
1531
+ sound: boolean;
1532
+ /** Новые подписчики. */
1533
+ follows: boolean;
1534
+ /** Записи на вашей стене. */
1535
+ wallPosts: boolean;
1536
+ /** Реакции на ваши записи. */
1537
+ likes: boolean;
1538
+ /** Комментарии и ответы. */
1539
+ comments: boolean;
1540
+ /** Упоминания. */
1541
+ mentions: boolean;
1911
1542
  }
1912
1543
  //#endregion
1913
1544
  //#region src/notifications/normalize.d.ts
@@ -1952,39 +1583,13 @@ declare function normalizeNotification(input: unknown): Notification;
1952
1583
  * Кроме самого уведомления событие несёт служебные поля уровня конверта: актуальный
1953
1584
  * счётчик непрочитанных и признак звука.
1954
1585
  */
1955
- declare function readNotificationEvent(data: unknown): NotificationEvent;
1956
- /**
1957
- * Разбирает событие `unread_count` из потока.
1958
- *
1959
- * Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
1960
- */
1961
- declare function readUnreadCountEvent(data: unknown): number | undefined;
1962
- //#endregion
1963
- //#region src/realtime/reconnect.d.ts
1964
- /**
1965
- * Паузы перед попытками переподключения, мс.
1966
- *
1967
- * Значения совпадают с теми, что использует сайт итд.com, — поведение библиотеки
1968
- * не отличается от привычного пользователю.
1969
- */
1970
- declare const RECONNECT_BACKOFF: readonly number[];
1971
- /** Доля случайного разброса паузы. */
1972
- declare const RECONNECT_JITTER = 0.3;
1973
- /**
1974
- * Сколько раз пытаться переподключиться подряд.
1975
- *
1976
- * После исчерпания поток сообщает `giveup` и ждёт ручного `connect()`.
1977
- */
1978
- declare const MAX_RECONNECT_ATTEMPTS = 15;
1979
- /** Настройки переподключения. */
1980
- interface ReconnectOptions {
1981
- /** Таблица пауз. Последнее значение действует для всех дальнейших попыток. */
1982
- backoff?: readonly number[];
1983
- /** Доля разброса, 0…1. */
1984
- jitter?: number;
1985
- /** Предел числа попыток. */
1986
- maxAttempts?: number;
1987
- }
1586
+ declare function readNotificationEvent(data: unknown): NotificationEvent;
1587
+ /**
1588
+ * Разбирает событие `unread_count` из потока.
1589
+ *
1590
+ * Возвращает `undefined`, если сервер прислал событие без вложенного `payload`.
1591
+ */
1592
+ declare function readUnreadCountEvent(data: unknown): number | undefined;
1988
1593
  //#endregion
1989
1594
  //#region src/realtime/transport.d.ts
1990
1595
  /** Событие, пришедшее по каналу реального времени. */
@@ -2041,6 +1646,120 @@ declare class UnauthorizedStreamError extends Error {
2041
1646
  constructor();
2042
1647
  }
2043
1648
  //#endregion
1649
+ //#region src/realtime/updates.d.ts
1650
+ /** Типы нормализованных обновлений потока. */
1651
+ declare const RealtimeUpdateType: Readonly<{
1652
+ readonly Notification: "notification";
1653
+ readonly UnreadCount: "unreadCount";
1654
+ readonly Unknown: "unknown";
1655
+ }>;
1656
+ /** Источники нормализованных обновлений потока. */
1657
+ declare const RealtimeUpdateOrigin: Readonly<{
1658
+ readonly Stream: "stream";
1659
+ readonly Sync: "sync";
1660
+ }>;
1661
+ type RealtimeUpdateOrigin = (typeof RealtimeUpdateOrigin)[keyof typeof RealtimeUpdateOrigin];
1662
+ /** Уведомление с типом, суженным фильтром потока. */
1663
+ type NotificationOfType<T extends NotificationType> = Omit<Notification, 'type'> & {
1664
+ type: T;
1665
+ };
1666
+ /** Конверт уведомления с типом, суженным фильтром потока. */
1667
+ type NotificationEventOfType<T extends NotificationType> = Omit<NotificationEvent, 'notification'> & {
1668
+ notification: NotificationOfType<T>;
1669
+ };
1670
+ /** Нормализованное уведомление из потока. */
1671
+ interface RealtimeNotificationUpdate<T extends NotificationType = NotificationType> {
1672
+ readonly type: typeof RealtimeUpdateType.Notification;
1673
+ readonly data: NotificationEventOfType<T>;
1674
+ }
1675
+ /** Актуальное число непрочитанных уведомлений. */
1676
+ interface RealtimeUnreadCountUpdate {
1677
+ readonly type: typeof RealtimeUpdateType.UnreadCount;
1678
+ readonly data: number;
1679
+ }
1680
+ /** Неизвестное библиотеке событие потока. */
1681
+ interface RealtimeUnknownUpdate {
1682
+ readonly type: typeof RealtimeUpdateType.Unknown;
1683
+ readonly name: string;
1684
+ readonly data: unknown;
1685
+ }
1686
+ /** Данные, проходящие через промежуточные обработчики потока. */
1687
+ type RealtimeUpdate = RealtimeNotificationUpdate | RealtimeUnreadCountUpdate | RealtimeUnknownUpdate;
1688
+ /** Тип нормализованного обновления потока. */
1689
+ type RealtimeUpdateType = RealtimeUpdate['type'];
1690
+ /** Обновление потока указанного типа. */
1691
+ type RealtimeUpdateOfType<T extends RealtimeUpdateType> = Extract<RealtimeUpdate, {
1692
+ type: T;
1693
+ }>;
1694
+ /** Контекст обработки одного обновления потока. */
1695
+ interface RealtimeContext<U extends RealtimeUpdate = RealtimeUpdate> {
1696
+ /** Нормализованные данные обновления. */
1697
+ readonly update: U;
1698
+ /** Поток, который получил обновление. */
1699
+ readonly stream: ItdRealtime;
1700
+ /** Исходный кадр транспорта. Для начальной REST-синхронизации равен `undefined`. */
1701
+ readonly raw: TransportEvent | undefined;
1702
+ /** Откуда получены данные. */
1703
+ readonly origin: RealtimeUpdateOrigin;
1704
+ }
1705
+ /** Контекст уведомления с типом, суженным фильтром. */
1706
+ type RealtimeNotificationContext<T extends NotificationType = NotificationType> = RealtimeContext<RealtimeNotificationUpdate<T>>;
1707
+ /** Условия отбора уведомлений. Все указанные поля объединяются через логическое И. */
1708
+ interface RealtimeNotificationFilter<T extends NotificationType = NotificationType> {
1709
+ /** Один или несколько канонических типов уведомления. */
1710
+ type?: T | readonly T[];
1711
+ /** Идентификатор хотя бы одного участника уведомления. */
1712
+ actorId?: string;
1713
+ /** Идентификатор объекта события. */
1714
+ entityId?: string | null;
1715
+ /** Идентификатор родительского объекта. */
1716
+ parentEntityId?: string | null;
1717
+ /** Дополнительная проверка после сопоставления полей. */
1718
+ predicate?: (context: RealtimeNotificationContext<T>) => boolean;
1719
+ }
1720
+ /** Краткая или объектная форма фильтра уведомлений. */
1721
+ type RealtimeNotificationSelector<T extends NotificationType = NotificationType> = T | readonly T[] | RealtimeNotificationFilter<T>;
1722
+ //#endregion
1723
+ //#region src/realtime/middleware.d.ts
1724
+ /** Продолжает цепочку промежуточных обработчиков потока. */
1725
+ type RealtimeNext = () => Promise<void>;
1726
+ /** Обрабатывает обновление потока до его передачи подписчикам. */
1727
+ type RealtimeMiddleware<C extends RealtimeContext = RealtimeContext> = (context: C, next: RealtimeNext) => void | Promise<void>;
1728
+ /** Асинхронный обработчик нормализованного обновления потока. */
1729
+ type RealtimeHandler<C extends RealtimeContext = RealtimeContext> = (context: C) => unknown | Promise<unknown>;
1730
+ /** Условие отбора контекста потока. */
1731
+ type RealtimePredicate = (context: RealtimeContext) => boolean;
1732
+ /** Проверка, сужающая тип контекста потока. */
1733
+ type RealtimeTypeGuard<C extends RealtimeContext> = (context: RealtimeContext) => context is C;
1734
+ /** Ключи, по которым обновления нельзя обрабатывать одновременно. */
1735
+ type RealtimeSequentializer = (context: RealtimeContext) => PropertyKey | readonly PropertyKey[] | undefined;
1736
+ //#endregion
1737
+ //#region src/realtime/reconnect.d.ts
1738
+ /**
1739
+ * Паузы перед попытками переподключения, мс.
1740
+ *
1741
+ * Значения совпадают с теми, что использует сайт итд.com, — поведение библиотеки
1742
+ * не отличается от привычного пользователю.
1743
+ */
1744
+ declare const RECONNECT_BACKOFF: readonly number[];
1745
+ /** Доля случайного разброса паузы. */
1746
+ declare const RECONNECT_JITTER = 0.3;
1747
+ /**
1748
+ * Сколько раз пытаться переподключиться подряд.
1749
+ *
1750
+ * После исчерпания поток сообщает `giveup` и ждёт ручного `connect()`.
1751
+ */
1752
+ declare const MAX_RECONNECT_ATTEMPTS = 15;
1753
+ /** Настройки переподключения. */
1754
+ interface ReconnectOptions {
1755
+ /** Таблица пауз. Последнее значение действует для всех дальнейших попыток. */
1756
+ backoff?: readonly number[];
1757
+ /** Доля разброса, 0…1. */
1758
+ jitter?: number;
1759
+ /** Предел числа попыток. */
1760
+ maxAttempts?: number;
1761
+ }
1762
+ //#endregion
2044
1763
  //#region src/realtime/stream.d.ts
2045
1764
  /** События потока уведомлений. */
2046
1765
  interface RealtimeEvents {
@@ -2081,10 +1800,17 @@ interface RealtimeEvents {
2081
1800
  };
2082
1801
  /** Попытки исчерпаны — соединение восстановится только ручным `connect()`. */
2083
1802
  giveup: undefined;
2084
- /** Любое событие потока в необработанном виде, включая неизвестные библиотеке. */
2085
- message: {
2086
- name: string;
2087
- data: unknown;
1803
+ /** Любой исходный кадр транспорта. Отправляется до нормализации и промежуточных обработчиков. */
1804
+ message: TransportEvent;
1805
+ /** Промежуточный обработчик потока завершился исключением. */
1806
+ middlewareError: {
1807
+ error: unknown;
1808
+ context: RealtimeContext;
1809
+ };
1810
+ /** Обработчик `onUpdate` завершился исключением. */
1811
+ handlerError: {
1812
+ error: unknown;
1813
+ context: RealtimeContext;
2088
1814
  };
2089
1815
  }
2090
1816
  /** Способ получения событий. */
@@ -2138,11 +1864,16 @@ interface RealtimeOptions extends ReconnectOptions {
2138
1864
  reconnectOnVisible?: boolean;
2139
1865
  /** Переподключаться при восстановлении сети. По умолчанию `true`. Только в браузере. */
2140
1866
  reconnectOnOnline?: boolean;
1867
+ /** Максимальное число одновременно обрабатываемых обновлений. По умолчанию 1. */
1868
+ concurrency?: number;
1869
+ /** Возвращает ключи обновлений, которые нельзя обрабатывать одновременно. */
1870
+ sequentialize?: RealtimeSequentializer;
2141
1871
  }
2142
1872
  /** Что поток получает от клиента. */
2143
1873
  interface RealtimeDeps {
2144
1874
  baseUrl: string;
2145
1875
  fetch: typeof fetch;
1876
+ clock?: ItdClock;
2146
1877
  /** Общие заголовки клиента для адреса — см. {@link TransportContext.baseHeaders}. */
2147
1878
  baseHeaders: (url: string) => Promise<Headers>;
2148
1879
  /** Идентификаторы аккаунта и сессии создавшего поток клиента. */
@@ -2156,6 +1887,8 @@ interface RealtimeDeps {
2156
1887
  fetchUnreadCount: () => Promise<number>;
2157
1888
  /** Вызывается при явном закрытии потока. */
2158
1889
  onClose?: (() => void) | undefined;
1890
+ /** Вызывается при запуске ранее закрытого потока. */
1891
+ onConnect?: (() => void) | undefined;
2159
1892
  logger?: Logger | undefined;
2160
1893
  }
2161
1894
  /**
@@ -2166,16 +1899,19 @@ interface RealtimeDeps {
2166
1899
  *
2167
1900
  * @example
2168
1901
  * ```ts
1902
+ * import { NotificationType } from 'itd-api';
1903
+ *
2169
1904
  * const stream = itd.realtime();
2170
1905
  *
2171
- * stream.on('notification', ({ notification, unreadCount }) => {
2172
- * console.log(formatNotificationText(notification), unreadCount);
1906
+ * stream.onNotification(NotificationType.PostComment, async ({ update }) => {
1907
+ * await saveCommentNotification(update.data.notification);
2173
1908
  * });
2174
1909
  * stream.on('status', (status) => console.log('соединение:', status));
2175
1910
  *
2176
1911
  * await stream.connect();
2177
1912
  * // …позже
2178
1913
  * stream.disconnect();
1914
+ * await stream.drain();
2179
1915
  * ```
2180
1916
  */
2181
1917
  declare class ItdRealtime {
@@ -2195,6 +1931,29 @@ declare class ItdRealtime {
2195
1931
  on<K extends keyof RealtimeEvents>(event: K, listener: Listener<RealtimeEvents[K]>): Unsubscribe;
2196
1932
  /** Подписывается на одно срабатывание. */
2197
1933
  once<K extends keyof RealtimeEvents>(event: K, listener: Listener<RealtimeEvents[K]>): Unsubscribe;
1934
+ /**
1935
+ * Добавляет промежуточный обработчик нормализованных обновлений.
1936
+ *
1937
+ * Обработчики выполняются в порядке регистрации. Если `next()` не вызван, обновление не
1938
+ * передаётся дальше по цепочке, асинхронным обработчикам и слушателям событий.
1939
+ *
1940
+ * @returns функция удаления обработчика
1941
+ */
1942
+ use(middleware: RealtimeMiddleware): Unsubscribe;
1943
+ /** Подписывает асинхронный обработчик на все нормализованные обновления. */
1944
+ onUpdate(handler: RealtimeHandler): Unsubscribe;
1945
+ /** Подписывает асинхронный обработчик на обновление указанного типа. */
1946
+ onUpdate<T extends RealtimeUpdateType>(type: T, handler: RealtimeHandler<RealtimeContext<RealtimeUpdateOfType<T>>>): Unsubscribe;
1947
+ /** Подписывает асинхронный обработчик по функции сужения типа. */
1948
+ onUpdate<C extends RealtimeContext>(guard: RealtimeTypeGuard<C>, handler: RealtimeHandler<C>): Unsubscribe;
1949
+ /** Подписывает асинхронный обработчик по пользовательскому условию. */
1950
+ onUpdate(predicate: RealtimePredicate, handler: RealtimeHandler): Unsubscribe;
1951
+ /** Подписывает асинхронный обработчик на уведомления, подходящие под фильтр. */
1952
+ onNotification<T extends NotificationType>(selector: RealtimeNotificationSelector<T>, handler: RealtimeHandler<RealtimeNotificationContext<T>>): Unsubscribe;
1953
+ /** Подписывает асинхронный обработчик по функции сужения типа уведомления. */
1954
+ onNotification<C extends RealtimeNotificationContext>(guard: (context: RealtimeNotificationContext) => context is C, handler: RealtimeHandler<C>): Unsubscribe;
1955
+ /** Подписывает асинхронный обработчик по пользовательскому условию. */
1956
+ onNotification(predicate: (context: RealtimeNotificationContext) => boolean, handler: RealtimeHandler<RealtimeNotificationContext>): Unsubscribe;
2198
1957
  /**
2199
1958
  * Поднимает соединение.
2200
1959
  *
@@ -2206,10 +1965,73 @@ declare class ItdRealtime {
2206
1965
  connect(): Promise<void>;
2207
1966
  /** Закрывает соединение и отменяет запланированные попытки. */
2208
1967
  disconnect(): void;
2209
- /** Снимает все подписки. Соединение при этом не закрывается. */
1968
+ /** Ждёт завершения всех принятых обновлений. */
1969
+ drain(): Promise<void>;
1970
+ /** Снимает подписки `on()` и `once()`. Остальные обработчики остаются. */
2210
1971
  removeAllListeners(): void;
2211
1972
  }
2212
1973
  //#endregion
1974
+ //#region src/core/plugins/registry.d.ts
1975
+ /**
1976
+ * Список подключённых плагинов и собранная из них цепочка обёрток.
1977
+ *
1978
+ * Живёт в клиенте, а работает в транспорте: {@link HttpClient} прогоняет через `run`
1979
+ * каждый запрос, если плагины есть.
1980
+ */
1981
+ declare class PluginRegistry {
1982
+ #private;
1983
+ /** Сколько плагинов подключено. */
1984
+ get size(): number;
1985
+ /** Имена опций активных плагинов. */
1986
+ get optionKeys(): ReadonlySet<string>;
1987
+ /** Имена плагинов в фактическом порядке выполнения. */
1988
+ names(): string[];
1989
+ /** Подключён ли плагин с таким именем. */
1990
+ has(name: string): boolean;
1991
+ /** Проверяет добавление без вызова `install()`. @internal */
1992
+ assertCanAdd(plugin: ItdPlugin): void;
1993
+ /** Проверяет удаление без изменения реестра. @internal */
1994
+ assertCanRemove(name: string): void;
1995
+ /**
1996
+ * Подключает плагин.
1997
+ *
1998
+ * @throws {ItdConfigError} если плагин задан неверно, уже подключён, нарушает зависимости
1999
+ * или заявил занятое имя опции
2000
+ */
2001
+ add(plugin: ItdPlugin, context: Omit<PluginContext, 'use' | 'useHooks'>): void;
2002
+ /**
2003
+ * Отключает плагин и вызывает его функцию очистки.
2004
+ *
2005
+ * Новые запросы перестают видеть плагин сразу. Если его обёртка уже выполняется,
2006
+ * очистка дождётся завершения этого логического запроса.
2007
+ *
2008
+ * @returns `false`, если такого плагина не было
2009
+ */
2010
+ remove(name: string): Promise<boolean>;
2011
+ /**
2012
+ * Отключает все плагины окончательно.
2013
+ *
2014
+ * Очистка идёт изнутри наружу — в порядке, обратном выполнению обёрток.
2015
+ */
2016
+ dispose(): Promise<void>;
2017
+ /**
2018
+ * Объединяет конструкторские хуки с хуками подключаемых плагинов.
2019
+ *
2020
+ * Возвращённый объект динамический: подключение и отключение плагина начинает действовать
2021
+ * со следующего логического запроса без пересоздания транспорта.
2022
+ */
2023
+ hooks(base: ClientHooks): ClientHooks;
2024
+ /**
2025
+ * Прогоняет запрос через цепочку обёрток.
2026
+ *
2027
+ * Снимок цепочки берётся в начале: `unuse()` влияет на новые запросы, но не обрывает
2028
+ * уже выполняющийся посередине.
2029
+ *
2030
+ * @param execute настоящий запрос, вызывается самой внутренней обёрткой
2031
+ */
2032
+ run(request: RawRequestOptions, execute: (request: RawRequestOptions) => Promise<unknown>): Promise<unknown>;
2033
+ }
2034
+ //#endregion
2213
2035
  //#region src/core/http.d.ts
2214
2036
  /** Что нужно фасаду для работы. */
2215
2037
  interface HttpClientDeps {
@@ -2250,6 +2072,48 @@ declare class HttpClient {
2250
2072
  request<T = unknown>(options: PipelineRequest): Promise<T>;
2251
2073
  }
2252
2074
  //#endregion
2075
+ //#region src/models/account.d.ts
2076
+ /** Активная сессия входа. */
2077
+ interface Session {
2078
+ id: string;
2079
+ /** Та ли это сессия, из которой выполнен запрос. */
2080
+ isCurrent: boolean;
2081
+ createdAt: IsoDate;
2082
+ lastUsedAt: IsoDate;
2083
+ expiresAt: IsoDate;
2084
+ ipAddress: string;
2085
+ /** Код страны по IP, например `RU`. */
2086
+ ipCountry: string | null;
2087
+ ipCity: string | null;
2088
+ deviceType: Loose<'desktop' | 'mobile'>;
2089
+ osName: string | null;
2090
+ osVersion: string | null;
2091
+ /** Название браузера или приложения. */
2092
+ clientName: string | null;
2093
+ clientVersion: string | null;
2094
+ deviceModel: string | null;
2095
+ }
2096
+ /** Состояние платной подписки и её цена. */
2097
+ interface Subscription {
2098
+ /** Активна ли подписка сейчас. */
2099
+ active: boolean;
2100
+ /** Включено ли автопродление. */
2101
+ recurringEnabled: boolean;
2102
+ /** Цена в рублях. */
2103
+ price: number;
2104
+ }
2105
+ /** Сохранённый способ оплаты. */
2106
+ interface PaymentMethod {
2107
+ id: string;
2108
+ /** Последние четыре цифры карты. */
2109
+ last4?: string;
2110
+ /** Платёжная система: `visa`, `mastercard`, `mir`. */
2111
+ brand?: string;
2112
+ /** Основной ли это способ оплаты. */
2113
+ isDefault?: boolean;
2114
+ expiresAt?: IsoDate | null;
2115
+ }
2116
+ //#endregion
2253
2117
  //#region src/core/pagination.d.ts
2254
2118
  /**
2255
2119
  * Страница списка — единая форма для всех трёх схем пагинации API.
@@ -2609,148 +2473,62 @@ declare class AuthResource extends BaseResource {
2609
2473
  * отзывать было бы уже нечем.
2610
2474
  */
2611
2475
  logoutAll(options?: RequestOptions): Promise<void>;
2612
- /** Забывает сессию локально, не обращаясь к серверу. */
2613
- signOut(): Promise<void>;
2614
- /**
2615
- * Запрашивает письмо с кодом для сброса пароля.
2616
- *
2617
- * @returns `flowToken`, который нужно передать в {@link resetPassword}
2618
- */
2619
- forgotPassword(input: ForgotPasswordInput, options?: RequestOptions): Promise<string>;
2620
- /**
2621
- * Устанавливает новый пароль по коду из письма.
2622
- *
2623
- * Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
2624
- * при нехватке любого отвечает `422`.
2625
- */
2626
- resetPassword(input: ResetPasswordInput, options?: RequestOptions): Promise<void>;
2627
- /**
2628
- * Полный сброс пароля с кодом из письма.
2629
- *
2630
- * Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
2631
- * остальное библиотека делает сама.
2632
- *
2633
- * @example
2634
- * ```ts
2635
- * await itd.auth.resetPasswordWithOtp({
2636
- * email,
2637
- * turnstileToken,
2638
- * newPassword,
2639
- * getOtp: () => rl.question('Код из письма: '),
2640
- * });
2641
- * ```
2642
- */
2643
- resetPasswordWithOtp(input: ForgotPasswordInput & {
2644
- newPassword: string;
2645
- getOtp: () => string | Promise<string>;
2646
- }, options?: RequestOptions): Promise<void>;
2647
- /**
2648
- * Меняет пароль. Требует действующей сессии.
2649
- *
2650
- * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
2651
- *
2652
- * @example
2653
- * ```ts
2654
- * await itd.auth.changePassword({ currentPassword, newPassword });
2655
- * ```
2656
- */
2657
- changePassword(input: {
2658
- currentPassword: string;
2659
- newPassword: string;
2660
- }, options?: RequestOptions): Promise<void>;
2661
- /** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
2662
- sessions(options?: RequestOptions): Promise<Session[]>;
2663
- /** Завершает указанную сессию. */
2664
- revokeSession(sessionId: string, options?: RequestOptions): Promise<void>;
2665
- /** Завершает все сессии, кроме текущей. */
2666
- revokeOtherSessions(options?: RequestOptions): Promise<void>;
2667
- }
2668
- //#endregion
2669
- //#region src/builders/base.d.ts
2670
- /** Метка билдера. Через `Symbol.for` — чтобы распознавание переживало смешивание ESM и CJS. */
2671
- declare const BUILDER: unique symbol;
2672
- /**
2673
- * Билдер входных данных.
2674
- *
2675
- * Билдеры необязательны: любой метод, принимающий билдер, принимает и обычный объект.
2676
- * Проверки одинаковы в обоих случаях.
2677
- */
2678
- interface ItdBuilder<T> {
2679
- /** @internal */
2680
- readonly [BUILDER]: true;
2681
- /**
2682
- * Собирает и проверяет результат.
2683
- *
2684
- * @throws {ItdConfigError} если нарушены требования к данным
2685
- */
2686
- build(): T;
2687
- /** Чтобы билдер корректно вёл себя внутри `JSON.stringify`. */
2688
- toJSON(): T;
2689
- }
2690
- /**
2691
- * Три равноправные формы входа: обычный объект, готовый билдер или функция-настройщик.
2692
- *
2693
- * @example
2694
- * ```ts
2695
- * itd.posts.create({ content: 'привет' }); // объект
2696
- * itd.posts.create(post().content('привет')); // билдер
2697
- * itd.posts.create((p) => p.content('привет')); // функция
2698
- * ```
2699
- */
2700
- type BuilderInput<T, B extends ItdBuilder<T>> = T | B | ((builder: B) => B | T);
2701
- /** Является ли значение билдером. */
2702
- declare function isBuilder<T>(value: unknown): value is ItdBuilder<T>;
2703
- //#endregion
2704
- //#region src/builders/poll.d.ts
2705
- /**
2706
- * Билдер опроса.
2707
- *
2708
- * Неизменяемый: каждый вызов возвращает новый экземпляр, поэтому заготовку можно
2709
- * переиспользовать, не боясь её испортить. Создаётся функцией {@link poll}.
2710
- */
2711
- declare class PollBuilder implements ItdBuilder<CreatePollInput> {
2712
- #private;
2713
- /** @internal */
2714
- readonly [BUILDER]: true;
2715
- /** @internal Создавайте билдер функцией {@link poll}. */
2716
- constructor(state: CreatePollInput);
2717
- /** Задаёт вопрос. */
2718
- question(text: string): PollBuilder;
2719
- /** Добавляет один вариант ответа. */
2720
- option(text: string): PollBuilder;
2476
+ /** Забывает сессию локально, не обращаясь к серверу. */
2477
+ signOut(): Promise<void>;
2721
2478
  /**
2722
- * Добавляет несколько вариантов сразу.
2479
+ * Запрашивает письмо с кодом для сброса пароля.
2480
+ *
2481
+ * @returns `flowToken`, который нужно передать в {@link resetPassword}
2482
+ */
2483
+ forgotPassword(input: ForgotPasswordInput, options?: RequestOptions): Promise<string>;
2484
+ /**
2485
+ * Устанавливает новый пароль по коду из письма.
2486
+ *
2487
+ * Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
2488
+ * при нехватке любого отвечает `422`.
2489
+ */
2490
+ resetPassword(input: ResetPasswordInput, options?: RequestOptions): Promise<void>;
2491
+ /**
2492
+ * Полный сброс пароля с кодом из письма.
2493
+ *
2494
+ * Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
2495
+ * остальное библиотека делает сама.
2723
2496
  *
2724
2497
  * @example
2725
2498
  * ```ts
2726
- * poll('ну как?').options('да', 'нет', 'не знаю');
2499
+ * await itd.auth.resetPasswordWithOtp({
2500
+ * email,
2501
+ * turnstileToken,
2502
+ * newPassword,
2503
+ * getOtp: () => rl.question('Код из письма: '),
2504
+ * });
2727
2505
  * ```
2728
2506
  */
2729
- options(...texts: string[]): PollBuilder;
2730
- /** Разрешает выбор нескольких вариантов. */
2731
- multipleChoice(enabled?: boolean): PollBuilder;
2732
- build(): CreatePollInput;
2733
- toJSON(): CreatePollInput;
2507
+ resetPasswordWithOtp(input: ForgotPasswordInput & {
2508
+ newPassword: string;
2509
+ getOtp: () => string | Promise<string>;
2510
+ }, options?: RequestOptions): Promise<void>;
2511
+ /**
2512
+ * Меняет пароль. Требует действующей сессии.
2513
+ *
2514
+ * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
2515
+ *
2516
+ * @example
2517
+ * ```ts
2518
+ * await itd.auth.changePassword({ currentPassword, newPassword });
2519
+ * ```
2520
+ */
2521
+ changePassword(input: {
2522
+ currentPassword: string;
2523
+ newPassword: string;
2524
+ }, options?: RequestOptions): Promise<void>;
2525
+ /** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
2526
+ sessions(options?: RequestOptions): Promise<Session[]>;
2527
+ /** Завершает указанную сессию. */
2528
+ revokeSession(sessionId: string, options?: RequestOptions): Promise<void>;
2529
+ /** Завершает все сессии, кроме текущей. */
2530
+ revokeOtherSessions(options?: RequestOptions): Promise<void>;
2734
2531
  }
2735
- /**
2736
- * Начинает сборку опроса.
2737
- *
2738
- * @param question вопрос; можно задать позже методом {@link PollBuilder.question}
2739
- *
2740
- * @example
2741
- * ```ts
2742
- * import { poll } from 'itd-api';
2743
- *
2744
- * const q = poll('Какой язык лучше?')
2745
- * .options('TypeScript', 'JavaScript')
2746
- * .multipleChoice();
2747
- *
2748
- * await itd.posts.create({ content: 'голосуем', poll: q });
2749
- * ```
2750
- */
2751
- declare function poll(question?: string): PollBuilder;
2752
- /** Что принимает параметр опроса: объект, билдер или функция-настройщик. */
2753
- type PollInput = BuilderInput<CreatePollInput, PollBuilder>;
2754
2532
  //#endregion
2755
2533
  //#region src/types/params.d.ts
2756
2534
  /** Данные для создания опроса. */
@@ -2764,8 +2542,13 @@ interface CreatePollInput {
2764
2542
  /** Разрешить выбор нескольких вариантов. По умолчанию `false`. */
2765
2543
  multipleChoice?: boolean;
2766
2544
  }
2767
- /** Данные для создания поста. */
2768
- interface CreatePostInput {
2545
+ /**
2546
+ * Нормализованные данные для создания поста.
2547
+ *
2548
+ * Это форма, которую возвращают `PostBuilder.build()` и `resolvePost()` после
2549
+ * преобразования вложенных builders. Для входа `itd.posts.create()` см. {@link CreatePostInput}.
2550
+ */
2551
+ interface CreatePostData {
2769
2552
  /** Текст поста. */
2770
2553
  content?: string;
2771
2554
  /**
@@ -2784,8 +2567,8 @@ interface CreatePostInput {
2784
2567
  attachmentIds?: string[];
2785
2568
  /** Файлы, которые нужно загрузить перед публикацией. Порядок сохраняется. */
2786
2569
  files?: FileInput[];
2787
- /** Опрос: обычный объект, {@link PollBuilder} или функция-настройщик. */
2788
- poll?: PollInput;
2570
+ /** Готовые данные опроса. */
2571
+ poll?: CreatePollInput;
2789
2572
  }
2790
2573
  /** Поля поста, которые принимает `itd.posts.update()`. */
2791
2574
  interface UpdatePostInput {
@@ -2821,6 +2604,41 @@ interface CreateReportInput {
2821
2604
  description?: string;
2822
2605
  }
2823
2606
  //#endregion
2607
+ //#region src/builders/base.d.ts
2608
+ /** Метка билдера. Через `Symbol.for` — чтобы распознавание переживало смешивание ESM и CJS. */
2609
+ declare const BUILDER: unique symbol;
2610
+ /**
2611
+ * Билдер входных данных.
2612
+ *
2613
+ * Билдеры необязательны: любой метод, принимающий билдер, принимает и обычный объект.
2614
+ * Проверки одинаковы в обоих случаях.
2615
+ */
2616
+ interface ItdBuilder<T> {
2617
+ /** @internal */
2618
+ readonly [BUILDER]: true;
2619
+ /**
2620
+ * Собирает и проверяет результат.
2621
+ *
2622
+ * @throws {ItdConfigError} если нарушены требования к данным
2623
+ */
2624
+ build(): T;
2625
+ /** Чтобы билдер корректно вёл себя внутри `JSON.stringify`. */
2626
+ toJSON(): T;
2627
+ }
2628
+ /**
2629
+ * Три равноправные формы входа: обычный объект, готовый билдер или функция-настройщик.
2630
+ *
2631
+ * @example
2632
+ * ```ts
2633
+ * itd.posts.create({ content: 'привет' }); // объект
2634
+ * itd.posts.create(post().content('привет')); // билдер
2635
+ * itd.posts.create((p) => p.content('привет')); // функция
2636
+ * ```
2637
+ */
2638
+ type BuilderInput<T, B extends ItdBuilder<T>> = T | B | ((builder: B) => B | T);
2639
+ /** Является ли значение билдером. */
2640
+ declare function isBuilder<T>(value: unknown): value is ItdBuilder<T>;
2641
+ //#endregion
2824
2642
  //#region src/builders/comment.d.ts
2825
2643
  /** Внутреннее состояние {@link CommentBuilder}. */
2826
2644
  interface CommentState extends CreateCommentInput {
@@ -2871,21 +2689,172 @@ declare class CommentBuilder implements ItdBuilder<CreateCommentInput> {
2871
2689
  build(): CreateCommentInput;
2872
2690
  toJSON(): CreateCommentInput;
2873
2691
  }
2874
- /**
2875
- * Начинает сборку комментария.
2876
- *
2877
- * @param content текст; можно задать позже методом {@link CommentBuilder.content}
2878
- *
2879
- * @example
2880
- * ```ts
2881
- * import { comment } from 'itd-api';
2882
- *
2883
- * await itd.posts.comment(postId, comment('согласен').attach({ url: memeUrl }));
2884
- * ```
2885
- */
2886
- declare function comment(content?: string): CommentBuilder;
2887
- /** Что принимает параметр комментария: объект, билдер или функция-настройщик. */
2888
- type CommentInput = BuilderInput<CreateCommentInput, CommentBuilder>;
2692
+ /**
2693
+ * Начинает сборку комментария.
2694
+ *
2695
+ * @param content текст; можно задать позже методом {@link CommentBuilder.content}
2696
+ *
2697
+ * @example
2698
+ * ```ts
2699
+ * import { comment } from 'itd-api';
2700
+ *
2701
+ * await itd.posts.comment(postId, comment('согласен').attach({ url: memeUrl }));
2702
+ * ```
2703
+ */
2704
+ declare function comment(content?: string): CommentBuilder;
2705
+ /** Что принимает параметр комментария: объект, билдер или функция-настройщик. */
2706
+ type CommentInput = BuilderInput<CreateCommentInput, CommentBuilder>;
2707
+ //#endregion
2708
+ //#region src/models/content.d.ts
2709
+ /** Вложение поста или комментария. */
2710
+ interface Attachment {
2711
+ id: string;
2712
+ type: AttachmentType;
2713
+ /** Адрес файла на CDN. */
2714
+ url: string;
2715
+ /** Ширина изображения или видео в пикселях. */
2716
+ width?: number;
2717
+ /** Высота изображения или видео в пикселях. */
2718
+ height?: number;
2719
+ mimeType: string;
2720
+ /** Исходное имя файла. Приходит не всегда. */
2721
+ filename?: string;
2722
+ /** Размер в байтах. Приходит не всегда. */
2723
+ size?: number;
2724
+ /** Длительность аудио или видео в секундах. */
2725
+ duration?: number | null;
2726
+ /** Порядковый номер во вложениях поста. */
2727
+ order?: number;
2728
+ }
2729
+ /** Вариант ответа в опросе. */
2730
+ interface PollOption {
2731
+ id: string;
2732
+ text: string;
2733
+ /** Сколько голосов отдано за этот вариант. */
2734
+ votesCount: number;
2735
+ /** Порядковый номер варианта, начиная с нуля. */
2736
+ position: number;
2737
+ }
2738
+ /** Опрос внутри поста. */
2739
+ interface Poll {
2740
+ id: string;
2741
+ /** Пост, которому принадлежит опрос. */
2742
+ postId: string;
2743
+ question: string;
2744
+ /** Можно ли выбрать несколько вариантов. */
2745
+ multipleChoice: boolean;
2746
+ options: PollOption[];
2747
+ totalVotes: number;
2748
+ /** Голосовали ли вы. */
2749
+ hasVoted: boolean;
2750
+ /** За что проголосовали вы. Пустой массив, если голоса не было. */
2751
+ votedOptionIds: string[];
2752
+ createdAt: IsoDate;
2753
+ }
2754
+ /** Пост ленты, стены или профиля. */
2755
+ interface Post {
2756
+ id: string;
2757
+ content: string;
2758
+ /** Разметка текста. Передаётся без изменений, см. {@link Span}. */
2759
+ spans: Span[];
2760
+ author: Author;
2761
+ attachments: Attachment[];
2762
+ likesCount: number;
2763
+ commentsCount: number;
2764
+ repostsCount: number;
2765
+ viewsCount: number;
2766
+ /** Чья это стена, если пост опубликован не у себя. */
2767
+ wallRecipientId: UserId | null;
2768
+ /** Владелец стены. Приходит не во всех ответах. */
2769
+ wallRecipient?: Author | null;
2770
+ /** Поставили ли вы реакцию. */
2771
+ isLiked: boolean;
2772
+ /** Делали ли вы репост. */
2773
+ isReposted: boolean;
2774
+ /** Засчитан ли просмотр. */
2775
+ isViewed: boolean;
2776
+ /** Ваш ли это пост. */
2777
+ isOwner: boolean;
2778
+ /** Исходный пост, если это репост. */
2779
+ originalPost?: Post | null;
2780
+ poll?: Poll | null;
2781
+ /** Преобладающая реакция — эмодзи либо `null`. */
2782
+ dominantEmoji?: string | null;
2783
+ /** Когда пост отредактировали. `null`, если не редактировали. */
2784
+ editedAt: IsoDate | null;
2785
+ createdAt: IsoDate;
2786
+ /**
2787
+ * Служебная метка показа для телеметрии.
2788
+ *
2789
+ * Нужна только эндпоинтам `itd.telemetry.*`. В остальных случаях игнорируйте.
2790
+ */
2791
+ vs?: string;
2792
+ /**
2793
+ * Топовые комментарии. Приходят только в ответе `GET /api/posts/{id}`.
2794
+ *
2795
+ * В списках постов поле отсутствует.
2796
+ */
2797
+ comments?: Comment[];
2798
+ }
2799
+ /** На чей комментарий дан ответ. */
2800
+ interface CommentReplyTo {
2801
+ id: string;
2802
+ username: string;
2803
+ displayName: string;
2804
+ }
2805
+ /** Комментарий к посту или ответ на комментарий. */
2806
+ interface Comment {
2807
+ id: string;
2808
+ /** Текст. У голосового комментария пустой. */
2809
+ content: string;
2810
+ /**
2811
+ * Разметка текста, включая автоматически найденные сервером хэштеги и упоминания.
2812
+ *
2813
+ * Методы создания и редактирования комментария принимают только `content`, поэтому
2814
+ * библиотека не отправляет ручные spans в этих операциях.
2815
+ * Поле необязательно: отдельные ответы сервера могут его не содержать.
2816
+ */
2817
+ spans?: Span[];
2818
+ author: Author;
2819
+ likesCount: number;
2820
+ repliesCount: number;
2821
+ isLiked: boolean;
2822
+ createdAt: IsoDate;
2823
+ /** Вложения. У голосового — одно аудио с `mimeType: 'audio/ogg'`. */
2824
+ attachments?: Attachment[];
2825
+ /** Вложенные ответы. В списках приходит превью, полный список — через `itd.comments.replies()`. */
2826
+ replies?: Comment[];
2827
+ /** Заполнено только у ответов. */
2828
+ replyTo?: CommentReplyTo;
2829
+ }
2830
+ /** Хэштег. */
2831
+ interface Hashtag {
2832
+ id: string;
2833
+ /** Название без решётки. */
2834
+ name: string;
2835
+ /** Сколько постов с этим хэштегом. */
2836
+ postsCount: number;
2837
+ }
2838
+ /** Счётчики поста из `itd.posts.stats()`. */
2839
+ interface PostStats {
2840
+ id: string;
2841
+ likesCount: number;
2842
+ commentsCount: number;
2843
+ repostsCount: number;
2844
+ viewsCount: number;
2845
+ /** Преобладающая реакция — эмодзи либо `null`. */
2846
+ dominantEmoji: string | null;
2847
+ }
2848
+ /** Результат реакции на пост. */
2849
+ interface LikeResult {
2850
+ liked: boolean;
2851
+ likesCount: number;
2852
+ }
2853
+ /** Результат закрепления поста в профиле. */
2854
+ interface PinPostResult {
2855
+ success: boolean;
2856
+ pinnedPostId: string | null;
2857
+ }
2889
2858
  //#endregion
2890
2859
  //#region src/resources/comments.d.ts
2891
2860
  /** Параметры запроса ответов на комментарий. */
@@ -3091,6 +3060,116 @@ declare class NotificationsResource extends BaseResource {
3091
3060
  updateSettings(input: UpdateNotificationSettingsInput, options?: RequestOptions): Promise<NotificationSettings>;
3092
3061
  }
3093
3062
  //#endregion
3063
+ //#region src/models/platform.d.ts
3064
+ /** Клан в рейтинге. */
3065
+ interface Clan {
3066
+ /** Эмодзи клана — оно же аватар его участников. */
3067
+ avatar: string;
3068
+ memberCount: number;
3069
+ }
3070
+ /** Запись журнала изменений платформы. */
3071
+ interface ChangelogEntry {
3072
+ version: string;
3073
+ date: string;
3074
+ changes: string[];
3075
+ }
3076
+ /** Кнопка в анонсе платформы. */
3077
+ interface AnnouncementButton {
3078
+ title: string;
3079
+ /** Оформление: `primary`, `secondary` и другие. */
3080
+ style: string;
3081
+ action: {
3082
+ type: string;
3083
+ [key: string]: unknown;
3084
+ };
3085
+ }
3086
+ /** Анонс на главной странице платформы. */
3087
+ interface Announcement {
3088
+ id: string;
3089
+ image: {
3090
+ url: string;
3091
+ width: number;
3092
+ height: number;
3093
+ };
3094
+ title: string;
3095
+ description: string;
3096
+ /** Дополнительный текст мелким шрифтом. */
3097
+ additional_text?: string;
3098
+ buttons: AnnouncementButton[];
3099
+ }
3100
+ /** Баннер текущего события — виджет «портал». */
3101
+ interface Portal {
3102
+ active: boolean;
3103
+ title: string;
3104
+ url: string;
3105
+ }
3106
+ /** Статус заявки на верификацию. `none` означает, что заявка не подавалась. */
3107
+ interface VerificationStatus {
3108
+ status: Loose<'none' | 'pending' | 'approved' | 'rejected'>;
3109
+ }
3110
+ /** Созданная жалоба. */
3111
+ interface Report {
3112
+ id: string;
3113
+ createdAt: IsoDate;
3114
+ }
3115
+ //#endregion
3116
+ //#region src/models/status.d.ts
3117
+ /** Происшествие в истории сервиса. */
3118
+ interface StatusIncidentLine {
3119
+ /** Вид происшествия. */
3120
+ t: IncidentKind;
3121
+ /**
3122
+ * Готовая строка для показа: `недоступен 6 мин (12:00–12:06)`. Время московское.
3123
+ * Длительность и границы интервала отдельными полями не приходят.
3124
+ */
3125
+ text: string;
3126
+ }
3127
+ /** Одни сутки в истории сервиса. */
3128
+ interface StatusDay {
3129
+ /** Худшее состояние за сутки. */
3130
+ type: ServiceState;
3131
+ /** Дата суток, `YYYY-MM-DD`. Сутки нарезаны по UTC. */
3132
+ date_key: string;
3133
+ /** Доступность за сутки в процентах. */
3134
+ uptime: number;
3135
+ /** Происшествия за сутки. */
3136
+ lines: StatusIncidentLine[];
3137
+ }
3138
+ /** Сервис платформы и его история доступности. */
3139
+ interface ServiceStatus {
3140
+ /** Идентификатор: `auth`, `main`, `media` и прочие. */
3141
+ id: string;
3142
+ /** Отображаемое название. */
3143
+ name: string;
3144
+ current_status: ServiceState;
3145
+ /** Пояснение к текущему состоянию, например `No downtime`. */
3146
+ current_message: string;
3147
+ /** Задержка последней проверки в миллисекундах. */
3148
+ latency_ms: number;
3149
+ /**
3150
+ * Момент последней проверки. Сервер отдаёт `YYYY-MM-DD HH:mm:ss` в UTC, библиотека
3151
+ * приводит значение к ISO.
3152
+ */
3153
+ last_checked: IsoDate;
3154
+ /** Доступность за 90 суток в процентах. */
3155
+ uptime_90d: number;
3156
+ /**
3157
+ * История по суткам. Ключ — сколько суток назад, `'0'` — сегодня.
3158
+ *
3159
+ * Объект разреженный: сутки без данных сервер пропускает. Ровный массив даёт
3160
+ * `statusDays()`.
3161
+ */
3162
+ days: Record<string, StatusDay | undefined>;
3163
+ }
3164
+ /** Состояние платформы — ответ `itd.platform.status()`. */
3165
+ interface PlatformStatus {
3166
+ /** Худшее состояние среди сервисов. */
3167
+ overall_status: ServiceState;
3168
+ /** Когда данные последний раз пересчитаны. */
3169
+ updated_at: IsoDate;
3170
+ services: ServiceStatus[];
3171
+ }
3172
+ //#endregion
3094
3173
  //#region src/resources/platform.d.ts
3095
3174
  /** Требования к версии одного приложения платформы. */
3096
3175
  interface PlatformClientVersion {
@@ -3266,8 +3345,64 @@ declare function parseMarkdown(source: string, options?: ParseMarkupOptions): Te
3266
3345
  */
3267
3346
  declare function parseHtml(source: string, options?: ParseMarkupOptions): TextMarkup;
3268
3347
  //#endregion
3348
+ //#region src/builders/poll.d.ts
3349
+ /**
3350
+ * Билдер опроса.
3351
+ *
3352
+ * Неизменяемый: каждый вызов возвращает новый экземпляр, поэтому заготовку можно
3353
+ * переиспользовать, не боясь её испортить. Создаётся функцией {@link poll}.
3354
+ */
3355
+ declare class PollBuilder implements ItdBuilder<CreatePollInput> {
3356
+ #private;
3357
+ /** @internal */
3358
+ readonly [BUILDER]: true;
3359
+ /** @internal Создавайте билдер функцией {@link poll}. */
3360
+ constructor(state: CreatePollInput);
3361
+ /** Задаёт вопрос. */
3362
+ question(text: string): PollBuilder;
3363
+ /** Добавляет один вариант ответа. */
3364
+ option(text: string): PollBuilder;
3365
+ /**
3366
+ * Добавляет несколько вариантов сразу.
3367
+ *
3368
+ * @example
3369
+ * ```ts
3370
+ * poll('ну как?').options('да', 'нет', 'не знаю');
3371
+ * ```
3372
+ */
3373
+ options(...texts: string[]): PollBuilder;
3374
+ /** Разрешает выбор нескольких вариантов. */
3375
+ multipleChoice(enabled?: boolean): PollBuilder;
3376
+ build(): CreatePollInput;
3377
+ toJSON(): CreatePollInput;
3378
+ }
3379
+ /**
3380
+ * Начинает сборку опроса.
3381
+ *
3382
+ * @param question вопрос; можно задать позже методом {@link PollBuilder.question}
3383
+ *
3384
+ * @example
3385
+ * ```ts
3386
+ * import { poll } from 'itd-api';
3387
+ *
3388
+ * const q = poll('Какой язык лучше?')
3389
+ * .options('TypeScript', 'JavaScript')
3390
+ * .multipleChoice();
3391
+ *
3392
+ * await itd.posts.create({ content: 'голосуем', poll: q });
3393
+ * ```
3394
+ */
3395
+ declare function poll(question?: string): PollBuilder;
3396
+ /** Что принимает параметр опроса: объект, билдер или функция-настройщик. */
3397
+ type PollInput = BuilderInput<CreatePollInput, PollBuilder>;
3398
+ //#endregion
3269
3399
  //#region src/builders/post.d.ts
3270
3400
  declare const BUILD_UPDATE: unique symbol;
3401
+ /** Данные для создания поста, включая поддерживаемые builder-формы вложенного опроса. */
3402
+ interface CreatePostInput extends Omit<CreatePostData, 'poll'> {
3403
+ /** Опрос: обычный объект, {@link PollBuilder} или функция-настройщик. */
3404
+ poll?: PollInput;
3405
+ }
3271
3406
  /** Внутреннее состояние {@link PostBuilder}. */
3272
3407
  interface PostState extends CreatePostInput {
3273
3408
  content: string;
@@ -3289,7 +3424,7 @@ interface PostState extends CreatePostInput {
3289
3424
  * await itd.posts.create(onWall.content('второй')); // заготовка не испорчена
3290
3425
  * ```
3291
3426
  */
3292
- declare class PostBuilder implements ItdBuilder<CreatePostInput> {
3427
+ declare class PostBuilder implements ItdBuilder<CreatePostData> {
3293
3428
  #private;
3294
3429
  /** @internal */
3295
3430
  readonly [BUILDER]: true;
@@ -3355,10 +3490,10 @@ declare class PostBuilder implements ItdBuilder<CreatePostInput> {
3355
3490
  * ```
3356
3491
  */
3357
3492
  poll(input: PollInput): PostBuilder;
3358
- build(): CreatePostInput;
3493
+ build(): CreatePostData;
3359
3494
  /** @internal Собирает данные по правилам `posts.update`, не применяя правила создания. */
3360
3495
  [BUILD_UPDATE](): UpdatePostInput;
3361
- toJSON(): CreatePostInput;
3496
+ toJSON(): CreatePostData;
3362
3497
  }
3363
3498
  /**
3364
3499
  * Начинает сборку поста.
@@ -4189,10 +4324,12 @@ declare class ItdClient {
4189
4324
  *
4190
4325
  * @example
4191
4326
  * ```ts
4327
+ * import { NotificationType } from 'itd-api';
4328
+ *
4192
4329
  * const stream = itd.realtime();
4193
4330
  *
4194
- * stream.on('notification', ({ notification }) => {
4195
- * console.log(formatNotificationText(notification));
4331
+ * stream.onNotification(NotificationType.PostComment, async ({ update }) => {
4332
+ * await handleComment(update.data.notification);
4196
4333
  * });
4197
4334
  * stream.on('unreadCount', (count) => setBadge(count));
4198
4335
  *
@@ -4204,8 +4341,8 @@ declare class ItdClient {
4204
4341
  * Освобождает ресурсы клиента: закрывает все потоки уведомлений, отправляет открытые
4205
4342
  * накопители {@link telemetry}, затем останавливает очередь запросов.
4206
4343
  *
4207
- * После вызова клиентом можно пользоваться снова — новые запросы поднимут всё заново,
4208
- * но уже созданные потоки и успешно закрытые накопители останутся закрытыми.
4344
+ * Метод дожидается активных обработчиков потока. После вызова клиентом можно пользоваться
4345
+ * снова; ранее созданный поток можно запустить повторным `connect()`.
4209
4346
  *
4210
4347
  * Общая очередь, полученная от {@link ItdAccounts}, не останавливается: её гасит сам
4211
4348
  * контейнер, когда закрывает все аккаунты разом.
@@ -4513,6 +4650,22 @@ declare class ItdAccounts {
4513
4650
  */
4514
4651
  declare function createAccounts(options?: ItdAccountsOptions): ItdAccounts;
4515
4652
  //#endregion
4653
+ //#region src/core/attachments/factories.d.ts
4654
+ /** Создаёт URL-источник в выбранном режиме. */
4655
+ declare function fromUrl(url: string, options: UrlFileOptions & {
4656
+ mode: typeof FileTransferMode.Stream;
4657
+ }): StreamFile;
4658
+ declare function fromUrl(url: string, options?: UrlFileOptions & {
4659
+ mode?: typeof FileTransferMode.Buffer;
4660
+ }): LazyFile;
4661
+ declare function fromUrl(url: string, options: UrlFileOptions): LazyFile | StreamFile;
4662
+ /**
4663
+ * Создаёт повторяемый пользовательский поток.
4664
+ *
4665
+ * Фабрика вызывается заново для каждой попытки; возвращать один и тот же поток нельзя.
4666
+ */
4667
+ declare function fromStream(factory: (context: FileContext) => ReadableStream<Uint8Array> | FileStreamContent | Promise<ReadableStream<Uint8Array> | FileStreamContent>, options?: FromStreamOptions): StreamFile;
4668
+ //#endregion
4516
4669
  //#region src/core/errors.d.ts
4517
4670
  /** Бренд, по которому ошибки библиотеки распознаются надёжнее, чем через `instanceof`. */
4518
4671
  declare const ITD_ERROR: unique symbol;
@@ -4858,6 +5011,46 @@ type AllowedMimeType = (typeof ALLOWED_MIME_TYPES)[number];
4858
5011
  * ```
4859
5012
  */
4860
5013
  declare function utcStampToIso(value: string): string;
5014
+ /**
5015
+ * Разбирает дату API в объект `Date`.
5016
+ *
5017
+ * @returns `null`, если строки нет или она не разбирается
5018
+ *
5019
+ * @example
5020
+ * ```ts
5021
+ * const created = toDate(post.createdAt);
5022
+ * ```
5023
+ */
5024
+ declare function toDate(value: IsoDate | null | undefined): Date | null;
5025
+ //#endregion
5026
+ //#region src/models/guards.d.ts
5027
+ /**
5028
+ * Свой ли это профиль.
5029
+ *
5030
+ * @example
5031
+ * ```ts
5032
+ * if (isMyProfile(profile)) console.log(profile.subscription.isActive);
5033
+ * ```
5034
+ */
5035
+ declare function isMyProfile(profile: Profile): profile is MyProfile;
5036
+ //#endregion
5037
+ //#region src/models/status-helpers.d.ts
5038
+ /**
5039
+ * Разворачивает историю сервиса в массив на 90 суток.
5040
+ * Сутки без данных становятся `null`.
5041
+ *
5042
+ * @returns массив, где индекс — сколько суток назад: `[0]` — сегодня
5043
+ *
5044
+ * @example
5045
+ * ```ts
5046
+ * const status = await itd.platform.status();
5047
+ * const days = statusDays(status.services[0]);
5048
+ *
5049
+ * days[0]?.uptime; // доступность за сегодня
5050
+ * days.filter((day) => day === null).length; // за сколько суток данных нет
5051
+ * ```
5052
+ */
5053
+ declare function statusDays(service: ServiceStatus): (StatusDay | null)[];
4861
5054
  //#endregion
4862
5055
  //#region src/notifications/text.d.ts
4863
5056
  /**
@@ -4926,17 +5119,52 @@ declare function resolveNotificationUrl(notification: Notification): string;
4926
5119
  //#region src/realtime/poll.d.ts
4927
5120
  /** Настройки опроса. */
4928
5121
  interface PollTransportOptions {
5122
+ /** Часы опроса. Обычно подменяются только в тестах. */
5123
+ clock?: ItdClock;
4929
5124
  /** Как часто опрашивать сервер, мс. По умолчанию 15 000. */
4930
5125
  interval?: number;
4931
5126
  /** Сколько уведомлений запрашивать за раз. По умолчанию 20. */
4932
5127
  limit?: number;
4933
5128
  }
4934
5129
  //#endregion
5130
+ //#region src/realtime/router.d.ts
5131
+ /** Выбирает маршрут обновления. `undefined` и `null` означают отсутствие маршрута. */
5132
+ type RealtimeRouteSelector<K extends PropertyKey, C extends RealtimeContext = RealtimeContext> = (context: C) => K | null | undefined | Promise<K | null | undefined>;
5133
+ /**
5134
+ * Направляет обновления потока в именованные цепочки промежуточных обработчиков.
5135
+ *
5136
+ * @example
5137
+ * ```ts
5138
+ * import { RealtimeRouter, RealtimeUpdateType } from 'itd-api';
5139
+ *
5140
+ * const router = new RealtimeRouter((context) => context.update.type);
5141
+ * router.route(RealtimeUpdateType.Notification, async (context, next) => {
5142
+ * if (context.update.type === RealtimeUpdateType.Notification) {
5143
+ * await handleNotification(context.update.data.notification);
5144
+ * }
5145
+ * await next();
5146
+ * });
5147
+ * stream.use(router.middleware());
5148
+ * ```
5149
+ */
5150
+ declare class RealtimeRouter<K extends PropertyKey = PropertyKey, C extends RealtimeContext = RealtimeContext> {
5151
+ #private;
5152
+ constructor(selector: RealtimeRouteSelector<K, C>);
5153
+ /** Добавляет промежуточные обработчики к маршруту и возвращает функцию их удаления. */
5154
+ route(key: K, ...middleware: readonly RealtimeMiddleware<C>[]): Unsubscribe;
5155
+ /** Добавляет промежуточные обработчики для обновлений без зарегистрированного маршрута. */
5156
+ otherwise(...middleware: readonly RealtimeMiddleware<C>[]): Unsubscribe;
5157
+ /** Возвращает промежуточный обработчик для `stream.use()`. */
5158
+ middleware(): RealtimeMiddleware<C>;
5159
+ }
5160
+ //#endregion
4935
5161
  //#region src/realtime/sse.d.ts
4936
5162
  /** Путь потока уведомлений. */
4937
5163
  declare const STREAM_PATH = "/api/notifications/stream";
4938
5164
  /** Настройки SSE-транспорта. */
4939
5165
  interface SseTransportOptions {
5166
+ /** Часы потока. Обычно подменяются только в тестах. */
5167
+ clock?: ItdClock;
4940
5168
  /**
4941
5169
  * Сколько миллисекунд ждать данных, прежде чем считать соединение мёртвым.
4942
5170
  *
@@ -4986,5 +5214,5 @@ interface RenderSpansOptions {
4986
5214
  */
4987
5215
  declare function renderSpans(content: string, spans?: readonly Span[] | null | undefined, options?: RenderSpansOptions): string;
4988
5216
  //#endregion
4989
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, type AccountEvents, type Actor, type AddAccountOptions, type AllowedMimeType, type Announcement, type AnnouncementButton, type Attachment, AttachmentType, type AudioMimeType, type AuthEvents, type AuthIdentity, type AuthInput, type AuthResource, type AuthState, type Author, type AutoSpansOptions, BUILT_IN_SERVICES, type BuilderInput, type CaptchaCredentials, type ChangelogEntry, type Clan, type ClientHooks, type Comment, type CommentBuilder, type CommentInput, type CommentReplyTo, CommentSort, type CommentsParams, type CommentsResource, type CreateCommentInput, type CreatePollInput, type CreatePostInput, type CreateReportInput, type Credentials, type CredentialsAuth, DEFAULT_BASE_URL, DEFAULT_FILE_STREAM_BUFFER_BYTES, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_URL_FILE_MAX_BYTES, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, type DwellEntry, type ErrorContextHook, type FeedParams, FeedTab, type FileContent, type FileContext, type FileInput, type FileStreamContent, type FileStreamOptions, FileTransferMode, type FilesResource, type FollowResult, type ForgotPasswordInput, type FromStreamOptions, type Hashtag, type HashtagPostsParams, type HashtagsResource, IMAGE_MIME_TYPES, type ImageMimeType, IncidentKind, type InteractionEntry, InteractionType, type IsoDate, ItdAbortError, ItdAccounts, type ItdAccountsOptions, ItdApiError, type ItdApiErrorInit, ItdApiErrorKind, ItdAuthError, type ItdBuilder, ItdClient, type ItdClientOptions, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, type ItdFieldErrors, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, type ItdPlugin, ItdRateLimitError, ItdRealtime, ItdServerError, type ItdSession, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, type LazyFile, type LikeResult, LikesVisibility, type Listener, type Logger, type Loose, MAX_RECONNECT_ATTEMPTS, type MarkupBuilder, type MarkupContent, type MarkupInput, type MarkupSpan, MemoryMultiTokenStorage, MemoryTokenStorage, type MultiTokenStorage, type MyProfile, NOTIFICATION_TYPE_ALIASES, type Notification, type NotificationEvent, type NotificationListParams, type NotificationSettings, NotificationType, type NotificationsResource, type Page, type PageState, PaginationMode, Paginator, type PaginatorOptions, type ParseMarkupOptions, type PaymentMethod, type PhotoOpenInput, type Pin, type PinPostResult, type PinsResult, type PlatformClientVersion, type PlatformResource, type PlatformStatus, type PlatformVersions, type PluginContext, type PluginTeardown, type Poll, type PollBuilder, type PollInput, type PollOption, type PollTransportOptions, type Portal, type Post, type PostBuilder, type PostInput, type PostStats, type PostUpdateInput, type PostsResource, type PrivacySettings, type Profile, type PublicProfile, type QueryParams, type QueryValue, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, type RateLimitOptions, type RateLimitScope, type RawRequestOptions, type RealtimeDeps, type RealtimeEvents, type RealtimeOptions, RealtimeStatus, type RealtimeTransport, RealtimeTransportKind, type ReconnectOptions, type RecordStorageSource, type RemoveAccountOptions, type RenderSpansOptions, type RepliesParams, type Report, type ReportBuilder, type ReportInput, ReportReason, ReportTargetType, type ReportsResource, type RequestContext, type RequestOptions, type ResetPasswordInput, type ResponseContext, type RetryContext, type RetryOptions, RuntimeMode, STATUS_SERVICE, STREAM_PATH, type SearchResource, type SearchResult, type ServiceDefinition, ServiceRegistry, ServiceState, type ServiceStatus, type Session, type SignInResult, SignInStatus, type Span, SpanRenderFormat, SpanType, type SseTransportOptions, type StatusDay, type StatusIncidentLine, type StreamFile, type Subscription, type SubscriptionResource, type SubscriptionState, TURNSTILE_SITE_KEY, type TelemetryBatch, type TelemetryBatchOptions, type TelemetryClock, type TelemetryOptions, type TelemetryResource, type TextMarkup, type TokenStorage, type Transformer, type TransportContext, type TransportEvent, UnauthorizedStreamError, type Unsubscribe, type UpdateNotificationSettingsInput, type UpdatePostInput, type UpdatePrivacyInput, type UpdateProfileInput, type UploadOptions, type UploadedFile, type UrlFile, type UrlFileOptions, type UserId, type UserListParams, type UserPostsParams, type UserRef, type UserSummary, type UsersResource, VIDEO_MIME_TYPES, type VerificationResource, type VerificationStatus, type VideoMimeType, type VideoProgressInput, ViewReason, ViewSource, type ViewTracker, type ViewTrackerInput, type ViewTrackerOptions, WallAccess, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, parseHtml, parseMarkdown, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, toDate, utcStampToIso };
5217
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, type AccountEvents, type Actor, type AddAccountOptions, type AllowedMimeType, type Announcement, type AnnouncementButton, type Attachment, AttachmentType, type AudioMimeType, type AuthEvents, type AuthIdentity, type AuthInput, type AuthResource, type AuthState, type Author, type AutoSpansOptions, BUILT_IN_SERVICES, type BuilderInput, type CaptchaCredentials, type ChangelogEntry, type Clan, type ClientHooks, type Comment, type CommentBuilder, type CommentInput, type CommentReplyTo, CommentSort, type CommentsParams, type CommentsResource, type CreateCommentInput, type CreatePollInput, type CreatePostData, type CreatePostInput, type CreateReportInput, type Credentials, type CredentialsAuth, DEFAULT_BASE_URL, DEFAULT_FILE_STREAM_BUFFER_BYTES, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_URL_FILE_MAX_BYTES, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, type DwellEntry, type ErrorContextHook, type FeedParams, FeedTab, type FileContent, type FileContext, type FileInput, type FileStreamContent, type FileStreamOptions, FileTransferMode, type FilesResource, type FollowResult, type ForgotPasswordInput, type FromStreamOptions, type Hashtag, type HashtagPostsParams, type HashtagsResource, IMAGE_MIME_TYPES, type ImageMimeType, IncidentKind, type InteractionEntry, InteractionType, type IsoDate, ItdAbortError, ItdAccounts, type ItdAccountsOptions, ItdApiError, type ItdApiErrorInit, ItdApiErrorKind, ItdAuthError, type ItdBuilder, ItdClient, type ItdClientOptions, type ItdClock, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, type ItdFieldErrors, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, type ItdPlugin, ItdRateLimitError, ItdRealtime, ItdServerError, type ItdSession, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, type LazyFile, type LikeResult, LikesVisibility, type Listener, type Logger, type Loose, MAX_RECONNECT_ATTEMPTS, type MarkupBuilder, type MarkupContent, type MarkupInput, type MarkupSpan, MemoryMultiTokenStorage, MemoryTokenStorage, type MultiTokenStorage, type MyProfile, NOTIFICATION_TYPE_ALIASES, type Notification, type NotificationEvent, type NotificationEventOfType, type NotificationListParams, type NotificationOfType, type NotificationSettings, NotificationType, type NotificationsResource, type Page, type PageState, PaginationMode, Paginator, type PaginatorOptions, type ParseMarkupOptions, type PaymentMethod, type PhotoOpenInput, type Pin, type PinPostResult, type PinsResult, type PlatformClientVersion, type PlatformResource, type PlatformStatus, type PlatformVersions, type PluginContext, type PluginTeardown, type Poll, type PollBuilder, type PollInput, type PollOption, type PollTransportOptions, type Portal, type Post, type PostBuilder, type PostInput, type PostStats, type PostUpdateInput, type PostsResource, type PrivacySettings, type Profile, type PublicProfile, type QueryParams, type QueryValue, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, type RateLimitOptions, type RateLimitScope, type RawRequestOptions, type RealtimeContext, type RealtimeDeps, type RealtimeEvents, type RealtimeHandler, type RealtimeMiddleware, type RealtimeNext, type RealtimeNotificationContext, type RealtimeNotificationFilter, type RealtimeNotificationSelector, type RealtimeNotificationUpdate, type RealtimeOptions, type RealtimePredicate, type RealtimeRouteSelector, RealtimeRouter, type RealtimeSequentializer, RealtimeStatus, type RealtimeTransport, RealtimeTransportKind, type RealtimeTypeGuard, type RealtimeUnknownUpdate, type RealtimeUnreadCountUpdate, type RealtimeUpdate, type RealtimeUpdateOfType, RealtimeUpdateOrigin, RealtimeUpdateType, type ReconnectOptions, type RecordStorageSource, type RemoveAccountOptions, type RenderSpansOptions, type RepliesParams, type Report, type ReportBuilder, type ReportInput, ReportReason, ReportTargetType, type ReportsResource, type RequestContext, type RequestOptions, type ResetPasswordInput, type ResponseContext, type RetryContext, type RetryOptions, RuntimeMode, STATUS_SERVICE, STREAM_PATH, type SearchResource, type SearchResult, type ServiceDefinition, ServiceRegistry, ServiceState, type ServiceStatus, type Session, type SignInResult, SignInStatus, type Span, SpanRenderFormat, SpanType, type SseTransportOptions, type StatusDay, type StatusIncidentLine, type StreamFile, type Subscription, type SubscriptionResource, type SubscriptionState, TURNSTILE_SITE_KEY, type TelemetryBatch, type TelemetryBatchOptions, type TelemetryClock, type TelemetryOptions, type TelemetryResource, type TextMarkup, type TokenStorage, type Transformer, type TransportContext, type TransportEvent, UnauthorizedStreamError, type Unsubscribe, type UpdateNotificationSettingsInput, type UpdatePostInput, type UpdatePrivacyInput, type UpdateProfileInput, type UploadOptions, type UploadedFile, type UrlFile, type UrlFileOptions, type UserId, type UserListParams, type UserPostsParams, type UserRef, type UserSummary, type UsersResource, VIDEO_MIME_TYPES, type VerificationResource, type VerificationStatus, type VideoMimeType, type VideoProgressInput, ViewReason, ViewSource, type ViewTracker, type ViewTrackerInput, type ViewTrackerOptions, WallAccess, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createMultiTokenStorage, createRecordMultiStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, parseHtml, parseMarkdown, poll, post, readNotificationEvent, readUnreadCountEvent, renderSpans, report, resolveNotificationUrl, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso };
4990
5218
  //# sourceMappingURL=index.d.cts.map