asyncapi: 3.0.0
info:
  title: Aoba Voice Call Signaling API
  version: 1.0.0
  description: |
    WebRTC voice call signaling over XMPP IQ stanzas.

    ## Purpose

    This API provides WebRTC call setup and management via XMPP, including:
    - Call initiation and acceptance
    - SDP offer/answer exchange
    - ICE candidate exchange
    - Call termination and status updates

    ## Transport

    Voice call signals are transmitted via XMPP IQ stanzas using the custom namespace
    `urn:xmpp:call:0`. This allows call signaling to share the existing XMPP WebSocket
    connection used for chat messaging.

    ## Call Flow

    ```
    Caller                                          Callee
      │                                                │
      │  1. call:initiate                             │
      │  ─────────────────────────────────────────►   │
      │                                                │
      │  2. call:accept (or call:reject/call:busy)    │
      │  ◄─────────────────────────────────────────   │
      │                                                │
      │  3. call:offer (SDP)                          │
      │  ─────────────────────────────────────────►   │
      │                                                │
      │  4. call:answer (SDP)                         │
      │  ◄─────────────────────────────────────────   │
      │                                                │
      │  5. call:ice-candidate (multiple, both ways)  │
      │  ◄────────────────────────────────────────►   │
      │                                                │
      │  [WebRTC media connection established]         │
      │                                                │
      │  6. call:mute-change (optional, both ways)    │
      │  ◄────────────────────────────────────────►   │
      │                                                │
      │  7. call:hangup                               │
      │  ◄────────────────────────────────────────►   │
      │                                                │
    ```

    ## Signal Types

    | Signal | Direction | Description |
    |--------|-----------|-------------|
    | `call:initiate` | Caller → Callee | Start a new call |
    | `call:accept` | Callee → Caller | Accept incoming call |
    | `call:reject` | Callee → Caller | Decline incoming call |
    | `call:offer` | Caller → Callee | WebRTC SDP offer |
    | `call:answer` | Callee → Caller | WebRTC SDP answer |
    | `call:ice-candidate` | Both ways | ICE candidate for NAT traversal |
    | `call:hangup` | Either party | End the call |
    | `call:busy` | Callee → Caller | Callee is on another call |
    | `call:mute-change` | Both ways | Mute state changed |

    ## End Reasons

    When a call ends (via `call:hangup`), the reason is included:
    - `completed`: Normal hangup after connected call
    - `rejected`: Callee declined the call
    - `timeout`: No answer within timeout period (30 seconds)
    - `busy`: Callee was on another call
    - `failed`: ICE/network connection failed
    - `cancelled`: Caller cancelled before callee answered

  license:
    name: MIT
  contact:
    name: Aoba Team

servers:
  development:
    host: localhost:8082
    protocol: ws
    description: Local development server
  staging:
    host: staging.aoba.chat
    protocol: wss
    description: Staging server (TLS required)
  production:
    host: api.aoba.chat
    protocol: wss
    description: Production server (TLS required)

defaultContentType: application/json

channels:
  xmpp:
    address: /xmpp
    title: XMPP WebSocket Channel (Voice Calls)
    description: |
      Voice call signaling via XMPP IQ stanzas.

      **Namespace**: `urn:xmpp:call:0`
      **Authentication**: JWT token must be present in `auth_token` cookie.

      Voice call signals are embedded in XMPP IQ stanzas and share the same
      WebSocket connection as chat messages. The signal payload is JSON-encoded
      within the IQ stanza.

      **IQ Stanza Format**:
      ```xml
      <iq type="set" to="{recipientJID}" id="{messageId}">
        <call xmlns="urn:xmpp:call:0">
          {JSON signal payload}
        </call>
      </iq>
      ```
    messages:
      CallInitiate:
        $ref: '#/components/messages/CallInitiate'
      CallAccept:
        $ref: '#/components/messages/CallAccept'
      CallReject:
        $ref: '#/components/messages/CallReject'
      CallOffer:
        $ref: '#/components/messages/CallOffer'
      CallAnswer:
        $ref: '#/components/messages/CallAnswer'
      CallIceCandidate:
        $ref: '#/components/messages/CallIceCandidate'
      CallHangup:
        $ref: '#/components/messages/CallHangup'
      CallBusy:
        $ref: '#/components/messages/CallBusy'
      CallMuteChange:
        $ref: '#/components/messages/CallMuteChange'
    bindings:
      ws:
        method: GET
        headers:
          type: object
          properties:
            Cookie:
              type: string
              description: Must contain auth_token={jwt}
            Sec-WebSocket-Protocol:
              type: string
              const: xmpp
              description: WebSocket subprotocol

operations:
  # Initiating a call
  initiateCall:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Initiate a voice call
    description: |
      Start a new voice call with another user. The callee will receive
      the initiate signal and can respond with accept, reject, or busy.
    messages:
      - $ref: '#/channels/xmpp/messages/CallInitiate'

  receiveCallInitiate:
    action: receive
    channel:
      $ref: '#/channels/xmpp'
    summary: Receive incoming call
    description: |
      Receive notification of an incoming voice call. Respond with
      accept, reject, or busy within the timeout period (30 seconds).
    messages:
      - $ref: '#/channels/xmpp/messages/CallInitiate'

  # Accepting/Rejecting calls
  acceptCall:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Accept incoming call
    description: Accept an incoming call. The caller will then send an SDP offer.
    messages:
      - $ref: '#/channels/xmpp/messages/CallAccept'

  rejectCall:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Reject incoming call
    description: Decline an incoming call.
    messages:
      - $ref: '#/channels/xmpp/messages/CallReject'

  sendBusy:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Send busy signal
    description: Indicate that you're already on another call.
    messages:
      - $ref: '#/channels/xmpp/messages/CallBusy'

  # SDP Exchange
  sendOffer:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Send SDP offer
    description: |
      Send WebRTC SDP offer after call is accepted.
      This contains the caller's media capabilities.
    messages:
      - $ref: '#/channels/xmpp/messages/CallOffer'

  receiveOffer:
    action: receive
    channel:
      $ref: '#/channels/xmpp'
    summary: Receive SDP offer
    description: Receive the caller's SDP offer.
    messages:
      - $ref: '#/channels/xmpp/messages/CallOffer'

  sendAnswer:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Send SDP answer
    description: |
      Send WebRTC SDP answer in response to offer.
      This contains the callee's media capabilities.
    messages:
      - $ref: '#/channels/xmpp/messages/CallAnswer'

  receiveAnswer:
    action: receive
    channel:
      $ref: '#/channels/xmpp'
    summary: Receive SDP answer
    description: Receive the callee's SDP answer.
    messages:
      - $ref: '#/channels/xmpp/messages/CallAnswer'

  # ICE Candidates
  sendIceCandidate:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Send ICE candidate
    description: |
      Send an ICE candidate for NAT traversal. Multiple candidates
      may be sent in both directions during connection establishment.
    messages:
      - $ref: '#/channels/xmpp/messages/CallIceCandidate'

  receiveIceCandidate:
    action: receive
    channel:
      $ref: '#/channels/xmpp'
    summary: Receive ICE candidate
    description: Receive an ICE candidate from the other party.
    messages:
      - $ref: '#/channels/xmpp/messages/CallIceCandidate'

  # Call management
  sendHangup:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: End the call
    description: |
      Terminate the call. Include the reason and optional duration
      if the call was connected.
    messages:
      - $ref: '#/channels/xmpp/messages/CallHangup'

  receiveHangup:
    action: receive
    channel:
      $ref: '#/channels/xmpp'
    summary: Receive hangup
    description: Receive notification that the other party ended the call.
    messages:
      - $ref: '#/channels/xmpp/messages/CallHangup'

  sendMuteChange:
    action: send
    channel:
      $ref: '#/channels/xmpp'
    summary: Send mute state change
    description: Notify the other party of your mute state change.
    messages:
      - $ref: '#/channels/xmpp/messages/CallMuteChange'

  receiveMuteChange:
    action: receive
    channel:
      $ref: '#/channels/xmpp'
    summary: Receive mute state change
    description: Receive notification of the other party's mute state.
    messages:
      - $ref: '#/channels/xmpp/messages/CallMuteChange'

components:
  messages:
    CallInitiate:
      name: call_initiate
      title: Call Initiate Signal
      summary: Start a new voice call
      contentType: application/json
      payload:
        $ref: '#/components/schemas/InitiateSignal'
      examples:
        - name: Initiate call
          payload:
            type: 'call:initiate'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:30:00Z'
            payload:
              callerName: 'Alice'
              callerAvatar: 'https://example.com/avatars/alice.jpg'

    CallAccept:
      name: call_accept
      title: Call Accept Signal
      summary: Accept an incoming call
      contentType: application/json
      payload:
        $ref: '#/components/schemas/AcceptSignal'
      examples:
        - name: Accept call
          payload:
            type: 'call:accept'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            recipientId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            timestamp: '2025-12-08T10:30:05Z'
            payload: {}

    CallReject:
      name: call_reject
      title: Call Reject Signal
      summary: Decline an incoming call
      contentType: application/json
      payload:
        $ref: '#/components/schemas/RejectSignal'
      examples:
        - name: Reject call
          payload:
            type: 'call:reject'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            recipientId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            timestamp: '2025-12-08T10:30:05Z'
            payload: {}

    CallOffer:
      name: call_offer
      title: SDP Offer Signal
      summary: WebRTC SDP offer
      contentType: application/json
      payload:
        $ref: '#/components/schemas/OfferSignal'
      examples:
        - name: SDP offer
          payload:
            type: 'call:offer'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:30:06Z'
            payload:
              sdp: 'v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\n...'
              type: 'offer'

    CallAnswer:
      name: call_answer
      title: SDP Answer Signal
      summary: WebRTC SDP answer
      contentType: application/json
      payload:
        $ref: '#/components/schemas/AnswerSignal'
      examples:
        - name: SDP answer
          payload:
            type: 'call:answer'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            recipientId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            timestamp: '2025-12-08T10:30:07Z'
            payload:
              sdp: 'v=0\r\no=- 654321 2 IN IP4 127.0.0.1\r\n...'
              type: 'answer'

    CallIceCandidate:
      name: call_ice_candidate
      title: ICE Candidate Signal
      summary: ICE candidate for NAT traversal
      contentType: application/json
      payload:
        $ref: '#/components/schemas/IceCandidateSignal'
      examples:
        - name: ICE candidate
          payload:
            type: 'call:ice-candidate'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:30:08Z'
            payload:
              candidate: 'candidate:1 1 UDP 2122194687 192.168.1.100 54321 typ host'
              sdpMid: '0'
              sdpMLineIndex: 0

    CallHangup:
      name: call_hangup
      title: Call Hangup Signal
      summary: End the call
      contentType: application/json
      payload:
        $ref: '#/components/schemas/HangupSignal'
      examples:
        - name: Normal hangup
          payload:
            type: 'call:hangup'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:35:00Z'
            payload:
              reason: 'completed'
              duration: 294
        - name: Call cancelled
          payload:
            type: 'call:hangup'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:30:10Z'
            payload:
              reason: 'cancelled'

    CallBusy:
      name: call_busy
      title: Call Busy Signal
      summary: Recipient is on another call
      contentType: application/json
      payload:
        $ref: '#/components/schemas/BusySignal'
      examples:
        - name: Busy signal
          payload:
            type: 'call:busy'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            recipientId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            timestamp: '2025-12-08T10:30:02Z'
            payload: {}

    CallMuteChange:
      name: call_mute_change
      title: Mute Change Signal
      summary: Mute state changed
      contentType: application/json
      payload:
        $ref: '#/components/schemas/MuteChangeSignal'
      examples:
        - name: Muted
          payload:
            type: 'call:mute-change'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:32:00Z'
            payload:
              muted: true
        - name: Unmuted
          payload:
            type: 'call:mute-change'
            callId: '550e8400-e29b-41d4-a716-446655440000'
            senderId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
            recipientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
            timestamp: '2025-12-08T10:33:00Z'
            payload:
              muted: false

  schemas:
    SignalType:
      type: string
      description: Type of call signal
      enum:
        - 'call:initiate'
        - 'call:accept'
        - 'call:reject'
        - 'call:offer'
        - 'call:answer'
        - 'call:ice-candidate'
        - 'call:hangup'
        - 'call:busy'
        - 'call:mute-change'

    EndReason:
      type: string
      description: Reason for call ending
      enum:
        - completed
        - rejected
        - timeout
        - busy
        - failed
        - cancelled

    SignalBase:
      type: object
      description: Base schema for all call signals
      required:
        - type
        - callId
        - senderId
        - recipientId
        - timestamp
        - payload
      properties:
        type:
          $ref: '#/components/schemas/SignalType'
        callId:
          type: string
          format: uuid
          description: Unique identifier for this call session
          example: '550e8400-e29b-41d4-a716-446655440000'
        senderId:
          type: string
          format: uuid
          description: UUID of the user sending the signal
          example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
        recipientId:
          type: string
          format: uuid
          description: UUID of the user receiving the signal
          example: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the signal
          example: '2025-12-08T10:30:00Z'
        payload:
          type: object
          description: Signal-specific payload data

    InitiatePayload:
      type: object
      description: Payload for call:initiate signal
      required:
        - callerName
      properties:
        callerName:
          type: string
          description: Display name of the caller
          example: 'Alice'
        callerAvatar:
          type: string
          format: uri
          description: URL of caller's avatar image
          example: 'https://example.com/avatars/alice.jpg'

    SDPPayload:
      type: object
      description: Payload for SDP offer/answer signals
      required:
        - sdp
        - type
      properties:
        sdp:
          type: string
          description: Session Description Protocol data
          example: 'v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\n...'
        type:
          type: string
          enum:
            - offer
            - answer
          description: SDP type

    IceCandidatePayload:
      type: object
      description: Payload for ICE candidate signal
      required:
        - candidate
        - sdpMid
        - sdpMLineIndex
      properties:
        candidate:
          type: string
          description: ICE candidate string
          example: 'candidate:1 1 UDP 2122194687 192.168.1.100 54321 typ host'
        sdpMid:
          type: string
          description: Media stream identification tag
          example: '0'
        sdpMLineIndex:
          type: integer
          description: Index of the media description in the SDP
          example: 0

    HangupPayload:
      type: object
      description: Payload for call:hangup signal
      required:
        - reason
      properties:
        reason:
          $ref: '#/components/schemas/EndReason'
        duration:
          type: integer
          description: Call duration in seconds (only if call was connected)
          example: 294

    MuteChangePayload:
      type: object
      description: Payload for call:mute-change signal
      required:
        - muted
      properties:
        muted:
          type: boolean
          description: Whether the sender is now muted
          example: true

    EmptyPayload:
      type: object
      description: Empty payload for signals that don't need additional data
      additionalProperties: false

    InitiateSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:initiate'
            payload:
              $ref: '#/components/schemas/InitiatePayload'

    AcceptSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:accept'
            payload:
              $ref: '#/components/schemas/EmptyPayload'

    RejectSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:reject'
            payload:
              $ref: '#/components/schemas/EmptyPayload'

    OfferSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:offer'
            payload:
              $ref: '#/components/schemas/SDPPayload'

    AnswerSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:answer'
            payload:
              $ref: '#/components/schemas/SDPPayload'

    IceCandidateSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:ice-candidate'
            payload:
              $ref: '#/components/schemas/IceCandidatePayload'

    HangupSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:hangup'
            payload:
              $ref: '#/components/schemas/HangupPayload'

    BusySignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:busy'
            payload:
              $ref: '#/components/schemas/EmptyPayload'

    MuteChangeSignal:
      allOf:
        - $ref: '#/components/schemas/SignalBase'
        - type: object
          properties:
            type:
              const: 'call:mute-change'
            payload:
              $ref: '#/components/schemas/MuteChangePayload'
