openapi: 3.0.3
info:
  title: DiscordTop Developer API - Version 7
  version: "7.0"
  description: |
    API Développeur de DiscordTop (v7).

    Cette API permet aux développeurs de :
    - vérifier le cooldown de vote d'un utilisateur ;
    - récupérer les statistiques publiques DiscordTop de la guilde associée au token développeur ;
    - récupérer les votes récents d'une guilde Premium ;
    - récupérer les votes récents d'un utilisateur précis sur une guilde Premium.

    Le token développeur identifie automatiquement la guilde concernée. Il doit être passé de préférence via
    `Authorization: Bearer <token>`, ou via le header `X-Api-Token`.

servers:
  - url: https://api.discordtop.net/v7
    description: API Developer - Production (v7)

tags:
  - name: Votes
    description: Vérification et consultation des votes utilisateurs.
  - name: Guild
    description: Consultation des statistiques DiscordTop de la guilde associée au token développeur.

paths:
  /vote-check:
    get:
      tags:
        - Votes
      operationId: voteCheck
      summary: Vérifier si un utilisateur est en cooldown de vote
      description: |
        Vérifie si un utilisateur, via `discord_id` ou `external_id`, a voté pour la guilde associée
        au token développeur dans la dernière heure.

        **Important :**
        - Un seul identifiant doit être fourni.
        - Le cooldown est fixé à **1h**.
        - Le token doit être passé en Bearer ou via `X-Api-Token`.
        - Le paramètre `api_token` reste supporté pour compatibilité, mais son usage est déconseillé.

      security:
        - ApiTokenAuth: []

      parameters:
        - $ref: '#/components/parameters/DiscordIdQuery'
        - $ref: '#/components/parameters/ExternalIdQuery'
        - $ref: '#/components/parameters/ApiTokenQuery'
        - $ref: '#/components/parameters/LocaleQuery'
        - $ref: '#/components/parameters/XLocaleHeader'
        - $ref: '#/components/parameters/AcceptLanguageHeader'
        - $ref: '#/components/parameters/XApiTokenHeader'

      responses:
        '200':
          description: Résultat de vérification du vote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoteCheckSuccess'
              examples:
                noVote:
                  summary: Aucun vote récent
                  value:
                    ok: true
                    guild_id: "1013462556775551067"
                    has_voted: false
                    is_doubled: false
                    last_vote_at: null
                    next_vote_at: null
                    cooldown_remaining_seconds: 0

                cooldownActive:
                  summary: Vote récent encore en cooldown
                  value:
                    ok: true
                    guild_id: "1013462556775551067"
                    has_voted: true
                    is_doubled: false
                    last_vote_at: "2026-06-02T10:42:00.000Z"
                    next_vote_at: "2026-06-02T11:42:00.000Z"
                    cooldown_remaining_seconds: 1800

                doubledVote:
                  summary: Vote considéré comme doublé
                  value:
                    ok: true
                    guild_id: "1013462556775551067"
                    has_voted: true
                    is_doubled: true
                    last_vote_at: "2026-06-02T10:42:02.000Z"
                    next_vote_at: "2026-06-02T11:42:02.000Z"
                    cooldown_remaining_seconds: 1798

        '400':
          description: Erreurs liées aux identifiants fournis.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingIdentifier:
                  summary: Aucun identifiant fourni
                  value:
                    ok: false
                    error: MISSING_IDENTIFIER
                    message: "Fournissez soit discord_id soit external_id !"
                multipleIdentifiers:
                  summary: Plusieurs identifiants fournis
                  value:
                    ok: false
                    error: MULTIPLE_IDENTIFIERS
                    message: "Vous devez fournir un seul identifiant à la fois !"

        '401':
          $ref: '#/components/responses/UnauthorizedApiToken'

        '422':
          $ref: '#/components/responses/ValidationError'

        '429':
          $ref: '#/components/responses/RateLimitExceeded'

        '500':
          $ref: '#/components/responses/InternalServerError'

  /guild/me:
    get:
      tags:
        - Guild
      operationId: getMyGuildStats
      summary: Récupérer les statistiques DiscordTop de sa guilde
      description: |
        Retourne les statistiques DiscordTop de la guilde associée au token développeur.

        **Important :**
        - Cette route est réservée aux serveurs Premium.
        - Le token doit être passé en Bearer ou via `X-Api-Token`.
        - La guilde doit être active sur DiscordTop.
        - Les informations retournées sont uniquement des métriques DiscordTop.

      security:
        - ApiTokenAuth: []

      parameters:
        - $ref: '#/components/parameters/ApiTokenQuery'
        - $ref: '#/components/parameters/LocaleQuery'
        - $ref: '#/components/parameters/XLocaleHeader'
        - $ref: '#/components/parameters/AcceptLanguageHeader'
        - $ref: '#/components/parameters/XApiTokenHeader'

      responses:
        '200':
          description: Statistiques DiscordTop de la guilde associée au token développeur.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GuildMeSuccess'
              examples:
                premiumGuildStats:
                  summary: Statistiques d'une guilde Premium
                  value:
                    ok: true
                    guild_id: "1013462556775551067"
                    votes: 42
                    lifetime_votes: 1820
                    votes_last_7_days: 15
                    rank: 18
                    boosts: 3
                    reviews: 28
                    rating: 4.7

        '401':
          $ref: '#/components/responses/UnauthorizedApiToken'

        '403':
          $ref: '#/components/responses/PremiumRequired'

        '429':
          $ref: '#/components/responses/RateLimitExceeded'

        '500':
          $ref: '#/components/responses/InternalServerError'

  /guild/votes:
    get:
      tags:
        - Votes
      operationId: listGuildVotes
      summary: Récupérer les votes récents de sa guilde
      description: |
        Retourne la liste des votes de la guilde associée au token développeur sur une période donnée.

        **Important :**
        - Cette route est réservée aux serveurs Premium.
        - La période demandée ne peut pas dépasser **31 jours**.
        - Les votes sont triés par `timestamp` croissant.
        - La pagination utilise un curseur opaque (`next_cursor`).
        - Le champ `is_doubled` indique si le vote est considéré comme doublé.
        - Aucune donnée personnelle autre que l'ID Discord de l'utilisateur n'est retournée.

      security:
        - ApiTokenAuth: []

      parameters:
        - $ref: '#/components/parameters/FromQuery'
        - $ref: '#/components/parameters/ToQuery'
        - $ref: '#/components/parameters/CursorQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - $ref: '#/components/parameters/ApiTokenQuery'
        - $ref: '#/components/parameters/LocaleQuery'
        - $ref: '#/components/parameters/XLocaleHeader'
        - $ref: '#/components/parameters/AcceptLanguageHeader'
        - $ref: '#/components/parameters/XApiTokenHeader'

      responses:
        '200':
          description: Liste paginée des votes de la guilde.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GuildVotesSuccess'
              examples:
                firstPage:
                  summary: Première page de votes
                  value:
                    ok: true
                    guild_id: "1013462556775551067"
                    votes:
                      - user_id: "149505704569339904"
                        timestamp: "2026-06-02T14:32:00.000Z"
                        is_doubled: true
                      - user_id: "511252691334660106"
                        timestamp: "2026-06-02T15:12:44.000Z"
                        is_doubled: false
                    total: 247
                    pagination:
                      limit: 100
                      has_more: true
                      next_cursor: "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNi0wMlQxNToxMjo0NC4wMDBaIiwiaWQiOiI5ODQyMSJ9"
                    period:
                      from: "2026-06-01T00:00:00.000Z"
                      to: "2026-06-30T23:59:59.999Z"

        '400':
          description: Erreurs liées aux dates ou au curseur.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidDate:
                  summary: Date invalide
                  value:
                    ok: false
                    error: INVALID_DATE
                    message: "Les dates doivent être au format ISO."
                invalidDateRange:
                  summary: Période invalide
                  value:
                    ok: false
                    error: INVALID_DATE_RANGE
                    message: "La date from doit être antérieure à la date to."
                dateRangeTooLarge:
                  summary: Période trop large
                  value:
                    ok: false
                    error: DATE_RANGE_TOO_LARGE
                    message: "La période demandée ne peut pas dépasser 31 jours."
                invalidCursor:
                  summary: Curseur invalide
                  value:
                    ok: false
                    error: INVALID_CURSOR
                    message: "Le curseur fourni est invalide."

        '401':
          $ref: '#/components/responses/UnauthorizedApiToken'

        '403':
          $ref: '#/components/responses/PremiumRequired'

        '422':
          $ref: '#/components/responses/ValidationError'

        '429':
          $ref: '#/components/responses/RateLimitExceeded'

        '500':
          $ref: '#/components/responses/InternalServerError'

  /guild/votes/users/{user_id}:
    get:
      tags:
        - Votes
      operationId: listGuildUserVotes
      summary: Récupérer les votes récents d'un utilisateur sur sa guilde
      description: |
        Retourne la liste des votes d'un utilisateur précis sur la guilde associée au token développeur.

        **Important :**
        - Cette route est réservée aux serveurs Premium.
        - La période demandée ne peut pas dépasser **31 jours**.
        - Le `user_id` est l'ID Discord de l'utilisateur.
        - Les votes sont triés par `timestamp` croissant.
        - La pagination utilise un curseur opaque (`next_cursor`).
        - Aucune donnée personnelle autre que l'ID Discord de l'utilisateur n'est retournée.

      security:
        - ApiTokenAuth: []

      parameters:
        - name: user_id
          in: path
          required: true
          description: ID Discord de l'utilisateur dont les votes doivent être récupérés.
          schema:
            type: string
            maxLength: 20
            pattern: '^[0-9]{15,20}$'
          example: "149505704569339904"
        - $ref: '#/components/parameters/FromQuery'
        - $ref: '#/components/parameters/ToQuery'
        - $ref: '#/components/parameters/CursorQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - $ref: '#/components/parameters/ApiTokenQuery'
        - $ref: '#/components/parameters/LocaleQuery'
        - $ref: '#/components/parameters/XLocaleHeader'
        - $ref: '#/components/parameters/AcceptLanguageHeader'
        - $ref: '#/components/parameters/XApiTokenHeader'

      responses:
        '200':
          description: Liste paginée des votes de l'utilisateur sur la guilde.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GuildUserVotesSuccess'
              examples:
                firstPage:
                  summary: Première page des votes d'un utilisateur
                  value:
                    ok: true
                    guild_id: "1013462556775551067"
                    user_id: "149505704569339904"
                    votes:
                      - user_id: "149505704569339904"
                        timestamp: "2026-06-02T14:32:00.000Z"
                        is_doubled: true
                    total: 12
                    pagination:
                      limit: 100
                      has_more: false
                      next_cursor: null
                    period:
                      from: "2026-06-01T00:00:00.000Z"
                      to: "2026-06-30T23:59:59.999Z"

        '400':
          description: Erreurs liées à l'utilisateur, aux dates ou au curseur.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidUserId:
                  summary: ID utilisateur invalide
                  value:
                    ok: false
                    error: INVALID_USER_ID
                    message: "L'identifiant utilisateur Discord est invalide."
                invalidDate:
                  summary: Date invalide
                  value:
                    ok: false
                    error: INVALID_DATE
                    message: "Les dates doivent être au format ISO."
                invalidDateRange:
                  summary: Période invalide
                  value:
                    ok: false
                    error: INVALID_DATE_RANGE
                    message: "La date from doit être antérieure à la date to."
                dateRangeTooLarge:
                  summary: Période trop large
                  value:
                    ok: false
                    error: DATE_RANGE_TOO_LARGE
                    message: "La période demandée ne peut pas dépasser 31 jours."
                invalidCursor:
                  summary: Curseur invalide
                  value:
                    ok: false
                    error: INVALID_CURSOR
                    message: "Le curseur fourni est invalide."

        '401':
          $ref: '#/components/responses/UnauthorizedApiToken'

        '403':
          $ref: '#/components/responses/PremiumRequired'

        '422':
          $ref: '#/components/responses/ValidationError'

        '429':
          $ref: '#/components/responses/RateLimitExceeded'

        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  securitySchemes:
    ApiTokenAuth:
      type: http
      scheme: bearer
      bearerFormat: DTOP-Dev-Token

  parameters:
    DiscordIdQuery:
      name: discord_id
      in: query
      required: false
      description: ID Discord de l'utilisateur à vérifier.
      schema:
        type: string
        maxLength: 20
        pattern: '^[0-9]{15,20}$'
      example: "511252691334660106"

    ExternalIdQuery:
      name: external_id
      in: query
      required: false
      description: Identifiant externe de l'utilisateur.
      schema:
        type: string
        maxLength: 191
      example: "my-user-123456"

    FromQuery:
      name: from
      in: query
      required: false
      description: |
        Date de début de la période à récupérer.

        Format recommandé : ISO 8601.
        Si absent, l'API peut utiliser le début du mois courant.
      schema:
        type: string
        format: date-time
      example: "2026-06-01T00:00:00.000Z"

    ToQuery:
      name: to
      in: query
      required: false
      description: |
        Date de fin de la période à récupérer.

        Format recommandé : ISO 8601.
        Si absent, l'API peut utiliser la date courante.
      schema:
        type: string
        format: date-time
      example: "2026-06-30T23:59:59.999Z"

    CursorQuery:
      name: cursor
      in: query
      required: false
      description: |
        Curseur opaque retourné par `pagination.next_cursor`.
        À réutiliser tel quel pour récupérer la page suivante.
      schema:
        type: string
      example: "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNi0wMlQxNToxMjo0NC4wMDBaIiwiaWQiOiI5ODQyMSJ9"

    LimitQuery:
      name: limit
      in: query
      required: false
      description: |
        Nombre maximum d'éléments retournés.
        Valeur par défaut : 100.
        Valeur maximum : 1000.
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 100
      example: 100

    ApiTokenQuery:
      name: api_token
      in: query
      required: false
      description: |
        Ancienne méthode pour passer le token dans l'URL.
        Il est recommandé d'utiliser `Authorization: Bearer <token>`.
      schema:
        type: string
      example: dtop_dev_xxx123
      deprecated: true

    LocaleQuery:
      name: locale
      in: query
      required: false
      description: |
        Force la langue de la réponse (`fr`, `en`, etc.).
        Si absent : utilise `X-Locale`, puis `Accept-Language`, sinon fallback `en`.
      schema:
        type: string
      example: fr

    XLocaleHeader:
      name: X-Locale
      in: header
      required: false
      description: Locale forcée, surpasse `Accept-Language`.
      schema:
        type: string
      example: en

    AcceptLanguageHeader:
      name: Accept-Language
      in: header
      required: false
      description: Détection automatique de la langue.
      schema:
        type: string
      example: fr-FR,fr;q=0.9,en;q=0.8

    XApiTokenHeader:
      name: X-Api-Token
      in: header
      required: false
      description: Token développeur alternatif.
      schema:
        type: string
      example: dtop_dev_abc123

  responses:
    UnauthorizedApiToken:
      description: Erreurs liées au token développeur.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            apiNotSet:
              summary: Token absent
              value:
                ok: false
                error: API_NOT_SET
                message: "Le token API n'est pas correctement défini !"
            invalidToken:
              summary: Token invalide
              value:
                ok: false
                error: INVALID_API_TOKEN
                message: "Le token API fourni est invalide !"

    PremiumRequired:
      description: La guilde associée au token n'a pas accès à cette route.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            premiumRequired:
              summary: Premium requis
              value:
                ok: false
                error: PREMIUM_REQUIRED
                message: "Cette route est réservée aux serveurs Premium !"

    ValidationError:
      description: Erreurs de validation Adonis Validator.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'

    RateLimitExceeded:
      description: Rate limit atteint.
      headers:
        Retry-After:
          description: Nombre de secondes à attendre avant de refaire une requête.
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
          examples:
            missingToken:
              summary: Token absent pour le rate limit
              value:
                message: "Rate limit exceeded. Missing API token."
            tokenLimited:
              summary: Token limité
              value:
                message: "Rate limit exceeded for this API token."

    InternalServerError:
      description: Erreur interne de l'API.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ServerError'

  schemas:
    VoteCheckSuccess:
      type: object
      required:
        - ok
        - guild_id
        - has_voted
        - is_doubled
        - last_vote_at
        - next_vote_at
        - cooldown_remaining_seconds
      properties:
        ok:
          type: boolean
          example: true
        guild_id:
          type: string
          example: "1013462556775551067"
        has_voted:
          type: boolean
          description: |
            `true` : l'utilisateur est encore dans le cooldown.
            `false` : aucun vote récent ou cooldown terminé.
          example: false
        is_doubled:
          type: boolean
          description: Indique si le vote d'un utilisateur est considéré comme doublé.
          example: false
        last_vote_at:
          type: string
          format: date-time
          nullable: true
          example: "2026-06-02T10:42:00.000Z"
        next_vote_at:
          type: string
          format: date-time
          nullable: true
          example: "2026-06-02T11:42:00.000Z"
        cooldown_remaining_seconds:
          type: integer
          example: 0

    GuildMeSuccess:
      type: object
      required:
        - ok
        - guild_id
        - votes
        - lifetime_votes
        - votes_last_7_days
        - rank
        - boosts
        - reviews
        - rating
      properties:
        ok:
          type: boolean
          example: true
        guild_id:
          type: string
          example: "1013462556775551067"
        votes:
          type: integer
          description: Nombre de votes de la guilde sur le classement mensuel en cours.
          example: 42
        lifetime_votes:
          type: integer
          description: Nombre total de votes reçus par la guilde depuis son ajout sur DiscordTop.
          example: 1820
        votes_last_7_days:
          type: integer
          description: Nombre de votes reçus sur les 7 derniers jours.
          example: 15
        rank:
          type: integer
          nullable: true
          description: Position actuelle de la guilde dans le classement mensuel.
          example: 18
        boosts:
          type: integer
          description: Nombre de boosts actifs sur la guilde.
          example: 3
        reviews:
          type: integer
          description: Nombre total d'avis publiés pour la guilde.
          example: 28
        rating:
          type: number
          format: float
          nullable: true
          description: Note moyenne des avis publiés, arrondie à une décimale. Null si aucun avis n'est disponible.
          example: 4.7

    GuildVotesSuccess:
      allOf:
        - $ref: '#/components/schemas/VotesListBaseSuccess'
        - type: object
          required:
            - guild_id
          properties:
            guild_id:
              type: string
              example: "1013462556775551067"

    GuildUserVotesSuccess:
      allOf:
        - $ref: '#/components/schemas/VotesListBaseSuccess'
        - type: object
          required:
            - guild_id
            - user_id
          properties:
            guild_id:
              type: string
              example: "1013462556775551067"
            user_id:
              type: string
              description: ID Discord de l'utilisateur demandé.
              example: "149505704569339904"

    VotesListBaseSuccess:
      type: object
      required:
        - ok
        - votes
        - total
        - pagination
        - period
      properties:
        ok:
          type: boolean
          example: true
        votes:
          type: array
          description: Liste des votes logiques retournés pour la page courante.
          items:
            $ref: '#/components/schemas/VoteEvent'
        total:
          type: integer
          description: |
            Nombre total de votes logiques correspondant aux filtres demandés, sur la période.
            Ce total n'est pas limité par `pagination.limit`.
          example: 247
        pagination:
          $ref: '#/components/schemas/CursorPagination'
        period:
          $ref: '#/components/schemas/Period'

    VoteEvent:
      type: object
      required:
        - user_id
        - timestamp
        - is_doubled
      properties:
        user_id:
          type: string
          description: ID Discord de l'utilisateur ayant voté.
          example: "149505704569339904"
        timestamp:
          type: string
          format: date-time
          description: Date du vote.
          example: "2026-06-02T14:32:00.000Z"
        is_doubled:
          type: boolean
          description: |
            Indique si ce vote est considéré comme doublé.

            Tant qu'aucune colonne dédiée n'existe en base de données, cette valeur peut être déduite
            par l'API à partir de deux votes similaires dans un lapse de temps très court.
          example: true

    CursorPagination:
      type: object
      required:
        - limit
        - has_more
        - next_cursor
      properties:
        limit:
          type: integer
          example: 100
        has_more:
          type: boolean
          example: true
        next_cursor:
          type: string
          nullable: true
          description: Curseur à utiliser pour récupérer la page suivante. Null s'il n'y a plus de page.
          example: "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNi0wMlQxNToxMjo0NC4wMDBaIiwiaWQiOiI5ODQyMSJ9"

    Period:
      type: object
      required:
        - from
        - to
      properties:
        from:
          type: string
          format: date-time
          example: "2026-06-01T00:00:00.000Z"
        to:
          type: string
          format: date-time
          example: "2026-06-30T23:59:59.999Z"

    ErrorResponse:
      type: object
      required:
        - ok
        - error
        - message
      properties:
        ok:
          type: boolean
          example: false
        error:
          type: string
          example: INVALID_API_TOKEN
        message:
          type: string
          example: "Le token API fourni est invalide !"

    ValidationError:
      type: object
      properties:
        errors:
          type: array
          items:
            type: object
            properties:
              rule:
                type: string
                example: maxLength
              field:
                type: string
                example: discord_id
              message:
                type: string
                example: "maxLength validation failed"

    RateLimitError:
      type: object
      properties:
        message:
          type: string
          example: "Rate limit exceeded for this API token."

    ServerError:
      type: object
      properties:
        message:
          type: string
          example: "Internal server error."
