Portail développeurs
Expérimental La plateforme de bots & d’apps est en développement actif. Les serveurs auto-hébergés font tourner les bots, les commandes slash et les composants de message, avec quelques différences par rapport au cloud. Voir ce qui est pris en charge.
← Docs

Salons d’app

Un salon d’app est un salon de la liste des salons d’un serveur dont le contenu est un ensemble d’écrans fournis par votre app tant qu’elle est en ligne : un planificateur de raids, un classement, un guichet de tickets, une page d’état de serveur. GameVox enregistre où se trouve le salon, qui peut le voir et ce que chacun peut y faire. Votre app possède le contenu et affiche chaque écran à la demande.

Vue d’ensemble

Une app est un seul lieu avec de nombreux écrans. Chaque écran est une route, une chaîne définie par votre app (home, raids, raids/42, roster?sort=ilvl). Un membre qui ouvre le salon arrive sur sa route de départ (en général votre home), puis se déplace via votre menu de navigation, via les liens et boutons des écrans, et avec Précédent et Suivant.

Ce que voient les membres

  • Le salon dans la liste des salons avec votre icône (ou celle choisie par le serveur), et un petit point tant que votre app est hors ligne.
  • Votre menu de navigation, Précédent et Suivant, un bouton Accueil et Actualiser, autour de l’écran que vous avez renvoyé.
  • Des écrans dessinés nativement par chaque client à partir de votre JSON, dans le thème du client, clair comme sombre.
  • Quand votre bot est hors ligne : le dernier instantané public que vous avez envoyé, signalé comme dernière mise à jour, ou un avis hors ligne s’il n’y en a pas.

Clients : l’app de bureau (Windows, macOS, Linux), l’app web, iOS et Android. L’app Xbox n’affiche pas les salons d’app. Un client trop ancien pour les connaître ne reçoit jamais de salons d’app dans sa liste de salons, si bien que personne ne voit un salon textuel vide.

Ce qui les distingue des commandes slash et des composants

  • Les commandes slash et les composants de message vivent dans des messages d’un salon textuel et atteignent votre app sous forme d’INTERACTION_CREATE avec un token d’interaction. Un salon d’app n’a aucun message : y publier est refusé.
  • Une requête de vue arrive comme un événement gateway à part entière, APP_VIEW_REQUEST, et vous y répondez par un seul appel REST portant un nonce à usage unique. Pas de token d’interaction, pas de report, pas de suivis. Les endpoints d’interaction HTTP ne sont pas utilisés : votre app a donc besoin d’une connexion gateway active.
  • Chaque écran est personnel. GameVox le demande à chaque fois qu’un membre l’ouvre ou l’utilise, et ne le stocke jamais (seul l’instantané public facultatif est stocké).
  • À l’intérieur de l’écran, votre app définit ses propres permissions, appelées capacités. Les admins du serveur les accordent par groupe, et GameVox vous indique à chaque requête celles que détient la personne qui regarde.
  • C’est une extension GameVox sans équivalent Discord. Les bots écrits avec des bibliothèques Discord continuent de fonctionner ; ils ne reçoivent simplement jamais l’événement.

Comment tout s’articule

Un membre ouvre le salon, navigue ou appuie sur un bouton
      │
      ▼
GameVox ──── APP_VIEW_REQUEST { nonce, kind, route, user, abilities } ────▶ votre bot
      ▲
      │
votre bot ──── POST /applications/@me/app-view-response { nonce, view } ────▶ GameVox
      │
      ▼
GameVox vérifie et nettoie la vue, puis le client la dessine

À chaque ouverture et à chaque action, GameVox vérifie que la personne a le droit de voir le salon, calcule ses capacités, et seulement ensuite interroge votre app. Votre réponse va à cette seule personne.

Cycle de vie

  1. Un serveur installe votre app de la manière habituelle (le lien d’installation ou la page d’autorisation OAuth2).
  2. À chaque démarrage, votre bot publie son manifeste. Une app sans manifeste ne peut pas avoir de salons d’app.
  3. Un admin du serveur choisit Créer un salon ▸ App. La liste montre les apps installées qui ont publié un manifeste, avec leur nom, leur description et leur icône. Le nom du salon est prérempli d’après votre app, et un menu facultatif S’ouvre sur liste vos éléments de navigation (par défaut : home). Créer des salons demande la permission habituelle de créer des salons. Votre bot peut aussi en créer un (voir plus bas).
  4. Dans les Réglages du salon, l’admin peut renommer le salon, changer son icône (celle de votre app, un emoji ou une image téléversée), changer l’écran d’ouverture, et définir les permissions : qui peut voir le salon, et quels groupes détiennent chacune de vos capacités.
  5. Les membres qui peuvent voir le salon l’ouvrent, et votre app répond à APP_VIEW_REQUEST pour chaque écran qu’ils consultent.
  6. Si votre app est retirée du serveur, ses salons d’app restent (la réinstaller les fait revenir) et affichent « Cette app a été retirée du serveur », avec un bouton Supprimer pour les gestionnaires. Si l’application est supprimée, ou n’a jamais publié de manifeste, le salon affiche « Cette app n’est plus disponible ».

Les salons d’app comptent dans la limite de salons d’un serveur comme n’importe quel autre salon. Un serveur a en général un salon par app ; un second salon pour la même app est autorisé (par exemple un salon d’équipe de raid qui s’ouvre sur le planificateur de raids) et c’est la même app avec la même navigation. Chaque requête porte le channel_id, vous pouvez donc conserver des données distinctes par salon.

Créer un salon d’app depuis votre bot

Utilisez la route habituelle de création de salon avec type 100 et un objet gamevox_app facultatif :

POST /api/v10/guilds/1491230000000000001/channels
Authorization: Bot <token>
Content-Type: application/json

{
  "type": 100,
  "name": "raid-planner",
  "position": 7,
  "gamevox_app": { "start_route": "raids", "icon": { "builtin": "calendar" } }
}

HTTP/1.1 201 Created

{
  "id": "1491240000000000002",
  "type": 100,
  "gamevox_type": "app",
  "name": "raid-planner",
  "guild_id": "1491230000000000001",
  "position": 7,
  "gamevox_app": { "application_id": "1491200000000000000", "start_route": "raids" }
}
  • Uniquement pour votre propre application, qui doit être installée sur ce serveur et avoir publié un manifeste. Le bot a besoin de la permission de créer des salons, comme pour tout salon.
  • gamevox_app.start_route est une route (vide ou absente : le home de votre manifeste). gamevox_app.icon est un emoji, un glyphe builtin, ou une url que votre manifeste contient déjà. Les bots ne peuvent pas téléverser d’image de salon ; seuls les admins le peuvent.
  • Sans position, le salon est placé en fin de liste. Le nom fait de 1 à 100 caractères.
  • Qui peut voir le salon et qui détient chaque capacité, c’est aux admins du serveur de le définir dans les Réglages du salon.

Un salon d’app est un objet salon normal avec des extensions GameVox, dans les réponses REST et dans les événements gateway de salon :

  • type vaut 100, volontairement hors de la plage de Discord. Une bibliothèque qui ne connaît pas ce nombre peut le traiter comme un salon ordinaire ; gamevox_type: "app" est le moyen fiable d’en reconnaître un.
  • GET /channels/{id}, GET /guilds/{id}/channels, PATCH et DELETE /channels/{id} fonctionnent comme pour les autres salons. PATCH accepte aussi gamevox_app: {"start_route", "icon"}, uniquement sur un salon de votre propre application, et demande la permission de modifier les salons.

Le manifeste

Chaque app publie un manifeste : son nom, son icône, sa route d’accueil, son menu de navigation et ses capacités. Publiez-le à chaque démarrage avec PUT /applications/@me/app-manifest. Il remplace celui qui est stocké et peut être répété sans risque.

PUT /api/v10/applications/@me/app-manifest
Authorization: Bot <token>
Content-Type: application/json

{
  "name": "Raid Planner",
  "description": "Plan raids and sign up, right in a channel.",
  "icon": { "builtin": "calendar" },
  "home": "home",
  "nav": [
    { "label": "Home", "route": "home", "icon": { "builtin": "home" } },
    { "label": "Raids", "route": "raids", "icon": { "builtin": "calendar" } },
    { "label": "Officers", "ability": "manage", "children": [
      { "label": "Roster", "route": "roster", "ability": "manage" }
    ] }
  ],
  "abilities": [
    { "key": "signup", "label": "Sign up for raids", "default": "everyone" },
    { "key": "manage", "label": "Manage raids",
      "description": "Create, edit and cancel raids", "default": "managers" }
  ]
}
ChampRemarques
name Obligatoire, jusqu’à 50 caractères. Peut avoir name_localizations.
description Jusqu’à 300 caractères. Peut avoir description_localizations.
icon Une icône (voir plus bas).
home La route d’arrivée. Par défaut home.
nav Le menu : jusqu’à 12 éléments de premier niveau et 30 au total.
abilities Jusqu’à 25 permissions définies par l’app.
kind Doit valoir layout (la valeur par défaut). Les autres types sont réservés et refusés.
storage Facultatif. Uniquement pour les apps qui acceptent des téléversements (voir Fichiers d’app). Une app qui ne déclare aucun stockage ne reçoit aucun téléversement.

Les éléments de navigation sont des {label, route, icon?, ability?, children?}. Un libellé fait jusqu’à 40 caractères. Un élément avec children est un groupe : il peut omettre sa propre route, et les groupes ne s’imbriquent que sur un niveau. Un élément avec une ability est masqué pour les personnes qui ne la détiennent pas, et cette capacité doit être déclarée dans abilities. Les routes respectent ^[A-Za-z0-9._~/?=&%:-]{1,200}$.

Les capacités sont des {key, label, description?, default} : une clé respectant ^[a-z0-9_]{1,32}$, un libellé jusqu’à 40 caractères, une description jusqu’à 200, et un default parmi everyone (chaque groupe qui peut voir le salon), managers (les groupes gestionnaires, voir Contexte par personne) ou nobody. Les admins peuvent remplacer la valeur par défaut par groupe sur chaque salon d’app.

Les icônes sont exactement l’une des formes {"emoji": "…"} (un seul emoji, dessiné en Twemoji), {"builtin": "…"} (un glyphe GameVox : calendar, trophy, list, chart, shield, sword, map, users, clock, star, flag, book, gift, bolt, home, game) ou {"image_data": "<base64>"} (un PNG ou WebP carré, de 16 à 2048 pixels, jusqu’à 128 Ko). GameVox analyse et stocke image_data et renvoie {"url": …} dans le manifeste stocké. Une URL que vous hébergez vous-même est refusée, car elle ne serait jamais montrée aux membres ; vous pouvez renvoyer une icône url que vous avez lue chez GameVox.

La réponse est un 200 avec le manifeste stocké (valeurs par défaut remplies, images remplacées par des URL). Une erreur donne un 400 qui nomme le champ, par exemple nav[2].route is not a valid route. Republier le même manifeste ne change rien. Un manifeste modifié fait actualiser chacun de vos salons d’app ouverts, si bien qu’un nouveau menu apparaît sans que personne ne recharge. GET /applications/@me/app-manifest renvoie ce qui est stocké, ou 404 si vous n’en avez jamais publié.

Localiser le manifeste

Chaque texte du manifeste que lit une personne peut porter des traductions, comme une commande slash Discord porte name_localizations. Placez une table à côté du texte qu’elle traduit. Le champ simple reste la valeur par défaut : c’est ce que voit un membre quand la table n’a pas d’entrée pour sa langue.

{
  "name": "Raid Planner",
  "name_localizations": { "de": "Raidplaner", "pt-BR": "Planejador de raids", "ja": "レイドプランナー" },
  "nav": [
    { "label": "Roster", "route": "roster",
      "label_localizations": { "de": "Aufstellung", "fr": "Effectif", "es-ES": "Plantilla" } }
  ],
  "abilities": [
    { "key": "manage", "label": "Manage raids", "default": "managers",
      "label_localizations": { "de": "Raids verwalten" },
      "description": "Create, edit and cancel raids",
      "description_localizations": { "de": "Raids erstellen, bearbeiten und absagen" } }
  ]
}
  • name_localizations et description_localizations sur le manifeste (textes jusqu’à 50 et 300 caractères), label_localizations sur un élément de navigation (jusqu’à 40), label_localizations et description_localizations sur une capacité (jusqu’à 40 et 200).
  • Les clés sont des codes de langue Discord (de, fr, pt-BR, es-ES, zh-CN, ja et le reste de la liste de Discord). Une langue pour laquelle Discord n’a pas de code (ar, he, fa, ms, sk, et, lv, sr, sl, tl, ur, bn, ta, sw, ca) utilise son code de langue GameVox. Les clés sont sensibles à la casse, une table contient au plus 64 langues, et une clé inconnue donne un 400 qui nomme le champ.
  • Une valeur vide, null ou égale au texte par défaut est écartée, donc null efface une langue. Une valeur au-delà de la limite du texte donne un 400, par exemple nav[2].label_localizations[de] is longer than 40 characters.
  • Les clés de capacité, les routes et les icônes ne sont pas localisées. Les vues ne font pas partie du manifeste : affichez chaque vue dans la langue de la personne qui la regarde, à l’aide du locale de la requête.

Le manifeste sans ses tables garde sa limite de 64 Ko. Les tables ont leur propre budget de 192 Ko en plus, si bien qu’un manifeste en 44 langues y tient (le document entier fait au plus 256 Ko). Un manifeste sans table reste limité à 64 Ko comme avant.

GameVox résout les tables pour chaque membre et envoie aux clients de simples chaînes. La langue est celle que le client du membre a signalée en dernier, la même que celle qui détermine la langue des descriptions des commandes slash. Cela couvre le nom et la description de l’app dans l’en-tête du salon et dans la liste Créer un salon, le menu, les noms et descriptions des capacités dans l’éditeur de permissions, et le nom de l’app dans les notifications. GET /applications/@me/app-manifest renvoie le manifeste stocké avec ses tables.

Requêtes et réponses

Quand un membre ouvre le salon, navigue, actualise ou appuie sur un bouton, votre bot reçoit un dispatch gateway (opcode 0) nommé APP_VIEW_REQUEST. Il ne nécessite aucun intent. Il ne va qu’à une session gateway active de votre application.

{
  "op": 0,
  "s": 812,
  "t": "APP_VIEW_REQUEST",
  "d": {
    "nonce": "2f6c1a9e-4b7d-4c1e-9a53-0d8e7f6a5b41",
    "kind": "action",
    "guild_id": "1491230000000000001",
    "channel_id": "1491240000000000002",
    "route": "raids/42",
    "params": { "raid": 42 },
    "action": "signup",
    "values": { "role": "healer", "note": "Late by 10 min" },
    "user": {
      "id": "1491250000000000003",
      "username": "ben",
      "display_name": "ben",
      "avatar_url": "https://..."
    },
    "abilities": ["signup"],
    "is_manager": false,
    "locale": "en-US",
    "platform": "desktop",
    "self_hosted": false,
    "plan": {
      "key": "free",
      "limits": {
        "max_file_bytes": 8388608, "max_files_per_field": 4, "max_total_bytes": 524288000,
        "max_uploads_per_user_per_day": 50, "kinds": [], "retention_days": 0, "max_video_bytes": 0
      }
    }
  }
}
ChampRemarques
nonce À usage unique. Renvoyez-le avec votre réponse.
kind open (le membre a ouvert route, y a navigué ou l’a actualisée) ou action (il a appuyé sur un bouton ou envoyé un formulaire).
guild_id, channel_id Le serveur et le salon d’app consulté, sous forme de snowflakes. Indexez les données par salon avec channel_id.
route La route à afficher. Jamais vide : quand le client n’en envoie pas, c’est la route de départ du salon, sinon votre home.
params Les paramètres du lien ou du bouton qui a mené ici (données simples, jusqu’à 2 Ko). Toujours un objet.
action, values Uniquement avec kind: "action" : le nom de l’action et les valeurs du formulaire envoyé (un objet vide pour un simple bouton).
user La personne qui regarde : id (snowflake), username, display_name (identique au nom d’utilisateur pour l’instant) et avatar_url (peut être vide).
abilities Les capacités que cette personne détient sur ce salon. Appliquez-les vous-même.
is_manager Vrai pour le propriétaire du serveur, un groupe admin, ou un groupe ayant la permission Modifier le serveur ou Gérer les intégrations.
locale La langue du client de la personne (par exemple en-US ou de) ; peut être vide. Utilisez-la pour répondre dans sa langue.
platform desktop, web, ios ou android.
self_hosted Vrai quand le salon se trouve sur un serveur auto-hébergé.
plan La formule de l’app sur ce serveur : {key, limits} (voir Formules).

Répondre

Répondez par un seul appel à POST /applications/@me/app-view-response, authentifié comme tout autre appel de bot (Authorization: Bot <token>) :

POST /api/v10/applications/@me/app-view-response
Authorization: Bot <token>
Content-Type: application/json

{
  "nonce": "2f6c1a9e-4b7d-4c1e-9a53-0d8e7f6a5b41",
  "toast": "Signed up as healer",
  "view": { "version": 1, "title": "Mythic raid", "route": "raids/42", "blocks": [ ... ] }
}

HTTP/1.1 204 No Content
  • view remplace l’écran de la personne. Pour une action qui n’a besoin que d’un toast, omettez-le : la personne garde l’écran actuel.
  • toast est une ligne courte (jusqu’à 200 caractères) affichée sous forme de toast. Il peut accompagner une view.
  • error (jusqu’à 300 caractères) affiche un message à la place d’un écran. Quand error est défini, view et toast sont ignorés.
  • open_url demande au client d’ouvrir une URL https après son avertissement habituel pour les liens externes. Tout autre schéma est écarté.
  • Un succès renvoie 204. La vue est vérifiée et nettoyée ensuite (voir Ce que fait le nettoyage), donc un 204 ne signifie pas que chaque champ a survécu.

Délais

  • La personne attend 10 secondes. Ensuite, elle voit « L’app n’a pas répondu. Réessayez. », et une réponse tardive est écartée. Visez une réponse bien plus rapide ; la bibliothèque de bots GameVox donne à chaque requête une échéance de 8 secondes.
  • Le nonce est accepté pendant 15 secondes et une seule fois. Une deuxième réponse, ou une réponse après son expiration, reçoit 404.
  • Si votre application maintient plusieurs sessions gateway (shards, ou chevauchement lors d’un redémarrage), plusieurs peuvent recevoir la même requête. Répondez une seule fois ; seule la première réponse compte.
  • Votre bot est considéré comme en ligne tant que sa session gateway est connectée et envoie ses heartbeats. Quand il est hors ligne, GameVox ne l’interroge pas : une ouverture affiche votre instantané, et une action indique au membre que l’app est hors ligne.

Le schéma de vue

Une vue est un document de mise en page JSON : kind layout, version 1. Chaque client la dessine nativement. Les champs inconnus sont ignorés et les types de bloc inconnus sont écartés, si bien que de nouveaux champs facultatifs peuvent être ajoutés sans rien casser.

{
  "version": 1,
  "title": "Raids",
  "subtitle": "Next 7 days",
  "route": "raids",
  "nav_active": "raids",
  "tabs": [
    { "label": "Upcoming", "route": "raids" },
    { "label": "Past", "route": "raids?past=1" }
  ],
  "actions": [
    { "label": "New raid", "route": "raids/new", "style": "primary", "ability": "manage" }
  ],
  "refresh_seconds": 60,
  "blocks": [
    { "type": "stats", "items": [
      { "label": "Signed up", "value": 18, "trend": "up" },
      { "label": "Open slots", "value": 2, "color": "warning" }
    ] },
    { "type": "countdown", "label": "Next pull", "until": "2026-11-02T19:00:00Z" },
    { "type": "table",
      "columns": [
        { "key": "name", "label": "Raid", "sortable": true },
        { "key": "when", "label": "When", "kind": "time" },
        { "key": "lead", "label": "Lead", "kind": "user" },
        { "key": "fill", "label": "Filled", "kind": "progress" }
      ],
      "rows": [
        { "id": "42",
          "cells": { "name": "Mythic", "when": "2026-11-02T19:00:00Z",
                     "lead": "1491250000000000003", "fill": { "value": 18, "max": 20 } },
          "action": { "route": "raids/42" } }
      ] },
    { "type": "text", "markdown": "Bring flasks. Questions to <@1491250000000000003>." }
  ]
}
ChampRemarques
version 1 ou absent. Toute autre version affiche « Cet écran nécessite une version plus récente de GameVox. »
kind layout ou absent. Même règle.
title Jusqu’à 100 caractères, affiché dans l’en-tête.
subtitle Jusqu’à 200 caractères.
route La route que montre cet écran. Elle peut différer de celle demandée (une redirection) ; le client l’enregistre dans son historique. Si vous l’omettez, GameVox renseigne la route demandée.
nav_active L’élément de navigation à mettre en évidence.
breadcrumbs Jusqu’à 5 {label, route?}, libellés jusqu’à 100 caractères.
tabs Jusqu’à 8 onglets de sous-navigation {label, route, badge?} ; l’onglet dont la route correspond est sélectionné. Libellés jusqu’à 40, badges jusqu’à 20.
actions Jusqu’à 4 boutons d’en-tête (voir Boutons).
refresh_seconds Les clients rouvrent l’écran à cette fréquence tant qu’il est visible et que la fenêtre a le focus : de 15 à 3600 secondes.
blocks Le contenu : au plus 60 blocs dans une liste et 600 blocs dans toute la vue.

Blocs

Chaque bloc a un type et un id facultatif (jusqu’à 64 caractères). Les remarques donnent les limites : tout ce qui dépasse une limite est coupé ou écarté.

TypeChampsRemarques
heading text, level Un titre. Texte jusqu’à 200 caractères, niveau 1 à 3 (2 par défaut).
text markdown Un paragraphe en markdown, jusqu’à 4 000 caractères.
callout style: info | success | warning | error, markdown Un encadré mis en évidence, en markdown. Le style vaut info par défaut.
divider Une ligne horizontale.
section title, description, blocks, collapsible, collapsed Un groupe de blocs avec un titre (100) et une description (200) facultatifs, éventuellement repliable. Les conteneurs s’imbriquent sur deux niveaux.
columns columns: [{width 1-12, blocks}] Des colonnes côte à côte sur une grille de 12 (jusqu’à 4 colonnes). Elles s’empilent sur les écrans étroits.
stats items: [{label, value, delta, trend, color, icon}] Des tuiles de statistiques (jusqu’à 8). Valeur jusqu’à 100 caractères, écart jusqu’à 30, tendance up, down ou flat.
key_value items: [{label, value, user}] Des paires libellé et valeur (jusqu’à 30). Valeurs jusqu’à 500 caractères. Une ligne peut n’être qu’un user, qui dessine une puce de membre.
table columns: [{key, label, align, sortable, kind}], rows: [{id, cells, action}], page_size, empty
kind: text | number | user | badge | progress | time | icon | image
Un tableau : jusqu’à 10 colonnes et 200 lignes, triées et paginées par le client (page_size de 1 à 100, 25 par défaut). Les valeurs des cellules suivent le kind de la colonne ; cellules jusqu’à 500 caractères. Les lignes sans id en reçoivent un.
list items: [{id, title, subtitle, user, icon, image, badges, meta, action, buttons}] Une liste (jusqu’à 100 éléments). Titres jusqu’à 200 caractères, jusqu’à 5 badges et 5 boutons par élément.
progress label, value, max, color Une barre de progression. max vaut 100 par défaut ; la valeur est maintenue entre 0 et max.
countdown label, until Un compte à rebours en direct vers une heure RFC 3339. Écarté sans until.
timeline items: [{time, title, markdown, icon, color}] Une chronologie (jusqu’à 50 entrées). Markdown des entrées jusqu’à 500 caractères.
calendar mode: month | week | agenda, start, events: [{id, title, start, end, color, action}] Un calendrier (jusqu’à 200 événements). Les écrans étroits affichent par défaut la vue agenda. Les boutons précédent et suivant envoient l’action calendar_nav (voir Actions).
chart kind: bar | line, series: [{name, color, points: [{x, y}]}], x_label, y_label Un graphique en barres ou en courbes : jusqu’à 5 séries de 100 points. x est un nombre ou un libellé.
board columns: [{id, title, cards: [{id, title, subtitle, user, image, badges, action}]}] Un tableau kanban de colonnes et de cartes : jusqu’à 6 colonnes de 50 cartes. Pas de glisser-déposer.
image url | file_id, alt, aspect Une image : une URL https ou un fichier d’app. Texte alternatif jusqu’à 200 caractères, aspect comme "16:9" ou 1.5.
video file_id, poster_file_id, title Un fichier vidéo d’app (uniquement pour les apps dont la formule autorise la vidéo ; voir Fichiers d’app).
buttons items: [Button] Une rangée de boutons (jusqu’à 5).
form id, fields: [Field], submit: Button Un formulaire (jusqu’à 15 champs). Voir Formulaires.
empty title, text, icon Un état vide : titre (100), texte (200) et icône.

Valeurs communes à plusieurs blocs

  • Les couleurs sont #rrggbb ou un jeton : accent, success, warning, danger, muted. Les badges sont {text, color?} (texte jusqu’à 30) ou une simple chaîne.
  • Les heures sont au format RFC 3339 et affichées dans le fuseau horaire de la personne. Les champs start et end du calendrier acceptent aussi une date YYYY-MM-DD.
  • Les membres (user) sont des snowflakes de membre. Le client dessine un avatar et le nom affiché, et un clic ouvre le profil.
  • Le markdown (text, callout, entrées de chronologie) est le markdown du chat GameVox. Le HTML n’est jamais rendu. Les mentions comme <@id>, <@&id> et <#id> sont dessinées sous forme de noms et ne notifient jamais personne. Tous les autres champs de texte sont du texte brut.
  • Les images doivent être des URL https sans identifiants, jusqu’à 2000 caractères. GameVox n’a pas de proxy d’images : une image qui n’est pas sur le stockage GameVox est conservée mais marquée "external": true, et les clients la traitent comme une image tierce dans le chat (pas de referrer, pas de cookies, éventuellement derrière un clic). Les fichiers d’app évitent cela.

Ce que fait le nettoyage

Votre vue est un contenu tiers dessiné dans le client de chaque membre : GameVox la reconstruit donc à partir d’une liste blanche avant qu’un client ne la voie :

  • Les types de bloc inconnus et les champs inconnus sont écartés.
  • Le texte est débarrassé des séquences UTF-8 invalides, des caractères de contrôle et des caractères de forçage bidirectionnel, puis coupé à sa limite.
  • Les listes sont coupées à leur nombre maximal ; les blocs au-delà de 60 dans une liste ou de 600 dans la vue sont écartés.
  • Les couleurs, routes, heures, ids et références de membre qui ne respectent pas leur forme sont vidés.
  • Les éléments auxquels il manque l’essentiel sont écartés : un élément de liste sans titre, une entrée de chronologie sans heure, un bouton sans libellé ou sans cible, un formulaire sans id ou submit.action.
  • Les params des boutons et des lignes doivent être des données simples (chaînes, nombres, booléens, listes et objets, jusqu’à 5 niveaux, 50 entrées chacun, chaînes jusqu’à 500 caractères, 2 Ko au total), sinon ils sont écartés avec leur bouton.
  • Une vue n’est refusée en bloc que si ce n’est pas un objet JSON, si elle dépasse 256 Ko (128 Ko pour un instantané), ou si elle a une autre version ou un autre kind. La personne voit alors « Cette app a envoyé un écran que GameVox n’a pas pu afficher. »

L’endpoint d’instantané répond avec des warnings qui listent ce qui a été modifié. Les réponses en direct, non. View.Validate() de la bibliothèque de bots Go liste les mêmes modifications avant l’envoi.

Actions et interactions

Boutons

Un bouton soit navigue (il a une route), soit envoie une action à votre app (il a une action) :

{
  "label": "Cancel raid",
  "action": "cancel",
  "params": { "raid": 42 },
  "style": "danger",
  "ability": "manage",
  "confirm": { "title": "Cancel this raid?", "text": "Everyone who signed up is told.", "confirm_label": "Cancel raid" }
}
  • label : obligatoire, jusqu’à 40 caractères.
  • action : un nom respectant ^[A-Za-z0-9._:-]{1,64}$. Appuyer dessus vous envoie kind: "action" avec ce nom et les params du bouton.
  • route : naviguer à la place. Le client ouvre cette route et vous recevez kind: "open" avec les params du bouton. Quand les deux sont définis, route l’emporte.
  • params : un objet jusqu’à 2 Ko, qui vous est rendu exactement tel que vous l’avez écrit.
  • style (primary, secondary, success, danger), icon, disabled.
  • ability : masquer le bouton aux personnes qui n’ont pas cette capacité.
  • confirm : {title, text, confirm_label} (80, 300 et 40 caractères) demande confirmation au membre avant l’envoi de l’action.

Les lignes de tableau, éléments de liste, cartes de tableau kanban et événements de calendrier acceptent une cible action sous les deux mêmes formes : {"action": "…", "params": …} ou {"route": "…", "params": …}. Les clients affichent un état occupé sur le contrôle pressé et n’envoient jamais deux fois le même appui.

Formulaires

Un bloc form a un id (unique dans la vue), jusqu’à 15 fields et un bouton submit, qui doit avoir une action. L’envoi vous transmet kind: "action" avec l’action et les params du bouton d’envoi, et les valeurs des champs dans values, indexées par clé de champ.

{
  "type": "form",
  "id": "signup",
  "fields": [
    { "key": "role", "type": "select", "label": "Role", "required": true,
      "options": [
        { "value": "tank", "label": "Tank" },
        { "value": "healer", "label": "Healer" },
        { "value": "dps", "label": "Damage" }
      ] },
    { "key": "note", "type": "string", "label": "Note", "max_length": 100 }
  ],
  "submit": { "label": "Sign up", "action": "signup", "params": { "raid": 42 }, "ability": "signup" }
}
  • key : respecte ^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$, unique dans le formulaire.
  • type : string, text, number, boolean, select, multiselect, channel, channels, role, roles, color, static, date, datetime, user (sélecteur de membre) ou file (voir Fichiers d’app).
  • label (100), help (200), placeholder (100), required, disabled, min, max, step, max_length (jusqu’à 2 000) et value pour préremplir.
  • options pour select et multiselect : jusqu’à 25 {value, label?, description?}. À défaut de libellé, la valeur est affichée.
  • channel_kinds restreint un sélecteur de salon. Les sélecteurs de salon et de rôle listent les salons et rôles du serveur ; GameVox les fournit, vous n’envoyez aucun id.

Valeurs reçues : des chaînes (jusqu’à 2 000 caractères), des nombres, des booléens, ou des listes de 25 chaînes au plus ; au maximum 30 valeurs et 32 Ko au total. Les sélecteurs de salon et de rôle donnent des snowflakes, un sélecteur de membre donne la snowflake du membre, et un champ fichier donne une liste d’objets fichier. GameVox ne vérifie que la forme : required, min, max et les listes d’options sont appliqués par le client, vérifiez donc chaque valeur vous-même.

Pagination du calendrier : les boutons précédent et suivant d’un bloc calendar envoient l’action calendar_nav avec les params {"start": "YYYY-MM-DD", "mode": "month|week|agenda", "id": "<block id>"}. Répondez par une vue dont le calendrier montre cette période.

Garder les écrans à jour

  • refresh_seconds dans une vue (de 15 à 3600) la rouvre à intervalle régulier tant qu’elle est visible.
  • POST /channels/{channel_id}/app-view/invalidate avec {"routes": [...]} indique à toutes les personnes qui regardent le salon que leur écran est périmé. Omettez routes pour actualiser tout le monde, ou listez jusqu’à 20 routes pour n’actualiser que les personnes qui s’y trouvent (raids correspond aussi à raids?past=1). Les clients rouvrent après un court délai aléatoire (de 1 à 3 secondes), si bien qu’une rafale vous coûte une requête par personne, pas une par événement. Au plus 5 appels par seconde et par salon passent immédiatement ; les autres sont regroupés en un seul, une seconde plus tard. Réponse : 204.
  • PUT /channels/{channel_id}/app-view/snapshot avec {"view": {…}} stocke un écran public, le même pour tous ceux qui peuvent voir le salon, jusqu’à 128 Ko. Il est affiché quand votre bot est hors ligne, signalé comme dernière mise à jour. Envoyez {"view": null} pour l’effacer (réponse {"cleared": true}). La réponse est {"warnings": [...]}. Jusqu’à 6 envois par tranche de 10 secondes et par salon. Ne mettez jamais rien de personnel dans un instantané.
  • Seule l’application à laquelle appartient un salon peut l’invalider ou définir son instantané.
POST /api/v10/channels/1491240000000000002/app-view/invalidate
{ "routes": ["raids", "raids/42"] }

HTTP/1.1 204 No Content

PUT /api/v10/channels/1491240000000000002/app-view/snapshot
{ "view": { "version": 1, "title": "Raids", "blocks": [ ... ] } }

HTTP/1.1 200 OK
{ "warnings": [] }

Contexte par personne

Chaque requête décrit une personne, si bien que les écrans peuvent être personnels : « vos personnages », « votre inscription », des boutons que seuls les gestionnaires voient.

  • Identité : user.id est la snowflake du membre, le même id que votre bot voit dans les événements gateway et en REST. Conservez les données par membre avec cet id.
  • Capacités : pour chaque capacité, la personne la détient si l’un de ses groupes qui peuvent voir le salon a soit une exception qui l’accorde, soit aucune exception et une valeur par défaut du manifeste qui la couvre (everyone : chacun de ces groupes ; managers : les groupes gestionnaires uniquement ; nobody : aucun). Le propriétaire du serveur détient toutes les capacités. Un membre sans groupe se rabat sur le groupe par défaut du serveur.
  • Les gestionnaires (is_manager et la valeur par défaut managers) sont le propriétaire du serveur, les groupes admin, et les groupes ayant la permission Modifier le serveur ou Gérer les intégrations.
  • La navigation est filtrée pour la personne : les éléments de navigation qui nécessitent une capacité qu’elle n’a pas sont masqués. C’est une commodité. Masquer un bouton n’est pas une mesure de sécurité, et un client modifié peut envoyer n’importe quelle action, route ou params.
  • Langue : locale est la langue du client de la personne. Un écran destiné à tous (un instantané) ne peut pas la connaître ; utilisez-y la langue du serveur.
  • Membres dans ce que vous envoyez : écrivez des snowflakes de membre dans les champs user, les cellules de membre des tableaux, les valeurs des sélecteurs de membre et les mentions. GameVox les traduit pour les clients et dans l’autre sens ; les params ne sont jamais touchés.

Fichiers d’app, formules et notifications d’app

Fichiers d’app

Une app peut accepter des téléversements (par exemple une capture d’un score) via un champ de formulaire de type file, et afficher des fichiers stockés dans ses écrans. Les fichiers résident dans un stockage GameVox privé, sont analysés comme les pièces jointes du chat, et ne sont jamais servis autrement que par des URL signées de courte durée.

  • Déclarez le stockage dans le manifeste : "storage": {"kinds": ["image"], "max_file_bytes"?, "max_total_bytes"?, "max_video_bytes"?}. Sans cela, votre app ne reçoit aucun téléversement. Les limites effectives sont celles de votre formule, abaissées par ce que vous déclarez.
  • Ajoutez un champ fichier : {"type": "file", "key", "label", "accept": ["image"], "max_files": 1 to 10, "required"?}. Le client téléverse directement vers le stockage et attend l’analyse avant que le formulaire puisse être envoyé.
  • Lisez les valeurs : la valeur du champ est une liste de {id, url, name, mime, size, width, height}. url est signée pour une heure ; récupérez le fichier dans votre gestionnaire ou appelez GET dessus plus tard pour obtenir une nouvelle URL.
  • Conservez ce que vous utilisez. Un téléversement reste pending tant qu’il n’est pas conservé, et un fichier en attente est supprimé après 24 heures. Répondre à l’action qui le contenait sans error le conserve automatiquement. POST /applications/@me/app-files/{id}/keep en conserve un manuellement. Un fichier conservé reste jusqu’à ce que vous le supprimiez, que la durée de rétention de la formule expire, qu’un modérateur le bloque, ou que votre app ait été retirée du serveur depuis 30 jours.
  • Affichez les fichiers avec {"file_id": "<id>"} au lieu d’une URL : le file_id d’un bloc image, l’image d’un élément de liste ou d’une carte de tableau, une cellule d’une colonne de type image. GameVox y substitue une URL signée pour la personne qui consulte. C’est le fait de placer un fichier dans une vue qui permet à cette personne de le voir. Les ids d’une autre app ou d’un autre serveur sont écartés, et une vue contient au plus 100 ids de fichier.
  • Les instantanés n’affichent que les fichiers que vous avez marqués publics : PATCH /applications/@me/app-files/{id} avec {"public_in_snapshot": true}.
  • Types : les images (PNG, JPEG, GIF, WebP) fonctionnent aujourd’hui. La vidéo (MP4 en H.264) ne fonctionne que là où GameVox a accordé à votre app une allocation vidéo sur ce serveur ; le WebM est refusé car il ne peut pas être analysé. document est réservé : un manifeste peut le nommer, mais aucun type de document n’est encore accepté.
// manifest
"storage": { "kinds": ["image"], "max_file_bytes": 4194304 }

// a field in a form
{ "key": "shot", "type": "file", "label": "Screenshot", "accept": ["image"], "max_files": 2 }

// what the action brings you
"values": {
  "shot": [ { "id": "8c1e2f4a-0b6d-4e7f-9a21-3c5d7e9f1a2b", "url": "https://...signed...",
              "name": "score.png", "mime": "image/png", "size": 183224, "width": 1920, "height": 1080 } ]
}

// showing it later
{ "type": "image", "file_id": "8c1e2f4a-0b6d-4e7f-9a21-3c5d7e9f1a2b", "alt": "Score screen" }

REST (tout sous /applications/@me/, token de bot, ids de guild, de salon et d’auteur du téléversement en snowflakes, ids de fichier en chaînes UUID) : GET app-files?guild_id=&state=&page= répond {files, page, has_more} (50 par page) ; GET, PATCH et DELETE app-files/{id} ; POST app-files/{id}/keep ; GET app-usage?guild_id= répond {plan, limits, used_bytes, files}. Le fichier d’une autre app donne 404 ; un fichier pas encore téléversé, supprimé ou bloqué donne 409 pour keep.

Formules

Chaque requête porte plan: {key, limits}, les limites de votre app sur ce serveur après le plafond storage de votre manifeste. Servez-vous-en pour conditionner vos propres extras (une app construite sur les données d’une autre entreprise doit suivre les règles de cette entreprise sur les fonctionnalités payantes). Aujourd’hui, chaque app tourne sur la formule free, sauf si l’équipe GameVox définit une autre formule pour un serveur ; il n’est pas encore possible d’acheter une formule.

ChampRemarques
max_file_bytes Plus grande image : 8 Mo.
max_files_per_field Fichiers par champ fichier : 4.
max_total_bytes Stockage par app et par serveur : 500 Mo. Les fichiers en attente et les fichiers conservés comptent tous les deux.
max_uploads_per_user_per_day Téléversements par membre et par jour : 50.
kinds Types autorisés : ["image"], puis restreints par votre manifeste (vide si vous ne déclarez aucun stockage).
retention_days Les fichiers conservés sont supprimés après ce nombre de jours ; 0 les garde jusqu’à ce que vous les supprimiez.
max_video_bytes Plus grande vidéo ; 0 signifie aucune vidéo.

Notifications d’app

Les bots ne peuvent pas envoyer de messages privés. Une app peut à la place notifier des membres (une réponse à un ticket, un rappel de raid) avec POST /applications/@me/app-notifications. Les membres la voient dans la cloche de notifications avec l’icône de votre app, sous forme de toast, et en notification push mobile s’ils autorisent les notifications d’app. Un appui ouvre votre salon d’app sur route.

POST /api/v10/applications/@me/app-notifications
Authorization: Bot <token>
Content-Type: application/json

{
  "guild_id": "1491230000000000001",
  "user_ids": ["1491250000000000003", "1491250000000000009"],
  "title": "Raid starts in 30 minutes",
  "body": "Mythic, 19:00. You signed up as healer.",
  "route": "raids/42",
  "dedupe_key": "raid-42-reminder"
}

HTTP/1.1 200 OK

{
  "delivered": ["1491250000000000003"],
  "skipped": [ { "user_id": "1491250000000000009", "reason": "muted" } ]
}
  • guild_id (obligatoire), user_ids (de 1 à 25 snowflakes de membre), title (obligatoire, jusqu’à 80 caractères), body (jusqu’à 300, peut être vide), route, channel_id (un salon d’app de votre app sur ce serveur), dedupe_key (jusqu’à 64). Un texte trop long donne un 400 qui nomme la limite ; le texte n’est jamais coupé.
  • Le titre et le corps sont du texte brut. Les caractères de contrôle et bidirectionnels sont retirés.
  • Un appui ouvre channel_id si vous en avez donné un, sinon le premier salon d’app de votre app que le membre peut voir. Une app sans salon d’app sur le serveur envoie des notifications qui n’ouvrent rien.
  • La même dedupe_key pour le même membre dans les 24 heures remplace la notification précédente (elle réapparaît comme non lue) au lieu d’en ajouter une.
  • La réponse liste les ids de membre delivered et ceux skipped avec une reason : not_member (aussi pour un id inconnu de GameVox), muted (le membre a mis votre app en sourdine sur ce serveur), cannot_view, rate_limited, ou error (réessayez plus tard). Un membre ignoré ne consomme rien.
  • Limites : 10 par heure et 30 par jour par app et par membre, 500 par heure par app et par serveur (une tentative refusée compte quand même), et 20 appels par tranche de 10 secondes par bot. 403 si votre app n’est pas installée sur ce serveur.
  • Les notifications sont conservées 30 jours, 200 par membre au maximum.

Serveurs auto-hébergés

Les salons d’app fonctionnent sur les serveurs auto-hébergés avec les mêmes événements et endpoints. Votre bot ne se connecte toujours qu’au cloud. Les différences :

  • self_hosted vaut true dans APP_VIEW_REQUEST.
  • L’instance du client vérifie qui peut voir le salon et calcule les capacités de la personne à partir de ses propres groupes, puis demande au cloud de vous envoyer la requête. GameVox fait confiance à l’instance pour cela, comme pour toute autre permission sur un serveur auto-hébergé.
  • L’instantané, l’invalidation ainsi que la création, la modification et la suppression d’un salon auto-hébergé sont transmis à l’instance comme les autres appels auto-hébergés : 503 quand l’instance n’est pas connectée, 504 quand elle ne répond pas à temps.
  • Un manifeste modifié atteint une instance en une minute environ (elle met le manifeste en cache 60 secondes) au lieu d’immédiatement.
  • Quand l’instance ne peut pas joindre le cloud, les membres voient votre instantané (sans les fichiers qu’il contient) ou un avis hors ligne.
  • Les fichiers d’app fonctionnent comme dans le cloud et sont stockés dans le stockage GameVox, pas sur l’instance. La vidéo nécessite une instance à jour.
  • Pour les notifications d’app, le cloud demande à l’instance qui peut voir le salon d’app. Une instance plus ancienne ou injoignable n’est pas interrogée : seule l’appartenance au serveur est alors vérifiée, et un channel_id que vous fournissez est transmis sans vérification.
  • Les traductions du manifeste fonctionnent aussi sur les serveurs auto-hébergés, dans la langue qu’annonce la connexion de chaque membre. Un serveur pas encore mis à jour affiche le texte par défaut.

Notes de sécurité pour les auteurs de bots

  • Faites respecter les capacités à chaque action. Vérifiez abilities (et vos propres règles) avant de modifier quoi que ce soit. Le fait que le client masque un bouton n’est qu’une politesse.
  • Traitez route, params, action et values comme des saisies utilisateur. Ils proviennent du client du membre. Un client modifié peut envoyer n’importe quelle route, n’importe quels params et n’importe quel nom d’action, et ignorer les champs obligatoires et les limites du formulaire. GameVox ne vérifie que leur forme.
  • Faites confiance aux ids que GameVox vous donne (guild_id, channel_id, user.id, abilities), pas aux ids présents dans les params. Pour agir sur « le raid 42 », vérifiez que le raid 42 appartient bien à ce salon.
  • Gardez les instantanés publics. Toute personne qui peut voir le salon les voit. N’y mettez jamais de données personnelles, et ne marquez publics que les fichiers qui peuvent être montrés à tous.
  • Ne divulguez pas les URL de fichier signées. Elles fonctionnent pendant une heure pour quiconque les détient. Mettez des file_id dans les vues et laissez GameVox les signer pour chaque personne.
  • Gardez vos nonces pour vous. Chacun répond à la requête d’une seule personne ; GameVox vérifie qu’il appartient à votre application.
  • Préférez les images hébergées par GameVox. Les URL d’images tierces sont marquées externes et peuvent être cachées derrière un clic ; elles indiquent aussi à l’hébergeur de l’image quand l’écran est affiché.
  • GameVox, de son côté, ne fait jamais confiance à votre vue : elle est reconstruite à partir d’une liste blanche, le HTML n’est jamais rendu, les mentions ne notifient jamais, et open_url est limité à https et passe toujours par l’avertissement pour les liens externes.

Erreurs et limites

Réponses REST

  • 400 : corps invalide. Les erreurs de manifeste et d’instantané nomment le champ. Une réponse sans nonce donne aussi un 400.
  • 403 : le nonce ou le salon appartient à une autre application, ou votre app n’est pas installée sur ce serveur.
  • 404 : nonce inconnu ou expiré ; salon inconnu, ou salon qui n’est pas un salon d’app (instantané, invalidation) ; fichier inconnu ; aucun manifeste publié (GET).
  • 409 : le fichier n’est pas dans un état qui le permet.
  • 413 : corps au-delà de la limite : manifeste 64 Ko (plus 192 Ko de traductions), réponse de vue 256 Ko, instantané 128 Ko, requête de notification 64 Ko.
  • 429 : limite de débit dépassée, avec les en-têtes de limite de débit habituels.

Limites de débit

  • Manifeste : 10 publications par minute par application (GET : 30 par minute).
  • Réponses de vue : 200 par seconde par bot.
  • Instantané : 6 envois par tranche de 10 secondes par salon.
  • Invalidation : 5 par seconde et par salon passent immédiatement ; au-delà, elles sont regroupées, pas refusées.
  • Fichiers d’app : 30 appels par tranche de 10 secondes par route ; usage : 30 par minute.
  • Notifications d’app : 20 appels par tranche de 10 secondes par bot, plus les limites par membre et par serveur ci-dessus.
  • Par personne : 20 ouvertures et 10 actions par tranche de 10 secondes. Par application, toutes personnes confondues : 600 requêtes par tranche de 10 secondes. Au-delà, la personne est invitée à ralentir ou informée que l’app est occupée ; votre bot n’est pas interrogé.

Ce que voit la personne

  • « L’app n’a pas répondu. Réessayez. » Aucune réponse en 10 secondes.
  • « L’app est hors ligne pour le moment. » Une action alors que votre bot est hors ligne.
  • « Cette app a envoyé un écran que GameVox n’a pas pu afficher. » La vue a été refusée en bloc.
  • « Cet écran nécessite une version plus récente de GameVox. » Une vue avec une autre version ou un autre kind.
  • Votre propre texte error, quand vous en envoyez un.

Un exemple minimal de bout en bout

Une app d’inscription aux raids avec un écran et un formulaire, en HTTP brut. Les ids sont raccourcis.

1. Publiez le manifeste au démarrage du bot :

PUT /api/v10/applications/@me/app-manifest
Authorization: Bot <token>

{ "name": "Raid sign-up", "icon": { "builtin": "calendar" }, "home": "home",
  "nav": [ { "label": "Home", "route": "home" } ],
  "abilities": [ { "key": "signup", "label": "Sign up", "default": "everyone" } ] }

HTTP/1.1 200 OK

2. Obtenez un salon. Un admin ajoute votre app avec Créer un salon ▸ App, ou votre bot en crée un :

POST /api/v10/guilds/1491230000000000001/channels
{ "type": 100, "name": "raid-signup" }

HTTP/1.1 201 Created
{ "id": "1491240000000000002", "type": 100, "gamevox_type": "app", ... }

3. Un membre l’ouvre. Votre bot reçoit :

{ "op": 0, "t": "APP_VIEW_REQUEST", "d": {
    "nonce": "a1...", "kind": "open", "guild_id": "1491230000000000001",
    "channel_id": "1491240000000000002", "route": "home", "params": {},
    "user": { "id": "1491250000000000003", "username": "ben", ... },
    "abilities": ["signup"], "is_manager": false, "locale": "en-US", "platform": "web", ... } }

4. Répondez avec l’écran (en moins de 10 secondes) :

POST /api/v10/applications/@me/app-view-response
Authorization: Bot <token>

{ "nonce": "a1...",
  "view": { "version": 1, "title": "Raid sign-up", "route": "home", "blocks": [
    { "type": "text", "markdown": "You are not signed up yet." },
    { "type": "form", "id": "signup",
      "fields": [ { "key": "role", "type": "select", "label": "Role", "required": true,
                    "options": [ { "value": "tank" }, { "value": "healer" }, { "value": "dps" } ] } ],
      "submit": { "label": "Sign up", "action": "signup", "ability": "signup" } }
  ] } }

HTTP/1.1 204 No Content

5. Le membre envoie le formulaire. Vous recevez une action avec les valeurs :

{ "op": 0, "t": "APP_VIEW_REQUEST", "d": {
    "nonce": "b2...", "kind": "action", "route": "home", "action": "signup",
    "params": {}, "values": { "role": "healer" },
    "channel_id": "1491240000000000002", "user": { "id": "1491250000000000003", ... },
    "abilities": ["signup"], ... } }

6. Vérifiez, stockez, répondez avec un toast et l’écran mis à jour, puis dites aux autres personnes d’actualiser :

POST /api/v10/applications/@me/app-view-response
{ "nonce": "b2...", "toast": "Signed up as healer",
  "view": { "version": 1, "title": "Raid sign-up", "route": "home", "blocks": [
    { "type": "text", "markdown": "You are signed up as **healer**." } ] } }

HTTP/1.1 204 No Content

POST /api/v10/channels/1491240000000000002/app-view/invalidate
{ "routes": ["home"] }

HTTP/1.1 204 No Content

Facultatif : un instantané à utiliser lorsque le bot est hors ligne. Il est public, il montre donc le nombre d’inscrits, pas leur identité :

PUT /api/v10/channels/1491240000000000002/app-view/snapshot
{ "view": { "version": 1, "title": "Raid sign-up", "blocks": [
    { "type": "stats", "items": [ { "label": "Signed up", "value": 18 } ] } ] } }

HTTP/1.1 200 OK
{ "warnings": [] }

Dans une bibliothèque Discord, lisez l’événement depuis son hook gateway brut (on_socket_raw_receive dans discord.py, l’événement raw dans discord.js ; voir le tableau dans Réglages intégrés). Une version discord.py des étapes 3 à 6 :

# discord.py: answer APP_VIEW_REQUEST from the raw gateway hook
# (create the client with enable_debug_events=True)
import json

API = "https://app-api.gamevox.com/api/v10"
ROLES = ("tank", "healer", "dps")
signups = {}  # (channel_id, user_id) -> role

def screen(req, note):
    return {"version": 1, "title": "Raid sign-up", "route": "home", "blocks": [
        {"type": "text", "markdown": note},
        {"type": "form", "id": "signup",
         "fields": [{"key": "role", "type": "select", "label": "Role", "required": True,
                     "options": [{"value": r} for r in ROLES]}],
         "submit": {"label": "Sign up", "action": "signup", "ability": "signup"}}]}

@client.event
async def on_socket_raw_receive(msg):
    packet = json.loads(msg)
    if packet.get("t") != "APP_VIEW_REQUEST":
        return
    req = packet["d"]
    key = (req["channel_id"], req["user"]["id"])
    reply = {"nonce": req["nonce"]}
    if req["kind"] == "action" and req["action"] == "signup":
        role = req["values"].get("role")
        if "signup" not in req["abilities"]:          # enforce, never trust the client
            reply["error"] = "You can't sign up in this channel."
        elif role not in ROLES:                       # values are user input
            reply["error"] = "Pick a role."
        else:
            signups[key] = role
            reply["toast"] = f"Signed up as {role}"
    if "error" not in reply:
        mine = signups.get(key)
        reply["view"] = screen(req, f"You are signed up as **{mine}**." if mine else "You are not signed up yet.")
    await session.post(f"{API}/applications/@me/app-view-response",
                       headers={"Authorization": f"Bot {TOKEN}"}, json=reply)

Les bots de GameVox utilisent la bibliothèque de bots Go de server/shared-gamebot, qui publie le manifeste, répond aux requêtes dans les délais, valide les vues et regroupe les invalidations pour vous.

Pas encore disponible

  • Un type web (une page isolée au lieu d’une mise en page) est réservé. Les manifestes et vues qui l’utilisent sont refusés.
  • Les téléversements de documents et l’achat d’une formule ne sont pas disponibles.
  • Le glisser-déposer sur les tableaux.

Composants de message  ·  ← Toute la doc