# Draft version. Refer to the official API documentation for more accurate information.
openapi: 3.0.0

info:
  title: PluralKit
  version: "1.0"

  description: |
    This is the API for [PluralKit](https://pluralkit.me/)! :)

    The API itself is stable, but this document (the OpenAPI description) is still subject to change, and may be updated, corrected or restructured in the future (as long as it's still coherent with the real API).

    # Authentication
    Authentication is handled using a "system token". At the moment, the only way
    to obtain a system token is to use the `pk;token` command through the Discord bot.

    This will generate an opaque string you must pass as the `Authorization` header to API requests.
    Many API endpoints are available anonymously, but most of them will hide information from 
    unauthenticated requests to align with the relevant privacy settings.

    # Errors
    Errors are just returned as HTTP response codes. Most error responses include a human-readable
    error message as the body, but this should not be relied on. Just read the response codes :)

  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html

externalDocs:
  url: https://pluralkit.me/api
  description: For more information, see the official PluralKit API documentation on the website.

servers:
  - url: https://api.pluralkit.me/v1
    description: Primary API server (v1)

paths:
  /s:
    get:
      summary: Returns your own system.
      description: Requires authentication, and will returns the system the token belongs to.
      tags: [Systems]
      operationId: GetOwnSystem
      security:
        - TokenAuth: []
      responses:
        "200":
          $ref: "#/components/responses/SystemResponse"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
    patch:
      summary: Updates an existing system.
      description: Requires authentication, and will update the system the token belongs to.
      tags: [Systems]
      operationId: UpdateSystem
      security:
        - TokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/System"

      responses:
        "200":
          description: The system was updated. Returns the updated system object.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/System"
        "401":
          $ref: "#/components/responses/UnauthorizedError"

  /s/{id}:
    parameters:
      - $ref: "#/components/parameters/SystemID"
    get:
      summary: Gets a system by its ID.
      tags: [Systems]
      operationId: GetSystem
      description: Partial information may be returned if not authenticated with this system's token.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/System"
        "404":
          description: System with the given ID not found.

  /s/{id}/members:
    parameters:
      - $ref: "#/components/parameters/SystemID"
    get:
      summary: Gets a system's members.
      description: |
        If the API token does not belong to this system, this list may exclude any private members in the system.
      tags: [Systems, Members]
      operationId: GetSystemMembers
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/System"
        "403":
          description: The system's member list is private, and the given token either missing, invalid, or does not correspond to the system.
        "404":
          description: System with the given ID not found.

  /s/{id}/fronters:
    # TODO: rename to "latest switch" or something?
    parameters:
      - $ref: "#/components/parameters/SystemID"
    get:
      summary: Gets a system's current fronters.
      tags: [Systems, Switches]
      operationId: GetSystemFronters
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FullSwitch"
        "403":
          description: The system's current fronters are private, and the given token is either missing, invalid, or does not correspond to the system.
        "404":
          description: System with the given ID was either not found, or does not have any switches registered.

  /s/{id}/switches:
    parameters:
      - $ref: "#/components/parameters/SystemID"
    get:
      summary: Gets a system's switch history.
      description: |
        Will return the system's switch history, up to 100 entries at a time, in reverse-chronological (latest first) order.

        For pagination, see the `before` query parameter.
      tags: [Systems, Switches]
      operationId: GetSystemSwitches
      parameters:
        - in: query
          name: before
          schema:
            type: string
            format: date-time
          description: |
            If provided, will only return switches that happened *before* (and not including) this timestamp.

            This can be used for pagination by calling the endpoint again with the timestamp of the last switch of the previous response.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                maxItems: 100
                items:
                  $ref: "#/components/schemas/Switch"
        "403":
          description: The system's switch history is private, and the given token is either missing, invalid, or does not correspond to the system.
        "404":
          description: System with the given ID not found.

  /s/switches:
    post:
      summary: Registers a new switch.
      tags: [Switches]
      operationId: RegisterSwitch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Switch"
      security:
        - TokenAuth: []
      responses:
        "204":
          description: The switch was logged.
        "401":
          $ref: "#/components/responses/UnauthorizedError"

  /m:
    post:
      summary: Creates a new member in your system.
      tags: [Members]
      operationId: CreateMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Member"
      security:
        - TokenAuth: []
      responses:
        "200":
          description: The member was created. Returns the newly created member object.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Member"
        "401":
          $ref: "#/components/responses/UnauthorizedError"

  /m/{id}:
    parameters:
      - $ref: "#/components/parameters/MemberID"
    get:
      summary: Gets a member by their ID.
      tags: [Members]
      operationId: GetMember
      security:
        - {}
        - TokenAuth: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Member"
        "404":
          $ref: "#/components/responses/MemberNotFoundError"
    patch:
      summary: Updates a member.
      tags: [Members]
      operationId: UpdateMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Member"
      security:
        - TokenAuth: []
      responses:
        "200":
          description: The member was updated. Returns trhe updated member object.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Member"
        "401":
          $ref: "#/components/responses/MemberAuthError"
        "404":
          $ref: "#/components/responses/MemberNotFoundError"

    delete:
      summary: Deletes a member.
      tags: [Members]
      operationId: DeleteMember
      security:
        - TokenAuth: []
      responses:
        "200": # TODO: should be 204 (but *is* 200)
          description: Member successfully deleted.
        "401":
          $ref: "#/components/responses/MemberAuthError"
        "404":
          $ref: "#/components/responses/MemberNotFoundError"

  /a/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: A Discord user ID.
        schema:
          $ref: "#/components/schemas/Snowflake"

    get:
      summary: Gets a system by (one of) its associated Discord accounts.
      description: |
        Note that it's currently not possible to get a system's registered accounts given a system ID through the API.
        Consider this endpoint "one-way".
      tags: [Systems, Accounts]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/System"
        "404":
          description: The given user ID does not correspond to any systems.

  /msg/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: |
          A Discord message ID.

          This may refer to either the original "trigger message" posted by the user,
          or to the resulting webhook message posted by PluralKit.
          The former may be useful for eg. logging bot integration.
        schema:
          $ref: "#/components/schemas/Snowflake"

    get:
      summary: Gets information about a proxied message by its message ID.
      tags: [Proxying]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "404":
          description: The given message ID does not correspond to a message proxied by PluralKit.

components:
  responses:
    UnauthorizedError:
      description: System token is missing or invalid.

    NotOwnSystemError:
      description: The given system does not correspond with this token.

    MemberAuthError:
      # TODO (relevant, ish): This is always returned as 401 but should be split into "invalid token" (401) and "not own member" (403) responses
      description: System token is missing, invalid, or the corresponding system does not own the given member.

    MemberNotFoundError:
      description: A member by the given ID was not found.

    SystemResponse:
      description: OK
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/System"

    MemberListResponse:
      description: OK
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: "#/components/schemas/Member"

  parameters:
    SystemID:
      in: path
      name: id
      required: true
      description: The ID of the system in question.
      schema:
        $ref: "#/components/schemas/ID"

    MemberID:
      in: path
      name: id
      required: true
      description: The ID of the member in question.
      schema:
        $ref: "#/components/schemas/ID"

  schemas:
    ID:
      title: ID (system or member)
      type: string
      readOnly: true
      minLength: 5
      maxLength: 5
      pattern: "^[a-z]{5}$"
      example: "abcde"
      description: |
        A unique identifier for a system or a member, as a randomly generated string of five lowercase letters.

        These IDs are guaranteed to be globally unique for the given model type (a system can have the same ID as a member, but two systems or two members can never share an ID).

        The IDs can on rare occasions change - eg. if a profane word is generated and later regenerated, or as a potential future Patreon perk. However, it's still reasonable to assume that the IDs are constant, and that tey won't change without the user's knowledge, so it's safe to store in things like settings menus and config files.
    System:
      properties:
        id:
          $ref: "#/components/schemas/ID"

        name:
          type: string
          nullable: true
          maxLength: 100
          description: The user-provided name of the system.
          example: "Boxes of Foxes"

        description:
          type: string
          nullable: true
          maxLength: 1000
          description: |
            The user-provided system description.

            May contain rich text in Markdown, including standard Markdown-formatted links, or Discord-formatted emoji/user/channel references.
          example: |-
            This system is very cool.

            It has cool people.
        tag:
          type: string
          maxLength: 78
          description: |
            The system tag, which is appended to the names (or display names, if set) of members when proxying through the bot.

            A space will automatically be inserted between the name and the tag, so no need to include one at the start.

            The character limit here is 78, as Discord's name length limit for webhooks is 80 characters. A 78-character system tag is thus the longest tag that can still accomodate a single-letter member name and the separating space.
          nullable: true
          example: "| BoF"

        avatar_url:
          type: string
          format: url
          nullable: true
          maxLength: 256
          example: "https://i.imgur.com/Abcdefg.png"
          description: |
            A link to the avatar/icon of the system.

            If used for proxying, the image must be at most 1000 pixels in width *and* height, and at most 1 MiB in size. This restriction is on Discord's end and is not verified through the API (it's simply stored as a string).

        tz:
          type: string
          format: timezone
          nullable: true
          default: UTC
          example: America/New_York
          description: |
            The system's registered time zone as an Olson time zone ID.

            This is used in the bot to convert various time stamps in commands on behalf of this system.

        created:
          type: string
          format: date-time
          readOnly: true
          description: The creation timestamp of the system.

        description_privacy:
          allOf:
            - $ref: "#/components/schemas/PrivacySetting"
            - description: |
                The system's description privacy setting, either "public" or "private".

                If this is set to "private", the field `description` will be returned as `null` on all requests not authorized with this system's token.

                In addition, this field will be returned as `null` if the request is not authorized with this system's token.

                Because of this, there is no way for an unauthorized user to tell the difference between a private description and a `null` description - this is intentional.
              example: public

        member_list_privacy:
          allOf:
            - $ref: "#/components/schemas/PrivacySetting"
            - description: |
                The system's member list privacy setting, either "public" or "private".

                If this is set to "private", this system's member list can not be queried without proper authorization.

                In addition, this field will be returned as `null` if the request is not authorized with this system's token.
              example: public

        front_privacy:
          allOf:
            - $ref: "#/components/schemas/PrivacySetting"
            - description: |
                The system's current fronter privacy setting, either "public" or "private".

                If this is set to "private", this system's current fronter can not be queried without proper authorization.

                In addition, this field will be returned as `null` if the request is not authorized with this system's token.
              example: public

        front_history_privacy:
          allOf:
            - $ref: "#/components/schemas/PrivacySetting"
            - description: |
                The system's front history privacy setting, either "public" or "private".

                If this is set to "private", this system's front/switch history can not be queried without proper authorization.

                In addition, this field will be returned as `null` if the request is not authorized with this system's token.
              example: public

    Member:
      properties:
        id:
          $ref: "#/components/schemas/ID"

        name:
          type: string
          maxLength: 100
          description: The user-provided name of the member.
          example: "Myriad Kit"

        display_name:
          type: string
          maxLength: 100
          description: The member's "display name", which will override the member's normal name when proxying.
          nullable: true
          example: Myriad 'Big Boss' Kit

        description:
          type: string
          maxLength: 1000
          description: |
            The user-provided description of the member.

            May contain rich text in Markdown, including standard Markdown-formatted links, or Discord-formatted emoji/user/channel references.

            If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`.

          example: Myriad is very cool and rad, and they love snuggling.

        color:
          type: string
          format: color
          minLength: 6
          maxLength: 6
          nullable: true
          pattern: "^[0-9A-Fa-f]{6}$"
          description: |
            The member's "color", displayed on member cards, as a 6-character hexadecimal color code (no leading #).

            If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`.

          example: "FF0000"

        birthday:
          type: string
          format: date
          example: "2018-07-11"
          nullable: true
          description: |
            The user-provided birthdate of the member.

            "Year-less" birthdays are supported. In this case, the year should be set to `0004`, and that specific year should be special-cased and hidden from the user. Previous versions used the year `0001` for the same purpose, and this value may still be both read and written with the API and should be treated the same as `0004`.

            The year `0004` was chosen because it is a leap year in the Gregorian calendar, and thus the date `0004-02-29` can be properly represented.

            If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`.

        pronouns:
          type: string
          maxLength: 100
          example: "they/them or xe/xem"
          nullable: true
          description: |
            The user-provided pronouns of the member.

            There is no specific schema, just a freeform text field.

            If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`.

        avatar_url:
          type: string
          format: url
          nullable: true
          maxLength: 256
          example: "https://i.imgur.com/Abcdefg.png"
          description: |
            A link to the avatar/icon of the member.

            If used for proxying, the image must be at most 1000 pixels in width *and* height, and at most 1 MiB in size. This restriction is on Discord's end and is not verified through the API (it's simply stored as a string).
        privacy:
          allOf:
            - $ref: "#/components/schemas/PrivacySetting"
            - description: |
                The member's privacy setting, either "public" or "private".

                If this is set to "private", the member will not appear in public member lists. It can still be looked up using its 5-character member ID, but will return limited information.

                Specifically, the properties `birthday`, `pronouns` and `description` will always be returned as `null` if a valid token for the system isn't provided, even if the underlying value is present.

                In addition, this field will be returned as `null` if the request is not authorized with this system's token.
              example: public

        proxy_tags:
          type: array
          items:
            $ref: "#/components/schemas/ProxyTag"
          description: |
            An unordered list of the member's proxy tag pairs.

            It is valid for a member to have any number of proxy tags, including none at all.

        prefix:
          type: string
          nullable: true
          description: |
            Previous versions of the API only supported a single proxy tag pair per member.
            This field will contain the prefix of the first proxy tag registered, or `null` if missing.
            Setting it will write to the first proxy tag's prefix, creating it if not present.

            This field is deprecated and will be removed in API v2.
          deprecated: true
          example: "{{"

        suffix:
          type: string
          nullable: true
          description: |
            Previous versions of the API only supported a single proxy tag pair per member.
            This field will contain the suffix of the first proxy tag registered, or `null` if missing.
            Setting it will write to the first proxy tag's suffix, creating it if not present.

            This field is deprecated and will be removed in API v2.
          deprecated: true
          example: "}}"

        keep_proxy:
          type: boolean
          default: false
          description: |
            Whether or not to include the used proxy tags in proxied messages.

    PrivacySetting:
      title: Privacy Setting
      type: string
      nullable: true
      description: A privacy setting for systems or members, either public or private. May occasionally be `null` in cases where you are not authorized to view the privacy setting's state.
      enum: [public, private]
      example: public

    ProxyTag:
      title: Proxy Tag
      description: |
        Represents a proxy tag to match messages on.

        A "proxy tag" consists of a prefix and a suffix, and a given proxy tag set matches a string
        if that string begins with the prefix and ends with the suffix.
        It's often represented to the user with the word `text` between them, eg. `[text]`, `{{text`, and so on.

        For example, the proxy tag pair "[" and "]" will match any string \[in square brackets\].

        Either the prefix or the suffix may be missing (or both may be present), but it is invalid for
        both values to be null.
      properties:
        prefix:
          type: string
          nullable: true
          description: |
            The proxy tag prefix. This is the string that goes *before* the inner text.

            An empty prefix is represented as `null` and an empty string will be converted as such.
          example: "{{"
          maxLength: 100
        suffix:
          type: string
          nullable: true
          description: |
            The proxy tag suffix. This is the string that goes *after* the inner text.

            An empty suffix is represented as `null` and an empty string will be converted as such.
          example: "}}"
          maxLength: 100

    Switch:
      title: Logged Switch
      properties:
        timestamp:
          type: string
          format: date-time
          description: The timestamp the switch was logged.
          readOnly: true
        members:
          type: array
          description: |
            A list of the IDs of the members in this switch. The order is significant. It is valid for the switch to have no members at all.
          items:
            $ref: "#/components/schemas/ID"

    FullSwitch:
      title: Logged Switch (with full Member object)
      properties:
        timestamp:
          type: string
          format: date-time
          description: The timestamp the switch was logged.
        members:
          type: array
          description: |
            A list of the members in this switch. The order is significant. It is valid for the switch to have no members at all.
          items:
            $ref: "#/components/schemas/Member"

    Snowflake:
      title: Snowflake (Discord ID)
      description: |
        A unique identifier used by Discord for its objects (accounts, guilds, channels, messages).

        This is internally stored as a 64-bit unsigned integer, but is represented as a string in the API
        to accomodate languages that store numbers as floating point values (and thus would not be able to represent it losslessly).
      type: string
      pattern: "^[0-9]{17,19}"
      minLength: 17
      maxLength: 19
      example: "466378653216014359" # PK's account ID :3

    Message:
      title: Message Info
      description: |
        An object containing information about a proxied message, including message, user and channel IDs, as well as the system and member it's related to.

        For privacy and performance reasons, this endpoint does not return the *contents* of the original message. This data isn't stored in the database either way - but given the channel and message ID, it can be fetched from Discord's own API.
      properties:
        timestamp:
          type: string
          format: date-time
          readOnly: true
          description: The time the message was proxied.
        id:
          allOf:
            - $ref: "#/components/schemas/Snowflake"
            - description: "The ID of the proxied webhook message posted by PluralKit."
              example: "123456789012345678"
        original:
          allOf:
            - $ref: "#/components/schemas/Snowflake"
            - description: "The ID of the original (now-deleted) trigger message containing the proxy tags."
              example: "123456789012345678"
        sender:
          allOf:
            - $ref: "#/components/schemas/Snowflake"
            - description: "The ID of the Discord user that sent the trigger message."
              example: "123456789012345678"
        channel:
          allOf:
            - $ref: "#/components/schemas/Snowflake"
            - description: "The ID of the Discord channel the relevant messages were posted in."
              example: "123456789012345678"
        system:
          allOf:
            - $ref: "#/components/schemas/System"
            - description: "The system that proxied the message."
        member:
          allOf:
            - $ref: "#/components/schemas/Member"
            - description: "The member that proxied the message."

  securitySchemes:
    TokenAuth:
      type: apiKey
      in: header
      name: Authorization
      description: A system token obtained from the `pk;proxy` command in the Discord bot.