Réglages intégrés
GameVox peut afficher le formulaire de configuration de votre app dans le client. Votre app décrit le formulaire via le gateway, une personne le modifie dans Réglages du serveur ▸ Intégrations ▸ Réglages, et les valeurs modifiées reviennent directement à votre bot. GameVox n’en stocke rien — votre app reste seule propriétaire de sa configuration.
Cela n’a pas d’équivalent Discord. C’est la seule partie de la plateforme d’apps GameVox qui n’est délibérément pas compatible, parce qu’il n’y a rien en face avec quoi l’être.
select, un boolean et un sélecteur channels, regroupés dans une section avec description. Le bot n’a envoyé aucun id de salon — la liste vient de GameVox. Noms de serveur et de salons masqués.Pourquoi cela existe
Sur Discord, un bot avec la moindre configuration embarque un tableau de bord web : un domaine, une connexion OAuth2, la gestion des sessions, une vérification de permissions qui relit la liste de guilds de la personne, une protection CSRF et l’hébergement de tout cela — en général juste pour qu’une personne choisisse un salon de journalisation et active trois options. GameVox sait déjà qui est cette personne, quel serveur elle configure et si elle en a le droit ; il propose donc cette surface directement à votre app.
| Discord | GameVox | |
|---|---|---|
| Où votre app se configure | Un tableau de bord web que vous hébergez | Réglages du serveur ▸ Intégrations ▸ Réglages |
| Ce que vous devez construire | App web, connexion OAuth2, sessions, vérifications de permissions, hébergement | Un gestionnaire gateway et un appel REST |
| Qui authentifie la personne | Vous, via OAuth2 | GameVox, avant même de solliciter votre app |
| Qui stocke la configuration | Vous | Vous. GameVox ne conserve rien. |
| Sélecteurs de salon et de rôle | Vous récupérez les salons et rôles de la guild et vous les affichez | Vous nommez un type ; GameVox fournit les choix |
| Coût pour exposer un simple interrupteur | Un tableau de bord | Une vingtaine de lignes |
Cela ne remplace rien. Les commandes slash continuent de fonctionner, et si vous avez déjà un tableau de bord vous pouvez le garder — les deux ne s’excluent pas, et beaucoup d’apps voudront le panneau pour les cinq réglages réellement modifiés et le tableau de bord pour tout le reste.
Fonctionnement
Quelqu’un ouvre l’onglet Réglages
│
▼
GameVox ──── APP_SETTINGS_REQUEST (action: "describe") ────▶ votre bot
│
votre bo ─── POST /applications/@me/settings-response ──────────┘
{ nonce, version: 2, sections: [ ... ] }
│
▼
GameVox affiche le formulaire, la personne le modifie et clique sur Enregistrer
│
▼
GameVox ──── APP_SETTINGS_REQUEST (action: "save", values) ─▶ votre bot
│
votre bo ─── POST /applications/@me/settings-response ──────────┘
{ nonce, message: "Enregistré." } Deux actions, une forme de requête, un endpoint de réponse. describe demande ce que votre app expose pour ce serveur ; save renvoie ce qui a été modifié. Les deux se répondent de la même façon.
1. Recevoir la requête
GameVox envoie APP_SETTINGS_REQUEST via le gateway à la session qui détient la connexion de votre app. Aucun intent ne la contrôle — la dépêche est adressée spécifiquement à votre app, elle n’est pas filtrée par un masque d’intents, elle arrive donc quel que soit ce avec quoi vous vous êtes identifié.
{
"op": 0,
"t": "APP_SETTINGS_REQUEST",
"d": {
"nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
"server_id": "1387452901234567890",
"action": "describe",
"schema_version": 2
}
} | Champ | Type | Remarques |
|---|---|---|
nonce | string | Opaque. Renvoyez-le tel quel ; c’est la seule chose qui relie votre réponse à la personne qui attend. Répondable pendant 15 secondes. |
server_id | snowflake | La guild, dans le même espace d’ID que toute autre dépêche. C’est l’id que portait votre GUILD_CREATE, votre cache de guilds le résout donc déjà. |
action | string | "describe" ou "save". |
schema_version | integer | Le schéma le plus riche que ce serveur peut afficher. Actuellement 2. Voir Servir un serveur plus ancien. |
values | object | Présent uniquement sur save. Les clés sont les clés de champ que vous avez envoyées. |
server_id est apposé par GameVox à partir de la requête authentifiée de la personne. Il n’est jamais relu depuis votre réponse : une app ne peut donc pas répondre à une question sur un serveur avec les réglages d’un autre.
Lire la dépêche dans votre bibliothèque
Aucune bibliothèque de bots ne fournit de gestionnaire pour un événement qui n’existe pas sur Discord ; le paquet est donc écarté avant d’atteindre un gestionnaire d’événements. Toutes exposent le flux de dépêches brut précisément pour ce cas :
| Bibliothèque | Hook | Activation |
|---|---|---|
| discord.js (v14) | client.on('raw', packet) | Actif par défaut. Émis pour chaque dépêche avant le traitement du paquet. |
| discord.py (v2) | on_socket_raw_receive(msg) | Passez enable_debug_events=True au client. |
| Eris | client.on('rawWS', packet) | Actif par défaut. |
| JDA | RawGatewayEvent | JDABuilder.setRawEventsEnabled(true) |
| DSharpPlus | DiscordClient.UnknownEvent | Actif par défaut. EventName plus le Json brut. |
| serenity | RawEventHandler::raw_event | ClientBuilder::raw_event_handler |
Face au vrai Discord, ce gestionnaire ne se déclenche jamais : la même build tourne donc sur les deux plateformes sans branchement.
2. Répondre
Répondez via REST, pas via le gateway. Toutes les bibliothèques exposent un client HTTP ; la plupart ne laissent pas le code applicatif écrire des opcodes arbitraires sur le socket, d’où cette forme de réponse.
POST https://bot-api.gamevox.com/api/v10/applications/@me/settings-response
Authorization: Bot YOUR_BOT_TOKEN
Content-Type: application/json {
"nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
"version": 2,
"message": "Note facultative affichée au-dessus du formulaire.",
"sections": [
{
"key": "welcome",
"label": "Messages de bienvenue",
"description": "Publié quand quelqu’un arrive.",
"fields": [
{ "key": "welcome.enabled", "label": "Envoyer un message de bienvenue",
"type": "boolean", "value": true }
]
}
]
} Une réponse réussie renvoie 204 No Content. Vous pouvez ajouter ?server_id= avec l’id de la guild ; cela ne sert qu’à attribuer l’appel dans le journal d’activité de votre app.
| Clé | Type | Remarques |
|---|---|---|
nonce | string | Obligatoire. Provient de la requête. |
version | integer | Le schéma dans lequel vous répondez. Envoyez 2 quand vous envoyez sections. |
sections | array | Formulaire du schéma v2. Exclut fields. |
fields | array | Formulaire plat du schéma v1. Rendu comme une unique section sans titre. |
message | string | Sur describe, une note au-dessus du formulaire. Sur save, le message de confirmation. Tronqué à 200 caractères. |
error | string | Affiché à la place du formulaire. Tronqué à 300 caractères. |
Répondez toujours, y compris en cas d’échec. Laisser tomber la requête en silence laisse la personne devant un indicateur de chargement jusqu’au délai de 12 secondes de GameVox, ce qui se lit comme « cette app est cassée » plutôt que « quelque chose a raté une fois ».
3. Types de champ
Douze types, et la liste est fermée — un type inconnu fait échouer toute la réponse au lieu de ne rien afficher.
| Type | Contrôle | La value que vous envoyez | Ce que vous récupérez à l’enregistrement |
|---|---|---|---|
string | Champ texte d’une ligne | string | string ("" si vide) |
text | Zone de texte | string | string |
boolean | Interrupteur | boolean | true / false |
number | Champ numérique | number | number, ou null si vidé |
select | Liste déroulante de vos options | valeur d’option | string, ou null si non défini |
multiselect | Liste à cocher de vos options | tableau de valeurs d’option | tableau de chaînes (peut être vide) |
channel | Liste déroulante des salons de ce serveur | snowflake de salon | chaîne snowflake, ou null |
channels | Liste à cocher des salons de ce serveur | tableau de snowflakes | tableau de chaînes snowflake |
role | Liste déroulante des groupes de ce serveur | snowflake de rôle | chaîne snowflake, ou null |
roles | Liste à cocher des groupes de ce serveur | tableau de snowflakes | tableau de chaînes snowflake |
color | Nuancier de couleur | #rrggbb | #rrggbb, toujours présent |
static | Ligne de texte en lecture seule | string | Jamais envoyé — la clé est absente de values |
Un champ couleur contient toujours une valeur : une app qui n’en avait stocké aucune récupérera #000000 au premier enregistrement. Si vous avez besoin de « pas de couleur », associez le nuancier à un boolean.
4. Attributs de champ
| Attribut | S’applique à | Effet |
|---|---|---|
key | tous | Obligatoire. [A-Za-z0-9][A-Za-z0-9._:-]{0,63}. Unique sur toute la réponse, pas seulement par section. |
label | tous | À défaut, la clé est utilisée. Tronqué à 100 caractères. |
help | tous | Ligne d’indication sous le contrôle. Tronquée à 200. |
placeholder | champs texte, number | Tronqué à 100. |
options | select, multiselect | Obligatoire pour ces deux-là. { value, label, description }, 25 au maximum. Ignoré sur tout autre type. |
min , max , step | number | Met en forme le contrôle. Indicatif — revérifiez la valeur reçue. |
max_length | string, text | Limite la saisie, jusqu’au plafond de la plateforme pour ce type. |
channel_kinds | channel, channels | Restreint le sélecteur. text, voice, forum, news, fileshare, header. Les types inconnus sont ignorés ; un résultat vide signifie tous les types. |
required | tous | Marque le libellé et remplace l’entrée « Aucun » du sélecteur par « Sélectionner… ». Purement visuel — appliquez-le réellement à l’enregistrement. |
disabled | tous | Grise le contrôle. Il est tout de même envoyé, avec sa valeur actuelle. |
secret | string, text | Masque la saisie et vide la valeur actuelle en sortie. Sur tout autre type, la réponse est rejetée. |
show_if | tous | { key, equals }. Masque le champ tant qu’un autre champ de la même réponse n’a pas cette valeur. |
show_if est un test d’égalité et rien d’autre — pas d’expressions, pas d’opérateurs. La clé référencée doit appartenir à un champ à valeur unique de la même réponse et ne peut pas être la clé du champ lui-même ; une condition qui échoue à l’un de ces points est ignorée et le champ s’affiche sans condition. equals peut être une chaîne, un booléen, un nombre ou null.
Un champ masqué est tout de même envoyé. Il porte une valeur que votre app a envoyée, et l’omettre se lirait chez vous comme si la personne l’avait effacée.
5. Sélecteurs de salon et de rôle
C’est la partie qui mérite d’être comprise, car elle inverse l’organisation habituelle. Votre app nomme un type ; GameVox fournit les choix.
Un champ de salon ne porte aucun id. Vous envoyez { "type": "channel" } et, si vous le souhaitez, un filtre channel_kinds. GameVox y joint la liste des salons de ce serveur, le client en construit un sélecteur, et seule une option émise par GameVox peut être renvoyée.
Chaque entrée du catalogue est indexée par la snowflake que votre app voit déjà pour ce salon ou ce groupe. Il n’y a aucune étape de traduction d’id à rater : la personne choisit un libellé, votre app reçoit un id dans son propre espace de noms, et une valeur qui désigne quelque chose hors de ce serveur ne correspond simplement à aucune option.
{ "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] }. La liste, les glyphes de type et les ids viennent de GameVox.| Situation | Ce que la personne voit |
|---|---|
| Rien n’est encore enregistré | Une entrée « Aucun », sélectionnée. Un champ required affiche « Sélectionner… » à la place, de sorte qu’un enregistrement ne puisse pas figer une valeur que personne n’a choisie. |
| L’id enregistré n’existe plus (salon supprimé) | Conservé sous « Sélection actuelle (plus dans la liste) », toujours sélectionné. Enregistrer ne l’efface pas en silence. |
| Des ids enregistrés dans un sélecteur multiple ne sont plus listés | Ils sont affichés et cochés à côté des actuels, pour la même raison. |
| Catalogue indisponible (instance auto-hébergée hors ligne) | Sélecteur désactivé affichant « Indisponible pour le moment ». La valeur enregistrée est renvoyée telle quelle à l’enregistrement, ouvrir le panneau pendant une panne ne peut donc pas effacer un réglage. |
| Le serveur compte plus de 500 salons ou groupes | La liste est coupée à 500 plutôt que d’envoyer un mégaoctet d’options. |
Le catalogue de rôles exclut deux choses : le groupe propriétaire du serveur, qu’aucune app ne devrait se voir proposer comme rôle attribuable, et les groupes par app qui portent les permissions des apps installées.
6. Scénarios
Scénario 1 — Le plus petit panneau utile
Un interrupteur, sans section. Un tableau fields plat est valide dans toutes les versions du schéma et s’affiche comme un unique groupe sans titre.
{
"nonce": nonce,
"fields": [
{ "key": "greetings", "label": "Accueillir les nouveaux membres", "type": "boolean", "value": true }
]
} Scénario 2 — Regrouper en sections
{
"nonce": nonce,
"version": 2,
"sections": [
{
"key": "general",
"label": "Général",
"description": "Comment le bot se comporte sur ce serveur.",
"fields": [
{ "key": "prefix", "label": "Préfixe de commande", "type": "string",
"value": "!", "max_length": 4, "help": "Utilisé pour les commandes texte historiques." }
]
},
{
"key": "logging",
"label": "Journalisation d’audit",
"fields": [
{ "key": "log.enabled", "label": "Journaliser les actions de modération", "type": "boolean", "value": false }
]
}
]
} La key d’une section est facultative mais doit être unique lorsqu’elle est présente. label et description sont facultatifs ; une section sans champ n’est pas affichée.
Scénario 3 — Demander un salon
Pas d’ids, pas de récupération de la liste des salons de la guild. Restreignez le sélecteur aux types capables de recevoir un message.
{
"key": "log.channel",
"label": "Salon de journalisation",
"type": "channel",
"value": stored.logChannelId ?? null,
"channel_kinds": ["text", "news"],
"help": "Où les actions de modération sont consignées."
} Utilisez "channel_kinds": ["header"] quand vous voulez une catégorie plutôt qu’un endroit où publier — les en-têtes sont les catégories de GameVox.
Scénario 4 — Afficher des champs sous condition
La forme habituelle : un booléen qui conditionne le reste de sa fonctionnalité.
"fields": [
{ "key": "welcome.enabled", "label": "Envoyer un message de bienvenue",
"type": "boolean", "value": true },
{ "key": "welcome.channel", "label": "Salon", "type": "channel",
"value": stored.welcomeChannel ?? null,
"channel_kinds": ["text", "news"],
"show_if": { "key": "welcome.enabled", "equals": true } },
{ "key": "welcome.message", "label": "Message", "type": "text",
"value": stored.welcomeMessage ?? "",
"max_length": 1800,
"placeholder": "Bienvenue {user} sur {server} !",
"help": "{user}, {server} et {membercount} sont remplacés à l’envoi.",
"show_if": { "key": "welcome.enabled", "equals": true } }
] Une condition peut aussi porter sur un select, ce qui permet de construire un sélecteur de mode : "show_if": { "key": "mode", "equals": "advanced" }.
Scénario 5 — Vos propres choix
select et multiselect sont les deux types qui portent leurs propres options. Les deux exigent au moins une option et en autorisent 25 au maximum.
{
"key": "automod.action",
"label": "Quand un filtre se déclenche",
"type": "select",
"value": stored.action,
"required": true,
"options": [
{ "value": "delete", "label": "Supprimer le message" },
{ "value": "warn", "label": "Avertir l’auteur", "description": "Supprime le message et envoie un MP au membre." },
{ "value": "timeout", "label": "Mettre l’auteur en pause", "description": "10 minutes." },
{ "value": "none", "label": "Ne rien faire" }
]
} {
"key": "automod.filters",
"label": "Filtres actifs",
"type": "multiselect",
"value": stored.filters,
"options": [
{ "value": "invites", "label": "Invitations Discord/GameVox" },
{ "value": "links", "label": "Liens" },
{ "value": "mentions", "label": "Mentions de masse" },
{ "value": "caps", "label": "Excès de majuscules" }
]
} La description d’une option apparaît après le libellé dans une liste déroulante, et en infobulle dans une liste à cocher.
Scénario 6 — Rôles
Même règle de catalogue que pour les salons. GameVox liste les groupes de ce serveur par rang, avec leurs couleurs.
"fields": [
{ "key": "autorole.role", "label": "Rôle attribué à l’arrivée",
"type": "role", "value": stored.autoRole ?? null },
{ "key": "moderator.roles", "label": "Rôles autorisés à utiliser les commandes de modération",
"type": "roles", "value": stored.modRoles,
"help": "Les membres ayant l’un de ces rôles peuvent lancer /ban et /timeout." }
] Scénario 7 — Nombres avec bornes
{
"key": "automod.threshold",
"label": "Messages avant de déclarer un raid",
"type": "number",
"value": stored.threshold ?? 10,
"min": 3,
"max": 100,
"step": 1,
"help": "Comptés sur une fenêtre glissante de 60 secondes."
} min, max et step ne font que mettre en forme le contrôle. Le navigateur de la personne n’est pas un validateur que vous maîtrisez : rebornez la valeur au retour — et notez que vider le champ envoie null, pas 0.
Scénario 8 — Secrets
Marquez un identifiant comme secret et GameVox vide sa valeur actuelle en sortie. Elle n’est ni dans la trame WebSocket, ni dans le DOM, ni sur une capture d’écran du panneau — la personne voit un champ masqué vide avec l’indication « Inchangé ».
{
"key": "integrations.apiKey",
"label": "Clé d’API météo",
"type": "string",
"secret": true,
"help": "Laissez vide pour conserver la clé actuelle."
} D’où le contrat : un secret envoyé vide signifie « ne rien changer », jamais « effacer ». S’il faut pouvoir supprimer un identifiant, prévoyez un booléen explicite pour cela. secret sur autre chose qu’un string ou un text fait échouer la réponse.
Scénario 9 — État en lecture seule
static affiche une ligne de texte et n’est jamais envoyé. Utilisez-le pour un état utile pendant la configuration mais non modifiable ici.
"fields": [
{ "key": "plan", "label": "Formule", "type": "static", "value": "Pro — 4 000 requêtes/jour" },
{ "key": "usage", "label": "Utilisées aujourd’hui", "type": "static", "value": "1 284 requêtes" },
{ "key": "lastSync", "label": "Dernière synchronisation", "type": "static", "value": "2026-09-02 14:31 UTC" }
] Scénario 10 — Traiter un enregistrement
La requête d’enregistrement porte values, indexé par les clés de champ que vous avez envoyées. Persistez, puis répondez par une confirmation.
// action === "save"
const v = req.values ?? {};
const guild = client.guilds.cache.get(req.server_id);
if (!guild) return reply(nonce, { error: "Ce bot n’est plus sur ce serveur." });
const patch = {};
// boolean : toujours un vrai booléen
patch.welcomeEnabled = v["welcome.enabled"] === true;
// channel : une chaîne snowflake ou null — revérifiez-la sur CETTE guild
const chan = v["welcome.channel"];
patch.welcomeChannel =
(typeof chan === "string" && guild.channels.cache.has(chan)) ? chan : null;
// number : nombre, ou null si le champ a été vidé
const n = v["automod.threshold"];
patch.threshold = typeof n === "number" ? Math.min(100, Math.max(3, Math.round(n))) : 10;
// secret : vide signifie inchangé
const key = v["integrations.apiKey"];
if (typeof key === "string" && key.trim() !== "") patch.apiKey = key.trim();
await store.update(req.server_id, patch);
await reply(nonce, { message: "Réglages enregistrés." }); Votre message devient le message de confirmation. Le formulaire reste exactement dans l’état où il était, la personne peut donc continuer à le modifier.
Scénario 11 — Refuser un enregistrement
Renvoyez error et votre texte s’affiche à la place d’une confirmation. Servez-vous-en pour tout ce que vous ne pouvez pas accepter : une valeur qui échoue à votre validation, une limite de formule, un identifiant externe devenu invalide.
// `required` est purement visuel — appliquez-le ici.
const action = v["automod.action"];
if (!["delete", "warn", "timeout", "none"].includes(action)) {
return reply(nonce, guildId, {
error: "Choisissez ce qu’AutoMod doit faire quand un filtre se déclenche.",
});
}
// Tout ce que seul votre côté peut savoir.
if (patch.apiKey && !(await upstream.verifyKey(patch.apiKey))) {
return reply(nonce, guildId, {
error: "Cette clé d’API a été refusée par le fournisseur météo.",
});
} Il en va de même pour describe : si votre base est indisponible, répondez avec une error plutôt que de ne pas répondre. « Cette app n’a pas pu lire ses réglages pour le moment » est un bien meilleur résultat que douze secondes de chargement.
Scénario 12 — Servir un serveur plus ancien
La requête porte schema_version, vous n’avez donc jamais à tâtonner. Servez le formulaire le plus riche que le serveur affichera réellement.
const version = typeof req.schema_version === "number" ? req.schema_version : 1;
if (version >= 2) {
await reply(nonce, { version: 2, sections: describeV2(guildId) });
} else {
// v1 : liste plate, quatre types — string, boolean, number, select
await reply(nonce, { fields: describeV1(guildId) });
} L’inverse est vrai aussi. Une réponse v2 qui atteint un client plus ancien s’affiche quand même : GameVox envoie un tableau fields aplati à côté de sections, et un client antérieur aux nouveaux types les dessine en champs texte. Dégradé, mais rien n’est masqué.
Scénario 13 — Serveurs auto-hébergés
Fonctionne sans changement. Sur un serveur auto-hébergé, les catalogues de salons et de rôles sont récupérés auprès de l’instance du client plutôt que des tables cloud, ce qui est invisible pour votre app — mêmes types de champ, mêmes snowflakes, même réponse.
La seule différence observable : si l’instance est injoignable, les sélecteurs arrivent vides et s’affichent comme « Indisponible pour le moment ». Les valeurs enregistrées de votre app sont renvoyées telles quelles, rien n’est perdu. Ne traitez pas un sélecteur vide comme « la personne l’a effacé ».
7. Ce que GameVox valide, et ce qu’il ne valide pas
GameVox vérifie la forme d’un enregistrement, et explicitement pas son sens. Il ne conserve pas le formulaire qu’il a affiché — ne rien stocker de votre configuration est justement le principe — si bien qu’au moment où un enregistrement arrive, plus rien de notre côté ne sait quelle clé était un sélecteur de salon et laquelle un champ libre.
| GameVox garantit | Vous devez tout de même vérifier |
|---|---|
Les clés respectent le jeu de caractères et ne sont pas __proto__, constructor ni prototype | Que la clé est bien une de celles que vous avez envoyées |
Les valeurs sont null, un booléen, un nombre, une chaîne ou un tableau de chaînes — jamais un objet imbriqué | Que le type correspond au champ que vous avez déclaré |
| Les chaînes sont bornées (2 000 caractères) et les tableaux contiennent au plus 25 entrées | Vos propres limites, plus strictes |
| Chaque id de sélecteur a été émis par GameVox depuis le catalogue de ce serveur | Que l’id existe toujours dans cette guild — les catalogues sont un instantané, et un salon peut être supprimé entre l’affichage et l’enregistrement |
| La personne est propriétaire du serveur ou détient « Gérer le serveur », et l’app est installée ici | Toute autorisation de votre côté (un palier d’abonnement, un compte lié) |
Ce n’est pas une faille introduite par le panneau — une personne peut déjà envoyer n’importe quoi en POST au tableau de bord d’une app. C’est le contrat, et c’est exactement pour cela que les types de sélecteur vous remettent des ids générés par GameVox plutôt que de laisser une app décider de ce que signifie un id.
8. Limites
Deux comportements ici, et la différence compte : les nombres et la structure font échouer toute la réponse, donc vous êtes prévenu ; les plafonds de texte tronquent en silence, donc personne ne voit une mise en page cassée.
| Limite | Valeur | En cas de dépassement |
|---|---|---|
| Sections par réponse | 12 | Rejeté |
| Champs par réponse (au total, pas par section) | 60 | Rejeté |
Options par select / multiselect | 1 à 25 | Rejeté |
| Entrées sélectionnées dans une valeur multiple | 25 | Rejeté |
| Clé de champ / de section | 64 caractères | Rejeté |
| Valeur d’option | 64 caractères | Rejeté |
| Corps de la réponse | 256 Ko | 413 |
| Libellé, texte indicatif | 100 caractères | Tronqué |
| Texte d’aide | 200 caractères | Tronqué |
| Description de section | 300 caractères | Tronqué |
message | 200 caractères | Tronqué |
error | 300 caractères | Tronqué |
Valeur d’un champ de la famille string | 1 000 caractères | Tronqué |
Valeur d’un champ text / static | 2 000 caractères | Tronqué |
| Salons ou rôles dans un catalogue | 500 | Coupé |
Un rejet est signalé à la personne sous la forme « Cette app a envoyé des réglages que GameVox n’a pas pu lire : <raison> », en nommant la limite atteinte et le champ concerné. Les clés en double, un type inconnu, un select sans options, et l’envoi simultané de sections et fields sont rejetés de la même façon.
Corrigé en silence plutôt que rejeté
- Une
valueactuelle qui ne correspond pas à son type devient vide. Une valeur périmée doit vider un champ, pas faire tomber le panneau. optionssur un type qui ne les utilise pas est ignoré.min/max/stepsur un champ non numérique sont ignorés.- Les entrées
channel_kindsinconnues sont ignorées. - Un
show_ifqui nomme une clé inconnue, lui-même, ou un champ à valeurs multiples est ignoré, et le champ s’affiche sans condition.
9. Délais et modes de défaillance
| Comportement | Valeur | Ce qui se passe |
|---|---|---|
| Fenêtre de réponse | 12 s | Le message affiché est « Cette app n’a pas répondu. Elle ne prend peut-être pas en charge les réglages intégrés. » |
| Durée de vie du nonce | 15 s | Une réponse tardive est écartée plutôt que livrée à personne. Répondre avec un nonce expiré renvoie 404. |
| Délai d’attente | 2 s par action, par app et par serveur | « Laissez un instant à l’app, puis réessayez. » describe et save ont des fenêtres distinctes : ouvrir le panneau ne bloque donc pas un enregistrement immédiat. |
| App hors ligne | — | Refusé d’emblée : « Cette app est hors ligne, elle ne peut pas être configurée pour le moment. » Aucune dépêche n’est tentée. |
| Mauvaise app qui répond | — | 403. Un nonce est lié à l’application pour laquelle il a été émis. |
| Réponse en double | — | La première réponse gagne ; la seconde n’a aucun effet. |
Chaque requête se traduit par une dépêche gateway vers votre app : le délai d’attente existe donc pour empêcher qu’on se serve de GameVox pour marteler le bot d’un tiers. Dimensionnez votre gestionnaire describe en conséquence : il s’exécute à l’ouverture de la page, une lecture en base est attendue, mais douze secondes sont le mur.
10. Exemple complet (discord.js)
Prêt à l’emploi, hormis votre propre couche de stockage. C’est la forme utilisée par l’implémentation de référence.
const SCHEMA_V2 = 2;
const POSTABLE = ["text", "news", "forum", "fileshare"];
client.on("raw", (packet) => {
if (!packet || packet.t !== "APP_SETTINGS_REQUEST") return;
void handleSettings(packet.d ?? {});
});
async function reply(nonce, guildId, body) {
await client.rest.post("/applications/@me/settings-response", {
body: { nonce, ...body },
query: new URLSearchParams({ server_id: guildId }),
});
}
async function handleSettings(req) {
const nonce = typeof req.nonce === "string" ? req.nonce : "";
const guildId = typeof req.server_id === "string" ? req.server_id : "";
if (!nonce || !guildId) return;
try {
if (req.action === "save") {
const message = await applySettings(guildId, req.values ?? {});
return await reply(nonce, guildId, { message });
}
const version = typeof req.schema_version === "number" ? req.schema_version : 1;
if (version >= SCHEMA_V2) {
return await reply(nonce, guildId, {
version: SCHEMA_V2,
sections: await describe(guildId),
});
}
return await reply(nonce, guildId, { fields: await describeLegacy(guildId) });
} catch (err) {
// Répondez même en cas d’échec — un abandon silencieux se lit comme une app cassée.
await reply(nonce, guildId, {
error: "Le bot n’a pas pu lire ses réglages pour ce serveur.",
}).catch(() => undefined);
}
}
async function describe(guildId) {
const cfg = await store.get(guildId);
return [
{
key: "general",
label: "Général",
description: "Comment le bot se comporte sur ce serveur.",
fields: [
{ key: "prefix", label: "Préfixe de commande", type: "string",
value: cfg.prefix, max_length: 4 },
{ key: "ignored", label: "Salons ignorés", type: "channels",
value: cfg.ignored, channel_kinds: POSTABLE,
help: "Les commandes ne reçoivent pas de réponse dans ces salons." },
],
},
{
key: "welcome",
label: "Messages de bienvenue",
description: "Publié quand quelqu’un arrive.",
fields: [
{ key: "welcome.enabled", label: "Envoyer un message de bienvenue",
type: "boolean", value: cfg.welcome.enabled === true },
{ key: "welcome.channel", label: "Salon", type: "channel",
value: cfg.welcome.channel ?? null, channel_kinds: POSTABLE,
show_if: { key: "welcome.enabled", equals: true } },
{ key: "welcome.message", label: "Message", type: "text",
value: cfg.welcome.message ?? "", max_length: 1800,
placeholder: "Bienvenue {user} sur {server} !",
show_if: { key: "welcome.enabled", equals: true } },
],
},
];
}
async function applySettings(guildId, v) {
const guild = client.guilds.cache.get(guildId);
if (!guild) throw new Error("not in guild");
const channel = (id) =>
typeof id === "string" ? (guild.channels.cache.has(id) ? id : null) : null;
await store.set(guildId, {
prefix: String(v.prefix ?? "!").slice(0, 4) || "!",
ignored: Array.isArray(v.ignored) ? v.ignored.filter(channel).slice(0, 25) : [],
welcome: {
enabled: v["welcome.enabled"] === true,
channel: channel(v["welcome.channel"]),
message: String(v["welcome.message"] ?? "").slice(0, 1800),
},
});
return "Réglages enregistrés.";
} 11. Liste de vérification avant publication
- Chaque clé de champ est stable. En renommer une orpheline ce qui a déjà été configuré.
- Chaque id issu d’un sélecteur est revérifié sur la guild avant écriture.
- Un
secretvide est traité comme « inchangé », jamais comme « effacé ». - Le chemin
describerépond bien en dessous de 12 secondes, lecture en base comprise. - Chaque chemin d’échec répond quand même — avec
error, pas par le silence. requiredetmin/maxsont réappliqués à l’enregistrement.- Un sélecteur vide signifie que le catalogue était indisponible, pas que le champ a été effacé.
- Le gestionnaire ne fait rien sur Discord : une seule build sert les deux plateformes.
Qui peut ouvrir le panneau
La personne propriétaire du serveur, ou un membre disposant de Gérer le serveur — la même barrière que pour l’installation et la désinstallation des apps. Sur un serveur auto-hébergé, la vérification se fait auprès de l’instance du client, de sorte qu’une administration locale ne soit pas refusée faute de ligne de permission dans le cloud.
GameVox refuse la requête d’emblée si votre app n’est pas installée sur ce serveur : une demande de panneau ne peut donc atteindre qu’une app déjà autorisée.