openapi: 3.1.0
info:
  title: Blah Chatserver Proto
  version: 0.0.1

paths:
  # OAPI does not support WebSocket interface definitions.
  # See: https://github.com/OAI/OpenAPI-Specification/issues/55#issuecomment-929382279
  /ws:
    get:
      summary: WebSocket endpoint
      description: |
        This endpoint is for server-side-event dispatching.

        Once connected, client must send a JSON text message of type
        `WithSig-Auth` for authentication.
        If server does not close it immediately, it means success.

        Since OAPI does not support WebSocket interface, we use request and
        response types documented here mean outgoing and incoming JSON text
        messages.

      parameters:
        - name: Connection
          in: header
          required: true
        - name: Upgrade
          in: header
          required: true

      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WSClientToServer'

      responses:
        101:
          headers:
            Connection:
              required: true
            Upgrade:
              required: true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WSServerToClient'

  /room:
    get:
      summary: List rooms
      parameters:
        - name: filter
          in: query
          required: true
          schema:
            enum:
              - public
              - joined
              - unseen
          description: |
            Must be one of following values:
            - "public": list all public rooms on the server.
            - "joined": list rooms the user have joined.
              Requires `Authorization`.
            - "unseen": list rooms the user have joined and have unseen
              messages.
              Requires `Authorization`.

        - name: top
          in: query
          schema:
            type: string
          description:
            The maximum number of items returned in each page. This is only an
            advice and server can clamp it to a smaller value.

        - name: skipToken
          in: query
          schema:
            type: string
          description:
            The page token returned from a previous list response to fetch the
            next page. NB. Other parameters (eg. `joined` and `page_len`)
            should be included (as the same value) for each page fetch.

        - name: Authorization
          in: header
          description: Optional proof of membership for private rooms.
          schema:
            $ref: '#/components/schemas/WithSig-Auth'

      responses:
        200:
          description: Filtered and paged rooms.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomList'

        401:
          description: Missing or invalid Authorization header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /room/create:
    post:
      summary: Create room

      description:
        When `typ="create_room"`, create a multi-user room.

        When `typ="create_peer_chat"`, create a peer-to-peer room between two
        users. There can be at most one peer room for each given user pair.

      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WithSig-CreateRoom'

      responses:
        200:
          description: Room created.
          content:
            application/json:
              schema:
                type: string
                description: Newly created room `rid`.

        403:
          description: The user does not have permission to create room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

        404:
          description: The peer user does not exist or disallows peer chat.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

        409:
          description: There is already a peer chat room between the user pair.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /room/{rid}:
    get:
      summary: Get room metadata
      parameters:
        - name: Authorization
          in: header
          description: Optional proof of membership for private rooms.
          schema:
            $ref: '#/components/schemas/WithSig-Auth'

      responses:
        200:
          description: The metadata of the specified room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomMetadata'

        404:
          description: |
            Room does not exist or the user does not have permission to get metadata of it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /room/{rid}/admin:
    post:
      summary: Room management

      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WithSig-RoomAdmin'

      responses:
        204:
          description: Operation completed.

        404:
          description: |
            Room does not exist or the user does not have permission for the
            operation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

        409:
          description:
            Operation is already done, eg. joining an already joined room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /room/{rid}/feed.json:
    get:
      summary: Get JSON feed of room
      description: |
        Get room {rid}'s content in JSON feed v1.1 format. The room must be
        public. For human and feed reader consumption only.
      responses:
        200:
          description: The JSON feed.
          content:
            text/feed+json:
              schema:
                $ref: 'https://www.jsonfeed.org/version/1.1/'

        404:
          description: Room does not exist or is private.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /room/{rid}/item:
    get:
      summary: List items in room
      description: |
        Return items in reversed time order, up to `skipToken` items in a
        single response, from room {rid}.
        The last (oldest) chat `cid` will be returned as `skipToken` in
        response, which can be used as query parameter for the next GET, to
        repeatedly fetch more history.

      parameters:
        - name: Authorization
          in: header
          description: Optional proof of membership for private rooms.
          schema:
            $ref: '#/components/schemas/WithSig-Auth'

        - name: top
          in: query
          schema:
            type: integer
          description: |
            The maximum number of items to return. This is an advice and may be
            further clamped by the server. It must not be zero.

        - name: skipToken
          in: query
          schema:
            type: string
          description: |
            Return items after (older than) an existing `cid`. Useful for
            pagination.

      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomItems'

        404:
          description: |
            Room does not exist or the user does not have permission to read it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

    post:
      summary: Post item in room
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WithSig-Chat'

      responses:
        200:
          content:
            application/json:
              schema:
                type: string
                description: Newly created item `cid`.

        403:
          description: The user does not have permission to post in this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

        404:
          description: The room does not exist or the user is not a room member.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /room/{rid}/item/{cid}/seen:
    post:
      summary: Mark item seen
      description: |
        Mark item {cid} in room {rid} seen by the current user.

        Server may enforce that last seen item does not go backward. Marking
        an older item seen or sending the same request multiple times can be a
        no-op.

      parameters:
        - name: Authorization
          in: header
          required: true
          description: Proof of membership for private rooms.
          schema:
            $ref: '#/components/schemas/WithSig-Auth'

      responses:
        204:
          description: Operation completed.

        404:
          description: |
            Room does not exist or the user is not in the room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

# Ideally we should generate these from src, but we need to
# WAIT: https://github.com/juhaku/utoipa/pull/1034
components:
  schemas:
    WSClientToServer:
      anyOf:
        - $ref: '#/components/schemas/WithSig-Auth'

    WSServerToClient:
      anyOf:
        - type: object
          properties:
            chat:
              $ref: '#/components/schemas/WithSig-Chat'

        - type: object
          properties:
            lagged:
              type: object
              const: {}

    ApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: A machine-readable error code string.
              example: invalid_signature
            message:
              type: string
              description: A human-readable error message.
              example: signature verification failed

    RoomList:
      type: object
      required:
        - rooms
      properties:
        rooms:
          type: array
          items:
            $ref: '#/components/schemas/RoomMetadataForList'
        next_token:
          type: string
          description: An opaque token to fetch the next page.

    RoomMetadataForList:
      type: object
      required: ['rid', 'title', 'attrs']
      properties:
        rid:
          type: string
        title:
          type: string
        attrs:
          description: Room attributes bitset, see `RoomAttrs`.
          type: integer
          format: int64
        last_item:
          $ref: '#/components/schemas/WithItemId-WithSig-Chat'
        last_seen_cid:
          description: The `cid` of the last chat being marked as seen.
          type: string
        unseen_cnt:
          description: |
            The number of unseen messages. Only available for
            GET `/room?filter=unseen`.
          type: integer
          format: uint64
        member_permission:
          type: integer
          format: int64
        peer_user:
          type: string
          description: |
            For peer chat room, this gives the identity of the peer user.

    RoomMetadata:
      type: object
      required: ['rid', 'title', 'attrs']
      properties:
        rid:
          type: string
        title:
          type: string
        attrs:
          type: integer
          format: int64

    RoomItems:
      type: object
      required:
        - items
      properties:
        items:
          description: Room items in reversed server-received time order.
          type: array
          items:
            $ref: '#/components/schemas/WithItemId-WithSig-Chat'
        skip_token:
          description: The token for fetching the next page.
          type: string

    RichText:
      type: array
      items:
        anyOf:
          - type: string
            description: Unstyled text piece.
          - type: array
            items: false
            prefixItems:
              - type: string
                description: The text piece to apply styles on.
              - type: object
                properties:
                  b:
                    type: boolean
                    description: Bold.
                  m:
                    type: boolean
                    description: Monospace.
                  i:
                    type: boolean
                    description: Italic.
                  s:
                    type: boolean
                    description: Strikethrough.
                  u:
                    type: boolean
                    description: Underline.
                  hashtag:
                    type: boolean
                    description: Hashtag.
                  link:
                    type: string
                    description: Link target.


    WithSig-Auth:
      type: object
      properties:
        sig:
          type: string
        signee:
          type: object
          properties:
            nonce:
              type: integer
              format: uint32
            payload:
              type: object
              properties:
                typ:
                  type: string
                  const: 'auth'

    WithSig-RoomAdmin:
      type: object
      properties:
        sig:
          type: string
        signee:
          type: object
          properties:
            nonce:
              type: integer
              format: uint32
            payload:
              oneOf:

                - description: Add member to the room.
                  type: object
                  properties:
                    typ:
                      type: string
                      const: 'add_member'
                    room:
                      type: string
                    permission:
                      type: integer
                      format: uint64
                    user:
                      type: string

                - description: Remove member from the room.
                  type: object
                  properties:
                    typ:
                      type: string
                      const: 'remove_member'
                    room:
                      type: string
                    user:
                      type: string


      example:
        sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
        signee:
          nonce: 670593955
          payload:
            permission: 1
            room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd
            typ: add_member
            user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
          timestamp: 1724966284
          user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7

    WithSig-Chat:
      type: object
      properties:
        sig:
          type: string
        signee:
          type: object
          properties:
            nonce:
              type: integer
              format: uint32
            payload:
              type: object
              properties:
                typ:
                  type: string
                  const: 'chat'
                room:
                  type: string
                rich_text:
                  $ref: '$/components/schemas/RichText'
      example:
        sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
        signee:
          nonce: 670593955
          payload:
            typ: chat
            room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd
            rich_text: ["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"]
          timestamp: 1724966284
          user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7

    WithItemId-WithSig-Chat:
      allOf:
        - $ref: '#/components/schemas/WithSig-Chat'
        - type: object
          properties:
            cid:
              type: string
              description: An opaque server-specific item identifier.

    WithSig-CreateRoom:
      type: object
      properties:
        sig:
          type: string
        signee:
          type: object
          properties:
            nonce:
              type: integer
              format: uint32
            payload:
              oneOf:
                - type: object
                  properties:
                    typ:
                      type: string
                      const: 'create_room'
                    title:
                      type: string
                    members:
                      type: array
                      items:
                        type: object
                        properties:
                          user:
                            type: string
                          permission:
                            type: integer
                            format: int64
                - type: object
                  properties:
                    typ:
                      type: string
                      const: 'create_peer_chat'
                    peer:
                      type: string

      example:
        sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
        signee:
          nonce: 670593955
          payload:
            typ: create_room
            attrs: 1 # PUBLIC_READABLE
            title: 'hello room'
            members:
              - user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
                permission: -1
          timestamp: 1724966284
          user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7