openapi: 3.1.0
info:
  title: Кличат API
  version: 1.0.0-draft.1
  summary: REST API платформы сообществ «Кличат»
  description: |
    Контракт REST API по спецификации, раздел 6.4. Формы объектов, коды ошибок и заголовки лимитов
    совпадают с документированным Bot API Discord v10, чтобы библиотеки ботов работали через
    подмену базового URL (раздел 6.7). Пути `/api/v9` и `/api/v10` — псевдонимы `/api/v1`.

    Идентификаторы — snowflake, передаются строками. Даты — ISO 8601 с часовым поясом.
    Ошибки — объект `{code, message, errors}`; 429 — `{message, retry_after, global}`.

    Статус: черновик фазы 0. Ресурсы помечены тегами по фазам, в которых они реализуются.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  contact:
    name: Кличат
    url: https://klichat.ru
servers:
  - url: https://api.klichat.ru/api/v1
    description: Продакшен (после запуска)
  - url: https://staging.klichat.ru/api/v1
    description: Staging
  - url: http://localhost:8080/api/v1
    description: Локальная разработка
security:
  - bearerAuth: []
  - botAuth: []
tags:
  - name: Service
    description: Служебные маршруты (фаза 0)
  - name: Auth
    description: Регистрация по номеру телефона, вход, сессии, 2FA (фаза 1)
  - name: Users
    description: Профиль, настройки, приватность (фаза 1)
  - name: Relationships
    description: Друзья, заявки, блокировки (фаза 1)
  - name: Guilds
    description: Серверы, участники, роли, баны, аудит, эмодзи (фаза 2)
  - name: Channels
    description: Каналы, перекрытия прав, закрепы, треды (фаза 2)
  - name: Messages
    description: Сообщения, реакции, вложения, поиск (фазы 1–2)
  - name: Invites
    description: Приглашения (фаза 2)
  - name: Voice
    description: Голосовые каналы и регионы (фаза 3)
  - name: Reports
    description: Жалобы (фаза 2)
  - name: Applications
    description: Приложения разработчиков, боты и OAuth2-авторизация бота на сервер (спецификация 6.7)
  - name: Discovery
    description: "Каталог публичных серверов: витрина без входа, предпросмотр, публикация владельцем и очередь модерации"
  - name: Billing
    description: «Кличат Плюс» и бусты серверов. Оплата включается KLICHAT_BILLING_PROVIDER=yookassa; при none ручки оплаты отвечают 501, а права выдаются вручную из панели модерации.
  - name: Webhooks
    description: Вебхуки (фаза 5)
  - name: Releases
    description: "Выпуски «Что нового»: карточка над панелью пользователя, история и редактор для сотрудников"
  - name: Interactions
    description: "Слэш-команды и кнопки (ADR 0010, docs/api/interactions.md): команды приложений, список и вызов для клиента, ответ бота по токену"

paths:
  /icons/{guildId}/{hash}:
    get:
      tags: [Guilds]
      summary: Иконка сервера
      description: Публично, без авторизации; кэш бессрочный — смена иконки меняет hash. Загрузка — PATCH /guilds/{id} с полем icon (data URI).
      operationId: getGuildIcon
      security: []
      parameters:
        - { name: guildId, in: path, required: true, schema: { type: string } }
        - { name: hash, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Картинка }
        "404": { description: Нет такой иконки }
  /banners/{guildId}/{hash}:
    get:
      tags: [Guilds]
      summary: Баннер сервера
      description: Публично, кэш бессрочный. Хэш анимированного начинается с a_; расширение после точки (.png, .gif) игнорируется.
      operationId: getGuildBanner
      security: []
      parameters:
        - { name: guildId, in: path, required: true, schema: { type: string } }
        - { name: hash, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Картинка }
        "404": { description: Нет такого баннера }
  /oauth2/token:
    post:
      tags: [Applications]
      summary: Обменять код на токен или обновить токен
      description: >-
        OAuth2 authorization code с PKCE (RFC 7636). Тело — application/x-www-form-urlencoded.
        Публичные клиенты подтверждают себя code_verifier, серверные — client_secret
        (в теле или через Basic). Токен выдаётся с префиксом klo_ и принимается только на GET /users/@me.
      operationId: oauth2Token
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id]
              properties:
                grant_type: { type: string, enum: [authorization_code, refresh_token] }
                client_id: { type: string }
                client_secret: { type: string }
                code: { type: string }
                code_verifier: { type: string }
                redirect_uri: { type: string }
                refresh_token: { type: string }
      responses:
        "200":
          description: Токены
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token: { type: string }
                  token_type: { type: string, example: Bearer }
                  expires_in: { type: integer }
                  refresh_token: { type: string }
                  scope: { type: string }
        "400": { description: "invalid_grant или unsupported_grant_type" }
        "401": { description: invalid_client }
  /oauth2/token/revoke:
    post:
      tags: [Applications]
      summary: Отозвать токен
      description: RFC 7009 — всегда отвечает успехом. Подходит и access, и refresh.
      operationId: oauth2Revoke
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200": { description: Отозван }
  /applications/{appId}/secret/reset:
    post:
      tags: [Applications]
      summary: Новый client_secret
      description: Показывается один раз; прежний перестаёт работать.
      operationId: resetClientSecret
      parameters:
        - { name: appId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Секрет
          content:
            application/json:
              schema:
                type: object
                properties:
                  client_secret: { type: string }
        "403": { description: Только владелец приложения }
  /gifs/trending:
    get:
      tags: [Gifs]
      summary: Популярные гифки
      description: Прокси к Klipy. Ключ приложения живёт на сервере; вместо идентификатора пользователя наружу уходит его отпечаток.
      operationId: getTrendingGifs
      parameters:
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 24 } }
      responses:
        "200":
          description: Страница выдачи
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GifPage" }
        "502": { description: Сервис гифок недоступен }
  /gifs/search:
    get:
      tags: [Gifs]
      summary: Поиск гифок
      operationId: searchGifs
      parameters:
        - { name: q, in: query, required: true, schema: { type: string } }
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 24 } }
      responses:
        "200":
          description: Страница выдачи
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GifPage" }
        "502": { description: Сервис гифок недоступен }
  /gifs/categories:
    get:
      tags: [Gifs]
      summary: Плитки категорий на первом экране вкладки
      operationId: getGifCategories
      responses:
        "200":
          description: Категории
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        query: { type: string }
                        preview: { type: string, format: uri }
        "502": { description: Сервис гифок недоступен }
  /discovery/categories:
    get:
      tags: [Discovery]
      summary: Категории каталога и условия публикации
      operationId: getDiscoveryCategories
      security: []
      responses:
        "200":
          description: Категории и пороги публикации
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                  limits:
                    type: object
                    additionalProperties: { type: integer }
  /discovery/guilds:
    get:
      tags: [Discovery]
      summary: Каталог публичных серверов
      description: Без авторизации. Поиск по названию, описанию и тегам, фильтр по категории; сортировка по онлайну.
      operationId: listDiscovery
      security: []
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: category, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 48, default: 24 } }
        - { name: offset, in: query, schema: { type: integer, minimum: 0 } }
      responses:
        "200":
          description: Карточки серверов
          content:
            application/json:
              schema:
                type: object
                properties:
                  guilds: { type: array, items: { $ref: "#/components/schemas/DiscoveryCard" } }
  /discovery/guilds/{slug}:
    get:
      tags: [Discovery]
      summary: Карточка сервера по адресу каталога
      operationId: getDiscoveryBySlug
      security: []
      parameters:
        - { name: slug, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Предпросмотр сервера
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GuildPreview" }
        "404": { description: Сервер не опубликован }
  /discovery/guilds/{guildId}/join:
    post:
      tags: [Discovery]
      summary: Вступить в сервер из каталога
      description: Без приглашения; работает только для серверов в каталоге. Проверки те же, что при вступлении по приглашению.
      operationId: joinDiscovery
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Сервер, как в GUILD_CREATE
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Guild" }
        "403": { description: Пользователь забанен на сервере }
        "404": { description: Сервер не опубликован в каталоге }
  /guilds/{guildId}/preview:
    get:
      tags: [Discovery]
      summary: Предпросмотр сервера без вступления
      description: Доступно без входа для серверов из каталога.
      operationId: getGuildPreview
      security: []
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Карточка сервера
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GuildPreview" }
        "404": { description: Сервер не опубликован }
  /guilds/{guildId}/discovery:
    get:
      tags: [Discovery]
      summary: Карточка каталога глазами владельца
      operationId: getDiscoverySettings
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Состояние публикации и невыполненные условия
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiscoverySettings" }
        "403": { description: Только владелец сервера }
    put:
      tags: [Discovery]
      summary: Опубликовать сервер или обновить карточку
      operationId: publishDiscovery
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category, description]
              properties:
                category: { type: string }
                tags: { type: array, items: { type: string }, maxItems: 3 }
                description: { type: string, minLength: 40, maxLength: 300 }
      responses:
        "200":
          description: Карточка опубликована
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiscoverySettings" }
        "400": { description: "Условия публикации не выполнены (поля ответа — что поправить)" }
        "409": { description: Больше трёх публичных серверов на владельца }
    delete:
      tags: [Discovery]
      summary: Убрать сервер из каталога
      operationId: unpublishDiscovery
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "204": { description: Убран }
  /moderation/stats:
    get:
      tags: [Moderation]
      summary: Статистика для владельца
      description: Люди, онлайн и пик за день, активные, сообщения, голос, серверы, деньги, воронка регистрации, ряды за 30 дней по Москве. Боты (флаг 1<<20) ни в одну цифру не входят, их число — users.bots. Только сотрудники платформы.
      operationId: moderationStats
      responses:
        "200":
          description: Срез на сейчас и ряды
          content:
            application/json:
              schema:
                type: object
                required: [at, users, online, active, messages, voice, guilds, money, funnel, push_users, series]
                properties:
                  at: { type: string, format: date-time }
                  users: { type: object, properties: { total: { type: integer }, new_day: { type: integer }, new_week: { type: integer }, new_month: { type: integer }, with_email: { type: integer }, deleted: { type: integer }, bots: { type: integer, description: "Учётные записи ботов" } } }
                  online: { type: object, properties: { now: { type: integer }, peak_day: { type: integer } } }
                  active: { type: object, properties: { day: { type: integer }, week: { type: integer }, month: { type: integer } } }
                  messages: { type: object, properties: { day: { type: integer }, week: { type: integer }, dm_day: { type: integer } } }
                  voice: { type: object, properties: { now: { type: integer }, streaming_now: { type: integer }, minutes_day: { type: integer }, stream_minutes_day: { type: integer }, call_minutes_day: { type: integer } } }
                  guilds: { type: object, properties: { total: { type: integer }, new_week: { type: integer }, active_week: { type: integer }, listed: { type: integer }, top: { type: array, items: { type: object, properties: { id: { $ref: "#/components/schemas/Snowflake" }, name: { type: string }, messages: { type: integer }, members: { type: integer } } } } } }
                  money: { type: object, properties: { plus_active: { type: integer }, boosts: { type: integer }, paid_month_rub: { type: integer }, referrals_qualified: { type: integer }, referrals_week: { type: integer }, sms_balance: { type: [number, "null"] } } }
                  funnel: { type: object, properties: { started: { type: integer }, confirmed: { type: integer }, recover_started: { type: integer } } }
                  push_users: { type: integer }
                  series: { type: object, properties: { days: { type: array, items: { type: string } }, new_users: { type: array, items: { type: integer } }, messages: { type: array, items: { type: integer } }, active: { type: array, items: { type: integer } }, peak_online: { type: array, items: { type: integer } } } }
        "403": { $ref: "#/components/responses/Forbidden" }

  /moderation/stats/people:
    get:
      tags: [Moderation]
      summary: Люди и серверы
      description: >-
        Кто в каких серверах, сколько там пишет и чем заходит — по именам; друзья, переписка в личке и
        звонки — обезличенной сетью (узлы без id, номера перемешаны на каждый запрос). Окно — days
        последних дней по Москве. Только сотрудники платформы; каждое открытие пишется в журнал.
      operationId: moderationStatsPeople
      parameters:
        - { name: days, in: query, schema: { type: integer, minimum: 7, maximum: 730, default: 30 } }
      responses:
        "200":
          description: Серверы, люди и обезличенные связи
          content:
            application/json:
              schema:
                type: object
                required: [at, range, servers, people, links, invites]
                properties:
                  at: { type: string, format: date-time }
                  range: { type: object, properties: { from: { type: string }, to: { type: string }, days: { type: integer } } }
                  servers:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { $ref: "#/components/schemas/Snowflake" }
                        name: { type: string }
                        icon: { type: [string, "null"] }
                        owner_id: { $ref: "#/components/schemas/Snowflake" }
                        created_at: { type: string, format: date-time }
                        listed: { type: boolean }
                        members: { type: integer }
                        messages: { type: integer }
                        writers: { type: integer }
                  people:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { $ref: "#/components/schemas/Snowflake" }
                        username: { type: string }
                        name: { type: string }
                        avatar: { type: [string, "null"] }
                        created_at: { type: string, format: date-time }
                        last_active: { type: [string, "null"] }
                        servers: { type: array, items: { $ref: "#/components/schemas/Snowflake" } }
                        messages: { type: integer }
                        clients: { type: array, items: { type: object, properties: { kind: { type: string, enum: [app, browser, phone] }, os: { type: string } } } }
                        invited: { type: integer, description: "Сколько людей пришло по его ссылке за всё время" }
                        invited_active: { type: integer, description: "Из них написали сообщение или побыли минуту в голосе" }
                  invites:
                    type: object
                    description: Приглашения по реферальной ссылке; кто кого привёл, не отдаётся
                    properties:
                      total: { type: integer }
                      active: { type: integer }
                      period: { type: integer, description: "Пришли по ссылке за окно" }
                      signups: { type: integer, description: "Все регистрации за окно" }
                      inviters: { type: integer, description: "Сколько людей привели хоть кого-то" }
                  links:
                    type: object
                    properties:
                      nodes: { type: integer }
                      friends: { type: array, items: { type: array, items: { type: integer } } }
                      dms: { type: array, items: { type: array, items: { type: integer } } }
                      calls: { type: array, items: { type: array, items: { type: integer } } }
                      groups: { type: object, properties: { chats: { type: integer }, people: { type: integer } } }
                      call_totals: { type: object, properties: { calls: { type: integer }, answered: { type: integer }, minutes: { type: integer } } }
        "403": { $ref: "#/components/responses/Forbidden" }

  /moderation/discovery:
    get:
      tags: [Discovery]
      summary: Очередь каталога (сотрудник)
      operationId: staffDiscovery
      parameters:
        - { name: state, in: query, schema: { type: string, enum: [listed, auto_hidden, removed] } }
        - { name: limit, in: query, schema: { type: integer, maximum: 100 } }
      responses:
        "200":
          description: Карточки с состоянием и числом жалоб
          content:
            application/json:
              schema:
                type: object
                properties:
                  guilds: { type: array, items: { $ref: "#/components/schemas/DiscoveryCard" } }
        "403": { description: Только для сотрудников платформы }
  /moderation/discovery/{guildId}:
    patch:
      tags: [Discovery]
      summary: Скрыть, вернуть или снять сервер (сотрудник)
      operationId: setDiscoveryState
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [state]
              properties:
                state: { type: string, enum: [listed, auto_hidden, removed] }
                reason: { type: string, maxLength: 200 }
      responses:
        "204": { description: Состояние изменено }
  /releases:
    get:
      tags: [Releases]
      summary: История выпусков «Что нового»
      description: Опубликованные выпуски, свежие сверху, до 50. Только людям, ботам недоступно.
      operationId: listReleases
      responses:
        "200":
          description: Выпуски
          content:
            application/json:
              schema:
                type: object
                properties:
                  releases: { type: array, items: { $ref: "#/components/schemas/Release" } }
        "401": { description: Нужен вход }
  /releases/pending:
    get:
      tags: [Releases]
      summary: Выпуск для карточки
      description: >-
        Последний выпуск с карточкой, если человек его ещё не открыл и не закрыл и зарегистрировался
        до его выхода. Иначе release — null.
      operationId: pendingRelease
      responses:
        "200":
          description: Выпуск или null
          content:
            application/json:
              schema:
                type: object
                properties:
                  release:
                    oneOf:
                      - $ref: "#/components/schemas/Release"
                      - type: "null"
        "401": { description: Нужен вход }
  /announcements/pending:
    get:
      tags: [Releases]
      summary: Независимые объявления для текущей учётной записи
      description: До 20 опубликованных плашек, свежие сверху. Учитывает аудиторию и отметки просмотра. В историю новостей не входят.
      operationId: pendingAnnouncements
      responses:
        "200":
          description: Объявления
          content:
            application/json:
              schema:
                type: object
                properties:
                  releases: { type: array, items: { $ref: "#/components/schemas/Release" } }
        "401": { description: Нужен вход }
  /releases/{releaseId}/seen:
    post:
      tags: [Releases]
      summary: Отметить выпуск просмотренным
      description: Закрыл карточку (opened=false) или открыл выпуск (opened=true). Карточка больше не показывается ни на одном устройстве.
      operationId: markReleaseSeen
      parameters:
        - { name: releaseId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                opened: { type: boolean }
      responses:
        "204": { description: Отмечено }
        "404": { description: Выпуск не найден или снят }
  /release-images/{name}:
    get:
      tags: [Releases]
      summary: Картинка пункта выпуска
      description: Без входа, кэш бессрочный — имя картинки это хэш содержимого.
      operationId: getReleaseImage
      security: []
      parameters:
        - { name: name, in: path, required: true, schema: { type: string, pattern: "^[0-9a-f]{32}\\.(png|jpg|webp|gif)$" } }
      responses:
        "200":
          description: Картинка
          content:
            image/webp: {}
            image/png: {}
            image/jpeg: {}
            image/gif: {}
        "404": { description: Нет такой картинки }
  /moderation/releases:
    get:
      tags: [Releases]
      summary: Все выпуски со статистикой (сотрудник)
      operationId: staffReleases
      responses:
        "200":
          description: Черновики и выпуски, свежие сверху
          content:
            application/json:
              schema:
                type: object
                properties:
                  releases: { type: array, items: { $ref: "#/components/schemas/StaffRelease" } }
        "403": { description: Только для сотрудников платформы }
    post:
      tags: [Releases]
      summary: Создать черновик (сотрудник)
      operationId: createRelease
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReleaseInput" }
      responses:
        "201":
          description: Черновик
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StaffRelease" }
        "400": { description: "Ошибки полей: title, items, items.N" }
        "403": { description: Только для сотрудников платформы }
  /moderation/releases/images:
    post:
      tags: [Releases]
      summary: Загрузить картинку пункта (сотрудник)
      description: PNG, JPEG, WebP или GIF до 700 КБ и 2400×2400. Ответ — имя для поля image пункта.
      operationId: uploadReleaseImage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image]
              properties:
                image: { type: string, description: data URI картинки }
      responses:
        "201":
          description: Картинка сохранена
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string, example: 0123456789abcdef0123456789abcdef.webp }
        "400": { description: Не картинка или слишком большая }
  /moderation/releases/{releaseId}:
    patch:
      tags: [Releases]
      summary: Изменить выпуск (сотрудник)
      description: У опубликованного правка сразу видна всем; пост в #обновления правится тоже.
      operationId: updateRelease
      parameters:
        - { name: releaseId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReleaseInput" }
      responses:
        "200":
          description: Выпуск
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StaffRelease" }
        "400": { description: Ошибки полей }
        "404": { description: Выпуск не найден }
    delete:
      tags: [Releases]
      summary: Удалить черновик или снятый выпуск (сотрудник)
      operationId: deleteRelease
      parameters:
        - { name: releaseId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "204": { description: Удалён }
        "404": { description: Выпуск не найден }
        "409": { description: Выпуск показывается — сначала снять с публикации }
  /moderation/releases/{releaseId}/publish:
    post:
      tags: [Releases]
      summary: Опубликовать (сотрудник)
      description: >-
        Выпуск попадает в историю; card — показывать карточкой, post_official — выложить тот же
        текст в #обновления официального сервера. Если пост не вышел, выпуск всё равно опубликован,
        а причина — в official_error.
      operationId: publishRelease
      parameters:
        - { name: releaseId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                card: { type: boolean, default: true }
                post_official: { type: boolean, default: false }
      responses:
        "200":
          description: Выпуск
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StaffRelease" }
        "404": { description: Выпуск не найден }
  /moderation/releases/{releaseId}/unpublish:
    post:
      tags: [Releases]
      summary: Снять с публикации (сотрудник)
      description: Карточка и строка в истории пропадают, пост в #обновления удаляется.
      operationId: unpublishRelease
      parameters:
        - { name: releaseId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Выпуск
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StaffRelease" }
        "404": { description: Выпуск не найден }
  /billing/plans:
    get:
      tags: [Billing]
      summary: Витрина тарифов и лимитов
      operationId: getBillingPlans
      responses:
        "200":
          description: Тарифы, лимиты уровней сервера и права «Плюса»
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled: { type: boolean, description: Подключена ли касса }
                  currency: { type: string, example: RUB }
                  plans:
                    type: array
                    items:
                      type: object
                      properties:
                        ID: { type: string, example: plus_1m }
                        Kind: { type: string, enum: [plus, boost] }
                        Name: { type: string }
                        PriceRub: { type: integer }
                        Months: { type: integer }
                  boosts: { type: array, items: { type: integer }, description: "Бустов для уровней 1, 2 и 3" }
                  tiers: { type: array, items: { $ref: "#/components/schemas/GuildLimits" } }
                  plus: { $ref: "#/components/schemas/UserLimits" }
                  free: { $ref: "#/components/schemas/UserLimits" }
  /billing/status:
    get:
      tags: [Billing]
      summary: Своя подписка, бусты и платежи
      operationId: getBillingStatus
      responses:
        "200":
          description: Состояние подписки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillingStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /billing/checkout:
    post:
      tags: [Billing]
      summary: Оплатить тариф
      description: Создаёт платёж в кассе и возвращает ссылку подтверждения. При выключенной оплате — 501.
      operationId: billingCheckout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan]
              properties:
                plan: { type: string, example: plus_1m }
                phone: { type: string, description: Для чека по 54-ФЗ }
      responses:
        "200":
          description: Ссылка оплаты
          content:
            application/json:
              schema:
                type: object
                properties:
                  confirmation_url: { type: string, format: uri }
        "400": { $ref: "#/components/responses/ValidationError" }
        "501": { description: Оплата не подключена }
  /billing/notifications:
    post:
      tags: [Billing]
      summary: Уведомление кассы
      description: Вебхук ЮKassa, без авторизации. Статус платежа перечитывается из API кассы, повторные уведомления безопасны.
      operationId: billingNotification
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                event: { type: string, example: payment.succeeded }
                object:
                  type: object
                  properties:
                    id: { type: string }
      responses:
        "200": { description: Принято }
  /guilds/{guildId}/premium:
    get:
      tags: [Billing]
      summary: Бусты и уровень сервера
      operationId: getGuildPremium
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Уровень, число бустов и те, кто их поставил
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GuildPremium" }
        "403": { description: Нужно быть участником сервера }
  /guilds/{guildId}/boosts:
    post:
      tags: [Billing]
      summary: Поставить свободный буст серверу
      operationId: boostGuild
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "200":
          description: Применённый слот
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Boost" }
        "409": { description: Нет свободного буста }
  /guilds/{guildId}/boosts/{boostId}:
    delete:
      tags: [Billing]
      summary: Снять свой буст с сервера
      operationId: unboostGuild
      parameters:
        - { name: guildId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
        - { name: boostId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      responses:
        "204": { description: Слот освобождён и остаётся у пользователя до конца срока }
        "403": { description: Чужой буст }
  /moderation/billing/users/{userId}/plus:
    post:
      tags: [Billing]
      summary: Выдать «Плюс» вручную (сотрудник)
      operationId: grantPlus
      parameters:
        - { name: userId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                months: { type: integer, minimum: 1, maximum: 24 }
      responses:
        "200":
          description: Новая дата окончания
          content:
            application/json:
              schema:
                type: object
                properties:
                  premium_until: { type: string, format: date-time }
        "403": { description: Только для сотрудников платформы }
  /moderation/billing/users/{userId}/boosts:
    post:
      tags: [Billing]
      summary: Выдать бусты вручную (сотрудник)
      operationId: grantBoosts
      parameters:
        - { name: userId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                count: { type: integer, minimum: 1, maximum: 14 }
                months: { type: integer, minimum: 1, maximum: 24 }
      responses:
        "200":
          description: Сколько слотов выдано
          content:
            application/json:
              schema:
                type: object
                properties:
                  granted: { type: integer }
        "403": { description: Только для сотрудников платформы }
  /version:
    get:
      tags: [Service]
      summary: Версия сервера
      operationId: getVersion
      security: []
      responses:
        "200":
          description: Версия сборки и версия API
          content:
            application/json:
              schema:
                type: object
                required: [version, api, env]
                properties:
                  version: { type: string, examples: ["0.1.0"] }
                  api: { type: integer, const: 1 }
                  env: { type: string, enum: [dev, staging, prod] }

  /gateway:
    get:
      tags: [Service]
      summary: Адрес WebSocket-шлюза
      operationId: getGateway
      security: []
      responses:
        "200":
          description: URL шлюза для клиента
          content:
            application/json:
              schema:
                type: object
                required: [url]
                properties:
                  url: { type: string, format: uri, examples: ["wss://gateway.klichat.ru"] }

  /gateway/bot:
    get:
      tags: [Service]
      summary: Адрес шлюза и число шардов для бота
      operationId: getGatewayBot
      security:
        - botAuth: []
      responses:
        "200":
          description: Параметры подключения бота
          content:
            application/json:
              schema:
                type: object
                required: [url, shards, session_start_limit]
                properties:
                  url: { type: string, format: uri }
                  shards: { type: integer, minimum: 1 }
                  session_start_limit:
                    type: object
                    required: [total, remaining, reset_after, max_concurrency]
                    properties:
                      total: { type: integer }
                      remaining: { type: integer }
                      reset_after: { type: integer, description: Миллисекунды }
                      max_concurrency: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ---------------------------------------------------------------- Auth
  /auth/methods:
    get:
      tags: [Auth]
      summary: Какие способы регистрации доступны с этого адреса
      description: По почте — только не из России (страна по реестру RIPE), сервер решает сам по адресу клиента. Ответ не кэшируется.
      operationId: authMethods
      security: []
      responses:
        "200":
          description: Способы
          content:
            application/json:
              schema:
                type: object
                required: [phone, email, country]
                properties:
                  phone: { type: boolean }
                  email: { type: boolean }
                  country: { type: string, description: RU или пусто }

  /auth/register/email:
    post:
      tags: [Auth]
      summary: Начать регистрацию по почте
      description: Код письмом вместо SMS; доступно только с нероссийских адресов (403 иначе). Капча — по тому же правилу, что и для номера. Дальше — тот же POST /auth/register с verification_id и кодом; у такой учётной записи нет номера, почта подтверждена сразу и служит для входа.
      operationId: registerEmailStart
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                captcha_token: { type: string }
      responses:
        "200":
          description: Код отправлен
          content:
            application/json:
              schema:
                type: object
                properties:
                  verification_id: { type: string }
                  expires_in: { type: integer }
                  email: { type: string, description: Адрес маской }
                  channel: { type: string, enum: [email] }
                  code_length: { type: integer }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: С российского адреса
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503":
          description: Письмо не отправить
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/register/phone:
    post:
      tags: [Auth]
      summary: Отправить код подтверждения на номер
      description: Принимаются только номера российских операторов (+7). Лимит — 3 SMS на номер в час.
      operationId: registerPhone
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone: { type: string, pattern: "^\\+7\\d{10}$", examples: ["+79161234567"] }
                captcha_token: { type: string, description: "Токен SmartCaptcha, обязателен после первой попытки" }
      responses:
        "200":
          description: Код отправлен
          content:
            application/json:
              schema:
                type: object
                required: [verification_id, expires_in, phone, channel]
                properties:
                  verification_id: { $ref: "#/components/schemas/Snowflake" }
                  expires_in: { type: integer, description: Секунды, examples: [600] }
                  phone: { type: string, description: Замаскированный номер, examples: ["+7916***4567"] }
                  channel: { type: string, enum: [sms, telegram, console], description: Как доставлен код }
        "400": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /auth/register/verify:
    post:
      tags: [Auth]
      summary: Подтвердить код и создать аккаунт
      operationId: registerVerify
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [verification_id, code, username, password, date_of_birth, consent]
              properties:
                verification_id: { $ref: "#/components/schemas/Snowflake" }
                code: { type: string, pattern: "^\\d{6}$" }
                username: { type: string, minLength: 2, maxLength: 32, pattern: "^[a-z0-9_.]+$" }
                display_name: { type: string, maxLength: 32 }
                password: { type: string, minLength: 8, maxLength: 128, format: password }
                date_of_birth: { type: string, format: date, description: "ГГГГ-ММ-ДД, порог 14+" }
                consent: { type: boolean, const: true, description: Согласие на обработку ПДн и принятие условий }
                email: { type: string, format: email, description: Необязательно; после регистрации придёт письмо со ссылкой для подтверждения }
      responses:
        "201":
          description: Аккаунт создан, выданы токены
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenPair" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: Код неверен или истёк (70003)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/desktop:
    post:
      tags: [Auth]
      summary: Начать вход приложения через браузер
      operationId: startDesktopLogin
      security: []
      description: Запрос живёт 5 минут. id можно передать в браузер в hash URL. secret хранится только в приложении. В браузере пользователь подтверждает вход одной кнопкой.
      responses:
        "201":
          description: Одноразовый запрос
          content:
            application/json:
              schema:
                type: object
                required: [id, secret, expires_in, interval]
                properties:
                  id: { type: string, pattern: "^[a-f0-9]{64}$" }
                  secret: { type: string, pattern: "^[a-f0-9]{64}$", description: Секрет получения сессии; не передавать в URL }
                  expires_in: { type: integer, examples: [300] }
                  interval: { type: integer, examples: [3] }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { description: Вход через браузер недоступен }
  /auth/desktop/{requestID}:
    parameters:
      - { name: requestID, in: path, required: true, schema: { type: string } }
    get:
      tags: [Auth]
      summary: Показать запрос входа в браузере
      operationId: getDesktopLogin
      responses:
        "200":
          description: Устройство, IP и оставшееся время; без секретов
          content:
            application/json:
              schema:
                type: object
                properties:
                  device: { type: string }
                  ip: { type: string }
                  status: { type: string, enum: [pending, approved, denied] }
                  expires_in: { type: integer }
        "401": { description: Требуется вход пользователя }
        "410": { description: Запрос истёк или уже использован }
  /auth/desktop/{requestID}/approve:
    parameters:
      - { name: requestID, in: path, required: true, schema: { type: string } }
    post:
      tags: [Auth]
      summary: Подтвердить вход приложения
      operationId: approveDesktopLogin
      responses:
        "204": { description: Подтверждено; приложение получает собственную сессию при poll }
        "401": { description: Требуется вход пользователя }
        "410": { description: Запрос истёк или уже обработан }
  /auth/desktop/{requestID}/deny:
    parameters:
      - { name: requestID, in: path, required: true, schema: { type: string } }
    post:
      tags: [Auth]
      summary: Отклонить вход приложения
      operationId: denyDesktopLogin
      responses:
        "204": { description: Отклонено }
        "401": { description: Требуется вход пользователя }
        "410": { description: Запрос истёк или уже обработан }
  /auth/desktop/{requestID}/poll:
    parameters:
      - { name: requestID, in: path, required: true, schema: { type: string } }
    post:
      tags: [Auth]
      summary: Получить результат входа приложения
      operationId: pollDesktopLogin
      security: []
      description: secret передаётся в теле. Сессия выдаётся только один раз; последующие попытки получают 410. Не чаще interval секунд.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [secret]
              properties:
                secret: { type: string }
      responses:
        "200":
          description: Ожидание, отказ или выданная сессия
          content:
            application/json:
              schema:
                type: object
                required: [status, session]
                properties:
                  status: { type: string, enum: [pending, denied, approved] }
                  session:
                    oneOf:
                      - { $ref: "#/components/schemas/TokenPair" }
                      - { type: "null" }
        "410": { description: "Запрос истёк, использован или неверный secret" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/desktop/{requestID}/cancel:
    parameters:
      - { name: requestID, in: path, required: true, schema: { type: string } }
    post:
      tags: [Auth]
      summary: Отменить запрос со стороны приложения
      operationId: cancelDesktopLogin
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [secret]
              properties:
                secret: { type: string }
      responses:
        "204": { description: Запрос отменён }
        "410": { description: "Запрос истёк, использован или неверный secret" }

  /auth/login:
    post:
      tags: [Auth]
      summary: Вход по номеру и паролю
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone, password]
              properties:
                phone: { type: string, pattern: "^\\+7\\d{10}$" }
                password: { type: string, format: password }
                code: { type: string, description: "Код 2FA (6 цифр из приложения или резервный код), если включена" }
                captcha_token: { type: string }
      responses:
        "200":
          description: Токены сессии
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenPair" }
        "401":
          description: "Неверные учётные данные (50014), требуется 2FA (60003, в теле `mfa: true`) или неверный код 2FA (60008)"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /auth/refresh:
    post:
      tags: [Auth]
      summary: Обновить access-токен по refresh-токену
      description: Refresh-токен ротируется; повторное использование отозванного токена отзывает всю цепочку.
      operationId: refreshToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token: { type: string }
      responses:
        "200":
          description: Новая пара токенов
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenPair" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /auth/recover/phone:
    post:
      tags: [Auth]
      summary: Отправить код восстановления пароля
      description: Ответ одинаков для существующего и несуществующего номера; SMS уходит только владельцу аккаунта. Лимиты как у регистрации.
      operationId: recoverPhone
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone: { type: string, pattern: "^\\+7\\d{10}$" }
                captcha_token: { type: string }
      responses:
        "200":
          description: Код отправлен (или сделан вид, что отправлен)
          content:
            application/json:
              schema:
                type: object
                required: [verification_id, expires_in, phone, channel]
                properties:
                  verification_id: { $ref: "#/components/schemas/Snowflake" }
                  expires_in: { type: integer }
                  phone: { type: string }
                  channel: { type: string }
        "400": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /auth/recover/verify:
    post:
      tags: [Auth]
      summary: Подтвердить код и задать новый пароль
      description: Все сессии пользователя отзываются, выдаётся новая пара токенов.
      operationId: recoverVerify
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [verification_id, code, new_password]
              properties:
                verification_id: { $ref: "#/components/schemas/Snowflake" }
                code: { type: string, pattern: "^\\d{6}$" }
                new_password: { type: string, format: password, minLength: 8, maxLength: 128 }
      responses:
        "200":
          description: Новая сессия
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenPair" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: Код неверен, истёк или уже использован (70003)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/verify/{verificationId}:
    get:
      tags: [Auth]
      summary: Состояние подтверждения номера
      description: Нужно мобильной авторизации — оператор подтверждает номер сам, и узнать об этом можно только спросив. Попытку не расходует. Если оператор отказал, сервер сам переводит проверку на наш SMS-код и отдаёт channel sms и шесть цифр.
      operationId: verifyStatus
      security: []
      parameters:
        - { name: verificationId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
        - { name: purpose, in: query, schema: { type: string, enum: [register, recover, change_phone], default: register } }
      responses:
        "200":
          description: Состояние
          content:
            application/json:
              schema:
                type: object
                required: [status, code_length, channel]
                properties:
                  status: { type: string, enum: [pending, needs_code, ok, failed] }
                  code_length: { type: integer, description: "Сколько цифр набирать — у оператора четыре, у нас шесть" }
                  channel: { type: string, description: "mobileid — код и SMS у оператора, sms — наш код" }
        "403":
          description: Запись не найдена, чужая цель или код истёк (код 70003)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/verify/{verificationId}/check:
    post:
      tags: [Auth]
      summary: Проверить код на шаге его ввода
      description: Ошибка в коде видна сразу, а не после ника или нового пароля. Расходует попытку. Проверенный код сервер запоминает — завершение регистрации или восстановления с ним больше попыток не тратит и проходит и со второго раза (для мобильной авторизации это важно — оператор принимает код один раз).
      operationId: verifyCheck
      security: []
      parameters:
        - { name: verificationId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [purpose, code]
              properties:
                purpose: { type: string, enum: [register, recover] }
                code: { type: string }
      responses:
        "204": { description: Код верный }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: Неверный код, код истёк, слишком много попыток или запись не найдена (код 70003)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/verify/{verificationId}/sms:
    post:
      tags: [Auth]
      summary: Не приходит код — перевести проверку на SMS от Кличата
      description: Для мобильной авторизации — оператор мог пообещать SMS и не прислать. Та же проверка продолжается нашим шестизначным кодом; повторный вызов второго SMS не шлёт. Для обычного SMS ничего не меняет.
      operationId: verifyForceSMS
      security: []
      parameters:
        - { name: verificationId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
        - { name: purpose, in: query, schema: { type: string, enum: [register, recover, change_phone], default: register } }
      responses:
        "200":
          description: Проверка идёт по SMS
          content:
            application/json:
              schema:
                type: object
                required: [status, code_length, channel]
                properties:
                  status: { type: string, enum: [pending, needs_code, ok, failed] }
                  code_length: { type: integer }
                  channel: { type: string }
        "403":
          description: Запись не найдена, чужая цель или код истёк (код 70003)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/email/verify:
    post:
      tags: [Auth]
      summary: Подтвердить почту по ссылке из письма
      description: Сессия не нужна — письмо могли открыть на другом устройстве. Ссылка одноразовая, действует сутки; адрес привязывается, если его не подтвердил кто-то другой.
      operationId: emailVerify
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200":
          description: Почта привязана
          content:
            application/json:
              schema:
                type: object
                required: [email]
                properties:
                  email: { type: string, format: email }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: Ссылка недействительна, использована или устарела (код 70004)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/recover/email:
    post:
      tags: [Auth]
      summary: Письмо со ссылкой на смену пароля
      description: Ответ одинаков для любого адреса; письмо уходит только на подтверждённую почту владельца, в фоне. Капча после первой попытки с IP, 10 попыток на IP и 3 на адрес в час.
      operationId: recoverEmail
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                captcha_token: { type: string }
      responses:
        "200":
          description: Принято (письмо отправлено или сделан вид, что отправлено)
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok: { type: boolean }
        "400": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /auth/recover/email/verify:
    post:
      tags: [Auth]
      summary: Новый пароль по ссылке из письма
      description: Ссылка одноразовая, действует час и только пока адрес привязан к этому пользователю. Все сессии отзываются, выдаётся новая пара токенов.
      operationId: recoverEmailVerify
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, new_password]
              properties:
                token: { type: string }
                new_password: { type: string, format: password, minLength: 8, maxLength: 128 }
      responses:
        "200":
          description: Пароль изменён, вход выполнен
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenPair" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: Ссылка недействительна, использована или устарела (код 70004)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/logout:
    post:
      tags: [Auth]
      summary: Завершить текущую сессию
      operationId: logout
      responses:
        "204": { description: Сессия отозвана }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /auth/sessions:
    get:
      tags: [Auth]
      summary: Список активных сессий (устройств)
      operationId: listSessions
      responses:
        "200":
          description: Сессии пользователя
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Session" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Auth]
      summary: Выйти на всех устройствах, кроме текущего
      operationId: revokeOtherSessions
      responses:
        "204": { description: Остальные сессии отозваны }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /auth/mfa:
    get:
      tags: [Auth]
      summary: Состояние 2FA
      operationId: getMfaStatus
      responses:
        "200":
          description: Включён ли TOTP и сколько резервных кодов не использовано
          content:
            application/json:
              schema:
                type: object
                required: [totp_enabled, backup_codes_left]
                properties:
                  totp_enabled: { type: boolean }
                  backup_codes_left: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /auth/mfa/codes:
    post:
      tags: [Auth]
      summary: Новые резервные коды
      description: Старые коды перестают действовать. Требует пароль и включённый TOTP (иначе 60002).
      operationId: regenerateBackupCodes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, format: password }
      responses:
        "200":
          description: Десять новых кодов вида xxxxxxxx-xxxx
          content:
            application/json:
              schema:
                type: object
                required: [backup_codes]
                properties:
                  backup_codes:
                    type: array
                    items: { type: string }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /auth/mfa/totp:
    post:
      tags: [Auth]
      summary: Включить TOTP
      description: "Секрет генерирует клиент (20 байт, base32) и показывает как QR `otpauth://totp/Кличат:username?secret=…&issuer=Кличат` (SHA1, 6 цифр, 30 с). Сервер хранит секрет зашифрованным."
      operationId: enableTotp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password, code, secret]
              properties:
                password: { type: string, format: password }
                secret: { type: string, description: "Секрет base32 без «=», не короче 16 символов" }
                code: { type: string, pattern: "^\\d{6}$", description: "Текущий код из приложения — подтверждает, что секрет сохранён" }
      responses:
        "200":
          description: TOTP включён, выданы резервные коды
          content:
            application/json:
              schema:
                type: object
                required: [backup_codes]
                properties:
                  backup_codes:
                    type: array
                    items: { type: string }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Auth]
      summary: Выключить TOTP
      operationId: disableTotp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, description: "Код из приложения или резервный код" }
      responses:
        "204": { description: TOTP выключен }
        "400": { description: "TOTP не включён (60002)" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ---------------------------------------------------------------- Users
  /avatars/{user}/{hash}:
    get:
      tags: [Media]
      summary: Аватар пользователя
      description: Публично, без авторизации. Кэш бессрочный (immutable) — смена аватара меняет hash. Расширение в hash допускается и игнорируется.
      operationId: getAvatar
      parameters:
        - { name: user, in: path, required: true, schema: { type: string } }
        - { name: hash, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Картинка, content: { image/png: {}, image/jpeg: {}, image/gif: {}, image/webp: {} } }
        "404": { $ref: "#/components/responses/NotFound" }
  /user-banners/{user}/{hash}:
    get:
      tags: [Media]
      summary: Баннер профиля
      description: Публично, без авторизации. Кэш бессрочный (immutable) — смена баннера меняет hash. У серверов свой путь /banners/{guild}/{hash}.
      operationId: getUserBanner
      parameters:
        - { name: user, in: path, required: true, schema: { type: string } }
        - { name: hash, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Картинка, content: { image/png: {}, image/jpeg: {}, image/gif: {}, image/webp: {} } }
        "404": { $ref: "#/components/responses/NotFound" }
  /users/@me/phone:
    post:
      tags: [Users]
      summary: Начать смену номера
      description: Проверяет пароль и шлёт код на новый номер. Лимиты и cooldown те же, что при регистрации.
      operationId: changePhoneStart
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password, phone]
              properties:
                password: { type: string, format: password }
                phone: { type: string, pattern: "^\\+7\\d{10}$" }
      responses:
        "200":
          description: Код отправлен на новый номер
          content:
            application/json:
              schema:
                type: object
                required: [verification_id, expires_in, phone, channel]
                properties:
                  verification_id: { $ref: "#/components/schemas/Snowflake" }
                  expires_in: { type: integer, description: Секунд до истечения кода }
                  phone: { type: string, description: Маскированный номер }
                  channel: { type: string, enum: [sms, telegram, console] }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: "На новый номер уже зарегистрирован аккаунт" }
        "429": { $ref: "#/components/responses/RateLimited" }
    put:
      tags: [Users]
      summary: Подтвердить новый номер
      operationId: changePhoneComplete
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [verification_id, code]
              properties:
                verification_id: { $ref: "#/components/schemas/Snowflake" }
                code: { type: string, pattern: "^\\d{6}$" }
      responses:
        "200":
          description: Пользователь с новым (маскированным) номером
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "403": { description: "Код неверен или истёк (70003)" }

  /users/@me/password:
    post:
      tags: [Users]
      summary: Сменить пароль
      description: >-
        Текущий пароль и новый по правилам регистрации (8–128 символов, не из списка частых, не совпадает
        с именем пользователя). Попытки считаются, как у входа: 10 за 15 минут на учётную запись, дальше 429;
        слабый новый пароль попытку не тратит. После смены отзываются все сеансы, кроме текущего,
        а на подтверждённую почту уходит письмо без ссылок. Только пользователям, не ботам.
      operationId: changePassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password, new_password]
              properties:
                password: { type: string, format: password, description: Текущий пароль }
                new_password: { type: string, format: password, minLength: 8, maxLength: 128 }
      responses:
        "204": { description: "Пароль изменён, остальные сеансы завершены" }
        "400":
          description: "Ошибка по полю: password — неверный текущий пароль; new_password — слабый или совпадает с текущим"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /users/@me/delete:
    post:
      tags: [Users]
      summary: Заявка на удаление учётной записи
      description: "Удаление с отсрочкой в 14 дней — всё это время можно отменить. Нужен текущий пароль. Отказ, если на человеке остаются серверы с другими участниками: их надо передать или распустить."
      operationId: requestAccountDeletion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, format: password }
      responses:
        "200":
          description: Заявка принята
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletionStatus" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Users]
      summary: Отменить удаление
      operationId: cancelAccountDeletion
      responses:
        "200":
          description: Заявка отменена
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletionStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /users/@me/export:
    get:
      tags: [Users]
      summary: Состояние выгрузки данных
      description: Последняя выгрузка. Готовая отдаётся со свежей ссылкой на скачивание, которая живёт час; сам архив хранится неделю.
      operationId: dataExportStatus
      responses:
        "200":
          description: Состояние
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DataExport" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Users]
      summary: Заказать выгрузку данных
      description: Архив собирается в фоне. Не чаще раза в сутки; пока предыдущая готовится — 409.
      operationId: requestDataExport
      responses:
        "200":
          description: Выгрузка поставлена в очередь
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DataExport" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: Выгрузка уже готовится
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /users/@me/email:
    post:
      tags: [Users]
      summary: Добавить или сменить почту
      description: Проверяет пароль и шлёт на адрес письмо со ссылкой; почта привяжется после перехода по ней (email_pending до тех пор). Не чаще раза в минуту, 5 писем на адрес и на пользователя в час.
      operationId: emailStart
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password, email]
              properties:
                password: { type: string, format: password }
                email: { type: string, format: email }
      responses:
        "200":
          description: Письмо отправлено
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503":
          description: Письмо не отправилось (почтовый сервер недоступен)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    delete:
      tags: [Users]
      summary: Отвязать почту
      description: "Нужен текущий пароль. Гасит и все ссылки из писем: восстановиться через эту почту больше нельзя."
      operationId: emailRemove
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, format: password }
      responses:
        "200":
          description: Почта отвязана
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /users/@me/email/resend:
    post:
      tags: [Users]
      summary: Отправить письмо для подтверждения ещё раз
      description: На адрес, который ждёт подтверждения; прежняя ссылка гаснет. Те же лимиты, что у добавления.
      operationId: emailResend
      responses:
        "200":
          description: Письмо отправлено
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /users/@me/username:
    patch:
      tags: [Users]
      summary: Изменить имя пользователя после @
      description: |
        Имя приводится к нижнему регистру. 2–32 символа: латиница, цифры, точка и
        подчёркивание; без точки в начале/конце и двух точек подряд. Занятые и
        служебные имена недоступны. Прежнее имя освобождается сразу.
        ID, отображаемое имя, друзья, сообщения и сеансы сохраняются.
        Одна успешная смена за 24 часа на учётную запись. Ошибки и сохранение
        текущего имени не расходуют суточный лимит. Не более 20 попыток в минуту.
        История (прежнее имя, новое имя, время) доступна сотрудникам платформы
        в карточке модерации; при удалении учётной записи история удаляется.
        Обновление рассылается через USER_UPDATE и GUILD_MEMBER_UPDATE.
      operationId: changeUsername
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username]
              properties:
                username: { type: string, minLength: 2, maxLength: 32 }
      responses:
        "200":
          description: Обновлённый пользователь
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429":
          description: Суточный лимит (code 20029) или лимит попыток; retry_after и Retry-After в секундах
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RateLimited" }

  /users/@me:
    get:
      tags: [Users]
      summary: Текущий пользователь
      operationId: getCurrentUser
      responses:
        "200":
          description: Полный объект пользователя (с телефоном в замаскированном виде)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Users]
      summary: Изменить профиль
      description: >-
        Только пользователи (не боты). display_name — пустая строка сбрасывает до ника;
        bio — строчка о себе до 190 символов (пустая убирает);
        avatar — data URI картинки (PNG/GIF/JPEG/WebP до 1 МБ, не больше 1024×1024) или null, чтобы убрать;
        banner — так же, но до 3 МБ и 2048×2048.
        avatar_decoration и profile_effect — имя рамки или эффекта из GET /users/@me/decorations, null — снять;
        display_name_styles — стиль ника или null. Недоступное украшение — 403 с объяснением, за что оно
        даётся; тогда из украшений не меняется ничего.
        Картинки кладутся в хранилище под ключи avatars/{user}/{hash} и user-banners/{user}/{hash},
        в ответе — новые хэши; отдаются без авторизации: GET /avatars/{user}/{hash} и
        GET /user-banners/{user}/{hash}. Изменение рассылается
        событиями GUILD_MEMBER_UPDATE (в серверы пользователя) и USER_UPDATE (себе, друзьям, собеседникам ЛС).
      operationId: updateCurrentUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name: { type: string, maxLength: 32 }
                bio: { type: string, maxLength: 190, description: "Строчка о себе; пустая убирает" }
                avatar: { type: [string, "null"], description: "data URI или null" }
                banner: { type: [string, "null"], description: "data URI или null" }
                avatar_decoration: { type: [string, "null"], description: "Рамка аватара; null — снять" }
                nameplate: { type: [string, "null"], description: "Табличка в списке участников; null — снять" }
                profile_effect: { type: [string, "null"], description: "Эффект карточки профиля; null — снять" }
                display_name_styles: { oneOf: [{ $ref: "#/components/schemas/NameStyle" }, { type: "null" }] }
                theme_colors: { oneOf: [{ type: array, items: { type: integer }, minItems: 2, maxItems: 2 }, { type: "null" }], description: "Цвета профиля (с «Плюсом»); null — обычные" }
                profile_showcase: { type: array, maxItems: 3, items: { type: string }, description: "Достижения для витрины; пустой список — снова три последних" }
                server_tag: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }], description: "Сервер, чей тег носить (нужно состоять на нём); null — без тега" }
      responses:
        "200":
          description: Обновлённый пользователь
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: "Украшение недоступно: за достижение, с «Плюсом» или не в свой сезон (50001)" }
        "501": { description: Хранилище картинок профиля не подключено }

  /users/@me/decorations:
    get:
      tags: [Users]
      summary: Украшения профиля
      description: >-
        Все рамки и эффекты с состоянием (выдано, по «Плюсу», сезон, закрыто) и тем, откуда они берутся,
        доступ к стилю ника и надетое сейчас. Надеть и снять — PATCH /users/@me.
      operationId: getDecorations
      responses:
        "200":
          description: Украшения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Decorations" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /users/@me/achievements:
    get:
      tags: [Users]
      summary: Достижения
      description: >-
        Все достижения в порядке показа с прогрессом и наградами. Прогресс — в показанных единицах
        (часы, дни), получено — earned_at. Новые (new) — полученные после последнего открытия раздела.
        Получение приходит событием ACHIEVEMENT_UNLOCK {id, name, rewards} только самому человеку.
      operationId: getAchievements
      responses:
        "200":
          description: Достижения
          content:
            application/json:
              schema:
                type: object
                properties:
                  achievements: { type: array, items: { $ref: "#/components/schemas/Achievement" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /users/@me/achievements/seen:
    post:
      tags: [Users]
      summary: Отметить новые достижения просмотренными
      operationId: seenAchievements
      responses:
        "204": { description: Отмечено }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /moderation/decorations/users/{userId}/{item}:
    parameters:
      - { name: userId, in: path, required: true, schema: { $ref: "#/components/schemas/Snowflake" } }
      - { name: item, in: path, required: true, schema: { type: string, examples: ["frame:klich"] }, description: "Вид и имя: frame:…, effect:… или name:neon" }
    put:
      tags: [Users]
      summary: Выдать украшение навсегда (сотрудник)
      operationId: grantDecoration
      responses:
        "200":
          description: Выдано; added — false, если уже было
          content:
            application/json:
              schema:
                type: object
                properties:
                  item: { type: string }
                  added: { type: boolean }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { description: Только для сотрудников платформы }
    delete:
      tags: [Users]
      summary: Забрать выданное украшение (сотрудник)
      description: Надетое снимается, изменение рассылается как смена профиля.
      operationId: revokeDecoration
      responses:
        "204": { description: Забрано }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { description: Только для сотрудников платформы }

  /users/@me/guild-order:
    patch:
      tags: [Users]
      summary: Порядок серверов в колонке слева
      description: >-
        Список id серверов в том порядке, в каком человек расставил их перетаскиванием.
        Повторы отбрасываются; серверы, которых в списке нет, показываются после него
        в порядке вступления. Хранится в настройках пользователя и приходит обратно
        в объекте CurrentUser полем guild_order.
      operationId: setGuildOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [guilds]
              properties:
                guilds:
                  type: array
                  maxItems: 200
                  items: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "204": { description: Порядок сохранён }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /users/@me/settings:
    get:
      tags: [Users]
      summary: Настройки пользователя
      description: >-
        Сейчас сервер хранит `status`, `custom_status` (`text` и `expires_at`), `show_current_game`, `theme` и `locale`;
        они же приходят в READY (`user_settings`). Только пользователям, не ботам.
      operationId: getUserSettings
      responses:
        "200":
          description: Настройки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserSettings" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Users]
      summary: Изменить настройки
      description: >-
        Меняются `status` (online, idle, dnd, invisible), `custom_status`: `{text, expires_at}` — текст до 128
        символов одной строкой; `null` или пустой текст убирают свой статус; `expires_at` (RFC 3339, в будущем,
        не дальше года) — когда он уберётся сам: истёкший не отдаётся сразу, а из базы его убирает worker
        раз в минуту и рассылает как обычную смену, — и `show_current_game`: `false`
        прячет запущенную игру из присутствия (даже если клиент её прислал) и историю игр от других,
        а новые игры в историю не пишутся. Остальные поля принимаются и
        пропускаются. Выбранное переживает переподключения: новые сессии шлюза приходят с ним.
        После изменения своим сессиям уходит `USER_SETTINGS_UPDATE` (тело — как ответ), друзьям и
        серверам — `PRESENCE_UPDATE` (невидимый для них не в сети, свой статус — активность type 4).
      operationId: updateUserSettings
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UserSettings" }
      responses:
        "200":
          description: Настройки после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserSettings" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /users/@me/relationships:
    get:
      tags: [Relationships]
      summary: Друзья, заявки и блокировки
      operationId: listRelationships
      responses:
        "200":
          description: Список связей
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Relationship" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Relationships]
      summary: Отправить заявку в друзья по username
      operationId: sendFriendRequestByUsername
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username]
              properties:
                username: { type: string }
      responses:
        "204": { description: "Заявка отправлена (или принята, если была встречная)" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /users/@me/relationships/{userId}:
    parameters:
      - $ref: "#/components/parameters/userId"
    put:
      tags: [Relationships]
      summary: Отправить или принять заявку, либо заблокировать
      operationId: putRelationship
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: integer
                  enum: [1, 2]
                  description: "1 — друг (заявка/принятие), 2 — блокировка; по умолчанию 1"
      responses:
        "204": { description: Связь создана или обновлена }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Relationships]
      summary: Удалить из друзей, отозвать заявку или разблокировать
      operationId: deleteRelationship
      responses:
        "204": { description: Связь удалена }
        "404": { $ref: "#/components/responses/NotFound" }

  /channels/{channelId}/forum-posts:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Threads]
      summary: Темы форума
      operationId: getForumPosts
      description: |
        Канал типа 15. Нужны VIEW_CHANNEL и READ_MESSAGE_HISTORY; проверяется доступ 18+.
        query ищет подстроку в названии без учёта регистра. archived выбирает активные или архивные темы.
        Закреплённые темы идут первыми; внутри группы sort=activity сортирует по последнему сообщению, sort=created — по созданию.
        tags — до 5 ID через запятую: достаточно совпадения любого выбранного тега.
        before берётся из next_before, привязан к фильтрам и сортировке; при их изменении начните новую страницу.
        Первое сообщение может отсутствовать после удаления. reply_count — текущее число остальных сообщений.
        Чтение списка не подтверждает прочтение переписки в темах.
      parameters:
        - name: tags
          in: query
          description: До 5 разных ID тегов этого форума через запятую
          schema: { type: string }
        - name: query
          in: query
          schema: { type: string, maxLength: 100 }
        - name: archived
          in: query
          schema: { type: boolean, default: false }
        - name: sort
          in: query
          schema: { type: string, enum: [activity, created], default: activity }
        - name: before
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 30 }
      responses:
        "200":
          description: Страница тем
          content:
            application/json:
              schema:
                type: object
                required: [posts, has_more, next_before]
                properties:
                  posts: { type: array, items: { $ref: "#/components/schemas/ForumPost" } }
                  has_more: { type: boolean }
                  next_before: { type: [string, "null"] }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Threads]
      summary: Создать тему форума с первым сообщением
      operationId: createForumPost
      description: |
        VIEW_CHANNEL и SEND_MESSAGES в форуме; CREATE_PUBLIC_THREADS не требуется.
        Первое сообщение обязательно. Тема типа 11 и сообщение с тем же ID создаются одной транзакцией.
        Проверяются возраст, таймаут, уровень проверки сервера, медленный режим и AutoMod.
        Автоархив через сутки без активности. Автор становится участником, счётчик сообщений равен 1.
        Возвращается ForumPost, рассылаются THREAD_CREATE и MESSAGE_CREATE, пишется аудит 110.
        Сообщения в контейнер форума через POST /messages запрещены; ответы идут в канал темы
        с SEND_MESSAGES_IN_THREADS. Первое сообщение содержит текст или 1–10 вложений.
        Слоты загружаются через POST /channels/{forumId}/attachments и требуют ATTACH_FILES.
        Перед прикреплением проверяются владелец, родительский канал, статус и размер объекта;
        записи файлов переносятся в канал темы в той же транзакции. applied_tags — до 5 тегов форума.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                applied_tags: { type: array, maxItems: 5, uniqueItems: true, items: { $ref: "#/components/schemas/Snowflake" } }
                attachments: { type: array, maxItems: 10, items: { $ref: "#/components/schemas/ForumAttachmentInput" } }
                name: { type: string, minLength: 1, maxLength: 100 }
                content: { type: string, maxLength: 4000, description: "До 2000 символов, до 4000 с Кличат Плюс" }
      responses:
        "201":
          description: Созданная тема и первое сообщение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ForumPost" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /channels/{channelId}/forum-tags:
    parameters:
      - $ref: "#/components/parameters/channelId"
    put:
      tags: [Threads]
      summary: Заменить список тегов форума
      operationId: setForumTags
      description: |
        VIEW_CHANNEL, MANAGE_CHANNELS и доступ по возрасту. До 20 тегов, имена 1–20 символов без повторов.
        У нового тега id отсутствует; у существующего передаётся прежний id этого форума.
        Пустой список удаляет все теги. Удалённые теги атомарно убираются из тем.
        CHANNEL_UPDATE и THREAD_UPDATE отражают изменения; аудит канала 11.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [available_tags]
              properties:
                available_tags: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/ForumTagInput" } }
      responses:
        "200":
          description: Форум с актуальными тегами
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /channels/{channelId}/forum-posts/{threadId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - name: threadId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    patch:
      tags: [Threads]
      summary: Теги и закрепление темы форума
      operationId: updateForumPost
      description: |
        Тема должна принадлежать указанному форуму. Теги меняет автор или MANAGE_THREADS.
        Автор не меняет теги закрытой/архивной темы; проверяются таймаут и уровень проверки сервера.
        Закрепление меняет только MANAGE_THREADS. До 50 закреплений на форум, включая архив.
        Закрепление не меняет состояние архива. Пустой applied_tags снимает все теги темы.
        Возвращается канал темы; THREAD_UPDATE и аудит темы 111. Укажите хотя бы одно поле.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                applied_tags: { type: array, maxItems: 5, uniqueItems: true, items: { $ref: "#/components/schemas/Snowflake" } }
                forum_pinned: { type: boolean }
      responses:
        "200":
          description: Обновлённая тема
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /channels/{channelId}/messages/bulk-delete:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Messages]
      summary: Удалить выбранные сообщения канала сервера
      operationId: bulkDeleteMessages
      description: >-
        Требует VIEW_CHANNEL, READ_MESSAGE_HISTORY и MANAGE_MESSAGES в канале.
        ЛС и группы не поддерживаются; CONNECT в голосовом канале не нужен.
        От 1 до 100 разных ID, без ограничения возраста сообщения.
        Удаляются только обычные сообщения/ответы/ответы приложений из этого канала;
        уже удалённые, ID других каналов, системные и ephemeral/loading пропускаются.
        Сообщения, вложения, реакции, опросы и пересылки удаляются одной транзакцией.
        Возвращаются фактически удалённые ID и актуальное последнее сообщение канала.
        При непустом результате рассылается MESSAGE_DELETE_BULK и записывается аудит 73
        с количеством и каналом, без копирования текста. reason попадает в аудит;
        если он не задан, используется X-Audit-Log-Reason. Повторный запрос допустим.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  minItems: 1
                  maxItems: 100
                  uniqueItems: true
                  items: { $ref: "#/components/schemas/Snowflake" }
                reason: { type: string, maxLength: 512 }
      responses:
        "200":
          description: Результат удаления, ids может быть пустым
          content:
            application/json:
              schema:
                type: object
                required: [ids, channel_id, guild_id, last_message_id]
                properties:
                  ids: { type: array, items: { $ref: "#/components/schemas/Snowflake" } }
                  channel_id: { $ref: "#/components/schemas/Snowflake" }
                  guild_id: { $ref: "#/components/schemas/Snowflake" }
                  last_message_id: { type: [string, "null"] }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { description: Канал не найден }

  /users/@me/saved-messages:
    get:
      tags: [Messages]
      summary: Личный список сохранённых сообщений
      operationId: getSavedMessages
      description: |
        Новые сохранения идут первыми. Курсор before берётся из next_before.
        Текст читается из оригинала после проверки текущих прав и блокировок.
        При удалении сообщения или утрате доступа available=false, без channel, guild_name и message.
        Просмотр и сохранение не меняют read_state. Закрытое ЛС остаётся доступным его получателю.
      parameters:
        - name: before
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 25 }
      responses:
        "200":
          description: Страница личного списка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SavedPage" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /channels/{channelId}/messages/{messageId}/saved:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    get:
      tags: [Messages]
      summary: Сохранено ли сообщение пользователем
      operationId: getSavedMessageStatus
      description: Проверяет только личную ссылку, не раскрывает содержимое или наличие чужого сообщения.
      responses:
        "200":
          description: Личное состояние
          content:
            application/json:
              schema:
                type: object
                required: [saved]
                properties:
                  saved: { type: boolean }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Messages]
      summary: Сохранить сообщение в личный список
      operationId: saveMessage
      description: |
        Нужны VIEW_CHANNEL и READ_MESSAGE_HISTORY. Можно сохранить обычное сообщение, ответ,
        ответ приложения, вложение или опрос. Системные сообщения, ответы «только вам» и ещё не готовые ответы приложения исключены.
        Повторное сохранение сохраняет исходные id и saved_at. SAVED_MESSAGE_CREATE получает только сам пользователь.
      responses:
        "204": { description: Сохранено }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Messages]
      summary: Убрать сообщение из личного списка
      operationId: unsaveMessage
      description: |
        Удаляет только свою ссылку. Доступ к исходной переписке не нужен; отсутствующая ссылка тоже возвращает 204.
        Оригинальное сообщение остаётся в переписке. SAVED_MESSAGE_DELETE получает только сам пользователь.
      responses:
        "204": { description: Убрано из сохранённого }
        "404": { $ref: "#/components/responses/NotFound" }

  /users/@me/inbox:
    get:
      tags: [Messages]
      summary: Непрочитанные беседы и упоминания
      description: |
        Просмотр не меняет read_state. История выдаётся только текущим участникам с VIEW_CHANNEL
        и READ_MESSAGE_HISTORY. Учитываются блокировки, заглушение и настройки упоминаний.
        unread возвращает по карточке на канал с тремя последними непрочитанными сообщениями;
        mentions — по карточке на сообщение. Темы форумов входят в unread при подписке
        или наличии непрочитанных упоминаний; история mentions доступна без подписки.
        Курсор before берётся из next_before.
        Пустая страница с has_more=true требует продолжить выборку.
      operationId: getInbox
      parameters:
        - name: mode
          in: query
          schema: { type: string, enum: [unread, mentions], default: unread }
        - name: before
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 25 }
        - name: include_muted
          in: query
          schema: { type: boolean, default: false }
        - name: unread_only
          in: query
          description: Во вкладке mentions показывать только непрочитанные упоминания.
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: Страница входящих
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InboxPage" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /users/@me/channels:
    get:
      tags: [Relationships]
      summary: Личные и групповые каналы
      operationId: listDMChannels
      responses:
        "200":
          description: Каналы ЛС
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Channel" }
    post:
      tags: [Relationships]
      summary: Открыть ЛС или создать группу
      operationId: createDMChannel
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [recipient_ids]
              properties:
                recipient_ids:
                  type: array
                  minItems: 1
                  maxItems: 9
                  description: Один получатель — ЛС (существующее или новое); от 2 до 9 друзей — новая группа (тип 3, до 10 участников)
                  items: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Существующий или новый канал
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "400":
          description: Кто-то из получателей не в друзьях (50033) или превышен предел участников (50035)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403":
          description: Получатель запретил ЛС (50007) или заблокировал (50033)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /users/@me/guilds:
    get:
      tags: [Users]
      summary: Серверы текущего пользователя
      operationId: listCurrentUserGuilds
      responses:
        "200":
          description: Краткие объекты серверов
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/GuildPartial" }

  /users/@me/guilds/{guildId}/settings:
    parameters:
      - name: guildId
        in: path
        required: true
        description: "Id сервера или `@me` — личные беседы: запись с `guild_id: null`, в ней только `channel_overrides` ЛС и групп"
        schema: { type: string }
    get:
      tags: [Users]
      summary: Настройки уведомлений сервера
      description: "Как `user_guild_settings` у Discord; приходят в READY и событии USER_GUILD_SETTINGS_UPDATE. Запись личных бесед (`guild_id: null`) в READY есть, только если в ЛС что-то настроено."
      operationId: getUserGuildSettings
      responses:
        "200":
          description: Настройки с переопределениями каналов
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserGuildSettings" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Users]
      summary: Изменить настройки уведомлений сервера
      description: "Частичное обновление; `channel_overrides` — объект channel_id → {muted, mute_config, message_notifications, collapsed}. Переопределение канала без отличий от сервера удаляется. Mute категории глушит все каналы в ней. Для `@me` принимаются только беседы, где вы получатель; поля уровня сервера там ничего не меняют."
      operationId: updateUserGuildSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message_notifications: { type: integer, enum: [0, 1, 2, 3], description: "0 все, 1 только упоминания, 2 ничего, 3 как на сервере" }
                muted: { type: boolean }
                mute_config: { type: [object, "null"], properties: { end_time: { type: [string, "null"], format: date-time } } }
                suppress_everyone: { type: boolean, description: "Не считать @everyone и @here упоминаниями" }
                suppress_roles: { type: boolean }
                channel_overrides:
                  type: object
                  additionalProperties:
                    type: object
                    properties:
                      message_notifications: { type: integer, enum: [0, 1, 2, 3] }
                      muted: { type: boolean }
                      mute_config: { type: [object, "null"], properties: { end_time: { type: [string, "null"], format: date-time } } }
                      collapsed: { type: boolean }
      responses:
        "200":
          description: Настройки после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserGuildSettings" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /users/@me/guilds/{guildId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
    delete:
      tags: [Users]
      summary: Покинуть сервер
      description: "Владелец сначала передаёт сервер (`owner_id` в PATCH /guilds/{guildId}) или удаляет его."
      operationId: leaveGuild
      responses:
        "204": { description: "Вышли; остальным участникам приходит GUILD_MEMBER_REMOVE, вам — GUILD_DELETE" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /users/{userId}:
    parameters:
      - $ref: "#/components/parameters/userId"
    get:
      tags: [Users]
      summary: Публичный профиль пользователя
      operationId: getUser
      responses:
        "200":
          description: >-
            Карточка профиля — пользователь плюс баннер, «о себе», дата регистрации, признак «Плюса»,
            общее со смотрящим и история игр (`games`). Историю видят сам человек и те, кому видна его
            игра сейчас: друзья и участники общих серверов, если человек её не скрыл
            (`show_current_game`). Ботам — пустой список.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profile" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ---------------------------------------------------------------- Guilds
  /guilds:
    post:
      tags: [Guilds]
      summary: Создать сервер
      description: Лимит — 100 серверов на пользователя (30001) и 2 создания в час. Ботам недоступно (20001).
      operationId: createGuild
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 2, maxLength: 100 }
                icon: { type: [string, "null"] }
                template: { type: string, enum: [default, gaming, study, community, friends], description: "Шаблон каналов; по умолчанию default (общий + голосовой)" }
      responses:
        "201":
          description: Созданный сервер с каналами и ролями шаблона
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Guild" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /guilds/{guildId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Сервер
      operationId: getGuild
      parameters:
        - name: with_counts
          in: query
          schema: { type: boolean }
      responses:
        "200":
          description: Сервер
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Guild" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Guilds]
      summary: Изменить сервер
      description: Требует MANAGE_GUILD.
      operationId: updateGuild
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 2, maxLength: 100 }
                icon: { type: [string, "null"] }
                banner:
                  type: [string, "null"]
                  description: |
                    Баннер над каналами: data URI (PNG, GIF, JPEG или WebP до 10 МБ, не больше 1920 точек по стороне, лучше 16:9
                    от 960×540), null — убрать. Со 2-го уровня сервера (features BANNER), анимированный — с 3-го (ANIMATED_BANNER),
                    иначе 400/50035 в поле banner; его хэш начинается с a_. Тело запроса с баннером — до 14 МБ.
                description: { type: [string, "null"], maxLength: 300 }
                verification_level: { type: integer, enum: [0, 1, 2, 3] }
                default_message_notifications: { type: integer, enum: [0, 1] }
                system_channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
                rules_channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
                afk_channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
                afk_timeout: { type: integer, enum: [60, 300, 900, 1800, 3600] }
                owner_id: { $ref: "#/components/schemas/Snowflake" }
                tag: { type: [string, "null"], description: "Тег сервера, 2–4 буквы или цифры; null — убрать (снимается у всех, кто его носил)" }
                tag_color: { type: [integer, "null"] }
                premium_progress_bar_enabled: { type: boolean, description: "Полоска бустов над каналами: сколько до следующего уровня. По умолчанию включена" }
      responses:
        "200":
          description: Сервер после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Guild" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Guilds]
      summary: Удалить сервер (только владелец)
      operationId: deleteGuild
      responses:
        "204": { description: Сервер удалён }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/channels:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Channels]
      summary: Каналы сервера
      operationId: listGuildChannels
      responses:
        "200":
          description: Каналы, включая категории
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Channel" }
    post:
      tags: [Channels]
      summary: Создать канал
      description: Требует MANAGE_CHANNELS. Лимит — 500 каналов (30013).
      operationId: createGuildChannel
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChannelCreate" }
      responses:
        "201":
          description: Созданный канал
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Channels]
      summary: Изменить порядок и родителей каналов
      operationId: reorderGuildChannels
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                type: object
                required: [id]
                properties:
                  id: { $ref: "#/components/schemas/Snowflake" }
                  position: { type: integer, minimum: 0 }
                  parent_id: { $ref: "#/components/schemas/SnowflakeNullable" }
      responses:
        "204": { description: Порядок обновлён }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/roles:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Роли сервера
      operationId: listGuildRoles
      responses:
        "200":
          description: Роли
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Role" }
    post:
      tags: [Guilds]
      summary: Создать роль
      description: Требует MANAGE_ROLES; выдать можно только права, которыми обладаешь. Лимит — 250 ролей (30005).
      operationId: createGuildRole
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RoleCreate" }
      responses:
        "201":
          description: Созданная роль
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Role" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Guilds]
      summary: Изменить порядок ролей
      operationId: reorderGuildRoles
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                type: object
                required: [id, position]
                properties:
                  id: { $ref: "#/components/schemas/Snowflake" }
                  position: { type: integer, minimum: 1 }
      responses:
        "200":
          description: Роли в новом порядке
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Role" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/roles/{roleId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - $ref: "#/components/parameters/roleId"
    patch:
      tags: [Guilds]
      summary: Изменить роль
      operationId: updateGuildRole
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RoleCreate" }
      responses:
        "200":
          description: Роль после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Role" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Guilds]
      summary: Удалить роль
      operationId: deleteGuildRole
      responses:
        "204": { description: Роль удалена }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /guilds/{guildId}/members:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Участники сервера (постранично)
      operationId: listGuildMembers
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 1000, default: 1 }
        - name: after
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Участники, отсортированные по user id
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Member" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/members/search:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Поиск участников по имени или нику
      operationId: searchGuildMembers
      parameters:
        - name: query
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 100 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 1000, default: 1 }
      responses:
        "200":
          description: Найденные участники
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Member" }

  /guilds/{guildId}/members/@me:
    parameters:
      - $ref: "#/components/parameters/guildId"
    patch:
      tags: [Guilds]
      summary: Изменить свой ник или аватар на сервере
      description: >-
        Ник требует CHANGE_NICKNAME (или MANAGE_NICKNAMES); пустой или null возвращает имя из профиля.
        avatar — аватар только для этого сервера, право «Кличат Плюс» (без него 403): data URI
        (PNG/GIF/JPEG/WebP до 2 МБ, не больше 1024×1024) или null — вернуть профильный, это можно
        и без «Плюса». Картинка — GET /member-avatars/{guild}/{hash}. Участники получают GUILD_MEMBER_UPDATE.
      operationId: updateCurrentMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nick: { type: [string, "null"], maxLength: 32 }
                avatar: { type: [string, "null"], description: "data URI или null" }
      responses:
        "200":
          description: Участник после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Member" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/members/{userId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - $ref: "#/components/parameters/userId"
    get:
      tags: [Guilds]
      summary: Участник
      operationId: getGuildMember
      responses:
        "200":
          description: Участник
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Member" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Guilds]
      summary: Изменить участника (ник, роли, таймаут, голос)
      description: MANAGE_NICKNAMES для ника, MANAGE_ROLES для ролей, MODERATE_MEMBERS для таймаута, MUTE/DEAFEN/MOVE_MEMBERS для голоса.
      operationId: updateGuildMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nick: { type: [string, "null"], maxLength: 32 }
                roles:
                  type: array
                  items: { $ref: "#/components/schemas/Snowflake" }
                mute: { type: boolean }
                deaf: { type: boolean }
                channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
                communication_disabled_until:
                  type: [string, "null"]
                  format: date-time
                  description: Таймаут до указанного момента, не больше 28 дней
      responses:
        "200":
          description: Участник после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Member" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Guilds]
      summary: Выгнать участника
      description: Требует KICK_MEMBERS и более высокую роль, чем у цели.
      operationId: kickGuildMember
      parameters:
        - $ref: "#/components/parameters/auditReason"
      responses:
        "204": { description: Участник исключён }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /guilds/{guildId}/members/{userId}/roles/{roleId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - $ref: "#/components/parameters/userId"
      - $ref: "#/components/parameters/roleId"
    put:
      tags: [Guilds]
      summary: Выдать роль
      operationId: addMemberRole
      responses:
        "204": { description: Роль выдана }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Guilds]
      summary: Снять роль
      operationId: removeMemberRole
      responses:
        "204": { description: Роль снята }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/bans:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Список банов
      operationId: listGuildBans
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
        - $ref: "#/components/parameters/after"
      responses:
        "200":
          description: Баны
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Ban" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/bans/{userId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - $ref: "#/components/parameters/userId"
    get:
      tags: [Guilds]
      summary: Бан пользователя
      operationId: getGuildBan
      responses:
        "200":
          description: Бан
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Ban" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Guilds]
      summary: Забанить
      description: Требует BAN_MEMBERS. Удаляет сообщения за указанное число секунд (до 7 дней).
      operationId: createGuildBan
      parameters:
        - $ref: "#/components/parameters/auditReason"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                delete_message_seconds: { type: integer, minimum: 0, maximum: 604800, default: 0 }
      responses:
        "204": { description: Пользователь забанен }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Guilds]
      summary: Разбанить
      operationId: deleteGuildBan
      parameters:
        - $ref: "#/components/parameters/auditReason"
      responses:
        "204": { description: Бан снят }
        "404": { $ref: "#/components/responses/NotFound" }

  /guilds/{guildId}/audit-logs:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Журнал аудита (90 дней)
      description: "Требует VIEW_AUDIT_LOG. Причина действия берётся из заголовка X-Audit-Log-Reason (до 512 символов) любого изменяющего запроса."
      operationId: getGuildAuditLog
      parameters:
        - name: user_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: action_type
          in: query
          schema: { type: integer }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
      responses:
        "200":
          description: Записи аудита
          content:
            application/json:
              schema:
                type: object
                required: [audit_log_entries, users]
                properties:
                  audit_log_entries:
                    type: array
                    items: { $ref: "#/components/schemas/AuditLogEntry" }
                  users:
                    type: array
                    items: { $ref: "#/components/schemas/User" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/emojis:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Эмодзи сервера
      operationId: listGuildEmojis
      responses:
        "200":
          description: Эмодзи
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Emoji" }
    post:
      tags: [Guilds]
      summary: Добавить эмодзи
      description: Требует MANAGE_GUILD_EXPRESSIONS. Лимит слотов зависит от уровня сервера (30008).
      operationId: createGuildEmoji
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, image]
              properties:
                name: { type: string, minLength: 2, maxLength: 32, pattern: "^[a-zA-Z0-9_]+$" }
                image: { type: string, description: "data URI, до 256 КБ" }
      responses:
        "201":
          description: Созданный эмодзи
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Emoji" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/emojis/{emojiId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - name: emojiId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Guilds]
      operationId: getGuildEmoji
      summary: Эмодзи сервера
      responses:
        "200":
          description: Эмодзи
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Emoji" }
        "404": { description: Нет такого эмодзи (10014) }
    patch:
      tags: [Guilds]
      operationId: updateGuildEmoji
      summary: Переименовать эмодзи
      description: Требует MANAGE_GUILD_EXPRESSIONS; аудит 61.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 2, maxLength: 32, pattern: "^[a-zA-Z0-9_]+$" }
      responses:
        "200":
          description: Эмодзи; участникам уйдёт GUILD_EMOJIS_UPDATE
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Emoji" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Guilds]
      operationId: deleteGuildEmoji
      summary: Удалить эмодзи
      description: Требует MANAGE_GUILD_EXPRESSIONS; файл удаляется, реакции с этим эмодзи остаются по id; аудит 62.
      responses:
        "204": { description: Удалено; GUILD_EMOJIS_UPDATE }
        "403": { $ref: "#/components/responses/Forbidden" }

  /emojis/{emojiId}:
    get:
      tags: [Guilds]
      operationId: emojiImage
      summary: Картинка эмодзи (публично)
      description: "`{id}.png`, `{id}.gif` — расширение только для браузера; ответ с Cache-Control на год."
      security: []
      parameters:
        - name: emojiId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Картинка }
        "404": { description: Нет такого эмодзи }

  /guilds/{guildId}/invites:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Invites]
      summary: Приглашения сервера
      description: Требует MANAGE_GUILD.
      operationId: listGuildInvites
      responses:
        "200":
          description: Приглашения
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Invite" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/messages/search:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Messages]
      summary: Поиск сообщений по серверу
      description: "Meilisearch по каналам, где у пользователя есть VIEW_CHANNEL и READ_MESSAGE_HISTORY; до 25 результатов за запрос, `offset` до 5000. Без `content` — новые сверху. 503 (40006), если индекс недоступен."
      operationId: searchGuildMessages
      parameters:
        - name: content
          in: query
          schema: { type: string }
        - name: author_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: channel_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: mentions
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: has
          in: query
          description: "Пока поддерживается только link; остальные значения появятся с вложениями"
          schema:
            type: string
            enum: [link, embed, file, video, image, sound]
        - name: min_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: max_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, maximum: 5000 }
      responses:
        "200":
          description: Результаты поиска
          content:
            application/json:
              schema:
                type: object
                required: [total_results, messages]
                properties:
                  total_results: { type: integer }
                  messages:
                    type: array
                    items:
                      type: array
                      items: { $ref: "#/components/schemas/Message" }
                      description: Найденное сообщение с контекстом до и после
        "403": { $ref: "#/components/responses/Forbidden" }

  # ---------------------------------------------------------------- Channels
  /channels/{channelId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Channels]
      summary: Канал
      operationId: getChannel
      responses:
        "200":
          description: Канал
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Channels]
      summary: Изменить канал
      description: "Требует MANAGE_CHANNELS. Для групп ЛС любой участник меняет `name` (до 100 символов, пустая строка убирает название) и `icon`; остальные поля игнорируются."
      operationId: updateChannel
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChannelUpdate" }
      responses:
        "200":
          description: Канал после изменения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Channels]
      summary: Удалить канал или покинуть группу
      description: "ЛС — скрыть из списка; группа — выйти (владение переходит следующему участнику, пустая группа удаляется); канал сервера — удалить."
      operationId: deleteChannel
      parameters:
        - $ref: "#/components/parameters/auditReason"
      responses:
        "200":
          description: Удалённый канал
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/recipients/{userId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/userId"
    put:
      tags: [Channels]
      summary: Добавить друга в группу
      description: "Только для групп ЛС (иначе 50003). Добавлять можно только своих друзей (50033), не больше 10 участников (50035). Всем участникам приходит CHANNEL_RECIPIENT_ADD и системное сообщение типа 1."
      operationId: addGroupRecipient
      responses:
        "204": { description: Добавлен }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Channels]
      summary: Исключить из группы
      description: "Владелец исключает участника (иначе 50013); свой id — то же, что выход. Остальным приходит CHANNEL_RECIPIENT_REMOVE и системное сообщение типа 2, исключённому — CHANNEL_DELETE."
      operationId: removeGroupRecipient
      responses:
        "204": { description: Исключён }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /channels/{channelId}/permissions/{overwriteId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - name: overwriteId
        in: path
        required: true
        description: ID роли или пользователя
        schema: { $ref: "#/components/schemas/Snowflake" }
    put:
      tags: [Channels]
      summary: Установить перекрытие прав
      description: Требует MANAGE_ROLES; менять можно только права, которыми обладаешь.
      operationId: putChannelPermission
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type: { type: integer, enum: [0, 1], description: "0 — роль, 1 — участник" }
                allow: { $ref: "#/components/schemas/Permissions" }
                deny: { $ref: "#/components/schemas/Permissions" }
      responses:
        "204": { description: Перекрытие сохранено }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Channels]
      summary: Удалить перекрытие
      operationId: deleteChannelPermission
      responses:
        "204": { description: Перекрытие удалено }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/messages:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Messages]
      summary: История сообщений
      description: Требует VIEW_CHANNEL и READ_MESSAGE_HISTORY. Ровно один из before/after/around.
      operationId: listMessages
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
        - $ref: "#/components/parameters/after"
        - name: around
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Сообщения от новых к старым
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Message" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Messages]
      summary: Отправить сообщение
      description: |
        Требует SEND_MESSAGES (ATTACH_FILES для вложений, MENTION_EVERYONE для @everyone).
        Лимиты: 2 000 символов (4 000 с «Кличат Плюс»), 10 вложений, 5 сообщений за 5 с на канал.
        `nonce` возвращается в событии MESSAGE_CREATE, чтобы клиент сопоставил оптимистичное сообщение.
      operationId: createMessage
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MessageCreate" }
      responses:
        "200":
          description: Созданное сообщение (200, как у Discord)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /channels/{channelId}/polls/{messageId}/answers/@me:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    put:
      tags: [Messages]
      summary: Проголосовать в опросе
      description: |
        Свои голоса целиком: новый список заменяет прежний, пустой — снимает голос. Нужна READ_MESSAGE_HISTORY.
        В опросе без `allow_multiselect` — не больше одного варианта. После итогов — 400/520001. Участникам канала
        уходят MESSAGE_POLL_VOTE_REMOVE и MESSAGE_POLL_VOTE_ADD по каждому изменённому варианту.
      operationId: votePoll
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [answer_ids]
              properties:
                answer_ids:
                  type: array
                  items: { type: string, examples: ["1"] }
      responses:
        "204": { description: Голос учтён }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/polls/{messageId}/answers/{answerId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
      - name: answerId
        in: path
        required: true
        schema: { type: integer, minimum: 1, maximum: 10 }
    get:
      tags: [Messages]
      summary: Кто голосовал за вариант
      operationId: listPollVoters
      parameters:
        - name: after
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        "200":
          description: Проголосовавшие по id пользователя
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items: { $ref: "#/components/schemas/User" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /channels/{channelId}/polls/{messageId}/expire:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    post:
      tags: [Messages]
      summary: Подвести итоги опроса раньше срока
      description: Только автор опроса. Опрос закрывается, в канал уходит сообщение типа 46 с итогами.
      operationId: expirePoll
      responses:
        "200":
          description: Сообщение с итогами опроса
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/messages/search:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Messages]
      summary: Поиск в личной беседе или группе
      description: "Только участникам этой беседы; поиск по тексту, автору и ссылкам. До 25 результатов, offset до 5000. Без content — новые сверху. Тексты берутся из БД, удалённые сообщения не возвращаются. После выхода из группы история недоступна."
      operationId: searchPrivateMessages
      parameters:
        - name: content
          in: query
          schema: { type: string }
        - name: author_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: mentions
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: has
          in: query
          description: "Сообщения с HTTP/HTTPS-ссылкой в тексте"
          schema:
            type: string
            enum: [link]
        - name: min_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: max_id
          in: query
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, maximum: 5000 }
      responses:
        "200":
          description: Результаты поиска
          content:
            application/json:
              schema:
                type: object
                required: [total_results, messages]
                properties:
                  total_results: { type: integer }
                  messages:
                    type: array
                    items:
                      type: array
                      items: { $ref: "#/components/schemas/Message" }
                      description: Массив с одним найденным сообщением; контекст загружается отдельно
        "403": { $ref: "#/components/responses/Forbidden" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503":
          description: Поиск временно недоступен (код 40006)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /channels/{channelId}/messages/{messageId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    get:
      tags: [Messages]
      summary: Сообщение
      operationId: getMessage
      responses:
        "200":
          description: Сообщение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Messages]
      summary: Редактировать своё сообщение или скрыть превью
      description: |
        `content` меняет только автор. `flags` (бит 4, SUPPRESS_EMBEDS) — автор или участник с MANAGE_MESSAGES
        на сервере: скрытие очищает `embeds`, снятие флага превью не восстанавливает. Нужно хотя бы одно из полей.
        С `content` упоминания (и роли, и @everyone) пересобираются из нового текста — по `allowed_mentions`
        правки, без него — все. Правка никого не зовёт повторно: счётчики и push не трогаются.
      operationId: updateMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content: { type: string, maxLength: 4000 }
                flags: { type: integer, description: "4 — скрыть превью ссылок (SUPPRESS_EMBEDS); остальные биты игнорируются" }
                embeds:
                  type: array
                  items: { $ref: "#/components/schemas/Embed" }
                components:
                  type: array
                  maxItems: 5
                  description: Кнопки и списки — только бот-автор; [] убирает их
                  items: { $ref: "#/components/schemas/Component" }
                allowed_mentions: { $ref: "#/components/schemas/AllowedMentions" }
                attachments:
                  type: array
                  description: Оставшиеся вложения; отсутствующие удаляются
                  items:
                    type: object
                    required: [id]
                    properties:
                      id: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Сообщение после правки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403":
          description: Чужое сообщение (50005)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    delete:
      tags: [Messages]
      summary: Удалить сообщение
      description: Своё — всегда; чужое — с MANAGE_MESSAGES.
      operationId: deleteMessage
      parameters:
        - $ref: "#/components/parameters/auditReason"
      responses:
        "204": { description: Сообщение удалено }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
      - $ref: "#/components/parameters/emoji"
    put:
      tags: [Messages]
      summary: Поставить реакцию
      description: Требует ADD_REACTIONS для первой реакции с данным эмодзи. Лимит — 20 разных эмодзи (30010).
      operationId: addReaction
      responses:
        "204": { description: Реакция добавлена }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Messages]
      summary: Снять свою реакцию
      operationId: removeOwnReaction
      responses:
        "204": { description: Реакция снята }

  /channels/{channelId}/messages/{messageId}/reactions/{emoji}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
      - $ref: "#/components/parameters/emoji"
    get:
      tags: [Messages]
      summary: Кто поставил реакцию
      operationId: listReactionUsers
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/after"
      responses:
        "200":
          description: Пользователи
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/User" }
    delete:
      tags: [Messages]
      summary: Снять все реакции с этим эмодзи
      description: Требует MANAGE_MESSAGES.
      operationId: removeReactionsForEmoji
      responses:
        "204": { description: Реакции сняты }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/messages/{messageId}/reactions:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    delete:
      tags: [Messages]
      summary: Снять все реакции
      description: Требует MANAGE_MESSAGES.
      operationId: removeAllReactions
      responses:
        "204": { description: Реакции сняты }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/messages/{messageId}/ack:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    post:
      tags: [Messages]
      summary: Отметить прочитанным до этого сообщения
      operationId: ackMessage
      description: Граница не двигается назад и не превышает последнее сообщение. Более новые сообщения сохраняют счётчик.
      parameters:
        - name: with_state
          in: query
          schema: { type: boolean, default: false }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                mention_count: { type: integer, minimum: 0 }
      responses:
        "204": { description: "read_state обновлён, остальным сессиям уйдёт MESSAGE_ACK" }
        "200":
          description: Актуальная граница и счётчик при with_state=true
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReadState" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /channels/{channelId}/messages/{messageId}/threads:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    post:
      tags: [Channels]
      summary: Создать ветку от сообщения
      description: |
        Нужны VIEW_CHANNEL и CREATE_PUBLIC_THREADS в канале. Id ветки равен id сообщения; у сообщения появляются
        флаг 32 (HAS_THREAD) и поле `thread` (MESSAGE_UPDATE). У одного сообщения только одна ветка (160004).
        Автор становится участником; писать в ветке разрешает SEND_MESSAGES_IN_THREADS.
      operationId: createThreadFromMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
                auto_archive_duration: { type: integer, enum: [60, 1440, 4320, 10080], default: 1440 }
      responses:
        "201":
          description: Канал-тред
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/threads:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Channels]
      summary: Создать ветку без сообщения
      description: В канал уходит системное сообщение типа 18 с названием ветки; id сообщения равен id ветки.
      operationId: createThread
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
                auto_archive_duration: { type: integer, enum: [60, 1440, 4320, 10080], default: 1440 }
      responses:
        "201":
          description: Канал-тред
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Channel" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/threads/archived/public:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Channels]
      summary: Архивные ветки канала
      description: От новых к старым по времени архивации; нужны VIEW_CHANNEL и READ_MESSAGE_HISTORY.
      operationId: listArchivedThreads
      parameters:
        - name: before
          in: query
          schema: { type: string, format: date-time }
          description: archive_timestamp последней полученной ветки
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        "200":
          description: Ветки и признак продолжения
          content:
            application/json:
              schema:
                type: object
                required: [threads, members, has_more]
                properties:
                  threads:
                    type: array
                    items: { $ref: "#/components/schemas/Channel" }
                  members:
                    type: array
                    items: { $ref: "#/components/schemas/ThreadMember" }
                  has_more: { type: boolean }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/thread-members:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Channels]
      summary: Участники ветки
      operationId: listThreadMembers
      responses:
        "200":
          description: Участники с профилями
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ThreadMember" }

  /channels/{channelId}/thread-members/@me:
    parameters:
      - $ref: "#/components/parameters/channelId"
    put:
      tags: [Channels]
      summary: Присоединиться к ветке
      description: |
        Автор ветки и все, кто в ней пишет, становятся участниками сами;
        в архивную ветку войти нельзя (50083). Для темы форума участие означает подписку.
        Без переопределений подписчик получает все ответы, остальные — только упоминания.
        Режим доставки выбирается в channel_overrides настроек сервера для ID темы;
        приоритет — тема, форум, явно выбранный режим сервера, затем подписка.
        Mute темы, форума, категории или сервера блокирует доставку независимо от режима.
      operationId: joinThread
      responses:
        "204": { description: Участник ветки }
        "400": { $ref: "#/components/responses/ValidationError" }
    delete:
      tags: [Channels]
      summary: Покинуть ветку
      description: Для темы форума снимает подписку; следующий ответ снова включает её. Свой явный режим уведомлений сохраняется.
      operationId: leaveThread
      responses:
        "204": { description: Больше не участник }

  /guilds/{guildId}/threads/active:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Активные ветки сервера
      description: Незаархивированные ветки в каналах, где у участника есть VIEW_CHANNEL, и его участие в них.
      operationId: listActiveThreads
      responses:
        "200":
          description: Ветки и участие запрашивающего
          content:
            application/json:
              schema:
                type: object
                required: [threads, members, has_more]
                properties:
                  threads:
                    type: array
                    items: { $ref: "#/components/schemas/Channel" }
                  members:
                    type: array
                    items: { $ref: "#/components/schemas/ThreadMember" }
                  has_more: { type: boolean }

  /channels/{channelId}/pins:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Channels]
      summary: Закреплённые сообщения
      operationId: listPins
      responses:
        "200":
          description: До 50 сообщений
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Message" }

  /channels/{channelId}/pins/{messageId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    put:
      tags: [Channels]
      summary: Закрепить сообщение
      description: Требует MANAGE_MESSAGES. Лимит — 50 закрепов (30003).
      operationId: pinMessage
      parameters:
        - $ref: "#/components/parameters/auditReason"
      responses:
        "204": { description: Закреплено }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Channels]
      summary: Открепить сообщение
      operationId: unpinMessage
      responses:
        "204": { description: Откреплено }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/typing:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Messages]
      summary: Индикатор «печатает» на 10 секунд
      operationId: triggerTyping
      responses:
        "204": { description: Участникам канала уйдёт TYPING_START }

  /channels/{channelId}/invites:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Invites]
      summary: Создать приглашение
      description: Требует CREATE_INSTANT_INVITE. Лимит — 5 в минуту, 1 000 активных на сервер (30016).
      operationId: createChannelInvite
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                max_age: { type: integer, minimum: 0, maximum: 604800, default: 86400, description: "0 — бессрочно" }
                max_uses: { type: integer, minimum: 0, maximum: 100, default: 0 }
                temporary: { type: boolean, default: false }
                unique: { type: boolean, default: false }
      responses:
        "201":
          description: Приглашение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invite" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /channels/{channelId}/attachments:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Messages]
      summary: Получить presigned URL для загрузки вложений
      description: |
        Нужны SEND_MESSAGES и ATTACH_FILES. Клиент загружает файл напрямую в S3 по `upload_url` (PUT с тем же
        Content-Type), затем ссылается на `upload_filename` в `attachments[].uploaded_filename` при отправке сообщения
        (content может быть пустым). Лимиты бесплатного тарифа: 25 МБ, 10 файлов, исполняемые файлы не принимаются;
        незавершённые загрузки удаляются через час. Размер и тип проверяются по объекту в S3 при отправке.
      operationId: createAttachmentUploads
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [files]
              properties:
                files:
                  type: array
                  minItems: 1
                  maxItems: 10
                  items:
                    type: object
                    required: [filename, file_size]
                    properties:
                      id: { type: string, description: Локальный id для сопоставления }
                      filename: { type: string, maxLength: 255 }
                      file_size: { type: integer, minimum: 1 }
                      content_type: { type: string }
      responses:
        "200":
          description: Ссылки для загрузки, действуют 10 минут
          content:
            application/json:
              schema:
                type: object
                required: [attachments]
                properties:
                  attachments:
                    type: array
                    items:
                      type: object
                      required: [id, upload_url, upload_filename]
                      properties:
                        id: { type: string }
                        upload_url: { type: string, format: uri }
                        upload_filename: { type: string }
        "400": { $ref: "#/components/responses/ValidationError" }
        "413":
          description: Файл слишком большой (40005)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /channels/{channelId}/voice/join:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Voice]
      summary: Войти в голосовой канал
      description: |
        Требует CONNECT (и SPEAK, чтобы говорить). Возвращает адрес узла LiveKit и JWT комнаты
        (комната = channel_id). Участникам сервера уйдёт VOICE_STATE_UPDATE (спецификация 5.4).
        В ЛС и группах тот же вызов начинает звонок или присоединяет к идущему: первый вошедший создаёт
        системное сообщение типа 3 и `CALL_CREATE` с `ringing` (все остальные получатели), следующие — `CALL_UPDATE`;
        выход последнего — `CALL_DELETE` и `MESSAGE_UPDATE` с `call.ended_timestamp`. `guild_id` в ответе — null.
      operationId: joinVoiceChannel
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                self_mute: { type: boolean, default: false }
                self_deaf: { type: boolean, default: false }
                session_id: { type: string, description: "Сессия шлюза (session_id из READY): её отключение снимает голосовое состояние" }
                region: { type: string, description: "Предпочитаемый регион SFU, по умолчанию ближайший" }
      responses:
        "200":
          description: Параметры подключения к SFU; повторный вход в тот же канал обновляет токен, вход в другой — переводит
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VoiceJoin" }
        "400":
          description: Не голосовой канал (50024)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403":
          description: Нет права CONNECT (50013) или канал полон (50013, «Voice channel is full»; MOVE_MEMBERS обходит лимит)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /channels/{channelId}/call:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Voice]
      operationId: getCall
      summary: Идущий звонок в ЛС или группе
      responses:
        "200":
          description: Звонок с состояниями участников
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Call" }
        "400": { description: Не личные сообщения (50024) }
        "403": { description: Не получатель канала (50013) }
        "404": { description: Звонка нет }

  /channels/{channelId}/call/ring:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Voice]
      operationId: ringCall
      summary: Позвонить получателям ещё раз
      description: Нужно самому быть в звонке. `recipients` null — всем, кто не в звонке. Звонок длится 60 секунд, потом `CALL_UPDATE` с пустым `ringing`.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                recipients:
                  type: [array, "null"]
                  items: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "204": { description: Уйдёт CALL_UPDATE }
        "400": { description: Вы не в звонке (40032) }
        "404": { description: Звонка нет }

  /channels/{channelId}/call/stop-ringing:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Voice]
      operationId: stopRinging
      summary: Перестать звонить
      description: "`recipients` null — отклонить входящий звонок самому; иначе перестать звонить перечисленным."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                recipients:
                  type: [array, "null"]
                  items: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "204": { description: Уйдёт CALL_UPDATE }
        "404": { description: Звонка нет }

  /users/@me/voice:
    get:
      tags: [Voice]
      summary: Своё голосовое состояние
      operationId: getMyVoiceState
      responses:
        "200":
          description: Состояние
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VoiceState" }
        "404":
          description: Не в голосовом канале (40032)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    patch:
      tags: [Voice]
      summary: Свои микрофон, звук, экран, камера
      operationId: updateMyVoiceState
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                self_mute: { type: boolean }
                self_deaf: { type: boolean }
                self_stream: { type: boolean }
                self_video: { type: boolean }
      responses:
        "200":
          description: Состояние; участникам уходит VOICE_STATE_UPDATE
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VoiceState" }
        "400":
          description: Не в голосовом канале (40032)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    delete:
      tags: [Voice]
      summary: Выйти из голосового канала
      description: "То же, что op 4 с channel_id null. Без состояния — тоже 204."
      operationId: leaveVoice
      responses:
        "204": { description: Вышли }

  /voice/webhook:
    post:
      tags: [Voice]
      summary: Вебхук LiveKit
      description: |
        Только для сервера LiveKit: подпись проверяется ключом API (заголовок Authorization с JWT).
        participant_joined подтверждает вход, participant_left и room_finished снимают состояния.
      operationId: voiceWebhook
      security: []
      responses:
        "204": { description: Принято }
        "401": { description: Подпись неверна }

  /voice/regions:
    get:
      tags: [Voice]
      summary: Регионы SFU
      operationId: listVoiceRegions
      responses:
        "200":
          description: Регионы
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required: [id, name, optimal]
                  properties:
                    id: { type: string, examples: [msk, nsk] }
                    name: { type: string, examples: [Москва, Новосибирск] }
                    optimal: { type: boolean, description: Ближайший к клиенту по измеренному RTT }
                    deprecated: { type: boolean }

  # ---------------------------------------------------------------- Invites
  /invites/{code}:
    parameters:
      - $ref: "#/components/parameters/inviteCode"
    get:
      tags: [Invites]
      summary: Превью приглашения
      operationId: getInvite
      security: []
      parameters:
        - name: with_counts
          in: query
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: Приглашение с кратким объектом сервера
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invite" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Invites]
      summary: Принять приглашение
      description: Ботам недоступно (20001). Забаненным — 40007. Проверка уровня верификации сервера — 40002.
      operationId: acceptInvite
      responses:
        "200":
          description: Приглашение и сервер, куда вступил пользователь
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invite" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Invites]
      summary: Отозвать приглашение
      description: Создатель приглашения или MANAGE_CHANNELS / MANAGE_GUILD.
      operationId: deleteInvite
      parameters:
        - $ref: "#/components/parameters/auditReason"
      responses:
        "200":
          description: Отозванное приглашение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invite" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ---------------------------------------------------------------- Auto Moderation
  /guilds/{guildId}/auto-moderation/rules:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      operationId: listAutoModerationRules
      summary: Правила автомода сервера
      description: Требует MANAGE_GUILD. Проверка выполняется при отправке и правке сообщений; администраторы сервера не проверяются.
      responses:
        "200":
          description: Список правил
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/AutoModerationRule" }
        "403": { description: Нет права MANAGE_GUILD (50013) }
    post:
      tags: [Guilds]
      operationId: createAutoModerationRule
      summary: Создать правило автомода
      description: |
        Лимиты как у Discord: 6 правил KEYWORD, по одному SPAM, KEYWORD_PRESET и MENTION_SPAM; до 1000 ключевых слов по 60 символов,
        10 регулярных выражений RE2 по 260 символов, до 100 исключений (1000 для списков), лимит упоминаний 1–50, таймаут до 28 дней
        (только для KEYWORD, KEYWORD_PRESET и MENTION_SPAM), канал оповещений — текстовый канал сервера.
        Ключевые слова: `слово` — целиком, `слово*` — начало, `*слово` — конец, `*слово*` — где угодно.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AutoModerationRuleInput" }
      responses:
        "200":
          description: Правило создано; участникам уйдёт AUTO_MODERATION_RULE_CREATE
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutoModerationRule" }
        "400": { description: Ошибка формы (50035) или лимит правил }
  /guilds/{guildId}/auto-moderation/rules/{ruleId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - name: ruleId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Guilds]
      operationId: getAutoModerationRule
      summary: Правило автомода
      responses:
        "200":
          description: Правило
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutoModerationRule" }
        "404": { description: Правила нет }
    patch:
      tags: [Guilds]
      operationId: updateAutoModerationRule
      summary: Изменить правило автомода
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AutoModerationRuleInput" }
      responses:
        "200":
          description: Правило изменено; AUTO_MODERATION_RULE_UPDATE
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutoModerationRule" }
        "404": { description: Правила нет }
    delete:
      tags: [Guilds]
      operationId: deleteAutoModerationRule
      summary: Удалить правило автомода
      responses:
        "204": { description: Удалено; AUTO_MODERATION_RULE_DELETE }
        "404": { description: Правила нет }

  # ---------------------------------------------------------------- Moderation (staff)
  /moderation/reports:
    get:
      tags: [Reports]
      operationId: moderationReports
      summary: Очередь жалоб (сотрудники)
      description: Только для аккаунтов с битом STAFF в `flags` (`klichat staff <username>`); иначе 403/50001. Новые первыми, `before` — id для следующей страницы.
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [open, resolved, rejected], default: open } }
        - { name: before, in: query, schema: { $ref: "#/components/schemas/Snowflake" } }
        - { name: limit, in: query, schema: { type: integer, maximum: 100, default: 50 } }
      responses:
        "200":
          description: Жалобы со снимками объектов
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ModerationReport" }
        "403": { description: Не сотрудник (50001) }
  /moderation/reports/{reportId}:
    parameters:
      - name: reportId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    patch:
      tags: [Reports]
      operationId: moderationResolveReport
      summary: Закрыть жалобу
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [resolved, rejected] }
                resolution: { type: string, maxLength: 500 }
      responses:
        "200":
          description: Закрыта; запись в журнале
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationReport" }
        "400": { description: Уже закрыта или неверный статус }
        "404": { description: Жалобы нет }
  /moderation/users/search:
    get:
      tags: [Users]
      operationId: moderationFindUser
      summary: Карточка аккаунта по имени (сотрудники)
      parameters:
        - { name: username, in: query, required: true, schema: { type: string } }
      responses:
        "200":
          description: Карточка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationUser" }
        "404": { description: Нет такого аккаунта }
  /moderation/users/{userId}:
    parameters:
      - $ref: "#/components/parameters/userId"
    get:
      tags: [Users]
      operationId: moderationUser
      summary: Карточка аккаунта (сотрудники)
      responses:
        "200":
          description: Карточка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationUser" }
        "404": { description: Нет такого аккаунта }
  /moderation/users/{userId}/ban:
    parameters:
      - $ref: "#/components/parameters/userId"
    put:
      tags: [Users]
      operationId: moderationBan
      summary: Заблокировать аккаунт
      description: Все сессии отзываются; вход и запросы с токеном отвечают 403/40007 с причиной и сроком. `until` null — бессрочно.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 500 }
                until: { type: [string, "null"], format: date-time }
      responses:
        "200":
          description: Карточка после блокировки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationUser" }
        "400": { description: Нельзя применить к себе (50035) или срок вне допустимого }
    delete:
      tags: [Users]
      operationId: moderationUnban
      summary: Снять блокировку
      responses:
        "200":
          description: Карточка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationUser" }
  /moderation/users/{userId}/restriction:
    parameters:
      - $ref: "#/components/parameters/userId"
    put:
      tags: [Users]
      operationId: moderationRestrict
      summary: Теневое ограничение
      description: До срока (не больше 90 дней) аккаунт не может отправлять заявки в друзья и создавать приглашения (403/20025); остальное работает.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [until]
              properties:
                reason: { type: string, maxLength: 500 }
                until: { type: string, format: date-time }
      responses:
        "200":
          description: Карточка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationUser" }
    delete:
      tags: [Users]
      operationId: moderationUnrestrict
      summary: Снять ограничение
      responses:
        "200":
          description: Карточка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModerationUser" }
  /moderation/messages/{channelId}/{messageId}:
    parameters:
      - $ref: "#/components/parameters/channelId"
      - $ref: "#/components/parameters/messageId"
    delete:
      tags: [Messages]
      operationId: moderationDeleteMessage
      summary: Удалить любое сообщение (сотрудники)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 500 }
      responses:
        "204": { description: "Удалено; MESSAGE_DELETE участникам, запись в журнале" }
        "404": { description: Сообщения нет }
  /moderation/actions:
    get:
      tags: [Reports]
      operationId: moderationActions
      summary: Журнал действий сотрудников
      parameters:
        - { name: before, in: query, schema: { $ref: "#/components/schemas/Snowflake" } }
        - { name: limit, in: query, schema: { type: integer, maximum: 100, default: 50 } }
      responses:
        "200":
          description: Действия, новые первыми
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ModerationAction" }

  # ---------------------------------------------------------------- Push
  /users/@me/push:
    get:
      tags: [Users]
      operationId: getPushSubscriptions
      summary: VAPID-ключ и push-подписки пользователя
      description: Подписки привязаны к сессии — выход из неё или отзыв убирают их.
      responses:
        "200":
          description: Ключ и подписки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PushInfo" }
        "401": { description: Нет токена }
    put:
      tags: [Users]
      operationId: putPushSubscription
      summary: Сохранить push-подписку текущего браузера
      description: Повтор с тем же `endpoint` обновляет ключи и сессию. До 20 подписок на пользователя.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PushSubscriptionInput" }
      responses:
        "200":
          description: Подписка сохранена
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PushSubscriptionInfo" }
        "400":
          description: "Push выключен на сервере (код 40006), некорректная подписка (50035) или слишком много подписок"
    delete:
      tags: [Users]
      operationId: deletePushSubscription
      summary: Удалить push-подписку
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [endpoint]
              properties:
                endpoint: { type: string, format: uri }
      responses:
        "204": { description: Удалено }
        "404": { description: Такой подписки нет }

  # ---------------------------------------------------------------- Reports
  /reports:
    post:
      tags: [Reports]
      summary: Пожаловаться на сообщение, пользователя или сервер
      operationId: createReport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target_type, target_id, category]
              properties:
                target_type: { type: string, enum: [message, user, guild] }
                target_id: { $ref: "#/components/schemas/Snowflake" }
                category:
                  type: string
                  enum: [spam, fraud, harassment, nsfw, violence, csam, extremism, copyright, other]
                text: { type: string, maxLength: 1000 }
      description: |
        Объект должен быть виден жалующемуся: сообщение — в доступном канале, сервер — где он участник.
        На себя, свои сообщения и свой сервер пожаловаться нельзя (50035). Со снимком объекта жалоба попадает
        в очередь платформенной модерации (`klichat reports` до появления панели). Лимит — 10 жалоб в час.
      responses:
        "201":
          description: Жалоба принята в очередь модерации
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReportResult" }
        "200":
          description: "Открытая жалоба от этого аккаунта на объект уже есть (existing: true), новая не создаётся"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReportResult" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404":
          description: Объект не найден или не виден
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /attachments/{sig}/{id}/{filename}:
    get:
      tags: [Messages]
      summary: Скачать вложение
      description: |
        Подписанная ссылка из `attachments[].url` объекта сообщения: без авторизации, но только с верной подписью.
        Картинки, видео, аудио, PDF и текст отдаются inline, остальное — как скачивание. Файл читается из S3 через API.
      operationId: getAttachment
      security: []
      parameters:
        - name: sig
          in: path
          required: true
          schema: { type: string }
        - name: id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: filename
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Файл
          content:
            "*/*":
              schema: { type: string, format: binary }
        "404":
          description: Подпись неверна или вложения нет
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /external/{sig}/{url}:
    get:
      tags: [Messages]
      summary: Картинка превью через прокси
      description: |
        Отдаёт внешнюю картинку из `embed.*.proxy_url`: без авторизации (её нет у `<img>`), но только по HMAC-подписи
        сервера, поэтому открытым прокси эндпоинт не является. Только `image/*` (не SVG), до 8 МиБ, кэш сутки.
        Внешний сайт видит адрес сервера, а не пользователя (спецификация 7.2).
      operationId: externalImage
      security: []
      parameters:
        - name: sig
          in: path
          required: true
          schema: { type: string, description: "base64url(HMAC-SHA256 от второго сегмента)" }
        - name: url
          in: path
          required: true
          schema: { type: string, description: "base64url исходного адреса" }
      responses:
        "200":
          description: Картинка
          content:
            image/*:
              schema: { type: string, format: binary }
        "404":
          description: Подпись неверна или картинка недоступна
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "415":
          description: По адресу не картинка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  # ---------------------------------------------------------------- Import (Discord)
  /guilds/{guildId}/imports:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Задания импорта сервера
      operationId: listGuildImports
      responses:
        "200":
          description: Последние 20 заданий
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ImportJob" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/imports/discord:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: "Перенос из Discord: состояние входа и серверы владельца"
      operationId: getDiscordImport
      responses:
        "200":
          description: Настроен ли перенос, ссылка входа и серверы, где владелец может переносить
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled: { type: boolean }
                  authorize_url: { type: string, format: uri }
                  linked: { type: boolean }
                  guilds:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        icon: { type: [string, "null"] }
                        owner: { type: boolean }
                        can_import: { type: boolean }
                        bot_present: { type: boolean, description: Бот Кличата уже на сервере }
    post:
      tags: [Guilds]
      summary: Снять структуру сервера Discord и завести задание
      description: Бот Кличата должен быть на исходном сервере. Коды переноса отдаются один раз.
      operationId: createDiscordImport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source_guild_id]
              properties:
                source_guild_id: { type: string }
                claims: { type: boolean, description: Сохранить роли участников под кодами переноса }
      responses:
        "201":
          description: Задание и коды переноса
          content:
            application/json:
              schema:
                type: object
                properties:
                  job: { $ref: "#/components/schemas/ImportJob" }
                  codes:
                    type: array
                    items:
                      type: object
                      properties:
                        username: { type: string }
                        display_name: { type: string }
                        code: { type: string }
        "400": { description: Нет прав на сервере Discord или бот ещё не добавлен }
        "501": { description: Перенос из Discord не настроен }

  /discord/link:
    post:
      tags: [Guilds]
      summary: Возврат из Discord после входа
      operationId: discordLink
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
                state: { type: string }
      responses:
        "200":
          description: Сервер Кличата из подписанного state и серверы владельца в Discord
          content:
            application/json:
              schema:
                type: object
                properties:
                  guild_id: { $ref: "#/components/schemas/Snowflake" }
  /discord/invite:
    get:
      tags: [Guilds]
      summary: Ссылка добавления бота Кличата на сервер Discord
      operationId: discordInvite
      parameters:
        - { name: guild_id, in: query, required: true, schema: { type: string } }
      responses:
        "200":
          description: Ссылка
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }

  /guilds/{guildId}/imports/{jobId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - name: jobId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Guilds]
      summary: Задание с предпросмотром или отчётом
      operationId: getGuildImport
      responses:
        "200":
          description: Задание
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ImportJob" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Guilds]
      summary: Отменить задание
      description: Незапущенное или завершённое; архив удаляется из хранилища.
      operationId: cancelGuildImport
      responses:
        "204": { description: Отменено }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }

  /guilds/{guildId}/imports/{jobId}/start:
    post:
      tags: [Guilds]
      summary: Запустить импорт с выбором
      description: |
        Статус `ready` или `failed`. Пустой список означает «всё»; переносятся роли → категории → каналы с ролевыми перекрытиями → ветки →
        эмодзи → стикеры → настройки → коды переноса. Прогресс — GUILD_IMPORT_UPDATE `{import: {status, progress}}`, результат — `report`.
      operationId: startGuildImport
      parameters:
        - $ref: "#/components/parameters/guildId"
        - name: jobId
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Snowflake" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ImportOptions" }
      responses:
        "200":
          description: Задание в статусе running
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ImportJob" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }

  /guilds/{guildId}/import-claims:
    post:
      tags: [Guilds]
      summary: Код переноса из Discord
      description: Участник вводит код из claims-codes.csv экспортёра и получает роли и личные перекрытия прав; код одноразовый, 90 дней, 5 попыток в час.
      operationId: claimGuildImport
      parameters:
        - $ref: "#/components/parameters/guildId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, examples: [ABCD-EFGH-JKLM] }
      responses:
        "200":
          description: Выданные роли
          content:
            application/json:
              schema:
                type: object
                required: [roles]
                properties:
                  roles:
                    type: array
                    items: { $ref: "#/components/schemas/Role" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ---------------------------------------------------------------- Stickers
  /guilds/{guildId}/stickers:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Стикеры сервера
      operationId: listGuildStickers
      responses:
        "200":
          description: Список стикеров
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Sticker" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Guilds]
      summary: Добавить стикер
      description: MANAGE_GUILD_EXPRESSIONS; PNG, APNG или GIF до 512 КБ в data URI, имя 2–30 символов; 5 слотов базово (400/30039). Аудит 90, событие GUILD_STICKERS_UPDATE.
      operationId: createGuildSticker
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, file]
              properties:
                name: { type: string, minLength: 2, maxLength: 30 }
                description: { type: string, maxLength: 100 }
                tags: { type: string, maxLength: 200, description: "Ключевые слова через запятую" }
                file: { type: string, description: "data:image/png;base64,… (PNG, APNG, GIF)" }
      responses:
        "201":
          description: Созданный стикер
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Sticker" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/stickers/{stickerId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - name: stickerId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Guilds]
      summary: Стикер
      operationId: getGuildSticker
      responses:
        "200":
          description: Стикер
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Sticker" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Guilds]
      summary: Изменить стикер
      description: Имя, описание, теги (MANAGE_GUILD_EXPRESSIONS). Аудит 91.
      operationId: updateGuildSticker
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 2, maxLength: 30 }
                description: { type: string, maxLength: 100 }
                tags: { type: string, maxLength: 200 }
      responses:
        "200":
          description: Обновлённый стикер
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Sticker" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Guilds]
      summary: Удалить стикер
      description: Файл удаляется из S3, в старых сообщениях стикер пропадает. Аудит 92.
      operationId: deleteGuildSticker
      responses:
        "204": { description: Удалён }
        "404": { $ref: "#/components/responses/NotFound" }

  /stickers/{sticker}:
    get:
      tags: [Guilds]
      summary: Картинка стикера
      description: "`/stickers/{id}.png` или `.gif` — публично, с долгим кэшем."
      operationId: getStickerImage
      security: []
      parameters:
        - name: sticker
          in: path
          required: true
          schema: { type: string, examples: ["1547686336335450112.png"] }
      responses:
        "200":
          description: Картинка
          content:
            image/png: { schema: { type: string, format: binary } }
            image/gif: { schema: { type: string, format: binary } }
        "404": { $ref: "#/components/responses/NotFound" }

  # ---------------------------------------------------------------- Soundboard
  /soundboard-default-sounds:
    get:
      tags: [Voice]
      summary: Стандартные звуки звуковой панели
      description: Есть у всех; id маленькие (1–999), как у Discord, `guild_id` нет. Файлы — `GET /soundboard-sounds/{id}`.
      operationId: listDefaultSoundboardSounds
      responses:
        "200":
          description: Стандартные звуки
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/SoundboardSound" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /guilds/{guildId}/soundboard-sounds:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Guilds]
      summary: Звуки сервера
      description: Любой участник. Как у Discord — список в поле `items`, с авторами (`user`).
      operationId: listGuildSoundboardSounds
      responses:
        "200":
          description: Звуки сервера
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/SoundboardSound" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Guilds]
      summary: Добавить звук
      description: |
        CREATE_GUILD_EXPRESSIONS или MANAGE_GUILD_EXPRESSIONS. MP3, OGG (Opus или Vorbis) или WAV до 512 КБ
        и 5,2 секунды в data URI; имя 2–32 символа, уникальное на сервере. Слоты по уровню сервера: 8 / 24 / 36 / 48
        (400/30045). Аудит 130, событие GUILD_SOUNDBOARD_SOUND_CREATE.
      operationId: createGuildSoundboardSound
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, sound]
              properties:
                name: { type: string, minLength: 2, maxLength: 32 }
                sound: { type: string, description: "data:audio/mpeg;base64,… (MP3, OGG, WAV)" }
                volume: { type: number, minimum: 0, maximum: 1, default: 1 }
                emoji_id: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }], description: "Эмодзи этого сервера" }
                emoji_name: { type: [string, "null"], description: "Обычный эмодзи; при emoji_id не нужен" }
      responses:
        "201":
          description: Созданный звук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SoundboardSound" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /guilds/{guildId}/soundboard-sounds/{soundId}:
    parameters:
      - $ref: "#/components/parameters/guildId"
      - name: soundId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Guilds]
      summary: Звук сервера
      operationId: getGuildSoundboardSound
      responses:
        "200":
          description: Звук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SoundboardSound" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Guilds]
      summary: Изменить звук
      description: Имя, громкость, эмодзи (null — снять). Свой звук — с CREATE_GUILD_EXPRESSIONS, любой — с MANAGE_GUILD_EXPRESSIONS. Аудит 131, событие GUILD_SOUNDBOARD_SOUND_UPDATE.
      operationId: updateGuildSoundboardSound
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 2, maxLength: 32 }
                volume: { type: number, minimum: 0, maximum: 1 }
                emoji_id: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }] }
                emoji_name: { type: [string, "null"] }
      responses:
        "200":
          description: Обновлённый звук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SoundboardSound" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Guilds]
      summary: Удалить звук
      description: Права — как у правки. Файл удаляется из S3. Аудит 132, событие GUILD_SOUNDBOARD_SOUND_DELETE.
      operationId: deleteGuildSoundboardSound
      responses:
        "204": { description: Удалён }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /channels/{channelId}/send-soundboard-sound:
    parameters:
      - $ref: "#/components/parameters/channelId"
    post:
      tags: [Voice]
      summary: Сыграть звук в голосовом канале
      description: |
        Только в канале, где сейчас сам (иначе 400/40032). В канале сервера нужны SPEAK и USE_SOUNDBOARD;
        с выключенным звуком, серверным запретом говорить и на сцене без слова — 403. Звук другого сервера
        (в звонке ЛС — любой серверный) — если человек там состоит, у него «Кличат Плюс», а в канале сервера есть
        USE_EXTERNAL_SOUNDS. Не больше трёх звуков за 6 секунд (429). Всем в канале, включая самого, уходит
        VOICE_CHANNEL_EFFECT_SEND: звук каждый клиент играет сам со своей громкостью панели.
      operationId: sendSoundboardSound
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sound_id]
              properties:
                sound_id: { $ref: "#/components/schemas/Snowflake" }
                source_guild_id: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }], description: "Сервер звука; для стандартных не нужен" }
      responses:
        "204": { description: Звук отправлен }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /soundboard-sounds/{sound}:
    get:
      tags: [Voice]
      summary: Файл звука
      description: "Стандартный звук (MP3, кэш на сутки) или звук сервера из S3 (кэш навсегда: новый файл — новый id). Публично."
      operationId: getSoundboardSoundFile
      security: []
      parameters:
        - name: sound
          in: path
          required: true
          schema: { type: string, examples: ["1", "1554471218277543936"] }
      responses:
        "200":
          description: Звук
          content:
            audio/mpeg: { schema: { type: string, format: binary } }
            audio/ogg: { schema: { type: string, format: binary } }
            audio/wav: { schema: { type: string, format: binary } }
        "404": { $ref: "#/components/responses/NotFound" }

  # ---------------------------------------------------------------- Webhooks
  # ---------------------------------------------------------------- Webhooks
  /channels/{channelId}/webhooks:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Webhooks]
      summary: Вебхуки канала
      description: MANAGE_WEBHOOKS в канале; в объектах есть `token` и `url`.
      operationId: listChannelWebhooks
      responses:
        "200":
          description: Список вебхуков
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Webhook" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Webhooks]
      summary: Создать вебхук
      description: MANAGE_WEBHOOKS в текстовом канале сервера; до 15 вебхуков на канал (400/30007). Аудит 50, событие WEBHOOKS_UPDATE.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
                avatar: { type: string, description: "data URI; пока не сохраняется" }
      responses:
        "200":
          description: Вебхук с токеном
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /guilds/{guildId}/webhooks:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Webhooks]
      summary: Вебхуки сервера
      operationId: listGuildWebhooks
      responses:
        "200":
          description: Список вебхуков всех каналов
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Webhook" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /webhooks/{webhookId}:
    parameters:
      - name: webhookId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Webhooks]
      summary: Вебхук
      operationId: getWebhook
      responses:
        "200":
          description: Вебхук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Webhooks]
      summary: Изменить вебхук
      description: Имя и перенос в другой текстовый канал того же сервера (MANAGE_WEBHOOKS в обоих). Аудит 51.
      operationId: updateWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
                channel_id: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Обновлённый вебхук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Webhooks]
      summary: Удалить вебхук
      operationId: deleteWebhook
      responses:
        "204": { description: Удалён }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks/{webhookId}/{webhookToken}:
    parameters:
      - name: webhookId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
      - name: webhookToken
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Webhooks]
      summary: Вебхук по токену
      description: Без входа; в ответе нет создателя.
      operationId: getWebhookByToken
      security: []
      responses:
        "200":
          description: Вебхук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Webhooks]
      summary: Переименовать вебхук по токену
      operationId: updateWebhookByToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
      responses:
        "200":
          description: Обновлённый вебхук
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Webhooks]
      summary: Удалить вебхук по токену
      operationId: deleteWebhookByToken
      security: []
      responses:
        "204": { description: Удалён }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Webhooks]
      summary: Исполнить вебхук
      description: |
        Без входа — токен в пути (неверный → 401/50027). Формат тела как у Discord: `content` до 2000 символов или до 10 `embeds`
        (rich-карточки с `fields`), `username` переопределяет имя автора. Автор сообщения — вебхук (`bot: true`, `webhook_id`).
        Упоминания и @everyone разбираются как у участника с MENTION_EVERYONE — мостам, которые пересылают чужой текст,
        стоит слать `allowed_mentions: {"parse": []}`. 30 сообщений в минуту на вебхук (429).
        `?wait=true` возвращает сообщение, иначе 204. Кнопки (`components`) вебхуку не положены — 400/50035.

        Тот же путь с id приложения и токеном взаимодействия — дополнительное сообщение к ответу бота (ADR 0010): сервер
        сначала ищет вебхук с таким id, а не найдя — взаимодействие. Тело — InteractionMessageData, ответ — всегда сообщение.
      operationId: executeWebhook
      security: []
      parameters:
        - name: wait
          in: query
          schema: { type: boolean, default: false }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookExecute" }
      responses:
        "204": { description: Принято (wait=false) }
        "200":
          description: Созданное сообщение (wait=true)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /webhooks/{webhookId}/{webhookToken}/github:
    post:
      tags: [Webhooks]
      summary: Вебхук GitHub
      description: |
        Принимает стандартный вебхук GitHub (заголовок `X-GitHub-Event`) и превращает его в карточку от имени «GitHub»:
        push (коммиты, force push, новые и удалённые ветки), create/delete, pull_request (открыт, черновик, готов к ревью,
        влит, закрыт), pull_request_review, issues, issue_comment, release, workflow_run, fork, star, discussion.
        ping и незнакомые события — 204 без сообщения.
      operationId: executeGitHubWebhook
      security: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: webhookToken
          in: path
          required: true
          schema: { type: string }
        - name: X-GitHub-Event
          in: header
          required: true
          schema: { type: string }
        - name: wait
          in: query
          schema: { type: boolean, default: false }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "204": { description: Принято или событие пропущено }
        "200":
          description: Созданное сообщение (wait=true)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks/{webhookId}/{webhookToken}/slack:
    post:
      tags: [Webhooks]
      summary: Вебхук Slack
      description: |
        Принимает тело входящего вебхука Slack (`text`, `username`, `icon_url`, `attachments` с `color`, `pretext`, `title`,
        `text`, `fields`, `image_url`, `footer`, `ts`) как JSON или `application/x-www-form-urlencoded` с полем `payload`.
        mrkdwn переводится в markdown (`<url|текст>`, `*жирный*`, `~зачёркнутый~`, `<!channel>` → @everyone). Ответ — текст `ok`.
      operationId: executeSlackWebhook
      security: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Snowflake" }
        - name: webhookToken
          in: path
          required: true
          schema: { type: string }
        - name: wait
          in: query
          schema: { type: boolean, default: false }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                payload: { type: string, description: JSON Slack }
      responses:
        "200":
          description: "«ok» текстом; при wait=true — сообщение"
          content:
            text/plain:
              schema: { type: string }
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /webhooks/{webhookId}/{webhookToken}/messages/{messageId}:
    parameters:
      - name: webhookId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
      - name: webhookToken
        in: path
        required: true
        schema: { type: string }
      - name: messageId
        in: path
        required: true
        description: "id сообщения; у взаимодействий ещё `@original` (можно кодированным `%40original`) — ответ бота"
        schema: { type: string }
    get:
      tags: [Webhooks]
      summary: Сообщение вебхука или ответа на взаимодействие
      description: Для id приложения и токена взаимодействия — ответ (`@original`) или дополнительное сообщение этого взаимодействия.
      operationId: getWebhookMessage
      security: []
      responses:
        "200":
          description: Сообщение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Webhooks]
      summary: Изменить сообщение вебхука
      description: |
        У взаимодействий — правка ответа или дополнительного сообщения (тело — InteractionMessageData, можно с кнопками); первая
        правка «думает…» снимает флаг 128.
      operationId: editWebhookMessage
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content: { type: string, maxLength: 2000 }
                embeds:
                  type: array
                  maxItems: 10
                  items: { $ref: "#/components/schemas/Embed" }
                allowed_mentions: { $ref: "#/components/schemas/AllowedMentions" }
      responses:
        "200":
          description: Обновлённое сообщение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Webhooks]
      summary: Удалить сообщение вебхука
      operationId: deleteWebhookMessage
      security: []
      responses:
        "204": { description: Удалено }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ---------------------------------------------------------------- Applications (боты)
  /applications:
    get:
      tags: [Applications]
      summary: Мои приложения
      description: Приложения текущего пользователя с ботами и числом серверов. Ботам недоступно (403/20001).
      operationId: listApplications
      responses:
        "200":
          description: Список приложений
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Application" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Applications]
      summary: Создать приложение с ботом
      description: |
        Создаёт приложение и его бот-аккаунт (username выводится из названия). Требует принятия соглашения разработчика;
        при `KLICHAT_APPLICATIONS_REQUIRE_MFA` — включённой 2FA (403/60003). Не больше 25 приложений у владельца (400/30032).
        Токен бота возвращается один раз.
      operationId: createApplication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, developer_agreement]
              properties:
                name: { type: string, minLength: 2, maxLength: 32 }
                description: { type: string, maxLength: 400 }
                developer_agreement: { type: boolean, description: "Принято соглашение разработчика (docs/legal/developer-agreement-outline.md)" }
      responses:
        "201":
          description: Приложение и токен бота
          content:
            application/json:
              schema:
                type: object
                required: [application, token]
                properties:
                  application: { $ref: "#/components/schemas/Application" }
                  token: { type: string, description: "klb_<application_id>.<43 символа base64url>; показывается один раз" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /applications/{appId}:
    parameters:
      - name: appId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Applications]
      summary: Приложение владельца
      operationId: getApplication
      responses:
        "200":
          description: Приложение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Application" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Applications]
      summary: Изменить приложение
      description: Имя (меняет и отображаемое имя бота), описание, публичность, redirect URI (только https или http://localhost), привилегированные intents.
      operationId: updateApplication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 2, maxLength: 32 }
                description: { type: string, maxLength: 400 }
                bot_public: { type: boolean }
                bot_require_code_grant: { type: boolean }
                redirect_uris:
                  type: array
                  maxItems: 10
                  items: { type: string, format: uri }
                privileged_intents: { type: integer, description: "Биты 1<<1 GUILD_MEMBERS, 1<<8 GUILD_PRESENCES, 1<<15 MESSAGE_CONTENT" }
      responses:
        "200":
          description: Обновлённое приложение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Application" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Applications]
      summary: Удалить приложение
      description: Бот покидает все серверы (управляемые роли удаляются), токены отзываются, сессии шлюза закрываются кодом 4004, бот-аккаунт гасится.
      operationId: deleteApplication
      responses:
        "204": { description: Удалено }
        "404": { $ref: "#/components/responses/NotFound" }

  /applications/{appId}/bot/reset:
    post:
      tags: [Applications]
      summary: Перевыпустить токен бота
      description: Старые токены отзываются, сессии шлюза бота закрываются кодом 4004. Новый токен показывается один раз.
      operationId: resetBotToken
      parameters:
        - name: appId
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Новый токен
          content:
            application/json:
              schema:
                type: object
                required: [token]
                properties:
                  token: { type: string }
        "404": { $ref: "#/components/responses/NotFound" }

  /applications/{appId}/guilds:
    get:
      tags: [Applications]
      summary: Серверы, где стоит бот приложения
      operationId: listApplicationGuilds
      parameters:
        - name: appId
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Snowflake" }
      responses:
        "200":
          description: Частичные объекты серверов
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/GuildPartial" }
        "404": { $ref: "#/components/responses/NotFound" }

  /applications/@me:
    get:
      tags: [Applications]
      summary: Приложение текущего бота
      description: Только по токену бота (пользователю — 403/20002). Тот же ответ у `GET /oauth2/applications/@me`, который discord.py читает при `login()`.
      operationId: getOwnApplication
      security:
        - botAuth: []
      responses:
        "200":
          description: Приложение в форме Discord
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Application" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /oauth2/applications/@me:
    get:
      tags: [Applications]
      summary: Приложение текущего бота (псевдоним для библиотек)
      operationId: getOwnApplicationOAuth
      security:
        - botAuth: []
      responses:
        "200":
          description: Приложение в форме Discord
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Application" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /oauth2/@me:
    get:
      tags: [Applications]
      summary: Сведения о текущем токене бота
      operationId: getOAuthMe
      security:
        - botAuth: []
      responses:
        "200":
          description: Приложение, scope и пользователь токена
          content:
            application/json:
              schema:
                type: object
                required: [application, scopes, expires, user]
                properties:
                  application: { $ref: "#/components/schemas/Application" }
                  scopes:
                    type: array
                    items: { type: string }
                  expires: { type: "null" }
                  user: { $ref: "#/components/schemas/User" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /oauth2/authorize:
    parameters:
      - name: client_id
        in: query
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
      - name: scope
        in: query
        schema: { type: string, default: bot, description: "Через пробел: bot, applications.commands, identify" }
      - name: permissions
        in: query
        schema: { $ref: "#/components/schemas/Permissions" }
      - name: guild_id
        in: query
        schema: { $ref: "#/components/schemas/Snowflake" }
    get:
      tags: [Applications]
      summary: Данные страницы добавления бота
      description: |
        Приложение, бот и серверы пользователя с MANAGE_GUILD. Приватного бота (`bot_public: false`) видит только владелец приложения.
        Ссылка авторизации в формате Discord ведёт на страницу веб-клиента `/oauth2/authorize?client_id=…&scope=bot&permissions=…&guild_id=…`;
        `GET /api/oauth2/authorize` без версии перенаправляет туда же (такой адрес собирает `generateInvite` в discord.js).
      operationId: getOAuthAuthorize
      responses:
        "200":
          description: Данные для подтверждения
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthorizeInfo" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Applications]
      summary: Добавить бота на сервер
      description: |
        Нужен scope `bot` и право MANAGE_GUILD на сервере. Бот получает управляемую роль с запрошенными правами, урезанными до прав
        приглашающего (ADMINISTRATOR — только от администратора или владельца). Повторная авторизация обновляет права роли.
        События: GUILD_ROLE_CREATE, GUILD_MEMBER_ADD, системное сообщение типа 7, боту — GUILD_CREATE; аудит 28 (BOT_ADD).
      operationId: postOAuthAuthorize
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [guild_id]
              properties:
                guild_id: { $ref: "#/components/schemas/Snowflake" }
                permissions: { $ref: "#/components/schemas/Permissions" }
                authorize: { type: boolean, default: true }
      responses:
        "200":
          description: Бот добавлен
          content:
            application/json:
              schema:
                type: object
                required: [guild_id, permissions, member, location]
                properties:
                  guild_id: { $ref: "#/components/schemas/Snowflake" }
                  permissions: { $ref: "#/components/schemas/Permissions" }
                  member: { $ref: "#/components/schemas/Member" }
                  location: { type: string, format: uri, description: "Адрес сервера в веб-клиенте" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ---------------------------------------------------------------- Interactions (ADR 0010)
  /applications/{appId}/commands:
    parameters:
      - $ref: "#/components/parameters/appId"
    get:
      tags: [Interactions]
      summary: Глобальные команды приложения
      description: Бот своего приложения (`Bot klb_…`) или владелец приложения. Чужое приложение — 403/50001 боту, 404/10002 человеку.
      operationId: listApplicationCommands
      responses:
        "200":
          description: Команды
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApplicationCommand" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Interactions]
      summary: Создать или обновить команду
      description: |
        Upsert по (тип, имя), как у Discord: новая — 201, существующая — 200 с прежним id. Проверки имени, описания, опций и
        пределов (100 слэш-команд, по 5 команд меню) — как у Discord; ошибки — 400/50035 с `errors` по полям («options.0.name»),
        сверх предела — 400/30032.
      operationId: createApplicationCommand
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApplicationCommandInput" }
      responses:
        "200":
          description: Команда с тем же типом и именем обновлена
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "201":
          description: Команда создана
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Interactions]
      summary: Заменить все команды
      description: |
        Bulk overwrite: команды с тем же (тип, имя) или с указанным `id` сохраняют id (так переименовывают), остальные создаются,
        лишние удаляются. Неизменные команды сохраняют и версию. Ошибки — 400/50035 с номером команды в пути («1.name»).
      operationId: overwriteApplicationCommands
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              maxItems: 110
              items: { $ref: "#/components/schemas/ApplicationCommandInput" }
      responses:
        "200":
          description: Новый список в порядке запроса
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApplicationCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /applications/{appId}/commands/{commandId}:
    parameters:
      - $ref: "#/components/parameters/appId"
      - $ref: "#/components/parameters/commandId"
    get:
      tags: [Interactions]
      summary: Команда
      operationId: getApplicationCommand
      responses:
        "200":
          description: Команда
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Interactions]
      summary: Изменить команду
      description: Меняет только присланные поля; `null` сбрасывает локализации и `contexts`. Тип не меняется.
      operationId: editApplicationCommand
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApplicationCommandInput" }
      responses:
        "200":
          description: Команда после правки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Interactions]
      summary: Удалить команду
      operationId: deleteApplicationCommand
      responses:
        "204": { description: Удалена }
        "404": { $ref: "#/components/responses/NotFound" }
  /applications/{appId}/guilds/{guildId}/commands:
    parameters:
      - $ref: "#/components/parameters/appId"
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Interactions]
      summary: Команды приложения на сервере
      description: Только если бот приложения состоит на сервере, иначе 403/50001.
      operationId: listGuildApplicationCommands
      responses:
        "200":
          description: Команды сервера
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApplicationCommand" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Interactions]
      summary: Создать или обновить команду сервера
      operationId: createGuildApplicationCommand
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApplicationCommandInput" }
      responses:
        "200":
          description: Обновлена
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "201":
          description: Создана
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Interactions]
      summary: Заменить команды сервера
      operationId: overwriteGuildApplicationCommands
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items: { $ref: "#/components/schemas/ApplicationCommandInput" }
      responses:
        "200":
          description: Новый список
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApplicationCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /applications/{appId}/guilds/{guildId}/commands/{commandId}:
    parameters:
      - $ref: "#/components/parameters/appId"
      - $ref: "#/components/parameters/guildId"
      - $ref: "#/components/parameters/commandId"
    get:
      tags: [Interactions]
      summary: Команда сервера
      operationId: getGuildApplicationCommand
      responses:
        "200":
          description: Команда
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Interactions]
      summary: Изменить команду сервера
      operationId: editGuildApplicationCommand
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApplicationCommandInput" }
      responses:
        "200":
          description: Команда после правки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommand" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Interactions]
      summary: Удалить команду сервера
      operationId: deleteGuildApplicationCommand
      responses:
        "204": { description: Удалена }
        "404": { $ref: "#/components/responses/NotFound" }
  /guilds/{guildId}/application-command-index:
    parameters:
      - $ref: "#/components/parameters/guildId"
    get:
      tags: [Interactions]
      summary: Список команд для «/» на сервере
      description: |
        Только людям. Глобальные команды приложений, чей бот на сервере, серверные команды этого сервера и встроенная музыка
        (на каждом сервере). Только слэш-команды с контекстом «сервер» и только те, что человек может вызвать по
        `default_member_permissions` (владелец и администратор — все). Не участник — 403.
      operationId: guildApplicationCommandIndex
      responses:
        "200":
          description: Приложения и их команды
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommandIndex" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /channels/{channelId}/application-command-index:
    parameters:
      - $ref: "#/components/parameters/channelId"
    get:
      tags: [Interactions]
      summary: Список команд для «/» в канале
      description: |
        В личке с ботом — его глобальные команды, где `contexts` содержит 1 (или null и `dm_permission` не false). В канале
        сервера — то же, что список сервера. В остальных личках — пустые списки.
      operationId: channelApplicationCommandIndex
      responses:
        "200":
          description: Приложения и их команды
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApplicationCommandIndex" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /interactions:
    post:
      tags: [Interactions]
      summary: Вызвать команду, нажать кнопку, запросить подсказки
      description: |
        Только людям (бот — 403/20001). Сервер проверяет права (`USE_APPLICATION_COMMANDS` в канале, бот на сервере — кроме
        встроенной музыки, `default_member_permissions`; у кнопок — что канал виден), значения опций и кнопки, создаёт
        взаимодействие, отдаёт его боту (`INTERACTION_CREATE`) и ждёт первого ответа до 3 секунд. Предел — 5 вызовов за 5 секунд
        на человека, у подсказок (type 4) — 20 за 5 секунд. `message` ошибок 400/403 — короткий текст для человека.
      operationId: createInteraction
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InteractionRequest" }
      responses:
        "204": { description: Бот ответил на команду или кнопку; сообщение придёт событием шлюза }
        "200":
          description: Варианты автодополнения
          content:
            application/json:
              schema:
                type: object
                required: [choices]
                properties:
                  choices:
                    type: array
                    maxItems: 25
                    items: { $ref: "#/components/schemas/ApplicationCommandChoice" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "504":
          description: "Бот не ответил за 3 секунды: `{code: 10062, message: «Приложение не ответило»}`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /interactions/{interactionId}/{interactionToken}/callback:
    parameters:
      - name: interactionId
        in: path
        required: true
        schema: { $ref: "#/components/schemas/Snowflake" }
      - name: interactionToken
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Interactions]
      summary: Ответ бота на взаимодействие
      description: |
        Без входа — токен в пути, как у Discord (discord.js шлёт его без Authorization). JSON или multipart с `payload_json` и
        `files[n]`. Первый ответ — не позже 3 секунд после создания, иначе 404/10062; повторный — 400/40060. Типы: 4 сообщение,
        5 «думает…», 6 и 7 — только у кнопок (ничего видимого / правка сообщения с кнопкой), 8 — варианты автодополнения;
        9 (модалка) и неподходящие — 400/50035.
      operationId: interactionCallback
      security: []
      parameters:
        - name: with_response
          in: query
          schema: { type: boolean, default: false }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InteractionCallback" }
          multipart/form-data:
            schema:
              type: object
              properties:
                payload_json: { type: string, description: InteractionCallback строкой }
      responses:
        "204": { description: Ответ принят }
        "200":
          description: Объект ответа (with_response=true)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InteractionCallbackResult" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Access-токен пользователя: `Authorization: Bearer <jwt>` (живёт 15 минут)"
    botAuth:
      type: apiKey
      in: header
      name: Authorization
      description: "Токен бота: `Authorization: Bot klb_<application_id>.<секрет>`"

  parameters:
    guildId:
      name: guildId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    channelId:
      name: channelId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    messageId:
      name: messageId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    userId:
      name: userId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    roleId:
      name: roleId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    appId:
      name: appId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    commandId:
      name: commandId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Snowflake" }
    inviteCode:
      name: code
      in: path
      required: true
      schema: { type: string, pattern: "^[A-Za-z0-9]{8}$" }
    emoji:
      name: emoji
      in: path
      required: true
      description: Unicode-эмодзи (URL-encoded) или `name:id` для кастомного
      schema: { type: string }
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    before:
      name: before
      in: query
      schema: { $ref: "#/components/schemas/Snowflake" }
    after:
      name: after
      in: query
      schema: { $ref: "#/components/schemas/Snowflake" }
    auditReason:
      name: X-Audit-Log-Reason
      in: header
      description: Причина действия, попадает в журнал аудита (до 512 символов)
      schema: { type: string, maxLength: 512 }

  responses:
    Unauthorized:
      description: Нет или просрочен токен (40001)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            unauthorized:
              value: { code: 40001, message: "Unauthorized" }
    Forbidden:
      description: Нет доступа (50001) или прав (50013)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            missingPermissions:
              value: { code: 50013, message: "Missing Permissions" }
    NotFound:
      description: Объект не найден (100xx)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            unknownChannel:
              value: { code: 10003, message: "Unknown Channel" }
    ValidationError:
      description: Ошибка валидации тела (50035)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            invalidFormBody:
              value:
                code: 50035
                message: "Invalid Form Body"
                errors:
                  username:
                    _errors:
                      - code: BASE_TYPE_BAD_LENGTH
                        message: "Must be between 2 and 32 in length."
    RateLimited:
      description: Превышен лимит запросов
      headers:
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
        X-RateLimit-Reset:
          schema: { type: number, description: "Unix-время сброса, секунды" }
        X-RateLimit-Reset-After:
          schema: { type: number, description: Секунды до сброса }
        X-RateLimit-Bucket:
          schema: { type: string }
        X-RateLimit-Global:
          schema: { type: boolean }
        X-RateLimit-Scope:
          schema: { type: string, enum: [user, global, shared] }
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/RateLimited" }

  schemas:
    SavedEntry:
      type: object
      required: [id, channel_id, message_id, saved_at, available]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        channel_id: { $ref: "#/components/schemas/Snowflake" }
        message_id: { $ref: "#/components/schemas/Snowflake" }
        saved_at: { type: string, format: date-time }
        available: { type: boolean }
        channel: { $ref: "#/components/schemas/Channel" }
        guild_name: { type: string }
        message: { $ref: "#/components/schemas/Message" }
    SavedPage:
      type: object
      required: [items, has_more, next_before]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/SavedEntry" }
        has_more: { type: boolean }
        next_before: { type: [string, "null"] }
    ReadState:
      type: object
      required: [id, last_message_id, mention_count]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        last_message_id: { type: [string, "null"] }
        mention_count: { type: integer, minimum: 0 }
    InboxEntry:
      type: object
      required: [channel, messages, jump_message_id, last_message_id, unread]
      properties:
        channel: { $ref: "#/components/schemas/Channel" }
        guild_name: { type: string }
        messages:
          type: array
          items: { $ref: "#/components/schemas/Message" }
        jump_message_id: { $ref: "#/components/schemas/Snowflake" }
        last_message_id: { $ref: "#/components/schemas/Snowflake" }
        unread: { type: boolean }
    InboxPage:
      type: object
      required: [items, has_more, next_before]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/InboxEntry" }
        has_more: { type: boolean }
        next_before: { type: [string, "null"] }
    GifPage:
      type: object
      properties:
        items:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              title: { type: string }
              preview: { $ref: "#/components/schemas/GifImage" }
              full: { $ref: "#/components/schemas/GifImage" }
        has_next: { type: boolean }
    GifImage:
      type: object
      properties:
        url: { type: string, format: uri }
        width: { type: integer }
        height: { type: integer }
    ReleaseItem:
      type: object
      required: [text]
      properties:
        text: { type: string, maxLength: 600, description: Текст в разметке чата }
        image: { type: string, description: "Имя картинки из POST /moderation/releases/images" }
    Announcement:
      type: object
      required: [style, audience, label, button, target]
      properties:
        style: { type: string, enum: [promo, info] }
        audience: { type: string, enum: [existing, all], description: "existing — зарегистрированные до первой публикации, all — включая новых пользователей" }
        label: { type: string, maxLength: 40 }
        button: { type: string, maxLength: 40 }
        target: { type: string, enum: [none, catalog, help, url] }
        url: { type: string, maxLength: 2048, description: "Для target=url — HTTPS, без логина и пароля" }
        games: { type: array, maxItems: 6, uniqueItems: true, items: { type: string, enum: [dota2, cs2, deadlock, minecraft, gta5rp, roblox] } }
    ReleaseInput:
      type: object
      required: [title, items]
      properties:
        announcement: { $ref: "#/components/schemas/Announcement" }
        title: { type: string, maxLength: 100 }
        items: { type: array, minItems: 1, maxItems: 6, items: { $ref: "#/components/schemas/ReleaseItem" } }
    Release:
      type: object
      required: [id, title, items, published_at, updated_at]
      properties:
        announcement: { $ref: "#/components/schemas/Announcement" }
        id: { $ref: "#/components/schemas/Snowflake" }
        title: { type: string }
        items: { type: array, items: { $ref: "#/components/schemas/ReleaseItem" } }
        published_at: { type: [string, "null"], format: date-time, description: "null — черновик" }
        updated_at: { type: string, format: date-time }
    StaffRelease:
      allOf:
        - $ref: "#/components/schemas/Release"
        - type: object
          properties:
            created_at: { type: string, format: date-time }
            unpublished_at: { type: [string, "null"], format: date-time }
            card: { type: boolean, description: Показывается карточкой }
            official: { type: boolean, description: "Есть пост в #обновления" }
            official_error: { type: string, description: Пост в канал не вышел или не обновился }
            seen: { type: integer, description: Видели карточку — открыли или закрыли }
            opened: { type: integer, description: Открыли выпуск }
            audience: { type: integer, description: Могли увидеть — зарегистрированы до выхода }
    DiscoveryCard:
      type: object
      description: Карточка сервера в каталоге.
      properties:
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        slug: { type: string }
        name: { type: string }
        icon: { type: [string, "null"] }
        banner: { type: [string, "null"] }
        description: { type: string }
        category: { type: string }
        tags: { type: array, items: { type: string } }
        approximate_member_count: { type: integer }
        approximate_presence_count: { type: integer }
        state: { type: string, enum: [listed, auto_hidden, removed] }
        reports_open: { type: integer }
    DiscoverySettings:
      type: object
      properties:
        listed: { type: boolean }
        state: { type: string, enum: [none, listed, auto_hidden, removed] }
        slug: { type: string }
        category: { type: string }
        tags: { type: array, items: { type: string } }
        description: { type: string }
        removed_reason: { type: [string, "null"] }
        url: { type: string }
        problems:
          type: array
          description: Невыполненные условия публикации.
          items:
            type: object
            properties:
              key: { type: string }
              text: { type: string }
    GuildPreview:
      type: object
      description: Сервер для тех, кто ещё не вступил.
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        icon: { type: [string, "null"] }
        banner: { type: [string, "null"] }
        description: { type: [string, "null"] }
        features: { type: array, items: { type: string } }
        approximate_member_count: { type: integer }
        approximate_presence_count: { type: integer }
        emojis: { type: array, items: { $ref: "#/components/schemas/Emoji" } }
        stickers: { type: array, items: { $ref: "#/components/schemas/Sticker" } }
    GuildLimits:
      type: object
      description: Лимиты сервера по уровню от бустов.
      properties:
        tier: { type: integer, enum: [0, 1, 2, 3] }
        emojis: { type: integer }
        stickers: { type: integer }
        sounds: { type: integer, description: "Слоты звуковой панели" }
        bitrate_kbps: { type: integer }
        upload_mb: { type: integer }
    UserLimits:
      type: object
      description: Что даёт подписка «Кличат Плюс».
      properties:
        plus: { type: boolean }
        upload_mb: { type: integer }
        message_len: { type: integer }
        stream_height: { type: integer }
        stream_fps: { type: integer }
        any_emoji: { type: boolean }
        animated_avatar: { type: boolean }
    Boost:
      type: object
      description: Слот буста. guild_id = null — слот свободен.
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        guild_id: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }] }
        ends_at: { type: string, format: date-time }
    BillingStatus:
      type: object
      properties:
        plus: { type: boolean }
        plus_until: { type: [string, "null"], format: date-time }
        auto_renew: { type: boolean }
        boosts: { type: array, items: { $ref: "#/components/schemas/Boost" } }
        boosts_free: { type: integer }
        limits: { $ref: "#/components/schemas/UserLimits" }
        provider: { type: string, enum: [none, yookassa] }
        payments:
          type: array
          items:
            type: object
            properties:
              id: { $ref: "#/components/schemas/Snowflake" }
              plan: { type: string }
              amount_rub: { type: integer }
              status: { type: string, enum: [pending, succeeded, canceled] }
              created_at: { type: string, format: date-time }
    GuildPremium:
      type: object
      properties:
        tier: { type: integer, enum: [0, 1, 2, 3] }
        boosts: { type: integer }
        next_tier: { type: [integer, "null"] }
        next_need: { type: [integer, "null"] }
        limits: { $ref: "#/components/schemas/GuildLimits" }
        boosters:
          type: array
          description: Кто бустит — одна строка на человека, по дате его первого буста
          items:
            type: object
            properties:
              user_id: { $ref: "#/components/schemas/Snowflake" }
              username: { type: string, description: "Отображаемое имя, если задано, иначе ник" }
              user: { $ref: "#/components/schemas/User" }
              boosts: { type: integer, minimum: 1, description: "Сколько бустов человек поставил на сервер" }
              since: { type: [string, "null"], format: date-time, description: "Когда поставлен первый из них" }
    Snowflake:
      type: string
      pattern: "^\\d{1,20}$"
      description: 64-битный идентификатор строкой
      examples: ["1173629456812345678"]
    SnowflakeNullable:
      type: [string, "null"]
      pattern: "^\\d{1,20}$"
    Permissions:
      type: string
      pattern: "^\\d{1,20}$"
      description: 64-битная маска прав строкой (спецификация 6.3)
      examples: ["3072"]
    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: integer, description: "Код ошибки, как у Discord" }
        message: { type: string }
        errors: { type: object, additionalProperties: true }
    RateLimited:
      type: object
      required: [message, retry_after, global]
      properties:
        message: { type: string }
        retry_after: { type: number, description: Секунды }
        global: { type: boolean }
        code: { type: integer }
    TokenPair:
      type: object
      required: [access_token, refresh_token, expires_in, token_type]
      properties:
        access_token: { type: string }
        refresh_token: { type: string }
        expires_in: { type: integer, description: Секунды жизни access-токена, examples: [900] }
        token_type: { type: string, const: Bearer }
        user: { $ref: "#/components/schemas/CurrentUser" }
    User:
      type: object
      required: [id, username, global_name, avatar]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        username: { type: string }
        global_name: { type: [string, "null"], description: Отображаемое имя }
        discriminator: { type: string, const: "0", description: "Всегда «0», для совместимости с библиотеками" }
        avatar: { type: [string, "null"], description: Хэш аватара }
        banner: { type: [string, "null"] }
        accent_color: { type: [integer, "null"] }
        bot: { type: boolean }
        public_flags: { type: integer }
        premium_type: { type: integer, enum: [0, 2], description: "0 — нет, 2 — «Кличат Плюс»" }
        avatar_decoration_data:
          description: "Рамка аватара, как у Discord; null — без рамки. Надетое по «Плюсу» видно, пока подписка действует"
          oneOf: [{ $ref: "#/components/schemas/AvatarDecoration" }, { type: "null" }]
        display_name_styles:
          description: "Стиль ника; null — обычный"
          oneOf: [{ $ref: "#/components/schemas/NameStyle" }, { type: "null" }]
        collectibles:
          description: "Табличка — фон строки в списке участников, как у Discord; null — без неё"
          oneOf:
            - type: object
              properties:
                nameplate:
                  oneOf:
                    - type: object
                      properties:
                        sku_id: { $ref: "#/components/schemas/Snowflake" }
                        asset: { type: string, description: "Картинка: /decorations/plate-{asset}.webp", examples: [space] }
                        label: { type: string, examples: [Космос] }
                        palette: { type: string }
                    - { type: "null" }
            - { type: "null" }
        primary_guild:
          description: "Тег сервера рядом с ником, как у Discord (color — наш); null — без тега"
          oneOf:
            - type: object
              properties:
                identity_guild_id: { $ref: "#/components/schemas/Snowflake" }
                identity_enabled: { type: boolean }
                tag: { type: string, examples: [ВЕЧ] }
                badge: { type: "null" }
                color: { type: integer }
            - { type: "null" }
    AvatarDecoration:
      type: object
      required: [asset, sku_id]
      properties:
        asset: { type: string, description: "Имя картинки: /decorations/frame-{asset}.webp (512 px) и frame-{asset}-256.webp", examples: [klich] }
        sku_id: { $ref: "#/components/schemas/Snowflake" }
    NameStyle:
      type: object
      description: >-
        Стиль ника, как display_name_styles у Discord, но шрифты и эффекты свои.
        font_id: 0 — обычный, 1 — Unbounded, 2 — Russo One, 3 — Press Start 2P, 4 — Pacifico, 5 — Comfortaa, 6 — Rubik Mono One.
        effect_id: 0 — цвет, 1 — градиент из двух цветов, 2 — свечение, 3 — неон.
      required: [font_id, effect_id, colors]
      properties:
        font_id: { type: integer, minimum: 0, maximum: 6 }
        effect_id: { type: integer, minimum: 0, maximum: 3 }
        colors: { type: array, maxItems: 2, items: { type: integer, minimum: 0, maximum: 16777215 }, description: "Цвета 0xRRGGBB; градиенту нужны два" }
    ProfileEffect:
      type: object
      required: [id]
      properties:
        id: { type: string, description: "Имя картинки: /decorations/effect-{id}.webp (480 px) и effect-{id}-768.webp", examples: [snow] }
    Decorations:
      type: object
      description: Экран «Украшения» — что есть, что доступно и почему, что надето
      required: [frames, effects, name_style, plus, current]
      properties:
        frames: { type: array, items: { $ref: "#/components/schemas/DecorationEntry" } }
        effects: { type: array, items: { $ref: "#/components/schemas/DecorationEntry" } }
        nameplates: { type: array, items: { $ref: "#/components/schemas/DecorationEntry" } }
        name_style:
          type: object
          description: "Стиль ника целиком — с «Плюсом»; эффект «Неон» — ещё и за достижение «Диджей»"
          properties:
            state: { $ref: "#/components/schemas/DecorationState" }
            neon: { $ref: "#/components/schemas/DecorationState" }
        theme: { $ref: "#/components/schemas/DecorationState" }
        server_tags:
          type: array
          description: Серверы человека с тегом — из них выбирают, чей носить
          items:
            type: object
            properties:
              guild_id: { $ref: "#/components/schemas/Snowflake" }
              guild_name: { type: string }
              guild_icon: { type: [string, "null"] }
              tag: { type: string }
              color: { type: integer }
        plus: { type: boolean, description: Действует ли «Кличат Плюс» }
        current:
          type: object
          description: "Надетое сейчас, включая выбранное по «Плюсу» после его конца (…_plus — выбрано по подписке)"
          properties:
            avatar_decoration: { type: [string, "null"] }
            profile_effect: { type: [string, "null"] }
            nameplate: { type: [string, "null"] }
            display_name_styles: { oneOf: [{ $ref: "#/components/schemas/NameStyle" }, { type: "null" }] }
            avatar_decoration_plus: { type: boolean }
            profile_effect_plus: { type: boolean }
            display_name_styles_plus: { type: boolean }
            theme_colors: { oneOf: [{ type: array, items: { type: integer } }, { type: "null" }] }
            profile_showcase: { type: array, items: { type: string } }
            server_tag: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }] }
    Achievement:
      type: object
      required: [id, name, description, goal, progress, unit, rewards, earned_at, new]
      properties:
        id: { type: string, examples: [voice_100h] }
        name: { type: string, examples: [Голос отряда] }
        description: { type: string, examples: [100 часов в голосовых каналах] }
        goal: { type: integer, examples: [100] }
        progress: { type: integer, examples: [12] }
        unit: { type: string, description: "Родительный падеж: «12 из 100 часов»", examples: [часов] }
        rewards:
          type: array
          items:
            type: object
            properties:
              item: { type: string, examples: ["frame:pricel"] }
              name: { type: string, examples: [Рамка «Прицел»] }
        earned_at: { type: [string, "null"], format: date-time }
        new: { type: boolean, description: Получено после последнего открытия раздела }
    DecorationState:
      type: string
      enum: [owned, plus, season, locked]
      description: "owned — выдано навсегда; plus — по «Плюсу»; season — идёт сезон, надеть — и останется навсегда; locked — закрыто"
    DecorationEntry:
      type: object
      required: [id, name, state, plus]
      properties:
        id: { type: string, examples: [klich] }
        name: { type: string, examples: [Клич] }
        state: { $ref: "#/components/schemas/DecorationState" }
        plus: { type: boolean, description: Предмет «Плюса» }
        achievement:
          description: За какое достижение выдаётся и сколько уже набрано
          oneOf:
            - type: object
              properties:
                id: { type: string }
                name: { type: string }
                progress: { type: integer, description: "В показанных единицах: часы, дни, участники" }
                goal: { type: integer }
                unit: { type: string, examples: [часов] }
            - { type: "null" }
        season:
          description: Сезонный предмет
          oneOf:
            - type: object
              properties:
                name: { type: string, examples: [Осень] }
                dates: { type: string, examples: ["с 1 сентября по 30 ноября"] }
                active: { type: boolean }
                until: { type: string, format: date-time, description: Конец текущего или ближайшего сезона }
            - { type: "null" }
    Profile:
      allOf:
        - $ref: "#/components/schemas/User"
        - type: object
          required: [bio, created_at, premium]
          properties:
            bio: { type: string, description: Строчка о себе }
            created_at: { type: string, format: date-time, description: Когда зарегистрировался }
            premium: { type: boolean, description: Действует ли «Кличат Плюс» }
            profile_effect:
              description: "Эффект карточки профиля; null — без эффекта"
              oneOf: [{ $ref: "#/components/schemas/ProfileEffect" }, { type: "null" }]
            theme_colors:
              description: "Цвета профиля [основной, акцент] 0xRRGGBB — рамка и фон карточки (с «Плюсом»); null — обычные"
              oneOf: [{ type: array, items: { type: integer }, minItems: 2, maxItems: 2 }, { type: "null" }]
            badges:
              type: array
              description: "Значки: staff, early (до 1 января 2027), plus, booster (бустит сейчас), friends (5 приглашённых), voice (100 часов в голосе), dev (есть бот)"
              items:
                type: object
                properties:
                  id: { type: string, examples: [early] }
                  name: { type: string, examples: [Ранний участник] }
            achievements_showcase:
              type: array
              description: "Витрина — до трёх полученных достижений: выбранные человеком, иначе три последних кроме «Первого слова»"
              items:
                type: object
                properties:
                  id: { type: string }
                  name: { type: string }
                  description: { type: string }
            mutual_guild_ids:
              type: array
              description: Серверы, где состоят оба
              items: { $ref: "#/components/schemas/Snowflake" }
            mutual_friends:
              type: array
              description: Общие друзья
              items: { $ref: "#/components/schemas/User" }
            games:
              type: array
              description: Вкладка «Активность» — последние 30 игр, свежие сначала
              items: { $ref: "#/components/schemas/GamePlayed" }
    GamePlayed:
      type: object
      required: [name, first_played_at, last_played_at, days, streak]
      properties:
        name: { type: string, maxLength: 64 }
        steam_app_id: { type: string, description: "Номер в Steam: обложка — GET /games/cover?steam=…" }
        first_played_at: { type: string, format: date-time }
        last_played_at: { type: string, format: date-time, description: "Обновляется не чаще раза в 5 минут, пока игра идёт" }
        days: { type: integer, description: В скольких разных днях играл (по московской дате) }
        streak: { type: integer, description: Дней подряд по сегодня или вчера; 0 — серия прервалась }
    DeletionStatus:
      type: object
      required: [deletion_requested_at, delete_at]
      properties:
        deletion_requested_at: { type: [string, "null"], format: date-time }
        delete_at: { type: [string, "null"], format: date-time, description: Когда учётная запись будет удалена }
    DataExport:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [none, pending, ready, failed] }
        requested_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time, description: Когда архив удаляется }
        size_bytes: { type: integer }
        url: { type: string, description: Временная ссылка на скачивание (час) }
    CurrentUser:
      allOf:
        - $ref: "#/components/schemas/User"
        - type: object
          required: [phone, verified, mfa_enabled, locale]
          properties:
            phone: { type: string, description: Замаскированный номер, examples: ["+7916***4567"] }
            email: { type: [string, "null"], format: email, description: Подтверждённая почта; null — не привязана }
            verified: { type: boolean, description: Почта подтверждена (имя поля как у Discord) }
            email_pending: { type: [string, "null"], format: email, description: "Адрес, на который ушло письмо со ссылкой, но по ней ещё не перешли" }
            deletion_requested_at: { type: [string, "null"], format: date-time, description: Заявка на удаление учётной записи }
            mfa_enabled: { type: boolean }
            locale: { type: string, examples: [ru] }
            bio: { type: string }
            flags: { type: integer }
            premium_until: { type: [string, "null"], format: date-time }
            nsfw_allowed: { type: boolean, description: "Есть 18 лет по дате рождения: доступ к каналам nsfw" }
            profile_effect:
              description: "Эффект своей карточки профиля; null — без эффекта"
              oneOf: [{ $ref: "#/components/schemas/ProfileEffect" }, { type: "null" }]
            guild_order:
              type: array
              description: Порядок серверов в колонке слева, как его расставил человек
              items: { $ref: "#/components/schemas/Snowflake" }
    UserSettings:
      type: object
      properties:
        theme: { type: string, enum: [dark, light, system] }
        locale: { type: string, enum: [ru, en] }
        status: { type: string, enum: [online, idle, dnd, invisible] }
        show_current_game: { type: boolean, description: "Показывать ли запущенную игру и историю игр другим; по умолчанию true" }
        custom_status:
          type: [object, "null"]
          properties:
            text: { type: string, maxLength: 128 }
            emoji_id: { $ref: "#/components/schemas/SnowflakeNullable" }
            emoji_name: { type: [string, "null"] }
            expires_at: { type: [string, "null"], format: date-time }
        message_display_compact: { type: boolean }
        font_scale: { type: integer, enum: [12, 14, 15, 16, 18, 20, 24] }
        dm_privacy: { type: string, enum: [everyone, friends, mutual_guilds] }
        friend_requests: { type: string, enum: [everyone, mutual_friends, mutual_guilds, nobody] }
        voice:
          type: object
          properties:
            mode: { type: string, enum: [voice_activity, push_to_talk] }
            input_device_id: { type: string }
            output_device_id: { type: string }
            sensitivity: { type: integer, minimum: -100, maximum: 0 }
            noise_suppression: { type: boolean }
    Session:
      type: object
      required: [id, device, created_at, last_seen_at, current]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        device: { type: string, examples: ["Chrome на macOS"] }
        ip: { type: string, description: Замаскированный IP }
        created_at: { type: string, format: date-time }
        last_seen_at: { type: string, format: date-time }
        current: { type: boolean }
    Relationship:
      type: object
      required: [id, type, user]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type:
          type: integer
          enum: [1, 2, 3, 4]
          description: "1 — друг, 2 — заблокирован, 3 — входящая заявка, 4 — исходящая заявка"
        user: { $ref: "#/components/schemas/User" }
        since: { type: string, format: date-time }
    GuildPartial:
      type: object
      required: [id, name, icon, owner, permissions]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        icon: { type: [string, "null"] }
        owner: { type: boolean }
        permissions: { $ref: "#/components/schemas/Permissions" }
        features:
          type: array
          items: { type: string }
    Guild:
      type: object
      required: [id, name, icon, owner_id, roles, verification_level]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        icon: { type: [string, "null"] }
        banner: { type: [string, "null"] }
        description: { type: [string, "null"] }
        owner_id: { $ref: "#/components/schemas/Snowflake" }
        verification_level: { type: integer, enum: [0, 1, 2, 3], description: "0 нет; 1 аккаунт старше 24 часов; 2 плюс 5 минут на сервере; 3 плюс 10 минут; владелец, администраторы и участники с ролями не проверяются; отказ — 403/40002" }
        default_message_notifications: { type: integer, enum: [0, 1] }
        system_channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        rules_channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        afk_channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        afk_timeout: { type: integer }
        premium_tier: { type: integer, enum: [0, 1, 2, 3] }
        premium_subscription_count: { type: integer }
        premium_progress_bar_enabled: { type: boolean, description: "Показывать полоску бустов над каналами; у новых серверов включена" }
        tag: { type: [string, "null"], description: "Тег сервера — 2–4 буквы или цифры, участники носят его рядом с ником" }
        tag_color: { type: [integer, "null"], description: "Цвет тега 0xRRGGBB; null — фирменный синий" }
        features:
          type: array
          description: "Кроме сохранённых — открытые уровнем: BANNER со 2-го, ANIMATED_BANNER с 3-го. Баннер без BANNER не показывается"
          items: { type: string, examples: [DISCOVERABLE, COMMUNITY, BANNER, ANIMATED_BANNER] }
        roles:
          type: array
          items: { $ref: "#/components/schemas/Role" }
        emojis:
          type: array
          items: { $ref: "#/components/schemas/Emoji" }
        soundboard_sounds:
          type: array
          description: Звуки звуковой панели сервера (без авторов)
          items: { $ref: "#/components/schemas/SoundboardSound" }
        approximate_member_count: { type: integer }
        approximate_presence_count: { type: integer }
    Role:
      type: object
      required: [id, name, color, hoist, position, permissions, managed, mentionable]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        color: { type: integer, description: "RGB числом, 0 — без цвета" }
        hoist: { type: boolean, description: Показывать группой в списке участников }
        position: { type: integer }
        permissions: { $ref: "#/components/schemas/Permissions" }
        managed: { type: boolean, description: "Роль бота, управляется системой" }
        mentionable: { type: boolean }
        icon: { type: [string, "null"] }
    RoleCreate:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        color: { type: integer, minimum: 0, maximum: 16777215 }
        hoist: { type: boolean }
        permissions: { $ref: "#/components/schemas/Permissions" }
        mentionable: { type: boolean }
    Member:
      type: object
      required: [user, nick, roles, joined_at]
      properties:
        user: { $ref: "#/components/schemas/User" }
        nick: { type: [string, "null"] }
        avatar: { type: [string, "null"], description: Аватар для этого сервера («Кличат Плюс») }
        roles:
          type: array
          items: { $ref: "#/components/schemas/Snowflake" }
        joined_at: { type: string, format: date-time }
        premium_since: { type: [string, "null"], format: date-time, description: С какого момента бустит сервер }
        deaf: { type: boolean }
        mute: { type: boolean }
        pending: { type: boolean, description: Не прошёл экран приветствия }
        communication_disabled_until: { type: [string, "null"], format: date-time }
        permissions: { $ref: "#/components/schemas/Permissions" }
    Channel:
      type: object
      required: [id, type]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type:
          type: integer
          enum: [0, 1, 2, 3, 4, 5, 11, 13, 15]
          description: "0 текст, 1 ЛС, 2 голос, 3 группа, 4 категория, 5 анонсы, 11 публичный тред, 13 стейдж, 15 форум"
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        topic: { type: [string, "null"] }
        position: { type: integer }
        parent_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        nsfw: { type: boolean }
        rate_limit_per_user: { type: integer, description: Секунды между сообщениями (slowmode) }
        bitrate: { type: integer }
        user_limit: { type: integer }
        rtc_region: { type: [string, "null"] }
        last_message_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        last_pin_timestamp: { type: [string, "null"], format: date-time }
        permission_overwrites:
          type: array
          items: { $ref: "#/components/schemas/Overwrite" }
        recipients:
          type: array
          description: Для ЛС и групп
          items: { $ref: "#/components/schemas/User" }
        owner_id: { $ref: "#/components/schemas/Snowflake" }
        icon: { type: [string, "null"] }
        available_tags: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/ForumTag" }, description: "Только у форума (15)" }
        applied_tags: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/Snowflake" }, description: "Теги темы (11), пустой массив снимает прежние теги" }
        forum_pinned: { type: boolean, description: "Закрепление темы в списке форума; false снимает закрепление" }
        thread_metadata:
          type: object
          properties:
            archived: { type: boolean }
            auto_archive_duration: { type: integer }
            archive_timestamp: { type: string, format: date-time }
            locked: { type: boolean, description: Разархивировать может только MANAGE_THREADS }
            create_timestamp: { type: string, format: date-time }
        message_count: { type: integer }
        member_count: { type: integer }
        member:
          description: Участие запрашивающего в ветке (READY, активные ветки, ответ на создание)
          allOf: [{ $ref: "#/components/schemas/ThreadMember" }]
    ForumTagInput:
      type: object
      required: [name]
      properties:
        id: { $ref: "#/components/schemas/Snowflake", description: "Только для существующего тега этого форума" }
        name: { type: string, minLength: 1, maxLength: 20 }
    ForumTag:
      allOf:
        - $ref: "#/components/schemas/ForumTagInput"
        - type: object
          required: [id]
    ForumAttachmentInput:
      type: object
      required: [uploaded_filename]
      properties:
        id: { type: string }
        filename: { type: string }
        uploaded_filename: { type: string }
        description: { type: string, maxLength: 1024 }
        spoiler: { type: boolean }
    ForumPost:
      type: object
      required: [thread, reply_count]
      properties:
        thread: { $ref: "#/components/schemas/Channel" }
        message: { $ref: "#/components/schemas/Message", description: "Первое сообщение; отсутствует, если оно удалено" }
        reply_count: { type: integer, minimum: 0 }
    ChannelCreate:
      type: object
      required: [name, type]
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        type: { type: integer, enum: [0, 2, 4, 5, 13, 15] }
        topic: { type: string, maxLength: 1024 }
        position: { type: integer, minimum: 0 }
        parent_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        nsfw: { type: boolean }
        rate_limit_per_user: { type: integer, minimum: 0, maximum: 21600 }
        bitrate: { type: integer, minimum: 8000, maximum: 384000 }
        user_limit: { type: integer, minimum: 0, maximum: 99 }
        permission_overwrites:
          type: array
          items: { $ref: "#/components/schemas/Overwrite" }
    ChannelUpdate:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        topic: { type: [string, "null"], maxLength: 1024 }
        position: { type: integer, minimum: 0 }
        parent_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        nsfw: { type: boolean }
        rate_limit_per_user: { type: integer, minimum: 0, maximum: 21600 }
        bitrate: { type: integer, minimum: 8000, maximum: 384000 }
        user_limit: { type: integer, minimum: 0, maximum: 99 }
        rtc_region: { type: [string, "null"] }
        archived: { type: boolean, description: "Только для веток: автор ветки или MANAGE_THREADS; в архивной ветке другие поля не меняются (50083)" }
        locked: { type: boolean, description: "Только для веток: MANAGE_THREADS; закрытую ветку из архива возвращает только MANAGE_THREADS" }
        auto_archive_duration: { type: integer, enum: [60, 1440, 4320, 10080], description: Только для веток }
        icon: { type: [string, "null"], description: Только для групп ЛС }
        permission_overwrites:
          type: array
          items: { $ref: "#/components/schemas/Overwrite" }
    Overwrite:
      type: object
      required: [id, type, allow, deny]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type: { type: integer, enum: [0, 1], description: "0 — роль, 1 — участник" }
        allow: { $ref: "#/components/schemas/Permissions" }
        deny: { $ref: "#/components/schemas/Permissions" }
    Message:
      type: object
      required: [id, channel_id, author, content, timestamp, edited_timestamp, tts, mention_everyone, mentions, mention_roles, attachments, embeds, pinned, type]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        channel_id: { $ref: "#/components/schemas/Snowflake" }
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        author: { $ref: "#/components/schemas/User" }
        member:
          type: object
          description: Данные участника, если сообщение в сервере
          properties:
            nick: { type: [string, "null"] }
            roles:
              type: array
              items: { $ref: "#/components/schemas/Snowflake" }
            joined_at: { type: string, format: date-time }
        content: { type: string }
        timestamp: { type: string, format: date-time }
        edited_timestamp: { type: [string, "null"], format: date-time }
        tts: { type: boolean, const: false, description: Всегда false; поле нужно библиотекам ботов }
        mention_everyone: { type: boolean }
        mentions:
          type: array
          items: { $ref: "#/components/schemas/User" }
        mention_roles:
          type: array
          items: { $ref: "#/components/schemas/Snowflake" }
        attachments:
          type: array
          items: { $ref: "#/components/schemas/Attachment" }
        embeds:
          type: array
          description: |
            Превью ссылок (до 5) строит worker после отправки и присылает MESSAGE_UPDATE; в ответе на POST список пуст.
            Не строятся без EMBED_LINKS в канале, для ссылок в `<угловых скобках>` и в коде, для доменов из стоп-списка.
          items: { $ref: "#/components/schemas/Embed" }
        call:
          type: [object, "null"]
          description: Звонок в ЛС (тип 3)
          properties:
            participants:
              type: array
              items: { $ref: "#/components/schemas/Snowflake" }
            ended_timestamp: { type: [string, "null"], format: date-time, description: null — звонок идёт }
        thread:
          description: Ветка, начатая от этого сообщения (flags & 32)
          allOf: [{ $ref: "#/components/schemas/Channel" }]
        reactions:
          type: array
          items: { $ref: "#/components/schemas/Reaction" }
        pinned: { type: boolean }
        type:
          type: integer
          description: "0 — обычное, 6 — закреп, 7 — вступление, 19 — ответ, 20 — ответ на слэш-команду, 21 — стартовое сообщение треда, 23 — ответ на команду меню, 46 — итоги опроса (embed poll_result, message_reference — опрос)"
        flags:
          type: integer
          description: "4 — без превью ссылок, 32 — от сообщения начата ветка, 64 — только вам (не хранится), 128 — бот думает, 4096 — без уведомлений, 8192 — голосовое, 16384 — пересланное (копия в message_snapshots)"
        poll: { $ref: "#/components/schemas/Poll" }
        message_reference:
          type: [object, "null"]
          properties:
            type: { type: integer, enum: [0, 1], description: "0 — ответ или ссылка системного сообщения (поле не приходит), 1 — пересылка" }
            message_id: { $ref: "#/components/schemas/Snowflake" }
            channel_id: { $ref: "#/components/schemas/Snowflake" }
            guild_id: { $ref: "#/components/schemas/Snowflake" }
        message_snapshots:
          type: array
          maxItems: 1
          description: |
            Пересланное (message_reference.type = 1, flags & 16384): копия исходного на момент пересылки. Своих content
            и attachments у пересланного нет. Файлы скопированы и принадлежат пересланному: удаление исходного их не
            трогает. Автор исходного не передаётся, как у Discord.
          items:
            type: object
            properties:
              message:
                type: object
                properties:
                  type: { type: integer, enum: [0, 19] }
                  content: { type: string }
                  embeds:
                    type: array
                    items: { $ref: "#/components/schemas/Embed" }
                  attachments:
                    type: array
                    items: { $ref: "#/components/schemas/Attachment" }
                  timestamp: { type: string, format: date-time }
                  edited_timestamp: { type: [string, "null"], format: date-time }
                  flags: { type: integer, description: "Только 4 (без превью) и 8192 (голосовое)" }
                  mentions:
                    type: array
                    description: Упомянутые в исходном — для отображения; пересылка никого не зовёт
                    items: { $ref: "#/components/schemas/User" }
                  mention_roles:
                    type: array
                    items: { $ref: "#/components/schemas/Snowflake" }
                  sticker_items:
                    type: array
                    items: { type: object, additionalProperties: true }
        referenced_message:
          type: [object, "null"]
          description: Сообщение, на которое отвечают (без вложенности)
          additionalProperties: true
        webhook_id: { $ref: "#/components/schemas/Snowflake", description: "Сообщение вебхука: author — вебхук с bot: true и именем из сообщения" }
        sticker_items:
          type: array
          items:
            type: object
            required: [id, name, format_type]
            properties:
              id: { $ref: "#/components/schemas/Snowflake" }
              name: { type: string }
              format_type: { type: integer, enum: [1, 2, 4], description: "1 PNG, 2 APNG, 4 GIF" }
        nonce: { type: [string, integer, "null"] }
        application_id: { $ref: "#/components/schemas/Snowflake", description: "Ответ на взаимодействие: приложение, чей бот ответил" }
        interaction_metadata: { $ref: "#/components/schemas/InteractionMetadata" }
        interaction:
          type: object
          description: Устаревшее поле Discord, только у ответов на команды (discord.js читает его)
          properties:
            id: { $ref: "#/components/schemas/Snowflake" }
            type: { type: integer }
            name: { type: string, description: Полное имя команды с подкомандами }
            user: { $ref: "#/components/schemas/User" }
        components:
          type: array
          description: Кнопки и списки; у сообщений без них — пустой список
          items: { $ref: "#/components/schemas/Component" }
    PollMedia:
      type: object
      properties:
        text: { type: string }
        emoji:
          type: [object, "null"]
          properties:
            id: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }] }
            name: { type: [string, "null"] }
    PollCreate:
      type: object
      description: Опрос при отправке, как у Discord.
      required: [question, answers]
      properties:
        question:
          type: object
          required: [text]
          properties:
            text: { type: string, minLength: 1, maxLength: 300 }
        answers:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: object
            required: [poll_media]
            properties:
              poll_media: { $ref: "#/components/schemas/PollMedia" }
        duration: { type: integer, minimum: 1, maximum: 768, default: 24, description: "Срок в часах" }
        allow_multiselect: { type: boolean, default: false }
        layout_type: { type: integer, enum: [1] }
    Poll:
      type: object
      description: |
        Опрос в сообщении. `results.answer_counts` — варианты с голосами; `me_voted` — голоса того, кто запросил
        (в событиях шлюза всегда false). Голоса меняются событиями MESSAGE_POLL_VOTE_ADD/REMOVE, итоги —
        MESSAGE_UPDATE с `is_finalized: true` и системным сообщением типа 46.
      properties:
        question: { $ref: "#/components/schemas/PollMedia" }
        answers:
          type: array
          items:
            type: object
            properties:
              answer_id: { type: integer, minimum: 1, maximum: 10 }
              poll_media: { $ref: "#/components/schemas/PollMedia" }
        expiry: { type: [string, "null"], format: date-time }
        allow_multiselect: { type: boolean }
        layout_type: { type: integer }
        results:
          type: object
          properties:
            is_finalized: { type: boolean }
            answer_counts:
              type: array
              items:
                type: object
                properties:
                  id: { type: integer }
                  count: { type: integer }
                  me_voted: { type: boolean }
    MessageCreate:
      type: object
      properties:
        content: { type: string, maxLength: 4000 }
        nonce: { type: [string, integer], description: "До 25 символов, возвращается в MESSAGE_CREATE" }
        tts: { type: boolean, const: false }
        embeds:
          type: array
          maxItems: 10
          description: "Rich-карточки (с fields): только для ботов, у пользователей игнорируются; лимиты как у Discord (50035 при нарушении)"
          items: { $ref: "#/components/schemas/Embed" }
        sticker_ids:
          type: array
          maxItems: 3
          description: "Стикеры серверов, где состоит отправитель. В канале сервера — его стикеры, а других серверов — только с Кличат Плюсом и правом USE_EXTERNAL_STICKERS (иначе 403/50013); стикер сервера, где отправитель не состоит, → 400/10060"
          items: { $ref: "#/components/schemas/Snowflake" }
        message_reference:
          type: object
          required: [message_id]
          description: |
            type 0 — ответ на сообщение этого канала. type 1 — пересылка сообщения из любого канала, где у отправителя
            VIEW_CHANNEL и READ_MESSAGE_HISTORY; `channel_id` обязателен. Пересылка уходит без content, attachments,
            sticker_ids и poll, иначе 400/50035. Файлы копируются: в канале назначения для них нужно ATTACH_FILES,
            для голосового — SEND_VOICE_MESSAGES, иначе 403/50013. Опросы и системные сообщения не пересылаются
            (400/50035). Пересланное пересылается копией исходного со ссылкой на исходное; править его нельзя.
          properties:
            type: { type: integer, enum: [0, 1], default: 0 }
            message_id: { $ref: "#/components/schemas/Snowflake" }
            channel_id: { $ref: "#/components/schemas/Snowflake", description: "Канал исходного; для пересылки обязателен" }
            guild_id: { $ref: "#/components/schemas/Snowflake", description: "Принимается и не проверяется" }
            fail_if_not_exists: { type: boolean, default: true, description: "Ответ: false — исходное удалено, сообщение уходит обычным" }
        attachments:
          type: array
          maxItems: 10
          items:
            type: object
            required: [id, uploaded_filename]
            properties:
              id: { type: string }
              filename: { type: string }
              uploaded_filename: { type: string, description: "Из ответа POST /channels/{id}/attachments" }
              description: { type: string, maxLength: 1024 }
              spoiler: { type: boolean }
              duration_secs: { type: number, maximum: 1200, description: "Голосовое (flags 8192): длительность, до 20 минут" }
              waveform: { type: string, description: "Голосовое: base64 до 256 значений громкости 0–255" }
        allowed_mentions: { $ref: "#/components/schemas/AllowedMentions" }
        poll:
          allOf: [{ $ref: "#/components/schemas/PollCreate" }]
          description: Опрос (SEND_POLLS); без вложений и стикеров. Такое сообщение нельзя править (400/520003).
        flags:
          type: integer
          description: |
            4 — без превью ссылок (SUPPRESS_EMBEDS). 8192 — голосовое сообщение (SEND_VOICE_MESSAGES; ATTACH_FILES не
            нужно): ровно одно звуковое вложение (audio/ogg, audio/webm, audio/mp4…) с `duration_secs` и `waveform`,
            без текста, стикеров и опроса; голосовое нельзя править. Остальные биты игнорируются.
        components:
          type: array
          maxItems: 5
          description: "Кнопки и списки — только ботам; от человека непустой список — 400/50035 «Кнопки могут отправлять только приложения»"
          items: { $ref: "#/components/schemas/Component" }
      anyOf:
        - required: [content]
        - required: [embeds]
        - required: [attachments]
        - required: [sticker_ids]
        - required: [poll]
        - required: [message_reference]
        - required: [components]
    AllowedMentions:
      type: object
      description: |
        Какие упоминания из текста на самом деле зовут, как у Discord. Нет поля — разбирается всё; есть — только
        перечисленное: остальных нет в `mentions`, `mention_roles` и `mention_everyone`, им не растёт счётчик и не
        приходит push. Права проверяются как без поля (@everyone — только с MENTION_EVERYONE), а id из `users` и
        `roles`, которых нет в тексте, никого не зовут. Ботам, которые повторяют чужой текст, — `{"parse": []}`.
        Ошибки — 400/50035 в поле `allowed_mentions`.
      properties:
        parse:
          type: array
          description: Какие типы разбирать из текста целиком; `everyone` — это и @everyone, и @here
          items: { type: string, enum: [roles, users, everyone] }
        roles:
          type: array
          maxItems: 100
          description: Какие из упомянутых в тексте ролей звать; непустой список вместе с `roles` в `parse` — 400
          items: { $ref: "#/components/schemas/Snowflake" }
        users:
          type: array
          maxItems: 100
          description: Кого из упомянутых в тексте звать; непустой список вместе с `users` в `parse` — 400
          items: { $ref: "#/components/schemas/Snowflake" }
        replied_user:
          type: boolean
          description: >-
            Упомянуть автора сообщения из message_reference (он попадает в mentions: счётчик и push
            при «только @упоминания»). Без allowed_mentions — да; с allowed_mentions без этого поля — нет, как у Discord.
            Правка с allowed_mentions без этого поля снимает упоминание автора исходного.
    Attachment:
      type: object
      required: [id, filename, size, url, proxy_url]
      description: "`url` и `proxy_url` — подписанные ссылки `GET /attachments/{sig}/{id}/{filename}`; размеры есть у картинок"
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        filename: { type: string }
        description: { type: string }
        content_type: { type: string }
        size: { type: integer }
        url: { type: string, format: uri }
        proxy_url: { type: string, format: uri, description: Ссылка через прокси/CDN с ресайзом }
        width: { type: [integer, "null"] }
        height: { type: [integer, "null"] }
        duration_secs: { type: number, description: "Голосовое сообщение: длительность в секундах" }
        waveform: { type: string, description: "Голосовое сообщение: волна — base64 до 256 значений громкости 0–255" }
        spoiler: { type: boolean }
        blocked: { type: boolean, description: "Антивирус нашёл угрозу: файл удалён, `url` и `proxy_url` пустые" }
    AutoModerationRuleInput:
      type: object
      properties:
        name: { type: string, maxLength: 100 }
        event_type: { type: integer, enum: [1], description: 1 — MESSAGE_SEND }
        trigger_type: { type: integer, enum: [1, 3, 4, 5], description: "1 KEYWORD, 3 SPAM, 4 KEYWORD_PRESET, 5 MENTION_SPAM" }
        trigger_metadata: { $ref: "#/components/schemas/AutoModerationTriggerMetadata" }
        actions:
          type: array
          maxItems: 3
          items: { $ref: "#/components/schemas/AutoModerationAction" }
        enabled: { type: boolean }
        exempt_roles:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/Snowflake" }
        exempt_channels:
          type: array
          maxItems: 50
          items: { $ref: "#/components/schemas/Snowflake" }
    AutoModerationRule:
      allOf:
        - $ref: "#/components/schemas/AutoModerationRuleInput"
        - type: object
          required: [id, guild_id, name, creator_id, event_type, trigger_type, trigger_metadata, actions, enabled, exempt_roles, exempt_channels]
          properties:
            id: { $ref: "#/components/schemas/Snowflake" }
            guild_id: { $ref: "#/components/schemas/Snowflake" }
            creator_id: { $ref: "#/components/schemas/Snowflake" }
    AutoModerationTriggerMetadata:
      type: object
      properties:
        keyword_filter: { type: array, items: { type: string, maxLength: 60 }, maxItems: 1000 }
        regex_patterns: { type: array, items: { type: string, maxLength: 260 }, maxItems: 10, description: "RE2 без учёта регистра; \\b — только ASCII" }
        presets: { type: array, items: { type: integer, enum: [1, 2, 3] }, description: "1 PROFANITY, 2 SEXUAL_CONTENT, 3 SLURS — встроенные списки" }
        allow_list: { type: array, items: { type: string, maxLength: 60 } }
        mention_total_limit: { type: integer, minimum: 1, maximum: 50 }
        mention_raid_protection_enabled: { type: boolean }
    AutoModerationAction:
      type: object
      required: [type]
      properties:
        type: { type: integer, enum: [1, 2, 3], description: "1 BLOCK_MESSAGE (custom_message), 2 SEND_ALERT_MESSAGE (channel_id), 3 TIMEOUT (duration_seconds)" }
        metadata:
          type: object
          properties:
            channel_id: { $ref: "#/components/schemas/Snowflake" }
            duration_seconds: { type: integer, minimum: 1, maximum: 2419200 }
            custom_message: { type: string, maxLength: 150 }
    ModerationReport:
      type: object
      required: [id, reporter_id, target_type, target_id, category, text, snapshot, status, created_at]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        reporter_id: { $ref: "#/components/schemas/Snowflake" }
        target_type: { type: string, enum: [message, user, guild] }
        target_id: { $ref: "#/components/schemas/Snowflake" }
        guild_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        category: { type: string }
        text: { type: string }
        snapshot: { type: object, description: Копия объекта на момент жалобы }
        status: { type: string, enum: [open, resolved, rejected] }
        resolver_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        resolution: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        resolved_at: { type: [string, "null"], format: date-time }
    ModerationUser:
      type: object
      required: [user, created_at, flags, banned, restricted, guilds, reports_against]
      properties:
        user: { $ref: "#/components/schemas/User" }
        created_at: { type: string, format: date-time }
        flags: { type: integer, description: Бит 1 — сотрудник }
        banned:
          type: [object, "null"]
          properties:
            at: { type: string, format: date-time }
            until: { type: [string, "null"], format: date-time }
            reason: { type: string }
        restricted:
          type: [object, "null"]
          properties:
            until: { type: string, format: date-time }
            reason: { type: string }
        guilds: { type: integer }
        reports_against: { type: integer }
    ModerationAction:
      type: object
      required: [id, staff_id, action, reason, meta, created_at]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        staff_id: { $ref: "#/components/schemas/Snowflake" }
        action: { type: string, enum: [ban, unban, restrict, unrestrict, report_resolved, report_rejected, message_delete] }
        target_user_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        target_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        reason: { type: string }
        meta: { type: object }
        created_at: { type: string, format: date-time }
    PushInfo:
      type: object
      required: [enabled, public_key, subscriptions]
      properties:
        enabled: { type: boolean, description: На сервере заданы VAPID-ключи }
        public_key: { type: string, description: "Публичный VAPID-ключ (base64url) для `pushManager.subscribe`" }
        subscriptions:
          type: array
          items: { $ref: "#/components/schemas/PushSubscriptionInfo" }
    PushSubscriptionInfo:
      type: object
      required: [endpoint, user_agent, created_at, current]
      properties:
        endpoint: { type: string, format: uri }
        user_agent: { type: string }
        created_at: { type: string, format: date-time }
        current: { type: boolean, description: Создана в текущей сессии }
    PushSubscriptionInput:
      type: object
      required: [endpoint, keys]
      description: "`PushSubscription.toJSON()` браузера"
      properties:
        endpoint: { type: string, format: uri, description: https-адрес push-службы браузера }
        keys:
          type: object
          required: [p256dh, auth]
          properties:
            p256dh: { type: string, description: "Публичный ключ P-256 (base64url, 65 байт)" }
            auth: { type: string, description: "Секрет аутентификации (base64url, 16 байт)" }
    Call:
      type: object
      required: [channel_id, message_id, region, ringing]
      description: Звонок в ЛС или группе (CALL_CREATE, CALL_UPDATE, READY.calls, GET /channels/{id}/call)
      properties:
        channel_id: { $ref: "#/components/schemas/Snowflake" }
        message_id: { $ref: "#/components/schemas/Snowflake", description: Системное сообщение типа 3 }
        region: { type: string }
        ringing:
          type: array
          description: Кому сейчас звонит
          items: { $ref: "#/components/schemas/Snowflake" }
        voice_states:
          type: array
          description: Кто в звонке (в CALL_CREATE, READY и GET; в CALL_UPDATE нет)
          items: { $ref: "#/components/schemas/VoiceState" }
    VoiceJoin:
      type: object
      required: [endpoint, token, region, expires_at, guild_id, channel_id, voice_state]
      properties:
        endpoint: { type: string, format: uri, examples: ["wss://livekit-msk-1.klichat.ru"] }
        token: { type: string, description: "JWT LiveKit на комнату channel_id: publish при SPEAK и без серверного mute, экран при STREAM; живёт 10 минут до подключения" }
        region: { type: string, examples: [msk] }
        expires_at: { type: string, format: date-time }
        guild_id: { $ref: "#/components/schemas/SnowflakeNullable", description: null в звонке ЛС }
        channel_id: { $ref: "#/components/schemas/Snowflake" }
        voice_state: { $ref: "#/components/schemas/VoiceState" }
    VoiceState:
      type: object
      required: [channel_id, user_id, session_id, deaf, mute, self_deaf, self_mute, self_stream, self_video, suppress]
      description: Кто в каком голосовом канале; `channel_id` null в VOICE_STATE_UPDATE — вышел
      properties:
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        channel_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        user_id: { $ref: "#/components/schemas/Snowflake" }
        member: { $ref: "#/components/schemas/Member" }
        session_id: { type: string }
        deaf: { type: boolean, description: Серверный (DEAFEN_MEMBERS) }
        mute: { type: boolean, description: Серверный (MUTE_MEMBERS) }
        self_deaf: { type: boolean }
        self_mute: { type: boolean }
        self_stream: { type: boolean, description: Демонстрирует экран }
        self_video: { type: boolean }
        suppress: { type: boolean }
        request_to_speak_timestamp: { type: [string, "null"], format: date-time }
    ThreadMember:
      type: object
      required: [id, user_id, join_timestamp, flags]
      properties:
        id: { $ref: "#/components/schemas/Snowflake", description: id ветки }
        user_id: { $ref: "#/components/schemas/Snowflake" }
        join_timestamp: { type: string, format: date-time }
        flags: { type: integer }
        user: { $ref: "#/components/schemas/User" }
    ReportResult:
      type: object
      required: [id, status]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        status: { type: string, enum: [open, resolved, rejected] }
        existing: { type: boolean, description: Жалоба уже была открыта раньше }
    Embed:
      type: object
      description: |
        Превью ссылки или карточка бота. `video.url` заполняется только для YouTube, RuTube и VK Видео (клиент открывает плеер в iframe).
        Картинки клиент грузит по `proxy_url` — подписанной ссылке на `GET /external/{sig}/{url}`, чтобы IP пользователя не утекал.
      properties:
        type: { type: string, enum: [rich, image, video, link, article] }
        title: { type: string, maxLength: 256 }
        description: { type: string, maxLength: 4096 }
        url: { type: string, format: uri }
        timestamp: { type: string, format: date-time }
        color: { type: integer }
        footer:
          type: object
          required: [text]
          properties:
            text: { type: string, maxLength: 2048 }
            icon_url: { type: string, format: uri }
        image:
          type: object
          required: [url]
          properties:
            url: { type: string, format: uri }
            proxy_url: { type: string, format: uri }
            width: { type: integer }
            height: { type: integer }
        thumbnail:
          type: object
          required: [url]
          properties:
            url: { type: string, format: uri }
            proxy_url: { type: string, format: uri }
            width: { type: integer }
            height: { type: integer }
        video:
          type: object
          properties:
            url: { type: string, format: uri }
            width: { type: integer }
            height: { type: integer }
        provider:
          type: object
          properties:
            name: { type: string }
            url: { type: string, format: uri }
        author:
          type: object
          required: [name]
          properties:
            name: { type: string, maxLength: 256 }
            url: { type: string, format: uri }
            icon_url: { type: string, format: uri }
        fields:
          type: array
          maxItems: 25
          items:
            type: object
            required: [name, value]
            properties:
              name: { type: string, maxLength: 256 }
              value: { type: string, maxLength: 1024 }
              inline: { type: boolean }
    Reaction:
      type: object
      required: [count, me, emoji]
      properties:
        count: { type: integer }
        me: { type: boolean }
        emoji:
          type: object
          required: [id, name]
          properties:
            id: { $ref: "#/components/schemas/SnowflakeNullable" }
            name: { type: [string, "null"] }
            animated: { type: boolean }
    Emoji:
      type: object
      required: [id, name]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        animated: { type: boolean }
        managed: { type: boolean }
        available: { type: boolean }
        require_colons: { type: boolean }
        user: { $ref: "#/components/schemas/User" }
        roles:
          type: array
          items: { $ref: "#/components/schemas/Snowflake" }
        url: { type: string, format: uri, description: "Адрес картинки (расширение формы Discord: клиент не собирает CDN-ссылку сам)" }
    Invite:
      type: object
      required: [code, guild, channel]
      properties:
        code: { type: string }
        guild: { $ref: "#/components/schemas/GuildPartial" }
        channel:
          type: object
          required: [id, name, type]
          properties:
            id: { $ref: "#/components/schemas/Snowflake" }
            name: { type: string }
            type: { type: integer }
        inviter: { $ref: "#/components/schemas/User" }
        approximate_member_count: { type: integer }
        approximate_presence_count: { type: integer }
        expires_at: { type: [string, "null"], format: date-time }
        uses: { type: integer }
        max_uses: { type: integer }
        max_age: { type: integer }
        temporary: { type: boolean }
        created_at: { type: string, format: date-time }
    Ban:
      type: object
      required: [user, reason]
      properties:
        user: { $ref: "#/components/schemas/User" }
        reason: { type: [string, "null"] }
    UserGuildSettings:
      type: object
      required: [guild_id, message_notifications, muted, suppress_everyone, suppress_roles, channel_overrides]
      properties:
        guild_id: { type: [string, "null"], description: "Id сервера; null — запись личных бесед" }
        message_notifications: { type: integer, enum: [0, 1, 2, 3] }
        muted: { type: boolean }
        mute_config: { type: [object, "null"], properties: { end_time: { type: [string, "null"], format: date-time } } }
        suppress_everyone: { type: boolean }
        suppress_roles: { type: boolean }
        channel_overrides:
          type: array
          items:
            type: object
            required: [channel_id, message_notifications, muted, collapsed]
            properties:
              channel_id: { $ref: "#/components/schemas/Snowflake" }
              message_notifications: { type: integer, enum: [0, 1, 2, 3] }
              muted: { type: boolean }
              mute_config: { type: [object, "null"], properties: { end_time: { type: [string, "null"], format: date-time } } }
              collapsed: { type: boolean }
    AuditLogEntry:
      type: object
      required: [id, action_type, user_id, target_id]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        action_type: { type: integer, description: "Коды действий как у Discord (1 GUILD_UPDATE, 10 CHANNEL_CREATE, 20 MEMBER_KICK, 22 MEMBER_BAN_ADD, 24 MEMBER_UPDATE, 30 ROLE_CREATE, 72 MESSAGE_DELETE, 73 MESSAGE_DELETE_BULK…)" }
        user_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        target_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        reason: { type: string }
        changes:
          type: array
          items:
            type: object
            required: [key]
            properties:
              key: { type: string }
              old_value: {}
              new_value: {}
        options: { type: object, additionalProperties: true }

    Application:
      type: object
      description: Приложение в форме Discord; `privileged_intents` и `redirect_uris` — расширения раздела «Приложения», `team` всегда null.
      required: [id, name, icon, description, bot_public, bot_require_code_grant, verify_key, team, flags, redirect_uris, privileged_intents]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        icon: { type: [string, "null"] }
        description: { type: string }
        bot_public: { type: boolean }
        bot_require_code_grant: { type: boolean }
        bot: { $ref: "#/components/schemas/User" }
        owner: { $ref: "#/components/schemas/User" }
        verify_key: { type: string }
        team: { type: "null" }
        flags: { type: integer, description: "GATEWAY_PRESENCE 1<<12, GATEWAY_GUILD_MEMBERS 1<<14, GATEWAY_MESSAGE_CONTENT 1<<18 по включённым intents" }
        redirect_uris:
          type: array
          items: { type: string }
        privileged_intents: { type: integer, description: "Разрешённые привилегированные intents: 1<<1, 1<<8, 1<<15" }
        approximate_guild_count: { type: integer }
        created_at: { type: string, format: date-time }
    AuthorizeInfo:
      type: object
      required: [application, bot, user, guilds, scopes, permissions, guild_id, authorized]
      properties:
        application: { $ref: "#/components/schemas/Application" }
        bot: { $ref: "#/components/schemas/User" }
        user: { $ref: "#/components/schemas/User" }
        guilds:
          type: array
          description: Серверы пользователя с правом MANAGE_GUILD
          items: { $ref: "#/components/schemas/GuildPartial" }
        scopes:
          type: array
          items: { type: string }
        permissions: { $ref: "#/components/schemas/Permissions" }
        guild_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        authorized: { type: boolean }
    Webhook:
      type: object
      description: Входящий вебхук в форме Discord; `token` и `url` отдаются тем, у кого MANAGE_WEBHOOKS, `user` — создатель (нет в ответах по токену).
      required: [id, type, guild_id, channel_id, name, avatar, application_id]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type: { type: integer, enum: [1], description: "1 — incoming" }
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        channel_id: { $ref: "#/components/schemas/Snowflake" }
        user: { $ref: "#/components/schemas/User" }
        name: { type: string, maxLength: 80 }
        avatar: { type: [string, "null"] }
        token: { type: string }
        application_id: { $ref: "#/components/schemas/SnowflakeNullable" }
        url: { type: string, format: uri, description: "Адрес исполнения: {KLICHAT_API_PUBLIC_URL}/webhooks/{id}/{token}" }
    WebhookExecute:
      type: object
      description: Тело исполнения вебхука; нужен content или embeds.
      properties:
        content: { type: string, maxLength: 2000 }
        username: { type: string, maxLength: 80, description: Имя автора для этого сообщения }
        avatar_url: { type: string, format: uri, description: "Принимается, пока не показывается" }
        tts: { type: boolean, description: Игнорируется }
        embeds:
          type: array
          maxItems: 10
          items: { $ref: "#/components/schemas/Embed" }
        allowed_mentions: { $ref: "#/components/schemas/AllowedMentions" }
    Sticker:
      type: object
      description: Стикер сервера в форме Discord (type 2); `url` — расширение для клиента.
      required: [id, name, description, tags, type, format_type, guild_id]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string, maxLength: 30 }
        description: { type: string }
        tags: { type: string }
        type: { type: integer, enum: [2] }
        format_type: { type: integer, enum: [1, 2, 4], description: "1 PNG, 2 APNG, 4 GIF" }
        available: { type: boolean }
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        user: { $ref: "#/components/schemas/User" }
        sort_value: { type: integer }
        url: { type: string, format: uri }
    SoundboardSound:
      type: object
      description: Звук звуковой панели в форме Discord — стандартный (без guild_id) или звук сервера. Файл — `GET /soundboard-sounds/{sound_id}`.
      required: [sound_id, name, volume, emoji_id, emoji_name, available]
      properties:
        sound_id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string, maxLength: 32 }
        volume: { type: number, minimum: 0, maximum: 1, description: "Громкость звука для всех; клиент умножает её на свою громкость панели" }
        emoji_id: { oneOf: [{ $ref: "#/components/schemas/Snowflake" }, { type: "null" }] }
        emoji_name: { type: [string, "null"] }
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        available: { type: boolean }
        user: { $ref: "#/components/schemas/User" }
    ImportOptions:
      type: object
      description: Выбор владельца в мастере импорта (исходные Discord-ID из предпросмотра; пустой список — ничего из группы, отсутствие поля — всё).
      properties:
        channels:
          type: array
          items: { type: string }
        roles:
          type: array
          items: { type: string }
        emojis:
          type: array
          items: { type: string }
        stickers:
          type: array
          items: { type: string }
        threads:
          type: array
          items: { type: string }
        settings: { type: boolean }
        claims: { type: boolean }
        renames:
          type: object
          additionalProperties: { type: string }
    ImportJob:
      type: object
      required: [id, guild_id, owner_id, source, status, options, error, created_at, started_at, finished_at]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        owner_id: { $ref: "#/components/schemas/Snowflake" }
        source: { type: string, enum: [exporter] }
        status: { type: string, enum: [uploaded, ready, running, done, failed, cancelled] }
        options: { $ref: "#/components/schemas/ImportOptions" }
        preview: { type: object, description: "Сводка и дерево архива: guild, summary, categories, uncategorized, roles, emojis, stickers, threads, claims, slots, warnings", additionalProperties: true }
        report: { type: object, description: "created, existing, skipped[{type, id, name, reason}], warnings, duration_ms", additionalProperties: true }
        error: { type: [string, "null"] }
        progress:
          type: object
          properties:
            step: { type: string }
            done: { type: integer }
            total: { type: integer }
        created_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
    ApplicationCommand:
      type: object
      description: Команда приложения в форме Discord (ADR 0010). `version` — snowflake, меняется, только если команда поменялась.
      required: [id, type, application_id, name, name_localizations, description, description_localizations, options, default_member_permissions, dm_permission, contexts, integration_types, nsfw, version]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type: { type: integer, enum: [1, 2, 3], description: "1 слэш-команда, 2 меню участника, 3 меню сообщения (2 и 3 клиент пока не показывает)" }
        application_id: { $ref: "#/components/schemas/Snowflake" }
        guild_id: { $ref: "#/components/schemas/Snowflake", description: "Только у команд сервера" }
        name: { type: string, minLength: 1, maxLength: 32, description: "У слэш-команд — ^[-_\\p{L}\\p{N}]{1,32}$ строчными (кириллица можно)" }
        name_localizations: { type: [object, "null"], additionalProperties: { type: string } }
        description: { type: string, maxLength: 100, description: "1–100 у слэш-команд, пусто у команд меню" }
        description_localizations: { type: [object, "null"], additionalProperties: { type: string } }
        options:
          type: array
          maxItems: 25
          items: { $ref: "#/components/schemas/ApplicationCommandOption" }
        default_member_permissions:
          type: [string, "null"]
          description: "Маска прав строкой: null — всем, «0» — только администраторам"
        dm_permission: { type: boolean }
        contexts:
          type: [array, "null"]
          description: "0 — сервер, 1 — личка с ботом, 2 — личные чаты; null — сервер и (с dm_permission) личка"
          items: { type: integer, enum: [0, 1, 2] }
        integration_types:
          type: array
          items: { type: integer, enum: [0, 1] }
        nsfw: { type: boolean }
        version: { $ref: "#/components/schemas/Snowflake" }
    ApplicationCommandInput:
      type: object
      description: Команда в запросе; у PUT можно указать `id` — команда с этим id переименовывается, а не создаётся заново.
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type: { type: integer, enum: [1, 2, 3], default: 1 }
        name: { type: string, minLength: 1, maxLength: 32 }
        name_localizations: { type: [object, "null"], additionalProperties: { type: string } }
        description: { type: string, maxLength: 100 }
        description_localizations: { type: [object, "null"], additionalProperties: { type: string } }
        options:
          type: array
          maxItems: 25
          items: { $ref: "#/components/schemas/ApplicationCommandOption" }
        default_member_permissions: { type: [string, integer, "null"] }
        dm_permission: { type: boolean }
        contexts:
          type: [array, "null"]
          items: { type: integer, enum: [0, 1, 2] }
        integration_types:
          type: [array, "null"]
          items: { type: integer, enum: [0, 1] }
        nsfw: { type: boolean }
    ApplicationCommandOption:
      type: object
      required: [type, name, description]
      properties:
        type:
          type: integer
          minimum: 1
          maximum: 11
          description: "1 SUB_COMMAND, 2 SUB_COMMAND_GROUP, 3 STRING, 4 INTEGER, 5 BOOLEAN, 6 USER, 7 CHANNEL, 8 ROLE, 9 MENTIONABLE, 10 NUMBER, 11 ATTACHMENT"
        name: { type: string, minLength: 1, maxLength: 32 }
        name_localizations: { type: object, additionalProperties: { type: string } }
        description: { type: string, minLength: 1, maxLength: 100 }
        description_localizations: { type: object, additionalProperties: { type: string } }
        required: { type: boolean }
        choices:
          type: array
          maxItems: 25
          items: { $ref: "#/components/schemas/ApplicationCommandChoice" }
        options:
          type: array
          maxItems: 25
          description: У подкоманд и групп
          items: { $ref: "#/components/schemas/ApplicationCommandOption" }
        channel_types:
          type: array
          items: { type: integer }
        min_value: { type: number }
        max_value: { type: number }
        min_length: { type: integer, minimum: 0, maximum: 6000 }
        max_length: { type: integer, minimum: 1, maximum: 6000 }
        autocomplete: { type: boolean, description: Нельзя вместе с choices }
    ApplicationCommandChoice:
      type: object
      required: [name, value]
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        name_localizations: { type: object, additionalProperties: { type: string } }
        value: { type: [string, number], description: "Строка до 100 символов или число до 2^53" }
    ApplicationCommandIndex:
      type: object
      required: [applications, application_commands]
      properties:
        applications:
          type: array
          description: Приложения, у которых есть команды в этом списке
          items:
            type: object
            required: [id, name, icon, bot_id]
            properties:
              id: { $ref: "#/components/schemas/Snowflake" }
              name: { type: string }
              icon: { type: [string, "null"] }
              bot_id: { $ref: "#/components/schemas/Snowflake" }
              bot: { $ref: "#/components/schemas/User", description: "Расширение: имя и аватар бота" }
        application_commands:
          type: array
          items: { $ref: "#/components/schemas/ApplicationCommand" }
    Component:
      type: object
      description: |
        Строка (1), кнопка (2) или список (3 текстовый, 5 участники, 6 роли, 7 участники и роли, 8 каналы) в форме Discord.
        До 5 строк; в строке до 5 кнопок или один список. Отправлять может только бот; людям и вебхукам — 400/50035.
      required: [type]
      properties:
        type: { type: integer, enum: [1, 2, 3, 5, 6, 7, 8] }
        id: { type: integer, minimum: 1 }
        components:
          type: array
          maxItems: 5
          description: У строки
          items: { $ref: "#/components/schemas/Component" }
        style: { type: integer, minimum: 1, maximum: 5, description: "Кнопка: 1–4 с custom_id, 5 — ссылка с url" }
        label: { type: string, maxLength: 80 }
        emoji: { $ref: "#/components/schemas/ComponentEmoji" }
        custom_id: { type: string, maxLength: 100, description: Уникален в сообщении }
        url: { type: string, format: uri }
        disabled: { type: boolean }
        options:
          type: array
          maxItems: 25
          description: Пункты текстового списка
          items: { $ref: "#/components/schemas/SelectOption" }
        channel_types:
          type: array
          items: { type: integer }
        placeholder: { type: string, maxLength: 150 }
        default_values:
          type: array
          items:
            type: object
            required: [id, type]
            properties:
              id: { $ref: "#/components/schemas/Snowflake" }
              type: { type: string, enum: [user, role, channel] }
        min_values: { type: integer, minimum: 0, maximum: 25 }
        max_values: { type: integer, minimum: 1, maximum: 25 }
    ComponentEmoji:
      type: object
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        name: { type: string }
        animated: { type: boolean }
    SelectOption:
      type: object
      required: [label, value]
      properties:
        label: { type: string, minLength: 1, maxLength: 100 }
        value: { type: string, minLength: 1, maxLength: 100 }
        description: { type: string, maxLength: 100 }
        emoji: { $ref: "#/components/schemas/ComponentEmoji" }
        default: { type: boolean }
    InteractionMetadata:
      type: object
      description: Кто и чем вызвал ответ приложения
      required: [id, type, user, authorizing_integration_owners]
      properties:
        id: { $ref: "#/components/schemas/Snowflake" }
        type: { type: integer, enum: [2, 3], description: "2 — команда, 3 — кнопка или список" }
        name: { type: string, description: "Полное имя команды с подкомандами: «музыка играй»" }
        command_type: { type: integer }
        user: { $ref: "#/components/schemas/User" }
        authorizing_integration_owners:
          type: object
          description: "{\"0\": id сервера} на сервере, {\"1\": id человека} в личке с ботом"
          additionalProperties: { $ref: "#/components/schemas/Snowflake" }
        original_response_message_id: { $ref: "#/components/schemas/Snowflake", description: "У дополнительных сообщений" }
        interacted_message_id: { $ref: "#/components/schemas/Snowflake", description: "У ответов на кнопку: сообщение с кнопкой" }
    InteractionRequest:
      type: object
      description: |
        Тело POST /interactions. `data` — как у Discord: у команды `{id, name, type, options, attachments?}` (значения опций; у
        подсказок сфокусированная опция — `focused: true` и введённый текст строкой), у кнопки `{custom_id, component_type,
        values?}`. `application_id` у кнопок можно не слать — приложение берётся по автору сообщения.
      required: [type, channel_id, data]
      properties:
        type: { type: integer, enum: [2, 3, 4], description: "2 команда, 3 кнопка или список, 4 подсказки" }
        application_id: { $ref: "#/components/schemas/Snowflake" }
        channel_id: { $ref: "#/components/schemas/Snowflake" }
        guild_id: { $ref: "#/components/schemas/Snowflake" }
        message_id: { $ref: "#/components/schemas/Snowflake", description: "У кнопок и списков" }
        nonce: { type: [string, integer], description: "До 25 символов; не используется" }
        data:
          type: object
          additionalProperties: true
          properties:
            id: { $ref: "#/components/schemas/Snowflake" }
            name: { type: string }
            type: { type: integer }
            options:
              type: array
              items:
                type: object
                required: [name]
                properties:
                  name: { type: string }
                  type: { type: integer }
                  value: { type: [string, number, boolean] }
                  focused: { type: boolean }
                  options: { type: array, items: { type: object, additionalProperties: true } }
            attachments:
              type: array
              description: "Файлы опций-вложений: загружены через POST /channels/{id}/attachments, значение опции — id отсюда"
              items:
                type: object
                required: [id, uploaded_filename]
                properties:
                  id: { type: [string, integer] }
                  filename: { type: string }
                  uploaded_filename: { type: string }
            custom_id: { type: string }
            component_type: { type: integer }
            values:
              type: array
              items: { type: string }
    InteractionMessageData:
      type: object
      description: Сообщение ответа, дополнительного сообщения или правки — как у ботов в REST
      properties:
        content: { type: string, maxLength: 2000 }
        embeds:
          type: array
          maxItems: 10
          items: { $ref: "#/components/schemas/Embed" }
        components:
          type: array
          maxItems: 5
          items: { $ref: "#/components/schemas/Component" }
        allowed_mentions: { $ref: "#/components/schemas/AllowedMentions" }
        flags: { type: integer, description: "4 без превью, 64 только вызвавшему, 4096 без уведомлений; 1<<15 — 400" }
        attachments:
          type: array
          description: Какие прежние вложения оставить (id) и новые файлы (номер files[n])
          items:
            type: object
            properties:
              id: { type: [string, integer] }
        tts: { type: boolean, description: Игнорируется }
    InteractionCallback:
      type: object
      required: [type]
      properties:
        type: { type: integer, enum: [4, 5, 6, 7, 8] }
        data:
          description: "4, 7 — InteractionMessageData; 5 — {flags: 64}; 8 — {choices: [{name, value}] до 25}"
          oneOf:
            - $ref: "#/components/schemas/InteractionMessageData"
            - type: object
              required: [choices]
              properties:
                choices:
                  type: array
                  maxItems: 25
                  items: { $ref: "#/components/schemas/ApplicationCommandChoice" }
    InteractionCallbackResult:
      type: object
      required: [interaction]
      properties:
        interaction:
          type: object
          required: [id, type, activity_instance_id, response_message_id, response_message_loading, response_message_ephemeral]
          properties:
            id: { $ref: "#/components/schemas/Snowflake" }
            type: { type: integer }
            activity_instance_id: { type: "null" }
            response_message_id: { type: [string, "null"] }
            response_message_loading: { type: [boolean, "null"] }
            response_message_ephemeral: { type: [boolean, "null"] }
        resource:
          type: object
          properties:
            type: { type: integer }
            message: { $ref: "#/components/schemas/Message" }
