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_CREATEavec 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
- Un serveur installe votre app de la manière habituelle (le lien d’installation ou la page d’autorisation OAuth2).
- À chaque démarrage, votre bot publie son manifeste. Une app sans manifeste ne peut pas avoir de salons d’app.
- 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).
- 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.
- Les membres qui peuvent voir le salon l’ouvrent, et votre app répond à
APP_VIEW_REQUESTpour chaque écran qu’ils consultent. - 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_routeest une route (vide ou absente : lehomede votre manifeste).gamevox_app.iconest unemoji, un glyphebuiltin, ou uneurlque 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 :
typevaut 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,PATCHetDELETE /channels/{id}fonctionnent comme pour les autres salons.PATCHaccepte aussigamevox_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" }
]
} | Champ | Remarques |
|---|---|
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_localizationsetdescription_localizationssur le manifeste (textes jusqu’à 50 et 300 caractères),label_localizationssur un élément de navigation (jusqu’à 40),label_localizationsetdescription_localizationssur une capacité (jusqu’à 40 et 200).- Les clés sont des codes de langue Discord (
de,fr,pt-BR,es-ES,zh-CN,jaet 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 un400qui nomme le champ. - Une valeur vide,
nullou égale au texte par défaut est écartée, doncnullefface une langue. Une valeur au-delà de la limite du texte donne un400, par exemplenav[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
localede 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
}
}
}
} | Champ | Remarques |
|---|---|
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 viewremplace l’écran de la personne. Pour une action qui n’a besoin que d’un toast, omettez-le : la personne garde l’écran actuel.toastest une ligne courte (jusqu’à 200 caractères) affichée sous forme de toast. Il peut accompagner uneview.error(jusqu’à 300 caractères) affiche un message à la place d’un écran. Quanderrorest défini,viewettoastsont ignorés.open_urldemande au client d’ouvrir une URLhttpsaprè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 un204ne 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>." }
]
} | Champ | Remarques |
|---|---|
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é.
| Type | Champs | Remarques |
|---|---|---|
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, emptykind: 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
#rrggbbou 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
startetenddu calendrier acceptent aussi une dateYYYY-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
httpssans 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
idousubmit.action. - Les
paramsdes 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
versionou un autrekind. 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 envoiekind: "action"avec ce nom et lesparamsdu bouton.route: naviguer à la place. Le client ouvre cette route et vous recevezkind: "open"avec lesparamsdu bouton. Quand les deux sont définis,routel’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) oufile(voir Fichiers d’app).label(100),help(200),placeholder(100),required,disabled,min,max,step,max_length(jusqu’à 2 000) etvaluepour préremplir.optionspourselectetmultiselect: jusqu’à 25{value, label?, description?}. À défaut de libellé, la valeur est affichée.channel_kindsrestreint 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_secondsdans une vue (de 15 à 3600) la rouvre à intervalle régulier tant qu’elle est visible.POST /channels/{channel_id}/app-view/invalidateavec{"routes": [...]}indique à toutes les personnes qui regardent le salon que leur écran est périmé. Omettezroutespour actualiser tout le monde, ou listez jusqu’à 20 routes pour n’actualiser que les personnes qui s’y trouvent (raidscorrespond 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/snapshotavec{"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.idest 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_manageret la valeur par défautmanagers) 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 :
localeest 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 ; lesparamsne 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}.urlest signée pour une heure ; récupérez le fichier dans votre gestionnaire ou appelezGETdessus plus tard pour obtenir une nouvelle URL. - Conservez ce que vous utilisez. Un téléversement reste
pendingtant qu’il n’est pas conservé, et un fichier en attente est supprimé après 24 heures. Répondre à l’action qui le contenait sanserrorle conserve automatiquement.POST /applications/@me/app-files/{id}/keepen 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 : lefile_idd’un bloc image, l’imaged’un élément de liste ou d’une carte de tableau, une cellule d’une colonne de typeimage. 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é.
documentest 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.
| Champ | Remarques |
|---|---|
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 un400qui 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_idsi 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_keypour 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
deliveredet ceuxskippedavec unereason: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, ouerror(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.
403si 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_hostedvauttruedansAPP_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 :
503quand l’instance n’est pas connectée,504quand 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_idque 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_iddans 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_urlest limité àhttpset 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
versionou un autrekind. - 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.