openapi: 3.1.0
info:
  title: OnpChat API
  version: "1.0.0"
  summary: انتشار پیام در کانال، گروه، کلاس و سانترالِ OnpChat
  description: |
    APIِ رسمیِ OnpChat برای انتشارِ پیام در فضاهایی که مدیرشان هستید.

    هر فضا (کانال/گروه/کلاس/سانترال) یک توکنِ مستقل دارد. توکن فقط روی همان فضا
    کار می‌کند و از پنلِ خودِ فضا صادر یا باطل می‌شود.

    این API مخاطبش **سرور** است نه مرورگر: هدرِ CORS ندارد و کوکی نمی‌پذیرد.
    توکن را در جاوااسکریپتِ سمتِ کاربر نگذارید.
  contact:
    name: OnpChat
    url: https://onpchat.ir

servers:
  - url: https://api.onpchat.ir
    description: سرورِ اصلی

security:
  - bearerAuth: []

tags:
  - name: token
    description: شناساییِ توکن
  - name: spaces
    description: فضاها — کانال، گروه، کلاس، سانترال

paths:
  /v1/me:
    get:
      tags: [token]
      summary: این توکن مالِ کدام فضاست
      operationId: getMe
      responses:
        "200":
          description: مشخصاتِ توکن
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  result:
                    type: object
                    properties:
                      entity_id:   { type: string, examples: ["#1786551855736"] }
                      entity_kind: { type: string, enum: [channel, group, class, santral] }
                      entity_name: { type: string }
                      scopes:
                        type: array
                        items: { type: string, enum: [publish, read, webhook] }
                      created_at:  { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/{space_type}/{id}:
    get:
      tags: [spaces]
      summary: مشخصاتِ فضا
      operationId: getSpace
      parameters:
        - $ref: "#/components/parameters/SpaceType"
        - $ref: "#/components/parameters/SpaceID"
      responses:
        "200":
          description: مشخصاتِ فضا
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  result:
                    type: object
                    properties:
                      id:           { type: string }
                      kind:         { type: string, enum: [channel, group, class, santral] }
                      name:         { type: string }
                      username:     { type: string }
                      description:  { type: string }
                      is_public:    { type: boolean }
                      member_count: { type: integer }
                      is_read_only:
                        type: boolean
                        description: فقط در کانال وجود دارد.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/{space_type}/{id}/messages:
    get:
      tags: [spaces]
      summary: تاریخچهٔ پیام‌ها
      operationId: listMessages
      description: |
        جدیدترین اول. صفحه‌بندی با کِرسرِ `seq` است نه با زمان: `seq` یک شمارندهٔ
        یکنواختِ هر چت است، پس دو پیامِ هم‌زمان هم ترتیبِ قطعی دارند و هیچ ردیفی بینِ
        دو صفحه جا نمی‌افتد. مقدارِ `next_cursor` را در `before_seq` صفحهٔ بعد بگذارید؛
        اگر `null` بود یعنی به انتها رسیده‌اید.
      parameters:
        - $ref: "#/components/parameters/SpaceType"
        - $ref: "#/components/parameters/SpaceID"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 30 }
        - name: before_seq
          in: query
          description: کِرسرِ صفحهٔ بعد (از `next_cursor` پاسخِ قبلی).
          schema: { type: integer, format: int64 }
      responses:
        "200":
          description: فهرستِ پیام‌ها
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  result:
                    type: object
                    properties:
                      messages:
                        type: array
                        items: { $ref: "#/components/schemas/Message" }
                      next_cursor:
                        type: [integer, "null"]
                        format: int64
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

    post:
      tags: [spaces]
      summary: انتشارِ پیام
      operationId: publishMessage
      description: |
        پیام با هویتِ همان مدیری منتشر می‌شود که توکن را ساخته است.

        در **کانال** این هویت به مشترکین نشان داده نمی‌شود؛ پیام از طرفِ خودِ کانال
        دیده می‌شود. در **گروه/کلاس/سانترال** نامِ همان مدیر نمایش داده می‌شود، چون
        این فضاها ذاتاً فرستنده را نشان می‌دهند.
      parameters:
        - $ref: "#/components/parameters/SpaceType"
        - $ref: "#/components/parameters/SpaceID"
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            کلیدِ یکتا برای همین پیام. اگر درخواست تایم‌اوت گرفت و دوباره فرستادید،
            با همان کلید پیامِ تکراری ساخته نمی‌شود. این کلید همان `id` پیام می‌شود.
          schema: { type: string, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                یکی از `text` یا `card` را بفرستید؛ نوعِ پست از روی همین تشخیص داده
                می‌شود و حالتی برای اعلام‌کردن وجود ندارد. اگر `text` همراهِ `card`
                بیاید، به‌عنوانِ متنِ همراهِ کارت (`caption`) به‌کار می‌رود.
              properties:
                text:
                  type: string
                  maxLength: 8000
                  description: متنِ پیام. آپلودِ مدیا در این نسخه نیست.
                card:
                  type: object
                  description: کارتِ لینک — همان چیزی که «ساختِ کارت» در اپ می‌سازد.
                  required: [url]
                  properties:
                    url:
                      type: string
                      format: uri
                      description: لینکی که کارت به آن می‌رود. بدونِ scheme، https فرض می‌شود.
                    title:       { type: string, maxLength: 200 }
                    description: { type: string, maxLength: 1000 }
                    image:
                      type: string
                      format: uri
                      description: |
                        کاور. باید نشانیِ کاملِ http/https باشد؛ نشانیِ نسبی رد می‌شود
                        چون کارت روی دستگاهِ خواننده رندر می‌شود.
                    caption:
                      type: string
                      maxLength: 2000
                      description: متنِ بیرونِ کارت، به‌عنوانِ حرفِ خودِ فرستنده.
            examples:
              simple:
                summary: متنِ ساده
                value: { text: "سلام! این پیام از API آمده." }
              card:
                summary: کارت
                value:
                  card:
                    url: "https://example.com/post"
                    title: "عنوانِ کارت"
                    description: "متنِ داخلِ کارت"
                    image: "https://example.com/cover.jpg"
                    caption: "حرفِ خودم بیرونِ کارت"
      responses:
        "200":
          description: منتشر شد
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  result:
                    type: object
                    properties:
                      id:        { type: string, description: شناسهٔ پیام }
                      entity_id: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        هدرِ `Authorization: Bearer <token>`.
        توکن را از پنلِ فضا در onpchat.ir بگیرید. کوکی و `?token=` پذیرفته نمی‌شود.

  parameters:
    SpaceType:
      name: space_type
      in: path
      required: true
      description: نوعِ فضا.
      schema:
        type: string
        enum: [channels, groups, classes, santrals]
    SpaceID:
      name: id
      in: path
      required: true
      description: |
        شناسهٔ فضا. پیشوندش بخشی از شناسه است و در URL باید encode شود:
        `#`→`%23`، `@`→`%40`، `%`→`%25`، `&`→`%26`.
        فرستادنِ شناسه بدونِ پیشوند هم پذیرفته می‌شود.
      schema: { type: string }
      examples:
        channel: { value: "%231786551855736" }
        group:   { value: "%401786579451315" }

  schemas:
    Message:
      type: object
      properties:
        id:         { type: string }
        text:       { type: string }
        timestamp:  { type: integer, format: int64, description: میلی‌ثانیه از epoch }
        seq:        { type: integer, format: int64, description: کِرسرِ صفحه‌بندی }
        view_count: { type: integer }
        image_url:  { type: string }
        sender:
          type: object
          description: فقط در گروه/کلاس/سانترال. کانال هویتِ نویسنده را برنمی‌گرداند.
          properties:
            mobile: { type: string }
            name:   { type: string }

    Error:
      type: object
      properties:
        ok: { type: boolean, const: false }
        error:
          type: object
          properties:
            code:
              type: string
              description: کدِ ماشین‌خوان. شرط‌هایتان را روی این بگذارید، نه روی متن.
            message: { type: string }

  responses:
    BadRequest:
      description: |
        ورودی نامعتبر. کدها: `bad_request`، `empty_message`،
        `bad_card` (کارتِ ناقص یا نشانیِ نامعتبر)، `bad_entity_id`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: |
        توکن نیست یا معتبر نیست. کدها: `missing_token`، `invalid_token`، `revoked_token`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: |
        اجازه ندارید. کدها: `entity_mismatch` (توکنِ فضای دیگری)،
        `wrong_entity_kind`، `missing_scope`، `sender_not_member`،
        `sender_banned`، `read_only`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: "فضا پیدا نشد. کد: `entity_not_found`"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TooLarge:
      description: "متن بلندتر از حدِ مجاز. کد: `message_too_long`"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: |
        سهمیه پر شده. کد: `rate_limited`.
        سهمیه: ۲۰ انتشار و ۱۲۰ خواندن در دقیقه، به‌ازای هر توکن.
      headers:
        Retry-After:
          description: چند ثانیه صبر کنید.
          schema: { type: integer }
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
        X-RateLimit-Reset:
          description: ثانیه تا ریست شدنِ سهمیه.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
