Portail développeurs
Expérimental La plateforme de bots et d'apps est en développement actif. La prise en charge des serveurs auto-hébergés est arrivée le 13/08/2026, avec moins de fonctionnalités que le cloud. Voir ce qui est pris en charge.
← Docs

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.

La fenêtre Réglages du serveur du client GameVox, montrant l’onglet Réglages d’une app installée avec une liste déroulante de langue, un interrupteur et une liste de salons à cocher.
Un vrai panneau, entièrement rendu à partir de la réponse d’un seul bot : un 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.

DiscordGameVox
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
  }
}
ChampTypeRemarques
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èqueHookActivation
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éTypeRemarques
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 envoyezCe 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

AttributS’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.

Un champ channels rendu sous forme de liste à cocher des salons du serveur, chacun précédé du glyphe de son type.
Tout ce que l’app a envoyé pour ce champ était { "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] }. La liste, les glyphes de type et les ids viennent de GameVox.
SituationCe 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 garantitVous 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.

LimiteValeurEn 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 value actuelle qui ne correspond pas à son type devient vide. Une valeur périmée doit vider un champ, pas faire tomber le panneau.
  • options sur un type qui ne les utilise pas est ignoré.
  • min / max / step sur un champ non numérique sont ignorés.
  • Les entrées channel_kinds inconnues sont ignorées.
  • Un show_if qui 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

ComportementValeurCe 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 secret vide est traité comme « inchangé », jamais comme « effacé ».
  • Le chemin describe ré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.
  • required et min/max sont 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.

← Migrer depuis Discord  ·  Retour à la documentation