openapi: 3.0.3
info:
  title: 2meet LMS JSON API
  version: 1.0.0
  description: |
    Modern JSON meeting API for reseller integrators.

    **Primary integrator surface** for Moodle/Canvas is the LMS wire API — see `docs/api/lms-integration.md`.

    Base URL: `https://{lms_api_slug}.{baseDomain}/lms/v1/`

    The LMS API slug is assigned manually by platform admin after partner onboarding.

servers:
  - url: https://{slug}.2meet.ir/lms/v1
    variables:
      slug:
        default: your-partner-slug
        description: LMS API slug from partner credentials

security:
  - LmsHMAC: []

paths:
  /meetings:
    post:
      summary: Create meeting
      operationId: createMeeting
      description: |
        Creates a meeting and returns join URLs.

        **Session cover (optional):** send `multipart/form-data` with field `meeting` (JSON string)
        and optional `cover` (image file). Or create with JSON only and upload cover later via
        `POST /meetings/{id}/cover`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMeetingRequest'
          multipart/form-data:
            schema:
              type: object
              required:
                - meeting
              properties:
                meeting:
                  type: string
                  description: JSON-encoded CreateMeetingRequest
                cover:
                  type: string
                  format: binary
                  description: Optional stage placeholder image (PNG/JPG/WEBP, max 5 MB)
      responses:
        '201':
          description: Meeting created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      summary: List meetings
      operationId: listMeetings
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: Meeting list
          content:
            application/json:
              schema:
                type: object
                properties:
                  meetings:
                    type: array
                    items:
                      $ref: '#/components/schemas/MeetingDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /meetings/{id}:
    get:
      summary: Get meeting
      operationId: getMeeting
      parameters:
        - $ref: '#/components/parameters/meetingId'
      responses:
        '200':
          description: Meeting detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingDetail'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      summary: End meeting
      operationId: endMeeting
      parameters:
        - $ref: '#/components/parameters/meetingId'
      responses:
        '200':
          description: Meeting ended
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ended
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /meetings/{id}/report:
    get:
      summary: Get per-participant attendance report
      operationId: getMeetingReport
      description: |
        Read-only per-participant attendance/duration report for a meeting, for grading or
        attendance compliance. `{id}` accepts the internal meeting UUID **or** the external
        `meetingID`. A meeting that was never joined returns `200` with an empty
        `participants` array.
      parameters:
        - $ref: '#/components/parameters/meetingId'
      responses:
        '200':
          description: Attendance report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttendanceReport'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /meetings/{id}/cover:
    post:
      summary: Upload session cover image (optional)
      operationId: uploadMeetingCover
      description: |
        Upload or replace the **optional** session cover — a static image shown on the video stage
        when the host camera is off or before the class starts. Does not affect file-share mode.

        **Formats:** PNG, JPG, WEBP — max 5 MB. Field name: `cover`.

        Participants load the image from the public URL
        `GET /api/v1/portal/sessions/{portalSessionId}/cover` when `has_cover` is true in session state.
      parameters:
        - $ref: '#/components/parameters/meetingId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - cover
              properties:
                cover:
                  type: string
                  format: binary
      responses:
        '200':
          description: Cover uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  has_cover:
                    type: boolean
                    example: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /meetings/{id}/join-links:
    post:
      summary: Regenerate join links
      operationId: regenerateJoinLinks
      parameters:
        - $ref: '#/components/parameters/meetingId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JoinLinksRequest'
      responses:
        '200':
          description: Fresh join URLs. Identity fields are baked into the ticket for the target role.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JoinLinks'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    LmsHMAC:
      type: apiKey
      in: header
      name: X-2meet-Signature
      description: |
        HMAC-SHA256 over `{timestamp}\n{METHOD}\n{path}\n{body}` using integration secret.
        Also send `X-2meet-Timestamp` (Unix seconds). Reseller resolved via subdomain or `X-2meet-Reseller-Slug`.

  parameters:
    meetingId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 50
        maximum: 100
    offset:
      name: offset
      in: query
      schema:
        type: integer
        default: 0

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Invalid or missing HMAC
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string

    CreateMeetingRequest:
      type: object
      required:
        - name
      properties:
        external_id:
          type: string
          description: Integrator-side meeting identifier (maps to meetingID on the wire API)
        name:
          type: string
        max_participants:
          type: integer
          default: 80
        session_mode:
          type: string
          enum: [interactive, lecture]
          default: interactive
          description: interactive = collaborative; lecture = host-centric classroom
        duration_minutes:
          type: integer
          description: |
            Hard time cap. Session auto-ends at actual_start_at + duration_minutes.
            Omit for demo meetings → defaults to 10 minutes so they don't linger.
            Wire create equivalent: `duration`.
        lobby_enabled:
          type: boolean
          default: false
        features:
          $ref: '#/components/schemas/SessionFeatures'
        logout_url:
          type: string
          format: uri
          description: |
            Meeting-default return URL after leave/end (BBB logoutURL).
            Absolute http(s) only. Join-links / wire join may override per user.
        metadata:
          type: object
          additionalProperties: true
          description: Optional freeform; not required for classroom create

    SessionFeatures:
      type: object
      properties:
        whiteboard:
          type: boolean
        polls:
          type: boolean
        recording:
          type: boolean
        screen_share:
          type: boolean
        hand_raise:
          type: boolean
        file_sharing:
          type: boolean
        breakout_rooms:
          type: boolean
        chat:
          type: boolean
        ai_transcript:
          type: boolean
        ai_summary:
          type: boolean
      example:
        whiteboard: true
        polls: true
        recording: false
        screen_share: true
        hand_raise: true
        file_sharing: true
        breakout_rooms: false
        chat: true
        ai_transcript: false
        ai_summary: false

    SessionCoverUploadResponse:
      type: object
      properties:
        has_cover:
          type: boolean
          description: True when a cover image is stored for this session

    MeetingResponse:
      type: object
      properties:
        meeting_id:
          type: string
          format: uuid
        external_id:
          type: string
        moderator_join_url:
          type: string
          format: uri
        attendee_join_url:
          type: string
          format: uri
        status:
          type: string
          enum: [active, ended]

    MeetingDetail:
      type: object
      properties:
        meeting_id:
          type: string
          format: uuid
        external_id:
          type: string
        name:
          type: string
        status:
          type: string
        participant_count:
          type: integer
        created_at:
          type: string
          format: date-time

    AttendanceParticipant:
      type: object
      properties:
        user_id:
          type: string
          description: LMS user_id supplied at join (stable identity), else display name
        display_name:
          type: string
        joined:
          type: boolean
        first_joined_at:
          type: string
          format: date-time
          nullable: true
        last_left_at:
          type: string
          format: date-time
          nullable: true
        total_duration_seconds:
          type: integer
        join_count:
          type: integer
        voice_minutes:
          type: integer
          format: int64
        video_minutes:
          type: integer
          format: int64
        recording_minutes:
          type: integer
          format: int64

    AttendanceReport:
      type: object
      properties:
        meeting_id:
          type: string
          description: External meeting id
        session_id:
          type: string
          format: uuid
          nullable: true
        session_name:
          type: string
        date:
          type: string
          format: date-time
          nullable: true
          description: COALESCE(actual_start_at, scheduled_start)
        participant_count:
          type: integer
          description: Distinct participants that joined
        total_duration_seconds:
          type: integer
          description: Session wall span (max last-left minus min first-join)
        participants:
          type: array
          items:
            $ref: '#/components/schemas/AttendanceParticipant'

    JoinLinks:
      type: object
      properties:
        moderator_join_url:
          type: string
          format: uri
        attendee_join_url:
          type: string
          format: uri

    JoinLinksRequest:
      type: object
      description: |
        Per-participant join identity (BBB-style). Call once per user before redirecting them.
        When `role` is omitted and identity fields are set, they apply to the moderator URL (legacy).
      properties:
        display_name:
          type: string
          description: Display name in the live room (default Guest when other identity fields are set)
        avatar_url:
          type: string
          format: uri
          description: http(s) image URL shown on tiles, participants list, and chat
        user_id:
          type: string
          description: Stable LMS user id for identity across reconnects
        logout_url:
          type: string
          format: uri
          description: |
            Per-ticket return URL after leave/end (BBB logoutURL). Overrides create-time default.
            Placeholders %%MEETINGID%%, %%USERID%%, %%USERNAME%% are substituted at join redeem.
        role:
          type: string
          enum: [moderator, attendee]
          description: Which join URL receives the identity fields
      example:
        display_name: Alice Teacher
        avatar_url: https://lms.example/avatars/alice.jpg
        user_id: "42"
        role: moderator
        logout_url: https://lms.example/mod/bigbluebuttonbn/view.php?id=9
