openapi: 3.1.0
info:
  title: Lucas Messenger Protocol API
  version: 1.0.0
  description: |
    Lucas is a decentralized internet messenger protocol using CommonMark-formatted messages.
    
    ## Key Features
    - **CommonMark Support**: Messages use plain CommonMark (no extensions)
    - **3072 Character Limit**: Same as RCS
    - **Dual Transport**: WebSocket for real-time, REST for fallback
    - **Email-style Usernames**: `user@domain.tld`
    - **Federated**: ActivityPub-style server-to-server communication
    
    ## Rate Limiting
    
    Lucas uses Twitter-style rate limiting with the following headers:
    
    | Header | Description |
    |--------|-------------|
    | `X-Rate-Limit-Limit` | Number of requests allowed in the window |
    | `X-Rate-Limit-Remaining` | Number of requests remaining |
    | `X-Rate-Limit-Reset` | Unix timestamp when the window resets |
    | `X-Rate-Limit-Reset-After` | Seconds until the window resets |
    | `X-Rate-Limit-Policy` | Rate limit policy name |
    
    ### Rate Limits by Endpoint Category
    
    | Category | Limit | Window | Description |
    |----------|-------|--------|-------------|
    | Authentication | 10 | 15 min | Login, register, token refresh |
    | Messages | 100 | 15 min | Send, edit, delete messages |
    | Conversations | 30 | 15 min | Create, update conversations |
    | Users | 60 | 15 min | Profile lookups, updates |
    | Groups | 20 | 15 min | Group management operations |
    | Federation | 1000 | 15 min | Server-to-server requests |
    | Read Receipts | 300 | 15 min | Mark messages as read |
    
    When rate limited, the API returns `429 Too Many Requests`:
    ```json
    {
      "error": "rate_limit_exceeded",
      "message": "Rate limit exceeded. Try again in 450 seconds.",
      "retry_after": 450
    }
    ```
  contact:
    name: Lucas Protocol
    url: https://github.com/lucas-protocol
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://{domain}/api/v1
    description: Lucas API Server
    variables:
      domain:
        default: localhost:3000
        description: Domain name of the Lucas server

security:
  - BearerAuth: []

tags:
  - name: Auth
    description: Authentication and registration
  - name: Users
    description: User profile management
  - name: Messages
    description: Message operations (send, edit, delete, history)
  - name: Conversations
    description: Direct and group conversations
  - name: Groups
    description: Group management
  - name: Presence
    description: Online status and typing indicators
  - name: Federation
    description: Server-to-server communication

paths:
  /auth/register:
    post:
      operationId: register
      summary: Register a new account
      description: Create a new Lucas account with email-style username
      tags: [Auth]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Account created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterResponse'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'

  /auth/login:
    post:
      operationId: login
      summary: Login to existing account
      description: Authenticate and receive JWT token
      tags: [Auth]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /auth/refresh:
    post:
      operationId: refreshToken
      summary: Refresh access token
      description: Exchange refresh token for new access token
      tags: [Auth]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
                  description: Refresh token from login
      responses:
        '200':
          description: Token refreshed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefreshResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /users/me:
    get:
      operationId: getMe
      summary: Get current user profile
      description: Returns the authenticated user's profile
      tags: [Users]
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

    put:
      operationId: updateMe
      summary: Update current user profile
      description: Update display name, avatar, or other profile fields
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /users/{userId}:
    get:
      operationId: getUser
      summary: Get user by ID
      description: Retrieve a user's public profile
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /conversations:
    get:
      operationId: listConversations
      summary: List user's conversations
      description: Get all conversations for the authenticated user
      tags: [Conversations]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
          description: Pagination cursor (conversation ID from previous response)
      responses:
        '200':
          description: List of conversations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationList'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /conversations/direct:
    post:
      operationId: createDirectConversation
      summary: Create or get direct conversation
      description: Create a 1:1 conversation or return existing one
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [recipientId]
              properties:
                recipientId:
                  type: string
                  description: User ID of the other participant
      responses:
        '201':
          description: Conversation created or retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /conversations/group:
    post:
      operationId: createGroupConversation
      summary: Create group conversation
      description: Create a new group chat with multiple members
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGroupRequest'
      responses:
        '201':
          description: Group created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /conversations/{conversationId}:
    get:
      operationId: getConversation
      summary: Get conversation details
      description: Retrieve conversation info and members
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      responses:
        '200':
          description: Conversation details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

    put:
      operationId: updateConversation
      summary: Update conversation settings
      description: Update group name, avatar, or other settings
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateConversationRequest'
      responses:
        '200':
          description: Conversation updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /conversations/{conversationId}/members:
    get:
      operationId: listMembers
      summary: List conversation members
      description: Get all members of a conversation
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      responses:
        '200':
          description: List of members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      operationId: addMember
      summary: Add member to conversation
      description: Add a user to a group conversation
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId:
                  type: string
                role:
                  type: string
                  enum: [member, admin]
                  default: member
      responses:
        '200':
          description: Member added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationMember'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'

    delete:
      operationId: removeMember
      summary: Remove member from conversation
      description: Remove a user from a group conversation
      tags: [Conversations]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId:
                  type: string
      responses:
        '204':
          description: Member removed
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /messages:
    post:
      operationId: sendMessage
      summary: Send a message
      description: Send a CommonMark message to a conversation
      tags: [Messages]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
      responses:
        '201':
          description: Message sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /messages/{messageId}:
    get:
      operationId: getMessage
      summary: Get a message by ID
      description: Retrieve a single message
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/MessageId'
      responses:
        '200':
          description: Message details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageDetail'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

    put:
      operationId: editMessage
      summary: Edit a message
      description: Edit an existing message (only by sender, within 24h)
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/MessageId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                  maxLength: 3072
                  description: New CommonMark content
      responses:
        '200':
          description: Message edited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Message too old to edit (超过 24 hours)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

    delete:
      operationId: deleteMessage
      summary: Delete a message
      description: Delete a message (sender or admin can delete)
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/MessageId'
      responses:
        '204':
          description: Message deleted
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /messages/conversations/{conversationId}/messages:
    get:
      operationId: listMessages
      summary: List messages in conversation
      description: Get message history for a conversation
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          schema:
            type: string
          description: Message ID for pagination (get messages before/after this)
        - name: direction
          in: query
          schema:
            type: string
            enum: [before, after, around]
            default: before
      responses:
        '200':
          description: Message history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageList'
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /messages/conversations/{conversationId}/read:
    post:
      operationId: markRead
      summary: Mark conversation as read
      description: Mark all messages up to a message ID as read
      tags: [Messages]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [lastReadId]
              properties:
                lastReadId:
                  type: string
                  description: ID of the last read message
      responses:
        '204':
          description: Read receipt sent
          headers:
            X-Rate-Limit-Limit:
              $ref: '#/components/headers/X-Rate-Limit-Limit'
            X-Rate-Limit-Remaining:
              $ref: '#/components/headers/X-Rate-Limit-Remaining'
            X-Rate-Limit-Reset:
              $ref: '#/components/headers/X-Rate-Limit-Reset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /presence:
    put:
      operationId: updatePresence
      summary: Update presence status
      description: Set online status and optional custom message
      tags: [Presence]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [online, away, busy, offline]
                  default: online
                customMessage:
                  type: string
                  maxLength: 128
                  nullable: true
      responses:
        '200':
          description: Presence updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Presence'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /presence/{userId}:
    get:
      operationId: getPresence
      summary: Get user presence
      description: Check a user's online status
      tags: [Presence]
      security:
        - BearerAuth: []
        - []
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: User presence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Presence'
        '404':
          $ref: '#/components/responses/NotFound'

  /typing/{conversationId}:
    post:
      operationId: sendTypingIndicator
      summary: Send typing indicator
      description: Notify others that you're typing
      tags: [Presence]
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      responses:
        '204':
          description: Typing indicator sent
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /.well-known/lucas.json:
    get:
      operationId: getServerInfo
      summary: Get server discovery info
      description: |
        Returns server metadata for federation.
        Used by other Lucas servers to discover this server.
      tags: [Federation]
      security: []
      responses:
        '200':
          description: Server info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerInfo'

  /federation/inbox:
    post:
      operationId: federationInbox
      summary: Receive federated message
      description: Endpoint for receiving messages from other servers
      tags: [Federation]
      security:
        - FederationSignature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FederationMessage'
      responses:
        '202':
          description: Message accepted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Invalid or missing signature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token from /auth/login or /auth/register

    FederationSignature:
      type: apiKey
      in: header
      name: X-Lucas-Signature
      description: Ed25519 signature for server-to-server requests

  parameters:
    UserId:
      name: userId
      in: path
      required: true
      schema:
        type: string
      description: User ID

    ConversationId:
      name: conversationId
      in: path
      required: true
      schema:
        type: string
      description: Conversation ID

    MessageId:
      name: messageId
      in: path
      required: true
      schema:
        type: string
      description: Message ID

  headers:
    X-Rate-Limit-Limit:
      schema:
        type: integer
      description: Number of requests allowed in the current window
      example: 100

    X-Rate-Limit-Remaining:
      schema:
        type: integer
      description: Number of requests remaining in the current window
      example: 95

    X-Rate-Limit-Reset:
      schema:
        type: integer
      description: Unix timestamp when the rate limit window resets
      example: 1689101700

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Error code
          examples: ["rate_limit_exceeded", "invalid_request", "unauthorized"]
        message:
          type: string
          description: Human-readable error message
        retry_after:
          type: integer
          description: Seconds to wait before retrying (for 429 errors)

    RegisterRequest:
      type: object
      required: [username, domain, password]
      properties:
        username:
          type: string
          minLength: 3
          maxLength: 64
          pattern: '^[a-zA-Z0-9_-]+$'
          description: Username part (before @)
        domain:
          type: string
          description: Server domain (e.g., example.com)
        password:
          type: string
          minLength: 8
          description: Account password
        displayName:
          type: string
          maxLength: 128
          description: Optional display name

    RegisterResponse:
      type: object
      properties:
        accessToken:
          type: string
          description: JWT access token (expires in 1 hour)
        refreshToken:
          type: string
          description: Refresh token (expires in 30 days)
        expiresIn:
          type: integer
          description: Access token expiry in seconds
          example: 3600
        tokenType:
          type: string
          example: Bearer
        user:
          type: object
          properties:
            id:
              type: string
            username:
              type: string
            domain:
              type: string
            address:
              type: string
              description: Full user address (username@domain)
            displayName:
              type: string
              nullable: true
            createdAt:
              type: string
              format: date-time

    LoginRequest:
      type: object
      required: [username, domain, password]
      properties:
        username:
          type: string
        domain:
          type: string
        password:
          type: string

    LoginResponse:
      type: object
      properties:
        accessToken:
          type: string
          description: JWT access token (expires in 1 hour)
        refreshToken:
          type: string
          description: Refresh token (expires in 30 days)
        expiresIn:
          type: integer
          description: Access token expiry in seconds
          example: 3600
        tokenType:
          type: string
          example: Bearer
        user:
          type: object
          properties:
            id:
              type: string
            username:
              type: string
            domain:
              type: string
            address:
              type: string
              description: Full user address (username@domain)
            displayName:
              type: string
              nullable: true
            createdAt:
              type: string
              format: date-time

    RefreshResponse:
      type: object
      properties:
        accessToken:
          type: string
          description: JWT access token (expires in 1 hour)
        refreshToken:
          type: string
          description: New refresh token (expires in 30 days)
        expiresIn:
          type: integer
          description: Access token expiry in seconds
          example: 3600
        tokenType:
          type: string
          example: Bearer
        user:
          type: object
          properties:
            id:
              type: string
            username:
              type: string
            domain:
              type: string
            address:
              type: string
              description: Full user address (username@domain)

    User:
      type: object
      properties:
        id:
          type: string
        username:
          type: string
        domain:
          type: string
        address:
          type: string
          description: Full user address (username@domain)
          example: alice@example.com
        displayName:
          type: string
          nullable: true
        avatarUrl:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        lastSeen:
          type: string
          format: date-time
          nullable: true

    UpdateUserRequest:
      type: object
      properties:
        displayName:
          type: string
          maxLength: 128
        avatarUrl:
          type: string
          format: uri

    Message:
      type: object
      properties:
        id:
          type: string
        conversationId:
          type: string
        senderId:
          type: string
        content:
          type: string
          maxLength: 3072
          description: CommonMark formatted content
        replyTo:
          type: string
          nullable: true
          description: ID of message being replied to
        edited:
          type: boolean
          description: Whether message has been edited
        editedAt:
          type: string
          format: date-time
          nullable: true
        deleted:
          type: boolean
          description: Whether message has been deleted (tombstone)
        createdAt:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true
          description: Optional metadata (reactions, etc.)

    MessageDetail:
      type: object
      properties:
        id:
          type: string
        conversationId:
          type: string
        senderId:
          type: string
        content:
          type: string
          maxLength: 3072
          description: CommonMark formatted content
        replyTo:
          type: string
          nullable: true
          description: ID of message being replied to
        edited:
          type: boolean
          description: Whether message has been edited
        editedAt:
          type: string
          format: date-time
          nullable: true
        deleted:
          type: boolean
          description: Whether message has been deleted (tombstone)
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: When the message was deleted
        deletedBy:
          type: string
          nullable: true
          description: User ID of whoever deleted the message
        createdAt:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true
          description: Optional metadata (reactions, etc.)

    SendMessageRequest:
      type: object
      required: [conversationId, content]
      properties:
        conversationId:
          type: string
        content:
          type: string
          minLength: 1
          maxLength: 3072
          description: CommonMark formatted content
        replyTo:
          type: string
          description: ID of message being replied to
        metadata:
          type: object
          additionalProperties: true

    MessageList:
      type: object
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/MessageDetail'
        cursor:
          type: string
          nullable: true
          description: Cursor for next page (message ID)
        hasMore:
          type: boolean
          description: Whether there are more messages

    Conversation:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [direct, group]
        name:
          type: string
          nullable: true
          description: Group name (null for direct messages)
        avatarUrl:
          type: string
          nullable: true
        members:
          type: array
          items:
            $ref: '#/components/schemas/ConversationMember'
        lastMessage:
          allOf:
            - $ref: '#/components/schemas/Message'
            - type: object
          nullable: true
        unreadCount:
          type: integer
          description: Number of unread messages
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ConversationList:
      type: object
      properties:
        conversations:
          type: array
          items:
            $ref: '#/components/schemas/Conversation'
        cursor:
          type: string
          nullable: true
          description: Cursor for next page (conversation ID)
        hasMore:
          type: boolean
          description: Whether there are more conversations

    ConversationMember:
      type: object
      properties:
        userId:
          type: string
        role:
          type: string
          enum: [member, admin, owner]
        joinedAt:
          type: string
          format: date-time
        user:
          type: object
          nullable: true
          properties:
            id:
              type: string
            username:
              type: string
            domain:
              type: string
            address:
              type: string
              description: Full user address (username@domain)
            displayName:
              type: string
              nullable: true
            avatarUrl:
              type: string
              nullable: true

    MemberList:
      type: object
      properties:
        members:
          type: array
          items:
            $ref: '#/components/schemas/ConversationMember'
        cursor:
          type: string
          nullable: true

    CreateGroupRequest:
      type: object
      required: [name, memberIds]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
        memberIds:
          type: array
          items:
            type: string
          minItems: 1
          description: User IDs to add as members
        avatarUrl:
          type: string
          format: uri

    UpdateConversationRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
        avatarUrl:
          type: string
          format: uri

    Presence:
      type: object
      properties:
        userId:
          type: string
        status:
          type: string
          enum: [online, away, busy, offline]
        customMessage:
          type: string
          nullable: true
        lastSeen:
          type: string
          format: date-time

    ServerInfo:
      type: object
      properties:
        version:
          type: string
          example: "1.0"
        domain:
          type: string
          example: "example.com"
        inbox:
          type: string
          format: uri
          description: Federation inbox URL
        publicKey:
          type: object
          properties:
            id:
              type: string
            type:
              type: string
            publicKeyPem:
              type: string
        description:
          type: string
        policies:
          type: object
          properties:
            federation:
              type: boolean
            registration:
              type: boolean
            maxMessageLength:
              type: integer
              example: 3072

    FederationMessage:
      type: object
      properties:
        type:
          type: string
          enum: [Create, Update, Delete]
        id:
          type: string
          description: ActivityPub-style activity ID
        actor:
          type: string
          description: Sender's actor URL
        object:
          type: object
          properties:
            type:
              type: string
            id:
              type: string
            attributedTo:
              type: string
            content:
              type: string
            to:
              type: array
              items:
                type: string
            published:
              type: string
              format: date-time

  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: invalid_request
            message: "Invalid request parameters"

    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: unauthorized
            message: "Invalid or missing authentication"

    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: forbidden
            message: "You don't have permission to do this"

    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: not_found
            message: "Resource not found"

    Conflict:
      description: Conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: conflict
            message: "Resource already exists"

    RateLimited:
      description: Rate limit exceeded
      headers:
        X-Rate-Limit-Limit:
          $ref: '#/components/headers/X-Rate-Limit-Limit'
        X-Rate-Limit-Remaining:
          $ref: '#/components/headers/X-Rate-Limit-Remaining'
        X-Rate-Limit-Reset:
          $ref: '#/components/headers/X-Rate-Limit-Reset'
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: rate_limit_exceeded
            message: "Rate limit exceeded. Try again in 450 seconds."
            retry_after: 450
