openapi: "3.1.0"
info:
  title: MiniApp — Incoming Events
  description: >
    Входящие события от внешнего приложения (MiniApp → Acloud).
    Внешнее приложение отправляет POST-запрос на endpoint хука.
  version: "2.1.0"

webhooks:

  message.new.personal:
    post:
      summary: Новое входящее сообщение от пользователя
      description: >
        Отправляется внешним приложением при получении нового сообщения от пользователя.
        Также используется как подтверждение инициализации чата в ответ на исходящий хук
        chat.init.personal.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.new.personal
                payload:
                  $ref: '#/components/schemas/MessageNewPayload'
            examples:
              message_new:
                summary: Новое сообщение
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_123
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Привет, хочу узнать про заказ
                    send_date: 1710752400
                    user_data:
                      name: Иван Иванов
                      username: ivan
                      phone: "+79001234567"
                      email: ivan@example.com
                      avatar_url: https://example.com/avatar.jpg
                      public_link: https://example.com/ivan
              message_new_with_attachment:
                summary: Новое сообщение с приложенным файлом
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_124
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Вот скриншот, как просили
                    send_date: 1710752500
                    user_data:
                      name: Иван Иванов
                      username: ivan
                      phone: "+79001234567"
                      email: ivan@example.com
                      avatar_url: https://example.com/avatar.jpg
                      public_link: https://example.com/ivan
                    attachments:
                      - id: ext_att_1001
                        url: https://miniapp.example/files/1001
                        filename: screenshot.png
                        size: 204800
                        type: photo
              message_new_with_failed_attachments:
                summary: >
                  Новое сообщение с декларацией о частичной доставке вложений:
                  внешнее приложение передало часть файлов и сообщает Acloud,
                  что часть файлов не вошла в массив attachments.
                value:
                  method: message.new.personal
                  payload:
                    external_message_id: msg_125
                    external_user_id: user_456
                    external_chat_id: chat_789
                    text: Отправляю фото
                    send_date: 1710752600
                    user_data:
                      name: Иван Иванов
                      username: ivan
                      phone: "+79001234567"
                    attachments:
                      - id: ext_att_1002
                        url: https://miniapp.example/files/1002
                        filename: photo_1.jpg
                        size: 524288
                        type: photo
                    has_failed_attachments: true
                    failed_attachments_count: 4
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Бот деактивирован или аккаунт заблокирован.
            Прекратите отправку событий до получения исходящего хука bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Бот или аккаунт удалён.
            Удалите интеграцию на своей стороне и прекратите отправку событий.
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.delete.personal:
    post:
      summary: Удаление сообщения пользователем
      description: >
        Отправляется внешним приложением при удалении сообщения пользователем.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.delete.personal
                payload:
                  $ref: '#/components/schemas/MessageDeletedPayload'
            examples:
              message_deleted:
                summary: Удаление сообщения
                value:
                  method: message.delete.personal
                  payload:
                    external_message_id: msg_123
                    deleted_date: 1710752400
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Сообщение с указанным external_message_id не найдено в скоупе бота-владельца webhook'а
            (cross-bot или не существует, или сообщение является исходящим менеджера и не подлежит изменению извне).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Бот деактивирован или аккаунт заблокирован.
            Прекратите отправку событий до получения исходящего хука bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Бот или аккаунт удалён.
            Удалите интеграцию на своей стороне и прекратите отправку событий.
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.edit.personal:
    post:
      summary: Редактирование сообщения пользователем
      description: >
        Отправляется внешним приложением при редактировании сообщения пользователем.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.edit.personal
                payload:
                  $ref: '#/components/schemas/MessageEditedPayload'
            examples:
              message_edited:
                summary: Редактирование сообщения
                value:
                  method: message.edit.personal
                  payload:
                    external_message_id: msg_123
                    edited_date: 1710752400
                    new_text: Исправленный текст сообщения
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Сообщение с указанным external_message_id не найдено в скоупе бота-владельца webhook'а
            (cross-bot или не существует, или сообщение является исходящим менеджера и не подлежит изменению извне).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Бот деактивирован или аккаунт заблокирован.
            Прекратите отправку событий до получения исходящего хука bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Бот или аккаунт удалён.
            Удалите интеграцию на своей стороне и прекратите отправку событий.
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  error:
    post:
      summary: Уведомление об ошибке при обработке исходящего хука
      description: >
        Внешнее приложение отправляет этот хук, если не смогло обработать
        исходящий запрос от Acloud (например, пользователь не найден в системе).
        Поле event_id возвращается без изменений из поля event_id payload исходящего хука.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - error
                payload:
                  $ref: '#/components/schemas/ErrorPayload'
            examples:
              error_user_not_found:
                summary: Пользователь не найден (ошибка message.new.personal)
                value:
                  method: error
                  payload:
                    message: "User not found"
                    event_id: '550e8400-e29b-41d4-a716-446655440000:79000'
              error_chat_init:
                summary: Ошибка инициализации чата (chat.init.personal)
                value:
                  method: error
                  payload:
                    message: "User lookup failed"
                    event_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7:79000'
      responses:
        "200":
          description: Ошибка принята и обработана
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "409":
          description: >
            Бот деактивирован или аккаунт заблокирован.
            Прекратите отправку событий до получения исходящего хука bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Бот или аккаунт удалён.
            Удалите интеграцию на своей стороне и прекратите отправку событий.
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.completed.personal:
    post:
      summary: Подтверждение доставки исходящего сообщения
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.completed.personal
                payload:
                  $ref: '#/components/schemas/MessageCompletedPayload'
            examples:
              message_completed:
                summary: Подтверждение полной доставки исходящего сообщения
                value:
                  method: message.completed.personal
                  payload:
                    inner_message_id: 9001
                    external_message_id: ext_msg_555
              message_completed_partial:
                summary: >
                  Подтверждение частичной доставки сообщения с файлами:
                  часть вложений доставлена клиенту, часть — нет
                value:
                  method: message.completed.personal
                  payload:
                    inner_message_id: 9002
                    external_message_id: ext_msg_556
                    delivered_attachments:
                      - "555"
                    failed_attachments:
                      - "556"
      responses:
        "200":
          description: Событие успешно обработано
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Ошибка валидации (неверный формат или отсутствуют обязательные поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: >
            Исходящее сообщение с указанным inner_message_id не найдено в скоупе бота-владельца webhook'а
            (cross-bot — попытка подтвердить доставку чужого сообщения).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Not found
        "409":
          description: >
            Бот деактивирован или аккаунт заблокирован.
            Прекратите отправку событий до получения исходящего хука bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Бот или аккаунт удалён.
            Удалите интеграцию на своей стороне и прекратите отправку событий.
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error

  message.read.personal:
    post:
      summary: Прочтение сообщения пользователем (accepted as no-op)
      description: >
        Отправляется внешним приложением при прочтении сообщения пользователем.
        Acloud принимает событие и возвращает 200 OK, но **полноценная обработка
        прочитанности пока не реализована** (no-op handler) — реальная семантика
        прочитанности — future work. Контракт зафиксирован, чтобы внешнее приложение,
        отправляющее этот хук, не получало 5xx.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - payload
              properties:
                method:
                  type: string
                  enum:
                    - message.read.personal
                payload:
                  $ref: '#/components/schemas/MessageReadPayload'
            examples:
              message_read:
                summary: Прочтение сообщения
                value:
                  method: message.read.personal
                  payload:
                    message_id: msg_123
                    read_date: 1710752400
                    user_id: user_456
                    chat_id: chat_789
      responses:
        "200":
          description: Событие принято (no-op, побочные эффекты отсутствуют)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "409":
          description: >
            Бот деактивирован или аккаунт заблокирован.
            Прекратите отправку событий до получения исходящего хука bot.activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        "410":
          description: >
            Бот или аккаунт удалён.
            Удалите интеграцию на своей стороне и прекратите отправку событий.
        "500":
          description: Внутренняя ошибка сервера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Internal error


components:
  schemas:

    IncomingEvent:
      type: object
      required:
        - method
        - payload
      properties:
        method:
          type: string
          enum:
            - message.new.personal
            - message.completed.personal
            - message.delete.personal
            - message.edit.personal
            - message.read.personal
            - error
          description: Тип события
        payload:
          oneOf:
            - $ref: '#/components/schemas/MessageNewPayload'
            - $ref: '#/components/schemas/MessageCompletedPayload'
            - $ref: '#/components/schemas/MessageDeletedPayload'
            - $ref: '#/components/schemas/MessageEditedPayload'
            - $ref: '#/components/schemas/MessageReadPayload'
            - $ref: '#/components/schemas/ErrorPayload'
          discriminator:
            propertyName: method

    # ─── message.new.personal ─────────────────────────────────────────────────

    MessageNewPayload:
      type: object
      description: Новое входящее сообщение от пользователя внешнего приложения
      required:
        - external_message_id
        - external_user_id
        - external_chat_id
      properties:
        external_message_id:
          type: string
          description: Уникальный ID сообщения во внешней системе
        external_user_id:
          type: string
          description: Уникальный ID пользователя во внешней системе
        external_chat_id:
          type: string
          description: Уникальный ID чата во внешней системе
        text:
          type: string
          default: ""
          description: Текст сообщения
        send_date:
          type: integer
          description: Unix timestamp времени отправки сообщения
          example: 1710752400
        user_data:
          $ref: '#/components/schemas/UserData'
        attachments:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/AttachmentObject'
          description: >
            Массив вложений к сообщению. Если не указан или пуст,
            сообщение состоит только из текста.
        direction:
          type: ["integer", "null"]
          enum: [0, 1, null]
          default: null
          description: >
            Направление сообщения. 0 — входящее (от пользователя к менеджеру),
            1 — исходящее (от менеджера к пользователю, отправлено во внешней системе).
            Если не указано — считается входящим.
        has_failed_attachments:
          type: ["boolean", "null"]
          default: null
          description: >
            Опциональный флаг декларации внешнего приложения, что в исходном
            сообщении пользователя было больше вложений, чем фактически
            передано в Acloud в массиве attachments. Причина может быть
            любой со стороны внешнего приложения (превышение лимита
            размера, ошибка скачивания, политика мессенджера и т.п.).
            На текущий момент Acloud принимает поле и логирует факт,
            но не выполняет специальной обработки и не сохраняет его в БД.
            Поле зарезервировано для будущей реализации — симметрично
            delivered_attachments / failed_attachments в исходящем хуке
            message.completed.personal.
        failed_attachments_count:
          type: ["integer", "null"]
          minimum: 0
          default: null
          description: >
            Опциональное число файлов, которые внешнее приложение не передало.
            Имеет смысл только при has_failed_attachments=true. Если передано
            без has_failed_attachments=true, значение игнорируется и в лог
            Acloud пишется предупреждение.
        event_id:
          type: ["string", "null"]
          default: null
          description: >
            Непрозрачный токен события из хука chat.init.personal. Возвращайте
            ТОЛЬКО в первом message.new.personal (direction: 1) чата,
            инициированного менеджером (echo, созданный в ответ на
            chat.init.personal): по нему Acloud привязывает первое сообщение к
            менеджеру-инициатору — иначе сообщение отобразится со стороны
            контакта. Не нужен для обычных входящих (direction: 0) и для
            последующих echo уже созданного чата.

    # ─── message.delete.personal ──────────────────────────────────────────────

    MessageDeletedPayload:
      type: object
      description: Событие удаления сообщения пользователем во внешней системе.
      required:
        - external_message_id
      properties:
        external_message_id:
          type: string
          description: ID сообщения во внешней системе
          example: msg_123
        deleted_date:
          type: integer
          description: Unix timestamp времени удаления сообщения
          example: 1710752400

    # ─── message.read.personal ────────────────────────────────────────────────

    MessageReadPayload:
      type: object
      description: >
        Событие прочтения сообщения пользователем во внешней системе.
        Acloud принимает payload, но в текущей версии **не обрабатывает** его
        (no-op handler). Контракт зафиксирован для совместимости с будущей
        обработкой; все поля опциональны (defaults в DTO).
      properties:
        message_id:
          type: string
          description: ID прочитанного сообщения во внешней системе
          example: msg_123
          default: ""
        read_date:
          type: integer
          description: Unix timestamp времени прочтения
          example: 1710752400
        user_id:
          type: string
          description: ID пользователя, прочитавшего сообщение
          example: user_456
          default: ""
        chat_id:
          type: string
          description: ID чата
          example: chat_789
          default: ""

    # ─── message.edit.personal ────────────────────────────────────────────────

    MessageEditedPayload:
      type: object
      description: Событие редактирования сообщения пользователем во внешней системе.
      required:
        - external_message_id
      properties:
        external_message_id:
          type: string
          description: ID сообщения во внешней системе
          example: msg_123
        edited_date:
          type: integer
          description: Unix timestamp времени редактирования сообщения
          example: 1710752400
        new_text:
          type: string
          description: Новый текст сообщения после редактирования
          default: ""
          example: Исправленный текст сообщения

    # ─── message.completed.personal ───────────────────────────────────────────

    MessageCompletedPayload:
      type: object
      description: >
        Внешнее приложение подтверждает успешную доставку сообщения,
        отправленного нашей системой. Используется для связки
        inner_message_id (наш) с external_message_id (их).


        Поведение при частичной доставке файлов: если из массива attachments
        исходящего хука часть файлов доставлена получателю, а часть — нет,
        внешнее приложение всё равно отправляет message.completed.personal
        и указывает результат доставки каждого файла в полях
        delivered_attachments и failed_attachments.

        Если все файлы доставлены — поля можно опустить или прислать
        полный список в delivered_attachments.

        Если ни один файл не удалось доставить (а текстовая часть тоже
        невалидна для клиента) — отправляется событие error, а не
        message.completed.personal.
      required:
        - inner_message_id
        - external_message_id
      properties:
        inner_message_id:
          type: integer
          description: ID сообщения в нашей системе (значение inner_message_id из исходящего хука message.new.personal)
          example: 9001
        external_message_id:
          type: string
          description: ID, присвоенный сообщению во внешней системе
          example: ext_msg_555
        delivered_attachments:
          type: array
          default: []
          items:
            type: string
          description: >
            Список ID вложений (соответствуют id из массива attachments исходящего
            message.new.personal), которые были успешно доставлены получателю.
            Поле опциональное: если все вложения доставлены, можно не присылать.

            ВАЖНО: на момент текущей версии Acloud принимает это поле, но
            не использует его для специальной логики — статус сообщения
            ставится в OK (полная доставка). Поле зарезервировано для
            будущей обработки частичной доставки. Внешнее приложение
            должно его присылать сейчас, чтобы при появлении обработки
            ему не пришлось менять свою сторону.
          example: ["555", "556"]
        failed_attachments:
          type: array
          default: []
          items:
            type: string
          description: >
            Список ID вложений, которые не удалось доставить получателю.
            Поле опциональное: если все вложения доставлены, можно не присылать.

            ВАЖНО: на момент текущей версии Acloud принимает это поле, но
            не использует его для специальной логики — статус сообщения
            ставится в OK даже при наличии failed_attachments. Поле
            зарезервировано для будущей обработки частичной доставки.
            Внешнее приложение должно его присылать сейчас, чтобы при
            появлении обработки ему не пришлось менять свою сторону.
          example: ["557"]
        event_id:
          type: ["string", "null"]
          default: null
          description: >
            Опциональный непрозрачный токен события из исходного исходящего
            хука message.new.personal. Можно возвращать для однородности с
            message.new.personal. На текущий момент Acloud принимает, но не
            использует (доставка сопоставляется по inner_message_id).
            Зарезервировано для будущей обработки подтверждения отправки.

    # ─── Shared types ─────────────────────────────────────────────────────────

    UserData:
      type: object
      description: Данные пользователя внешней системы
      properties:
        name:
          type: ["string", "null"]
          description: Отображаемое имя
        username:
          type: ["string", "null"]
          description: Никнейм
        phone:
          type: ["string", "null"]
        email:
          type: ["string", "null"]
        avatar_url:
          type: ["string", "null"]
          description: URL аватара
        public_link:
          type: ["string", "null"]
          description: Публичная ссылка на профиль

    AttachmentObject:
      type: object
      description: >
        Метаданные вложения во входящем сообщении. Acloud скачает файл по
        переданному URL и сохранит его у себя. Допустимый размер — до 50 МБ
        (52428800 байт); файлы больше отклоняются.
      required:
        - id
        - url
        - filename
      properties:
        id:
          type: string
          description: >
            Уникальный ID вложения во внешней системе.
            Используется для дедупликации при повторных webhook.
        url:
          type: string
          description: URL, по которому Acloud скачивает файл.
        filename:
          type: string
          description: Имя файла с расширением.
        size:
          type: integer
          default: 0
          description: >
            Размер файла в байтах. Не должен превышать 50 МБ (52428800).
            Acloud проверяет размер до и после скачивания.
        type:
          type: string
          default: file
          description: >
            Подсказка о типе вложения (photo / file / video / audio).
            Acloud не использует это поле — фактический тип определяется
            по mime скачанного файла.
        metadata:
          type: ["object", "null"]
          description: Произвольные метаданные (опционально).

    # ─── error ────────────────────────────────────────────────────────────────

    ErrorPayload:
      type: object
      description: >
        Payload события error. Внешнее приложение сигнализирует, что не смогло
        обработать исходящий хук (message.new.personal или chat.init.personal).
      required:
        - event_id
      properties:
        message:
          type: string
          description: Человекочитаемое описание ошибки
          example: "User not found"
        event_id:
          type: string
          description: >
            Значение поля event_id из payload исходящего хука. Возвращается
            без изменений (как непрозрачная строка). Если идентификатор больше
            не известен Acloud (например, истёк TTL), событие безопасно
            игнорируется без побочных эффектов.
          example: '550e8400-e29b-41d4-a716-446655440000:79000'

    # ─── Shared types ─────────────────────────────────────────────────────────

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string

    ConflictResponse:
      type: object
      description: >
        Бот деактивирован или аккаунт заблокирован.
        Внешнее приложение должно прекратить отправку событий для этого бота
        до получения исходящего хука bot.activated.
      required:
        - success
        - error
        - message
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Код ошибки
          enum:
            - bot_deactivated
            - account_expired
          example: bot_deactivated
        message:
          type: string
          description: Человекочитаемое описание причины
          example: Bot is deactivated
