Portal do Desenvolvedor
Experimental A plataforma de bots e apps está em desenvolvimento ativo. O suporte a servidores auto-hospedados chegou em 13/08/2026 com menos recursos que a nuvem. Veja o que é suportado.
← Documentação

Configurações no app

O GameVox pode exibir o formulário de configuração do seu app dentro do cliente. Seu app descreve o formulário pelo gateway, alguém o edita em Configurações do servidor ▸ Integrações ▸ Configurações, e os valores editados voltam direto para o seu bot. O GameVox não guarda nada — seu app segue sendo o único dono da configuração dele.

Isso não tem equivalente no Discord. É a única parte da plataforma de apps do GameVox que propositalmente não é compatível, porque não há nada do outro lado com que ser compatível.

A janela de Configurações do servidor do cliente GameVox, mostrando a aba Configurações de um app instalado com uma lista suspensa de idioma, uma chave e uma lista de canais para marcar.
Um painel real, renderizado inteiramente a partir da resposta de um único bot: um select, um boolean e um seletor channels, agrupados em uma seção com descrição. O bot não enviou nenhum id de canal — a lista veio do GameVox. Nomes de servidor e de canais ocultados.

Por que isso existe

No Discord, um bot com qualquer configuração traz um dashboard web: um domínio, um login OAuth2, gerenciamento de sessões, uma verificação de permissão que relê a lista de guilds da pessoa, proteção CSRF e hospedagem para tudo isso — normalmente só para alguém escolher um canal de log e ligar três recursos. O GameVox já sabe quem é essa pessoa, qual servidor ela está configurando e se ela pode, então oferece essa superfície direto ao seu app.

DiscordGameVox
Onde seu app é configurado Um dashboard web hospedado por você Configurações do servidor ▸ Integrações ▸ Configurações
O que você precisa construir App web, login OAuth2, sessões, verificações de permissão, hospedagem Um handler de gateway e uma chamada REST
Quem autentica a pessoa Você, via OAuth2 O GameVox, antes mesmo de acionar o seu app
Quem guarda a configuração Você Você. O GameVox não retém nada.
Seletores de canal e de cargo Você busca os canais e cargos da guild e os exibe Você indica um tipo; o GameVox fornece as opções
Custo de expor uma única chave Um dashboard Cerca de vinte linhas

Isso não substitui nada. Os comandos de barra continuam funcionando, e se você já tem um dashboard pode mantê-lo — os dois não se excluem, e muitos apps vão querer o painel para as cinco configurações que realmente mudam e o dashboard para o resto.

Como funciona

Alguém abre a aba Configurações
      │
      ▼
GameVox ──── APP_SETTINGS_REQUEST (action: "describe") ────▶ seu bot
                                                                │
seu bot  ─── POST /applications/@me/settings-response ──────────┘
             { nonce, version: 2, sections: [ ... ] }
      │
      ▼
O GameVox exibe o formulário, a pessoa edita e clica em Salvar
      │
      ▼
GameVox ──── APP_SETTINGS_REQUEST (action: "save", values) ─▶ seu bot
                                                                │
seu bot  ─── POST /applications/@me/settings-response ──────────┘
             { nonce, message: "Salvo." }

Duas ações, um formato de requisição, um endpoint de resposta. describe pergunta o que o seu app expõe para este servidor; save devolve o que foi alterado. Ambas são respondidas do mesmo jeito.

1. Recebendo a requisição

O GameVox envia APP_SETTINGS_REQUEST pelo gateway para a sessão que mantém a conexão do seu app. Nenhum intent controla isso — o despacho é endereçado especificamente ao seu app, não passa por uma máscara de intents, então chega independentemente de como você se identificou.

{
  "op": 0,
  "t": "APP_SETTINGS_REQUEST",
  "d": {
    "nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
    "server_id": "1387452901234567890",
    "action": "describe",
    "schema_version": 2
  }
}
CampoTipoObservações
nonce string Opaco. Devolva-o exatamente como veio; é a única coisa que liga a sua resposta à pessoa que está esperando. Pode ser respondido por 15 segundos.
server_id snowflake A guild, no mesmo espaço de ids de qualquer outro despacho. É o id que o seu GUILD_CREATE trouxe, então seu cache de guilds já resolve.
action string "describe" ou "save".
schema_version integer O esquema mais rico que este servidor consegue exibir. Atualmente 2. Veja Atendendo um servidor mais antigo.
values object Presente só no save. As chaves são as chaves de campo que você enviou.

O server_id é carimbado pelo GameVox a partir da requisição autenticada da pessoa. Ele nunca é relido da sua resposta, então um app não consegue responder uma pergunta sobre um servidor com as configurações de outro.

Lendo o despacho na sua biblioteca

Nenhuma biblioteca de bots traz um handler para um evento que não existe no Discord, então o pacote é descartado antes de chegar a um handler de eventos. Todas expõem o fluxo bruto de despachos justamente para este caso:

BibliotecaHookComo ativar
discord.js (v14) client.on('raw', packet) Ativo por padrão. Emitido para cada despacho antes do pacote ser tratado.
discord.py (v2) on_socket_raw_receive(msg) Passe enable_debug_events=True ao cliente.
Eris client.on('rawWS', packet) Ativo por padrão.
JDA RawGatewayEvent JDABuilder.setRawEventsEnabled(true)
DSharpPlus DiscordClient.UnknownEvent Ativo por padrão. EventName mais o Json bruto.
serenity RawEventHandler::raw_event ClientBuilder::raw_event_handler

Contra o Discord real esse handler nunca dispara, então a mesma build roda nas duas plataformas sem ramificação.

2. Respondendo

Responda por REST, não pelo gateway. Toda biblioteca expõe um cliente HTTP; a maioria não deixa o código da aplicação escrever opcodes arbitrários no socket, e é por isso que a resposta tem esse formato.

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": "Nota opcional exibida acima do formulário.",
  "sections": [
    {
      "key": "welcome",
      "label": "Mensagens de boas-vindas",
      "description": "Publicada quando alguém entra.",
      "fields": [
        { "key": "welcome.enabled", "label": "Enviar uma mensagem de boas-vindas",
          "type": "boolean", "value": true }
      ]
    }
  ]
}

Uma resposta bem-sucedida devolve 204 No Content. Você pode acrescentar ?server_id= com o id da guild; isso só serve para atribuir a chamada no registro de atividades do seu app.

ChaveTipoObservações
nonce string Obrigatória. Vem da requisição.
version integer O esquema em que você está respondendo. Envie 2 quando enviar sections.
sections array Formulário do esquema v2. Exclui fields.
fields array Formulário plano do esquema v1. Exibido como uma única seção sem título.
message string No describe, uma nota acima do formulário. No save, o aviso de confirmação. Cortado em 200 caracteres.
error string Exibido no lugar do formulário. Cortado em 300 caracteres.

Sempre responda, inclusive em caso de falha. Descartar a requisição em silêncio deixa a pessoa olhando um indicador de carregamento até o tempo limite de 12 segundos do GameVox, o que soa como “este app está quebrado” em vez de “algo falhou uma vez”.

3. Tipos de campo

Doze tipos, e a lista é fechada — um type desconhecido faz a resposta inteira falhar em vez de não exibir nada.

Tipo Controle O value que você enviaO que você recebe ao salvar
string Campo de texto de uma linha string string ("" quando vazio)
text Área de texto string string
boolean Chave liga/desliga boolean true / false
number Campo numérico number number, ou null se esvaziado
select Lista suspensa das suas options valor de opção string, ou null se não definido
multiselect Lista de marcação das suas options array de valores de opção array de strings (pode ser vazio)
channel Lista suspensa dos canais deste servidor snowflake de canal string snowflake, ou null
channels Lista de marcação dos canais deste servidor array de snowflakes array de strings snowflake
role Lista suspensa dos grupos deste servidor snowflake de cargo string snowflake, ou null
roles Lista de marcação dos grupos deste servidor array de snowflakes array de strings snowflake
color Amostra de cor #rrggbb #rrggbb, sempre presente
static Linha de texto somente leitura string Nunca enviado — a chave não aparece em values

Um campo de cor sempre tem valor, então um app que não guardava nenhum recebe #000000 no primeiro salvamento. Se você precisa de “sem cor”, combine a amostra com um boolean.

4. Atributos de campo

AtributoAplica-se aEfeito
key todos Obrigatório. [A-Za-z0-9][A-Za-z0-9._:-]{0,63}. Único em toda a resposta, não apenas por seção.
label todos Na falta, usa-se a chave. Cortado em 100 caracteres.
help todos Linha de dica abaixo do controle. Cortada em 200.
placeholder campos de texto, number Cortado em 100.
options select, multiselect Obrigatório para esses dois. { value, label, description }, no máximo 25. Descartado em qualquer outro tipo.
min , max , step number Molda o controle. Apenas orientativo — reconfira o valor recebido.
max_length string, text Limita a entrada, até o teto da plataforma para aquele tipo.
channel_kinds channel, channels Restringe o seletor. text, voice, forum, news, fileshare, header. Tipos desconhecidos são descartados; resultado vazio significa todos os tipos.
required todos Marca o rótulo e troca a opção “Nenhum” do seletor por “Selecionar…”. É apresentação — imponha de verdade ao salvar.
disabled todos Esmaece o controle. Ainda assim é enviado, com o valor atual.
secret string, text Mascara a entrada e esvazia o valor atual na saída. Em qualquer outro tipo, a resposta é recusada.
show_if todos { key, equals }. Esconde o campo até que outro campo da mesma resposta tenha esse valor.

show_if é um teste de igualdade e nada mais — sem expressões, sem operadores. A chave referenciada precisa pertencer a um campo de valor único da mesma resposta e não pode ser a própria chave do campo; uma condição que falhe em qualquer um desses pontos é descartada e o campo aparece sem condição. equals pode ser string, booleano, número ou null.

Um campo escondido ainda assim é enviado. Ele carrega um valor que o seu app mandou, e omiti-lo pareceria, do lado do seu app, que a pessoa apagou aquilo.

5. Seletores de canal e de cargo

Esta é a parte que vale entender, porque inverte o arranjo usual. Seu app indica um tipo; o GameVox fornece as opções.

Um campo de canal não carrega ids. Você envia { "type": "channel" } e, se quiser, um filtro channel_kinds. O GameVox anexa a lista de canais deste servidor, o cliente monta um seletor a partir dela, e só é possível enviar uma opção que o GameVox emitiu.

Cada entrada do catálogo é indexada pela mesma snowflake que o seu app já enxerga para aquele canal ou grupo. Não existe uma etapa de tradução de id para errar: a pessoa escolhe um rótulo, seu app recebe um id no próprio espaço de nomes, e um valor que aponte para algo fora deste servidor simplesmente não corresponde a nenhuma opção.

Um campo channels exibido como uma lista de marcação com os canais do próprio servidor, cada um precedido pelo símbolo do seu tipo.
Tudo o que o app enviou para este campo foi { "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] }. A lista, os símbolos de tipo e os ids são do GameVox.
SituaçãoO que a pessoa vê
Ainda não há nada salvo Uma opção “Nenhum”, selecionada. Um campo required mostra “Selecionar…” no lugar, então salvar não consegue gravar um valor que ninguém escolheu.
O id salvo não existe mais (canal excluído) Mantido como “Seleção atual (não está mais na lista)”, ainda selecionado. Salvar não apaga isso em silêncio.
Ids salvos num seletor múltiplo que não constam mais São exibidos e marcados ao lado dos atuais, pelo mesmo motivo.
Catálogo indisponível (instância auto-hospedada offline) Seletor desativado com o texto “Indisponível no momento”. O valor salvo é devolvido como está ao salvar, então abrir o painel durante uma indisponibilidade não apaga nenhuma configuração.
O servidor tem mais de 500 canais ou grupos A lista é cortada em 500 em vez de enviar um megabyte de opções.

O catálogo de cargos exclui duas coisas: o grupo de propriedade do servidor, que nenhum app deveria receber como cargo atribuível, e os grupos por app que carregam as permissões dos apps instalados.

6. Cenários

Cenário 1 — O menor painel útil

Uma chave, sem seções. Um array fields plano é válido em qualquer versão do esquema e aparece como um único grupo sem título.

{
  "nonce": nonce,
  "fields": [
    { "key": "greetings", "label": "Dar boas-vindas a novos integrantes", "type": "boolean", "value": true }
  ]
}

Cenário 2 — Agrupando em seções

{
  "nonce": nonce,
  "version": 2,
  "sections": [
    {
      "key": "general",
      "label": "Geral",
      "description": "Como o bot se comporta neste servidor.",
      "fields": [
        { "key": "prefix", "label": "Prefixo de comando", "type": "string",
          "value": "!", "max_length": 4, "help": "Usado para comandos de texto antigos." }
      ]
    },
    {
      "key": "logging",
      "label": "Registro de auditoria",
      "fields": [
        { "key": "log.enabled", "label": "Registrar ações de moderação", "type": "boolean", "value": false }
      ]
    }
  ]
}

A key de uma seção é opcional, mas precisa ser única quando existe. label e description são opcionais; uma seção sem campos não é exibida.

Cenário 3 — Pedir um canal

Sem ids e sem buscar a lista de canais da guild. Restrinja o seletor aos tipos que podem receber uma mensagem.

{
  "key": "log.channel",
  "label": "Canal de log",
  "type": "channel",
  "value": stored.logChannelId ?? null,
  "channel_kinds": ["text", "news"],
  "help": "Onde as ações de moderação são registradas."
}

Use "channel_kinds": ["header"] quando quiser uma categoria em vez de um lugar para publicar — cabeçalhos são as categorias do GameVox.

Cenário 4 — Mostrar campos condicionalmente

O formato usual: um booleano que libera o resto do recurso.

"fields": [
  { "key": "welcome.enabled", "label": "Enviar uma mensagem de boas-vindas",
    "type": "boolean", "value": true },

  { "key": "welcome.channel", "label": "Canal", "type": "channel",
    "value": stored.welcomeChannel ?? null,
    "channel_kinds": ["text", "news"],
    "show_if": { "key": "welcome.enabled", "equals": true } },

  { "key": "welcome.message", "label": "Mensagem", "type": "text",
    "value": stored.welcomeMessage ?? "",
    "max_length": 1800,
    "placeholder": "Boas-vindas, {user}, ao {server}!",
    "help": "{user}, {server} e {membercount} são substituídos no envio.",
    "show_if": { "key": "welcome.enabled", "equals": true } }
]

Condições também podem se basear em um select, e é assim que se monta um seletor de modo: "show_if": { "key": "mode", "equals": "advanced" }.

Cenário 5 — Suas próprias opções

select e multiselect são os dois tipos que trazem opções próprias. Ambos exigem pelo menos uma opção e permitem no máximo 25.

{
  "key": "automod.action",
  "label": "Quando um filtro é acionado",
  "type": "select",
  "value": stored.action,
  "required": true,
  "options": [
    { "value": "delete",  "label": "Excluir a mensagem" },
    { "value": "warn",    "label": "Advertir quem escreveu",  "description": "Exclui a mensagem e manda uma DM para a pessoa." },
    { "value": "timeout", "label": "Aplicar um tempo fora", "description": "10 minutos." },
    { "value": "none",    "label": "Não fazer nada" }
  ]
}
{
  "key": "automod.filters",
  "label": "Filtros ativos",
  "type": "multiselect",
  "value": stored.filters,
  "options": [
    { "value": "invites",   "label": "Convites do Discord/GameVox" },
    { "value": "links",     "label": "Links" },
    { "value": "mentions",  "label": "Menções em massa" },
    { "value": "caps",      "label": "Excesso de maiúsculas" }
  ]
}

A description de uma opção aparece depois do rótulo em uma lista suspensa e como dica em uma lista de marcação.

Cenário 6 — Cargos

A mesma regra de catálogo dos canais. O GameVox lista os grupos deste servidor por rank, com as cores deles.

"fields": [
  { "key": "autorole.role", "label": "Cargo dado na entrada",
    "type": "role", "value": stored.autoRole ?? null },

  { "key": "moderator.roles", "label": "Cargos que podem usar comandos de moderação",
    "type": "roles", "value": stored.modRoles,
    "help": "Quem tiver qualquer um destes cargos pode executar /ban e /timeout." }
]

Cenário 7 — Números com limites

{
  "key": "automod.threshold",
  "label": "Mensagens antes de declarar um raid",
  "type": "number",
  "value": stored.threshold ?? 10,
  "min": 3,
  "max": 100,
  "step": 1,
  "help": "Contadas em uma janela móvel de 60 segundos."
}

min, max e step só moldam o controle. O navegador da pessoa não é um validador sob o seu controle, então limite o valor de novo quando ele voltar — e note que esvaziar o campo envia null, não 0.

Cenário 8 — Segredos

Marque uma credencial como secret e o GameVox esvazia o valor atual na saída. Ele não está no frame do WebSocket, nem no DOM, nem em uma captura de tela do painel — a pessoa vê um campo mascarado vazio com o texto “Sem alteração”.

{
  "key": "integrations.apiKey",
  "label": "Chave da API de clima",
  "type": "string",
  "secret": true,
  "help": "Deixe em branco para manter a chave atual."
}

Daí decorre o contrato: um segredo enviado vazio significa “deixe como está”, nunca “apague isto”. Se for preciso remover uma credencial, ofereça um booleano explícito para isso. secret em qualquer coisa que não seja string ou text faz a resposta falhar.

Cenário 9 — Estado somente leitura

static exibe uma linha de texto e nunca é enviado. Use para informações necessárias durante a configuração, mas que não podem ser editadas aqui.

"fields": [
  { "key": "plan", "label": "Plano", "type": "static", "value": "Pro — 4.000 consultas/dia" },
  { "key": "usage", "label": "Usadas hoje", "type": "static", "value": "1.284 consultas" },
  { "key": "lastSync", "label": "Última sincronização", "type": "static", "value": "2026-09-02 14:31 UTC" }
]

Cenário 10 — Tratando um salvamento

A requisição de salvamento traz values, indexado pelas chaves de campo que você enviou. Persista e depois responda com uma confirmação.

// action === "save"
const v = req.values ?? {};

const guild = client.guilds.cache.get(req.server_id);
if (!guild) return reply(nonce, { error: "Este bot não está mais nesse servidor." });

const patch = {};

// boolean: sempre um booleano de verdade
patch.welcomeEnabled = v["welcome.enabled"] === true;

// channel: uma string snowflake ou null — reconfira contra ESTA guild
const chan = v["welcome.channel"];
patch.welcomeChannel =
  (typeof chan === "string" && guild.channels.cache.has(chan)) ? chan : null;

// number: número ou null quando o campo foi esvaziado
const n = v["automod.threshold"];
patch.threshold = typeof n === "number" ? Math.min(100, Math.max(3, Math.round(n))) : 10;

// secret: vazio significa sem alteração
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: "Configurações salvas." });

Sua message vira o aviso de confirmação. O formulário fica exatamente como estava, então dá para continuar editando.

Cenário 11 — Recusando um salvamento

Devolva error e o seu texto aparece no lugar da confirmação. Use para tudo que você não pode aceitar: um valor que não passa na sua validação, um limite de plano, uma credencial externa que deixou de funcionar.

// `required` é apresentação — imponha de verdade aqui.
const action = v["automod.action"];
if (!["delete", "warn", "timeout", "none"].includes(action)) {
  return reply(nonce, guildId, {
    error: "Escolha o que o AutoMod deve fazer quando um filtro for acionado.",
  });
}

// Qualquer coisa que só o seu lado sabe.
if (patch.apiKey && !(await upstream.verifyKey(patch.apiKey))) {
  return reply(nonce, guildId, {
    error: "Essa chave de API foi recusada pelo provedor de clima.",
  });
}

O mesmo vale para o describe: se o seu banco estiver fora do ar, responda com um error em vez de não responder. “Este app não conseguiu ler as configurações agora” é um resultado bem melhor do que doze segundos de carregamento.

Cenário 12 — Atendendo um servidor mais antigo

A requisição traz schema_version, então você nunca precisa adivinhar. Entregue o formulário mais rico que o servidor realmente vai exibir.

const version = typeof req.schema_version === "number" ? req.schema_version : 1;

if (version >= 2) {
  await reply(nonce, { version: 2, sections: describeV2(guildId) });
} else {
  // v1: lista plana, quatro tipos — string, boolean, number, select
  await reply(nonce, { fields: describeV1(guildId) });
}

O inverso também vale. Uma resposta v2 que chega a um cliente mais antigo ainda é exibida: o GameVox envia um array fields achatado junto com sections, e um cliente anterior aos tipos novos os desenha como campos de texto. Degradado, mas nada fica escondido.

Cenário 13 — Servidores auto-hospedados

Funciona sem mudanças. Em um servidor auto-hospedado, os catálogos de canais e cargos vêm da instância do cliente em vez das tabelas na nuvem, o que é invisível para o seu app — mesmos tipos de campo, mesmas snowflakes, mesma resposta.

A única diferença observável: se a instância estiver inacessível, os seletores chegam vazios e aparecem como “Indisponível no momento”. Os valores salvos do seu app são devolvidos como estão ao salvar, então nada se perde. Não trate um seletor vazio como “a pessoa apagou isso”.

7. O que o GameVox valida e o que não valida

O GameVox confere o formato de um salvamento e explicitamente não o significado dele. Ele não guarda o formulário que exibiu — não armazenar nada da sua configuração é justamente o ponto — então, quando um salvamento chega, nada do nosso lado sabe qual chave era um seletor de canal e qual era um campo de texto livre.

O GameVox garanteVocê ainda precisa conferir
As chaves respeitam o conjunto de caracteres e não são __proto__, constructor nem prototype Que a chave é uma que você realmente enviou
Os valores são null, booleano, número, string ou um array de strings — nunca um objeto aninhado Que o tipo corresponde ao campo que você declarou
As strings têm limite (2.000 caracteres) e os arrays comportam no máximo 25 entradas Os seus próprios limites, mais rígidos
Todo id de seletor foi emitido pelo GameVox a partir do catálogo deste servidor Que o id ainda existe nesta guild — catálogos são um instantâneo, e um canal pode ser excluído entre exibir e salvar
A pessoa é dona do servidor ou tem “Gerenciar servidor”, e o app está instalado aqui Qualquer autorização sua (um nível de plano, uma conta vinculada)

Isso não é uma brecha que o painel cria — a pessoa já pode enviar qualquer coisa por POST ao dashboard de um app. É o contrato, e é exatamente por isso que os tipos de seletor entregam ids gerados pelo GameVox em vez de deixar um app decidir o que um id significa.

8. Limites

Há dois comportamentos aqui, e a diferença importa: contagens e estrutura fazem a resposta inteira falhar, então você fica sabendo; os tetos de texto cortam em silêncio, então ninguém vê um layout quebrado.

LimiteValorSe estourar
Seções por resposta 12 Recusado
Campos por resposta (no total, não por seção) 60 Recusado
Opções por select / multiselect 1 a 25 Recusado
Entradas selecionadas em um valor múltiplo 25 Recusado
Chave de campo / de seção 64 caracteres Recusado
Valor de opção 64 caracteres Recusado
Corpo da resposta 256 KB 413
Rótulo, texto de exemplo 100 caracteres Cortado
Texto de ajuda 200 caracteres Cortado
Descrição de seção 300 caracteres Cortado
message 200 caracteres Cortado
error 300 caracteres Cortado
Valor de um campo da família string 1.000 caracteres Cortado
Valor de um campo text / static 2.000 caracteres Cortado
Canais ou cargos em um catálogo 500 Truncado

Uma recusa é comunicada como “Este app enviou configurações que o GameVox não conseguiu ler: <motivo>”, nomeando o limite atingido e o campo que o atingiu. Chaves duplicadas, um tipo desconhecido, um select sem opções e o envio simultâneo de sections e fields são recusados do mesmo jeito.

Corrigido em silêncio em vez de recusado

  • Um value atual que não combina com o tipo dele fica vazio. Um valor desatualizado deve esvaziar um campo, não derrubar o painel.
  • options em um tipo que não as usa é descartado.
  • min / max / step em um campo não numérico são descartados.
  • Entradas desconhecidas de channel_kinds são descartadas.
  • Um show_if que aponte para uma chave desconhecida, para si mesmo ou para um campo de múltiplos valores é descartado, e o campo aparece sem condição.

9. Tempos e modos de falha

ComportamentoValorO que acontece
Janela de resposta 12 s Aparece a mensagem “Este app não respondeu. Talvez ele não suporte configurações no app.”
Vida do nonce 15 s Uma resposta atrasada é descartada em vez de ser entregue a ninguém. Responder com um nonce expirado devolve 404.
Intervalo mínimo 2 s por ação, por app, por servidor “Dê um momento ao app e tente de novo.” describe e save têm janelas separadas, então abrir o painel não bloqueia um salvamento imediato.
App offline Recusado de saída: “Este app está offline, então não pode ser configurado agora.” Nenhum despacho é tentado.
App errado respondendo 403. Um nonce é vinculado à aplicação para a qual foi emitido.
Resposta duplicada A primeira resposta vence; a segunda não faz nada.

Cada requisição vira um despacho de gateway no seu app, então o intervalo mínimo existe para impedir que alguém use o GameVox para martelar o bot de terceiros. Dimensione seu handler de describe com isso em mente: ele roda ao abrir a página, uma leitura no banco é esperada, mas doze segundos é o teto.

10. Exemplo completo (discord.js)

Pronto para usar, tirando a sua própria camada de armazenamento. É o mesmo formato usado pela implementação de referência.

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) {
    // Responda mesmo em caso de falha — descartar em silêncio soa como um app quebrado.
    await reply(nonce, guildId, {
      error: "O bot não conseguiu ler as configurações dele para este servidor.",
    }).catch(() => undefined);
  }
}

async function describe(guildId) {
  const cfg = await store.get(guildId);
  return [
    {
      key: "general",
      label: "Geral",
      description: "Como o bot se comporta neste servidor.",
      fields: [
        { key: "prefix", label: "Prefixo de comando", type: "string",
          value: cfg.prefix, max_length: 4 },
        { key: "ignored", label: "Canais ignorados", type: "channels",
          value: cfg.ignored, channel_kinds: POSTABLE,
          help: "Comandos não são respondidos nestes canais." },
      ],
    },
    {
      key: "welcome",
      label: "Mensagens de boas-vindas",
      description: "Publicada quando alguém entra.",
      fields: [
        { key: "welcome.enabled", label: "Enviar uma mensagem de boas-vindas",
          type: "boolean", value: cfg.welcome.enabled === true },
        { key: "welcome.channel", label: "Canal", type: "channel",
          value: cfg.welcome.channel ?? null, channel_kinds: POSTABLE,
          show_if: { key: "welcome.enabled", equals: true } },
        { key: "welcome.message", label: "Mensagem", type: "text",
          value: cfg.welcome.message ?? "", max_length: 1800,
          placeholder: "Boas-vindas, {user}, ao {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 "Configurações salvas.";
}

11. Lista de verificação antes de publicar

  • Cada chave de campo é estável. Renomear uma deixa órfão o que já foi configurado.
  • Todo id vindo de um seletor é reconferido contra a guild antes de ser gravado.
  • Um secret vazio é tratado como “sem alteração”, nunca como “apagado”.
  • O caminho de describe responde bem abaixo de 12 segundos, incluindo a leitura no banco.
  • Todo caminho de falha ainda responde — com error, não com silêncio.
  • required e min/max são reaplicados ao salvar.
  • Um seletor vazio significa que o catálogo estava indisponível, não que o campo foi apagado.
  • O handler não faz nada no Discord, então uma única build serve para as duas plataformas.

Quem pode abrir o painel

Quem é dono do servidor, ou alguém com Gerenciar servidor — a mesma barreira que rege instalar e desinstalar apps. Em um servidor auto-hospedado a verificação é feita contra a instância do cliente, então a administração de lá não é recusada por falta de uma linha de permissão na nuvem.

O GameVox recusa a requisição de imediato se o seu app não estiver instalado naquele servidor, então um pedido de painel só chega a um app que já foi autorizado.

← Migrando do Discord  ·  Voltar para a documentação