Canais de app
Um canal de app é um canal na lista de canais de um servidor cujo conteúdo é um conjunto de telas que o seu app fornece enquanto está online: um planejador de raids, um ranking, uma central de tickets, uma página de status do servidor. O GameVox guarda onde o canal fica, quem pode vê-lo e o que cada um pode fazer. O seu app é dono do conteúdo e renderiza cada tela sob demanda.
Visão geral
Um app é um lugar só com muitas telas. Cada tela é uma rota, uma string definida pelo seu app (home, raids, raids/42, roster?sort=ilvl). Quem abre o canal cai na rota inicial dele (normalmente o seu home) e navega pelo seu menu de navegação, pelos links e botões dentro das telas, e com Voltar e Avançar.
O que os membros veem
- O canal na lista de canais com o seu ícone (ou um escolhido pelo servidor), e um pontinho enquanto o seu app está offline.
- O seu menu de navegação, Voltar e Avançar, um botão Início e Atualizar, em volta da tela que você devolveu.
- Telas desenhadas nativamente por cada cliente a partir do seu JSON, no tema do próprio cliente, claro e escuro.
- Enquanto o seu bot está offline: o último instantâneo público que você enviou, marcado como a última atualização, ou um aviso de offline se não houver nenhum.
Clientes: o app de desktop (Windows, macOS, Linux), o app web, iOS e Android. O app de Xbox não mostra canais de app. Um cliente antigo demais para conhecê-los nunca recebe canais de app na lista de canais, então ninguém vê um canal de texto vazio.
Como isso difere dos comandos de barra e dos componentes
- Comandos de barra e componentes de mensagem vivem em mensagens de um canal de texto e chegam ao seu app como
INTERACTION_CREATEcom um token de interação. Um canal de app não tem mensagem nenhuma: publicar nele é recusado. - Uma requisição de visualização chega como um evento de gateway próprio,
APP_VIEW_REQUEST, e você responde com uma única chamada REST que leva um nonce de uso único. Não há token de interação, nem adiamento, nem follow-ups. Endpoints de interação HTTP não são usados, então o seu app precisa de uma conexão de gateway ativa. - Cada tela é pessoal. O GameVox a pede toda vez que um membro a abre ou usa, e nunca a guarda (só o instantâneo público opcional é guardado).
- Dentro da tela, o seu app define as próprias permissões, chamadas de habilidades. Os admins do servidor as concedem por grupo, e o GameVox informa em cada requisição quais delas quem está vendo possui.
- Esta é uma extensão do GameVox sem equivalente no Discord. Bots escritos com bibliotecas do Discord continuam funcionando; eles só nunca recebem o evento.
Como tudo se encaixa
Um membro abre o canal, navega ou aperta um botão
│
▼
GameVox ──── APP_VIEW_REQUEST { nonce, kind, route, user, abilities } ────▶ seu bot
▲
│
seu bot ──── POST /applications/@me/app-view-response { nonce, view } ────▶ GameVox
│
▼
O GameVox confere e limpa a visualização, e o cliente a desenha A cada abertura e a cada ação, o GameVox confere se quem está vendo tem permissão para ver o canal, calcula as habilidades dessa pessoa e só então pergunta ao seu app. A sua resposta vai só para essa pessoa.
Ciclo de vida
- Um servidor instala o seu app do jeito de sempre (o link de instalação ou a página de autorização OAuth2).
- A cada inicialização, o seu bot publica o manifesto dele. Um app sem manifesto não pode ter canais de app.
- Um admin do servidor escolhe Criar canal ▸ App. A lista mostra os apps instalados que publicaram um manifesto, com nome, descrição e ícone. O nome do canal vem preenchido a partir do seu app, e um menu opcional Abre em lista os seus itens de navegação (padrão: home). Criar canais exige a permissão de sempre para criar canais. O seu bot também pode criar um (veja abaixo).
- Nas Configurações do canal, o admin pode renomear o canal, trocar o ícone (o do seu app, um emoji ou uma imagem enviada), mudar onde ele abre e definir permissões: quem pode ver o canal e quais grupos têm cada uma das suas habilidades.
- Os membros que podem ver o canal o abrem, e o seu app responde a
APP_VIEW_REQUESTpara cada tela que eles olham. - Se o seu app for removido do servidor, os canais de app dele continuam lá (reinstalar os traz de volta) e mostram “Este app foi removido do servidor”, com um botão Excluir para quem gerencia. Se a aplicação for excluída, ou nunca tiver publicado um manifesto, o canal mostra “Este app não está mais disponível”.
Canais de app contam para o limite de canais de um servidor como qualquer outro canal. Um servidor normalmente tem um canal por app; um segundo canal para o mesmo app é permitido (por exemplo, um canal de equipe de raid que abre no planejador de raids) e é o mesmo app com a mesma navegação. Toda requisição traz o channel_id, então você pode manter dados separados por canal.
Criando um canal de app pelo seu bot
Use a rota normal de criação de canal com type 100 e um objeto gamevox_app opcional:
POST /api/v10/guilds/1491230000000000001/channels
Authorization: Bot <token>
Content-Type: application/json
{
"type": 100,
"name": "raid-planner",
"position": 7,
"gamevox_app": { "start_route": "raids", "icon": { "builtin": "calendar" } }
}
HTTP/1.1 201 Created
{
"id": "1491240000000000002",
"type": 100,
"gamevox_type": "app",
"name": "raid-planner",
"guild_id": "1491230000000000001",
"position": 7,
"gamevox_app": { "application_id": "1491200000000000000", "start_route": "raids" }
} - Só para a sua própria aplicação, que precisa estar instalada naquele servidor e ter publicado um manifesto. O bot precisa da permissão de criar canais, como para qualquer canal.
gamevox_app.start_routeé uma rota (vazia ou ausente: ohomedo seu manifesto).gamevox_app.iconé umemoji, um símbolobuiltinou umaurlque o seu manifesto já contém. Bots não podem enviar uma imagem de canal; só admins podem.- Sem
position, o canal vai para o fim da lista. O nome tem de 1 a 100 caracteres. - Quem pode ver o canal e quem tem cada habilidade é definido pelos admins do servidor nas Configurações do canal.
Um canal de app é um objeto de canal normal com extensões do GameVox, nas respostas REST e nos eventos de canal do gateway:
typeé 100, de propósito fora da faixa do Discord. Uma biblioteca que não conhece o número pode tratá-lo como um canal comum;gamevox_type: "app"é o jeito confiável de reconhecer um.GET /channels/{id},GET /guilds/{id}/channels,PATCHeDELETE /channels/{id}funcionam como para os outros canais.PATCHtambém aceitagamevox_app: {"start_route", "icon"}, só em um canal da sua própria aplicação, e exige a permissão de editar canais.
O manifesto
Cada app publica um manifesto: nome, ícone, rota inicial, menu de navegação e habilidades. Publique-o a cada inicialização com PUT /applications/@me/app-manifest. Ele substitui o que está guardado e pode ser repetido sem problema.
PUT /api/v10/applications/@me/app-manifest
Authorization: Bot <token>
Content-Type: application/json
{
"name": "Raid Planner",
"description": "Plan raids and sign up, right in a channel.",
"icon": { "builtin": "calendar" },
"home": "home",
"nav": [
{ "label": "Home", "route": "home", "icon": { "builtin": "home" } },
{ "label": "Raids", "route": "raids", "icon": { "builtin": "calendar" } },
{ "label": "Officers", "ability": "manage", "children": [
{ "label": "Roster", "route": "roster", "ability": "manage" }
] }
],
"abilities": [
{ "key": "signup", "label": "Sign up for raids", "default": "everyone" },
{ "key": "manage", "label": "Manage raids",
"description": "Create, edit and cancel raids", "default": "managers" }
]
} | Campo | Observações |
|---|---|
name | Obrigatório, até 50 caracteres. Pode ter name_localizations. |
description | Até 300 caracteres. Pode ter description_localizations. |
icon | Um ícone (veja abaixo). |
home | A rota de entrada. Padrão home. |
nav | O menu: até 12 itens de primeiro nível e 30 no total. |
abilities | Até 25 permissões definidas pelo app. |
kind | Precisa ser layout (o padrão). Outros tipos são reservados e recusados. |
storage | Opcional. Só para apps que aceitam envio de arquivos (veja Arquivos de app). Um app que não declara armazenamento não recebe envios. |
Itens de navegação são {label, route, icon?, ability?, children?}. Um rótulo tem até 40 caracteres. Um item com children é um grupo: pode omitir a própria route, e grupos só se aninham em um nível. Um item com uma ability fica oculto para quem não a tem, e essa habilidade precisa estar declarada em abilities. Rotas seguem ^[A-Za-z0-9._~/?=&%:-]{1,200}$.
Habilidades são {key, label, description?, default}: uma chave que segue ^[a-z0-9_]{1,32}$, um rótulo de até 40 caracteres, uma descrição de até 200, e um default que é everyone (todo grupo que pode ver o canal), managers (grupos de gerência, veja Contexto de quem vê) ou nobody. Os admins podem sobrescrever o padrão por grupo em cada canal de app.
Ícones são exatamente um destes: {"emoji": "…"} (um único emoji, desenhado como Twemoji), {"builtin": "…"} (um símbolo do GameVox: calendar, trophy, list, chart, shield, sword, map, users, clock, star, flag, book, gift, bolt, home, game) ou {"image_data": "<base64>"} (um PNG ou WebP quadrado, de 16 a 2048 pixels, até 128 KB). O GameVox verifica e guarda image_data e devolve {"url": …} no manifesto guardado. Uma URL hospedada por você é recusada, porque nunca seria mostrada aos membros; você pode devolver um ícone url que leu do GameVox.
A resposta é 200 com o manifesto guardado (padrões preenchidos, imagens trocadas por URLs). Um erro vira um 400 que nomeia o campo, por exemplo nav[2].route is not a valid route. Publicar o mesmo manifesto de novo não muda nada. Um manifesto alterado faz todos os seus canais de app abertos se atualizarem, então um menu novo aparece sem ninguém recarregar. GET /applications/@me/app-manifest devolve o que está guardado, ou 404 se você nunca publicou um.
Localizando o manifesto
Todo texto do manifesto que uma pessoa lê pode ter traduções, assim como um comando slash do Discord tem name_localizations. Coloque um mapa ao lado do texto que ele traduz. O campo simples continua sendo o padrão: é o que o membro vê quando o mapa não tem entrada para o idioma dele.
{
"name": "Raid Planner",
"name_localizations": { "de": "Raidplaner", "pt-BR": "Planejador de raids", "ja": "レイドプランナー" },
"nav": [
{ "label": "Roster", "route": "roster",
"label_localizations": { "de": "Aufstellung", "fr": "Effectif", "es-ES": "Plantilla" } }
],
"abilities": [
{ "key": "manage", "label": "Manage raids", "default": "managers",
"label_localizations": { "de": "Raids verwalten" },
"description": "Create, edit and cancel raids",
"description_localizations": { "de": "Raids erstellen, bearbeiten und absagen" } }
]
} name_localizationsedescription_localizationsno manifesto (textos de até 50 e 300 caracteres),label_localizationsem um item de navegação (até 40),label_localizationsedescription_localizationsem uma habilidade (até 40 e 200).- As chaves são códigos de localidade do Discord (
de,fr,pt-BR,es-ES,zh-CN,jae o resto da lista do Discord). Um idioma para o qual o Discord não tem código (ar,he,fa,ms,sk,et,lv,sr,sl,tl,ur,bn,ta,sw,ca) usa a sua tag de idioma do GameVox. As chaves diferenciam maiúsculas de minúsculas, um mapa tem no máximo 64 localidades e uma chave desconhecida dá um400que nomeia o campo. - Um valor em branco,
nullou igual ao texto padrão é descartado, entãonulllimpa uma localidade. Um valor acima do limite do texto dá um400, por exemplonav[2].label_localizations[de] is longer than 40 characters. - Chaves de habilidade, rotas e ícones não são localizados. As visualizações não fazem parte do manifesto: renderize cada visualização no idioma de quem a vê, usando o
localeda requisição.
O manifesto sem seus mapas mantém o limite de 64 KB. Os mapas têm um orçamento próprio de 192 KB além disso, então cabe um manifesto em 44 idiomas (o documento inteiro tem no máximo 256 KB). Um manifesto sem mapas continua limitado a 64 KB como antes.
O GameVox resolve os mapas para cada membro e envia aos clientes strings simples. O idioma é o que o cliente do membro informou por último, o mesmo que define o idioma das descrições dos comandos slash. Isso vale para o nome e a descrição do app no cabeçalho do canal e na lista Criar canal, o menu, os nomes e descrições das habilidades no editor de permissões e o nome do app nas notificações. GET /applications/@me/app-manifest devolve o manifesto guardado com seus mapas.
Requisições e respostas
Quando um membro abre o canal, navega, atualiza ou aperta um botão, o seu bot recebe um dispatch do gateway (opcode 0) chamado APP_VIEW_REQUEST. Ele não precisa de intent. Vai só para uma sessão de gateway ativa da sua aplicação.
{
"op": 0,
"s": 812,
"t": "APP_VIEW_REQUEST",
"d": {
"nonce": "2f6c1a9e-4b7d-4c1e-9a53-0d8e7f6a5b41",
"kind": "action",
"guild_id": "1491230000000000001",
"channel_id": "1491240000000000002",
"route": "raids/42",
"params": { "raid": 42 },
"action": "signup",
"values": { "role": "healer", "note": "Late by 10 min" },
"user": {
"id": "1491250000000000003",
"username": "ben",
"display_name": "ben",
"avatar_url": "https://..."
},
"abilities": ["signup"],
"is_manager": false,
"locale": "en-US",
"platform": "desktop",
"self_hosted": false,
"plan": {
"key": "free",
"limits": {
"max_file_bytes": 8388608, "max_files_per_field": 4, "max_total_bytes": 524288000,
"max_uploads_per_user_per_day": 50, "kinds": [], "retention_days": 0, "max_video_bytes": 0
}
}
}
} | Campo | Observações |
|---|---|
nonce | Uso único. Devolva-o com a sua resposta. |
kind | open (o membro abriu, navegou até ou atualizou route) ou action (ele apertou um botão ou enviou um formulário). |
guild_id, channel_id | O servidor e o canal de app sendo visto, como snowflakes. Indexe os dados por canal com channel_id. |
route | A rota a mostrar. Nunca vazia: quando o cliente não envia nenhuma, é a rota inicial do canal, senão o seu home. |
params | Os params do link ou botão que trouxe até aqui (dados simples, até 2 KB). Sempre um objeto. |
action, values | Só em kind: "action": o nome da ação e os valores do formulário enviado (um objeto vazio para um botão simples). |
user | Quem está vendo: id (snowflake), username, display_name (igual ao nome de usuário por enquanto) e avatar_url (pode ser vazio). |
abilities | As habilidades que essa pessoa tem neste canal. Aplique-as você mesmo. |
is_manager | Verdadeiro para quem é dono do servidor, um grupo de admin, ou um grupo com a permissão Editar servidor ou Gerenciar integrações. |
locale | O idioma do cliente de quem está vendo (por exemplo en-US ou de); pode ser vazio. Use-o para responder no idioma da pessoa. |
platform | desktop, web, ios ou android. |
self_hosted | Verdadeiro quando o canal fica em um servidor auto-hospedado. |
plan | O plano do app neste servidor: {key, limits} (veja Planos). |
Respondendo
Responda com uma única chamada a POST /applications/@me/app-view-response, autenticada como qualquer outra chamada de bot (Authorization: Bot <token>):
POST /api/v10/applications/@me/app-view-response
Authorization: Bot <token>
Content-Type: application/json
{
"nonce": "2f6c1a9e-4b7d-4c1e-9a53-0d8e7f6a5b41",
"toast": "Signed up as healer",
"view": { "version": 1, "title": "Mythic raid", "route": "raids/42", "blocks": [ ... ] }
}
HTTP/1.1 204 No Content viewsubstitui a tela de quem está vendo. Em uma ação que só precisa de um toast, deixe-o de fora: a pessoa continua na tela atual.toasté uma linha curta (até 200 caracteres) exibida como toast. Pode ir junto com umaview.error(até 300 caracteres) mostra uma mensagem no lugar de uma tela. Quandoerrorestá definido,viewetoastsão ignorados.open_urlpede ao cliente que abra uma URLhttpsdepois do aviso normal de link externo. Qualquer outro esquema é descartado.- Sucesso é
204. A visualização é conferida e limpa depois disso (veja O que o sanitizador faz), então um204não quer dizer que todos os campos sobreviveram.
Tempos
- Quem está vendo espera 10 segundos. Depois disso, vê “O app não respondeu. Tente de novo.”, e uma resposta atrasada é descartada. Procure responder bem antes disso; a biblioteca de bots do GameVox dá a cada requisição um prazo de 8 segundos.
- O nonce é aceito por 15 segundos e uma única vez. Uma segunda resposta, ou uma resposta depois de expirado, recebe
404. - Se a sua aplicação mantém várias sessões de gateway (shards, ou sobreposição durante um reinício), mais de uma pode receber a mesma requisição. Responda uma vez só; só a primeira resposta conta.
- O seu bot conta como online enquanto a sessão de gateway dele está conectada e enviando heartbeats. Enquanto ele está offline, o GameVox não pergunta a ele: uma abertura mostra o seu instantâneo, e uma ação avisa o membro que o app está offline.
O esquema de visualização
Uma visualização é um documento de layout em JSON: tipo layout, versão 1. Cada cliente a desenha nativamente. Campos desconhecidos são ignorados e tipos de bloco desconhecidos são descartados, então novos campos opcionais podem ser acrescentados sem quebrar ninguém.
{
"version": 1,
"title": "Raids",
"subtitle": "Next 7 days",
"route": "raids",
"nav_active": "raids",
"tabs": [
{ "label": "Upcoming", "route": "raids" },
{ "label": "Past", "route": "raids?past=1" }
],
"actions": [
{ "label": "New raid", "route": "raids/new", "style": "primary", "ability": "manage" }
],
"refresh_seconds": 60,
"blocks": [
{ "type": "stats", "items": [
{ "label": "Signed up", "value": 18, "trend": "up" },
{ "label": "Open slots", "value": 2, "color": "warning" }
] },
{ "type": "countdown", "label": "Next pull", "until": "2026-11-02T19:00:00Z" },
{ "type": "table",
"columns": [
{ "key": "name", "label": "Raid", "sortable": true },
{ "key": "when", "label": "When", "kind": "time" },
{ "key": "lead", "label": "Lead", "kind": "user" },
{ "key": "fill", "label": "Filled", "kind": "progress" }
],
"rows": [
{ "id": "42",
"cells": { "name": "Mythic", "when": "2026-11-02T19:00:00Z",
"lead": "1491250000000000003", "fill": { "value": 18, "max": 20 } },
"action": { "route": "raids/42" } }
] },
{ "type": "text", "markdown": "Bring flasks. Questions to <@1491250000000000003>." }
]
} | Campo | Observações |
|---|---|
version | 1 ou ausente. Qualquer outra versão mostra “Esta tela precisa de uma versão mais nova do GameVox.” |
kind | layout ou ausente. Mesma regra. |
title | Até 100 caracteres, mostrado no cabeçalho. |
subtitle | Até 200 caracteres. |
route | A rota que esta tela mostra. Pode ser diferente da pedida (um redirecionamento); o cliente a registra no histórico. Se você deixar de fora, o GameVox preenche com a rota pedida. |
nav_active | Qual item de navegação destacar. |
breadcrumbs | Até 5 {label, route?}, rótulos de até 100 caracteres. |
tabs | Até 8 abas de subnavegação {label, route, badge?}; a aba cuja rota corresponde fica selecionada. Rótulos de até 40, badges de até 20. |
actions | Até 4 botões de cabeçalho (veja Botões). |
refresh_seconds | Os clientes reabrem a tela com essa frequência enquanto ela está visível e a janela está em foco: de 15 a 3600 segundos. |
blocks | O conteúdo: no máximo 60 blocos em uma lista e 600 blocos na visualização inteira. |
Blocos
Todo bloco tem um type e um id opcional (até 64 caracteres). As observações trazem os limites: o que passar de um limite é cortado ou descartado.
| Tipo | Campos | Observações |
|---|---|---|
heading | text, level | Um título. Texto de até 200 caracteres, nível de 1 a 3 (padrão 2). |
text | markdown | Um parágrafo em markdown, de até 4.000 caracteres. |
callout | style: info | success | warning | error, markdown | Uma caixa destacada com markdown. O estilo padrão é info. |
divider | Uma linha horizontal. | |
section | title, description, blocks, collapsible, collapsed | Um grupo de blocos com título (100) e descrição (200) opcionais, que pode ser recolhível. Contêineres se aninham em até dois níveis. |
columns | columns: [{width 1-12, blocks}] | Colunas lado a lado em uma grade de 12 (até 4 colunas). Elas se empilham em telas estreitas. |
stats | items: [{label, value, delta, trend, color, icon}] | Painéis de estatística (até 8). Valor de até 100 caracteres, variação de até 30, tendência up, down ou flat. |
key_value | items: [{label, value, user}] | Pares de rótulo e valor (até 30). Valores de até 500 caracteres. Uma linha pode ser só um user, que desenha um chip de membro. |
table | columns: [{key, label, align, sortable, kind}], rows: [{id, cells, action}], page_size, emptykind: text | number | user | badge | progress | time | icon | image | Uma tabela: até 10 colunas e 200 linhas, ordenadas e paginadas pelo cliente (page_size de 1 a 100, padrão 25). Os valores das células seguem o kind da coluna; células de até 500 caracteres. Linhas sem id recebem um. |
list | items: [{id, title, subtitle, user, icon, image, badges, meta, action, buttons}] | Uma lista (até 100 itens). Títulos de até 200 caracteres, até 5 badges e 5 botões por item. |
progress | label, value, max, color | Uma barra de progresso. max tem padrão 100; o valor é mantido entre 0 e max. |
countdown | label, until | Uma contagem regressiva ao vivo até um horário RFC 3339. Descartada sem until. |
timeline | items: [{time, title, markdown, icon, color}] | Uma linha do tempo (até 50 entradas). Markdown de cada entrada de até 500 caracteres. |
calendar | mode: month | week | agenda, start, events: [{id, title, start, end, color, action}] | Um calendário (até 200 eventos). Telas estreitas usam a visão de agenda por padrão. Os botões de anterior e próximo enviam a ação calendar_nav (veja Ações). |
chart | kind: bar | line, series: [{name, color, points: [{x, y}]}], x_label, y_label | Um gráfico de barras ou de linhas: até 5 séries de 100 pontos. x é um número ou um rótulo. |
board | columns: [{id, title, cards: [{id, title, subtitle, user, image, badges, action}]}] | Um quadro de colunas e cartões: até 6 colunas de 50 cartões. Sem arrastar e soltar. |
image | url | file_id, alt, aspect | Uma imagem: uma URL https ou um arquivo de app. Texto alternativo de até 200 caracteres, aspect como "16:9" ou 1.5. |
video | file_id, poster_file_id, title | Um arquivo de vídeo de app (só para apps cujo plano permite vídeo; veja Arquivos de app). |
buttons | items: [Button] | Uma fileira de botões (até 5). |
form | id, fields: [Field], submit: Button | Um formulário (até 15 campos). Veja Formulários. |
empty | title, text, icon | Um estado vazio: título (100), texto (200) e ícone. |
Valores usados em vários blocos
- Cores são
#rrggbbou um token:accent,success,warning,danger,muted. Badges são{text, color?}(texto de até 30) ou uma string simples. - Horários são RFC 3339 e aparecem no fuso horário de quem está vendo.
starteenddo calendário também aceitam uma dataYYYY-MM-DD. - Membros (
user) são snowflakes de membro. O cliente desenha um avatar e o nome de exibição, e um clique abre o perfil. - Markdown (
text,callout, entradas da linha do tempo) é o markdown do chat do GameVox. HTML nunca é renderizado. Menções como<@id>,<@&id>e<#id>são desenhadas como nomes e nunca notificam ninguém. Todos os outros campos de texto são texto simples. - Imagens precisam ser URLs
httpssem credenciais, de até 2000 caracteres. O GameVox não tem proxy de imagens: uma imagem que não está no armazenamento do GameVox é mantida, mas marcada com"external": true, e os clientes a tratam como uma imagem de terceiros no chat (sem referrer, sem cookies, talvez atrás de um clique). Arquivos de app evitam isso.
O que o sanitizador faz
A sua visualização é conteúdo de terceiros desenhado no cliente de cada membro, então o GameVox a reconstrói a partir de uma lista de permissões antes que qualquer cliente a veja:
- Tipos de bloco desconhecidos e campos desconhecidos são descartados.
- O texto perde UTF-8 inválido, caracteres de controle e caracteres de sobreposição bidirecional, e é cortado no limite.
- Listas são cortadas na contagem máxima; blocos além de 60 em uma lista ou de 600 na visualização são descartados.
- Cores, rotas, horários, ids e referências de membro que não seguem o formato esperado são esvaziados.
- Itens a que falta o necessário são descartados: um item de lista sem título, uma entrada da linha do tempo sem horário, um botão sem rótulo ou sem destino, um formulário sem
idousubmit.action. - Os
paramsde botões e linhas precisam ser dados simples (strings, números, booleanos, listas e objetos, até 5 níveis, 50 entradas cada, strings de até 500 caracteres, 2 KB no total), senão são descartados junto com o botão. - Uma visualização só é recusada por inteiro quando não é um objeto JSON, passa de 256 KB (128 KB para um instantâneo), ou tem outra
versionou outrokind. Quem está vendo então vê “Este app enviou uma tela que o GameVox não conseguiu mostrar.”
O endpoint de instantâneo responde com warnings listando o que foi alterado. Respostas ao vivo não. O View.Validate() da biblioteca de bots em Go lista as mesmas alterações antes do envio.
Ações e interações
Botões
Um botão ou navega (tem uma route) ou envia uma ação ao seu app (tem uma action):
{
"label": "Cancel raid",
"action": "cancel",
"params": { "raid": 42 },
"style": "danger",
"ability": "manage",
"confirm": { "title": "Cancel this raid?", "text": "Everyone who signed up is told.", "confirm_label": "Cancel raid" }
} label: obrigatório, até 40 caracteres.action: um nome que segue^[A-Za-z0-9._:-]{1,64}$. Apertá-lo envia a vocêkind: "action"com esse nome e osparamsdo botão.route: navegar em vez disso. O cliente abre essa rota e você recebekind: "open"com osparamsdo botão. Quando os dois estão definidos,routevence.params: um objeto de até 2 KB, devolvido a você exatamente como você escreveu.style(primary,secondary,success,danger),icon,disabled.ability: esconde o botão de quem não tem essa habilidade.confirm:{title, text, confirm_label}(80, 300 e 40 caracteres) pede confirmação ao membro antes de enviar a ação.
Linhas de tabela, itens de lista, cartões de quadro e eventos de calendário aceitam um destino action nos mesmos dois formatos: {"action": "…", "params": …} ou {"route": "…", "params": …}. Os clientes mostram um estado de carregamento no controle pressionado e nunca enviam o mesmo acionamento duas vezes.
Formulários
Um bloco form tem um id (único na visualização), até 15 fields e um botão submit, que precisa ter uma action. Enviar o formulário manda a você kind: "action" com a ação e os params do botão de envio, e os valores dos campos em values, indexados pela chave do campo.
{
"type": "form",
"id": "signup",
"fields": [
{ "key": "role", "type": "select", "label": "Role", "required": true,
"options": [
{ "value": "tank", "label": "Tank" },
{ "value": "healer", "label": "Healer" },
{ "value": "dps", "label": "Damage" }
] },
{ "key": "note", "type": "string", "label": "Note", "max_length": 100 }
],
"submit": { "label": "Sign up", "action": "signup", "params": { "raid": 42 }, "ability": "signup" }
} key: segue^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$, única no formulário.type:string,text,number,boolean,select,multiselect,channel,channels,role,roles,color,static,date,datetime,user(seletor de membro) oufile(veja Arquivos de app).label(100),help(200),placeholder(100),required,disabled,min,max,step,max_length(até 2.000) evaluepara preencher de antemão.optionsparaselectemultiselect: até 25{value, label?, description?}. Sem rótulo, mostra-se o valor.channel_kindsrestringe um seletor de canal. Seletores de canal e de cargo listam os canais e cargos do próprio servidor; o GameVox os fornece, você não envia ids.
Valores que você recebe: strings (até 2.000 caracteres), números, booleanos ou listas de até 25 strings; no máximo 30 valores e 32 KB no total. Seletores de canal e de cargo dão snowflakes, um seletor de membro dá a snowflake do membro, e um campo de arquivo dá uma lista de objetos de arquivo. O GameVox confere só o formato: required, min, max e as listas de opções são aplicados pelo cliente, então confira cada valor você mesmo.
Paginação do calendário: os botões de anterior e próximo de um bloco calendar enviam a ação calendar_nav com params {"start": "YYYY-MM-DD", "mode": "month|week|agenda", "id": "<block id>"}. Responda com uma visualização cujo calendário mostre esse intervalo.
Mantendo as telas atualizadas
refresh_secondsem uma visualização (de 15 a 3600) a reabre em intervalos enquanto ela está visível.POST /channels/{channel_id}/app-view/invalidatecom{"routes": [...]}avisa todos que estão vendo o canal que a tela deles está desatualizada. Deixeroutesde fora para atualizar todo mundo, ou liste até 20 rotas para atualizar só quem está nelas (raidstambém corresponde araids?past=1). Os clientes reabrem depois de um pequeno atraso aleatório (de 1 a 3 segundos), então uma rajada custa uma requisição por pessoa, não uma por evento. No máximo 5 chamadas por segundo por canal passam na hora; as demais são juntadas em mais uma, um segundo depois. Resposta:204.PUT /channels/{channel_id}/app-view/snapshotcom{"view": {…}}guarda uma tela pública, a mesma para todos que podem ver o canal, de até 128 KB. Ela é mostrada enquanto o seu bot está offline, marcada como a última atualização. Envie{"view": null}para apagá-la (resposta{"cleared": true}). A resposta é{"warnings": [...]}. Até 6 envios a cada 10 segundos por canal. Nunca coloque nada pessoal em um instantâneo.- Só a aplicação a que um canal pertence pode invalidá-lo ou definir o instantâneo dele.
POST /api/v10/channels/1491240000000000002/app-view/invalidate
{ "routes": ["raids", "raids/42"] }
HTTP/1.1 204 No Content
PUT /api/v10/channels/1491240000000000002/app-view/snapshot
{ "view": { "version": 1, "title": "Raids", "blocks": [ ... ] } }
HTTP/1.1 200 OK
{ "warnings": [] } Contexto de quem vê
Toda requisição descreve uma pessoa, então as telas podem ser pessoais: “seus personagens”, “sua inscrição”, botões que só quem gerencia vê.
- Identidade:
user.idé a snowflake do membro, o mesmo id que o seu bot vê nos eventos do gateway e no REST. Guarde os dados por membro com ele. - Habilidades: para cada habilidade, a pessoa a tem se algum dos grupos dela que podem ver o canal tiver uma sobrescrita que a conceda, ou não tiver sobrescrita e o padrão do manifesto a cobrir (
everyone: todo grupo assim;managers: só grupos de gerência;nobody: nenhum). Quem é dono do servidor tem todas as habilidades. Um membro sem grupo usa o grupo padrão do servidor. - Gerência (
is_managere o padrãomanagers) corresponde a quem é dono do servidor, aos grupos de admin e aos grupos com a permissão Editar servidor ou Gerenciar integrações. - A navegação é filtrada para quem está vendo: itens de navegação que exigem uma habilidade que a pessoa não tem ficam ocultos. Isso é uma cortesia. Esconder um botão não é segurança, e um cliente modificado pode enviar qualquer ação, rota ou params.
- Idioma:
localeé o idioma do cliente de quem está vendo. Uma tela feita para todos (um instantâneo) não tem como saber; use o idioma do servidor nesse caso. - Membros no que você envia: escreva snowflakes de membro em campos
user, células de membro de tabelas, valores de seletores de membro e menções. O GameVox as traduz para os clientes e de volta; osparamsnunca são tocados.
Arquivos de app, planos e notificações de app
Arquivos de app
Um app pode receber envios de arquivo (por exemplo, uma captura de tela de um placar) por um campo de formulário do tipo file, e mostrar arquivos guardados nas telas dele. Os arquivos ficam em um armazenamento privado do GameVox, são verificados como os anexos do chat e só são servidos por URLs assinadas de curta duração.
- Declare o armazenamento no manifesto:
"storage": {"kinds": ["image"], "max_file_bytes"?, "max_total_bytes"?, "max_video_bytes"?}. Sem isso, o seu app não recebe envios. Os limites efetivos são os do seu plano, reduzidos pelo que você declarar. - Adicione um campo de arquivo:
{"type": "file", "key", "label", "accept": ["image"], "max_files": 1 to 10, "required"?}. O cliente envia direto para o armazenamento e espera a verificação antes que o formulário possa ser enviado. - Leia os valores: o valor do campo é uma lista de
{id, url, name, mime, size, width, height}.urlé assinada por uma hora; busque o arquivo dentro do seu handler ou chameGETnele mais tarde para obter uma URL nova. - Guarde o que você usa. Um envio fica
pendingaté ser guardado, e um arquivo pendente é excluído depois de 24 horas. Responder semerrorà ação que o trouxe o guarda automaticamente.POST /applications/@me/app-files/{id}/keepguarda um manualmente. Um arquivo guardado fica até você excluí-lo, a retenção do plano acabar, um moderador bloqueá-lo, ou o seu app estar removido do servidor há 30 dias. - Mostre arquivos com
{"file_id": "<id>"}em vez de uma URL: ofile_idde um bloco de imagem, aimagede um item de lista ou cartão de quadro, uma célula de tabela em uma coluna do tipoimage. O GameVox coloca no lugar uma URL assinada para quem está vendo. Colocar um arquivo em uma visualização é o que permite que essa pessoa o veja. Ids de outro app ou de outro servidor são descartados, e uma visualização contém no máximo 100 ids de arquivo. - Instantâneos só mostram arquivos que você marcou como públicos:
PATCH /applications/@me/app-files/{id}com{"public_in_snapshot": true}. - Tipos: imagens (PNG, JPEG, GIF, WebP) funcionam hoje. Vídeo (MP4 com H.264) só funciona onde o GameVox concedeu ao seu app uma cota de vídeo naquele servidor; WebM é recusado porque não pode ser verificado.
documenté reservado: um manifesto pode nomeá-lo, mas nenhum tipo de documento é aceito ainda.
// manifest
"storage": { "kinds": ["image"], "max_file_bytes": 4194304 }
// a field in a form
{ "key": "shot", "type": "file", "label": "Screenshot", "accept": ["image"], "max_files": 2 }
// what the action brings you
"values": {
"shot": [ { "id": "8c1e2f4a-0b6d-4e7f-9a21-3c5d7e9f1a2b", "url": "https://...signed...",
"name": "score.png", "mime": "image/png", "size": 183224, "width": 1920, "height": 1080 } ]
}
// showing it later
{ "type": "image", "file_id": "8c1e2f4a-0b6d-4e7f-9a21-3c5d7e9f1a2b", "alt": "Score screen" } REST (tudo sob /applications/@me/, token de bot, ids de guild, de canal e de quem enviou como snowflakes, ids de arquivo como strings UUID): GET app-files?guild_id=&state=&page= responde {files, page, has_more} (50 por página); GET, PATCH e DELETE app-files/{id}; POST app-files/{id}/keep; GET app-usage?guild_id= responde {plan, limits, used_bytes, files}. O arquivo de outro app dá 404; um arquivo ainda não enviado, excluído ou bloqueado dá 409 no keep.
Planos
Toda requisição traz plan: {key, limits}, os limites do seu app naquele servidor depois do teto de storage do seu manifesto. Use isso para liberar extras seus (um app construído sobre dados de outra empresa precisa seguir as regras dessa empresa sobre recursos pagos). Hoje todo app roda no plano free, a menos que a equipe do GameVox defina outro plano para um servidor; ainda não há como comprar um plano.
| Campo | Observações |
|---|---|
max_file_bytes | Maior imagem: 8 MB. |
max_files_per_field | Arquivos por campo de arquivo: 4. |
max_total_bytes | Armazenamento por app por servidor: 500 MB. Arquivos pendentes e guardados contam. |
max_uploads_per_user_per_day | Envios por membro por dia: 50. |
kinds | Tipos permitidos: ["image"], depois restringidos pelo seu manifesto (vazio se você não declarar armazenamento). |
retention_days | Arquivos guardados são excluídos depois desse número de dias; 0 os mantém até você excluí-los. |
max_video_bytes | Maior vídeo; 0 significa sem vídeo. |
Notificações de app
Bots não podem enviar mensagens diretas. Em vez disso, um app pode notificar membros (uma resposta em um ticket, um lembrete de raid) com POST /applications/@me/app-notifications. Os membros a veem no sino de notificações com o ícone do seu app, como toast, e como push no celular se permitirem notificações de app. Tocar nela abre o seu canal de app em route.
POST /api/v10/applications/@me/app-notifications
Authorization: Bot <token>
Content-Type: application/json
{
"guild_id": "1491230000000000001",
"user_ids": ["1491250000000000003", "1491250000000000009"],
"title": "Raid starts in 30 minutes",
"body": "Mythic, 19:00. You signed up as healer.",
"route": "raids/42",
"dedupe_key": "raid-42-reminder"
}
HTTP/1.1 200 OK
{
"delivered": ["1491250000000000003"],
"skipped": [ { "user_id": "1491250000000000009", "reason": "muted" } ]
} guild_id(obrigatório),user_ids(de 1 a 25 snowflakes de membro),title(obrigatório, até 80 caracteres),body(até 300, pode ser vazio),route,channel_id(um canal de app do seu app naquele servidor),dedupe_key(até 64). Texto demais é um400que nomeia o limite; o texto nunca é cortado.- Título e corpo são texto simples. Caracteres de controle e bidirecionais são removidos.
- Um toque abre o
channel_idse você informou um, senão o primeiro canal de app do seu app que o membro pode ver. Um app sem canal de app no servidor envia notificações que não abrem nada. - A mesma
dedupe_keypara o mesmo membro dentro de 24 horas substitui a notificação anterior (ela volta a aparecer como não lida) em vez de adicionar outra. - A resposta lista os ids de membro
deliverede osskippedcom umreason:not_member(também para um id que o GameVox não conhece),muted(o membro silenciou o seu app naquele servidor),cannot_view,rate_limitedouerror(tente de novo mais tarde). Um membro pulado não consome nada. - Limites: 10 por hora e 30 por dia por app por membro, 500 por hora por app por servidor (uma tentativa recusada também conta), e 20 chamadas a cada 10 segundos por bot.
403se o seu app não estiver instalado naquele servidor. - As notificações ficam guardadas por 30 dias, no máximo 200 por membro.
Servidores auto-hospedados
Canais de app funcionam em servidores auto-hospedados com os mesmos eventos e endpoints. O seu bot continua se conectando só à nuvem. As diferenças:
self_hostedétrueemAPP_VIEW_REQUEST.- A instância do cliente confere quem pode ver o canal e calcula as habilidades de quem está vendo a partir dos próprios grupos, e depois pede à nuvem que envie a requisição a você. O GameVox confia na instância para isso, como faz com qualquer outra permissão em um servidor auto-hospedado.
- Instantâneo, invalidação e criação, edição e exclusão de um canal auto-hospedado são encaminhados à instância como as outras chamadas auto-hospedadas:
503quando a instância não está conectada,504quando ela não responde a tempo. - Um manifesto alterado chega a uma instância em cerca de um minuto (ela guarda o manifesto em cache por 60 segundos) em vez de na hora.
- Quando a instância não consegue alcançar a nuvem, os membros veem o seu instantâneo (sem os arquivos dele) ou um aviso de offline.
- Arquivos de app funcionam como na nuvem e ficam no armazenamento do GameVox, não na instância. Vídeo exige uma instância atualizada.
- Para notificações de app, a nuvem pergunta à instância quem pode ver o canal de app. Uma instância mais antiga ou inacessível não é consultada: aí só a participação no servidor é conferida, e um
channel_idque você informar é repassado sem conferência. - As traduções do manifesto também funcionam em servidores auto-hospedados, no idioma que a conexão de cada membro informa. Um servidor que ainda não foi atualizado mostra o texto padrão.
Notas de segurança para quem escreve bots
- Aplique as habilidades em toda ação. Confira
abilities(e as suas próprias regras) antes de mudar qualquer coisa. O cliente esconder um botão é só uma cortesia. - Trate route, params, action e values como entrada do usuário. Eles vêm do cliente do membro. Um cliente modificado pode enviar qualquer rota, quaisquer params e qualquer nome de ação, e pular os campos obrigatórios e os limites do formulário. O GameVox só confere o formato deles.
- Confie nos ids que o GameVox entrega (
guild_id,channel_id,user.id,abilities), não em ids dentro dos params. Para agir sobre “o raid 42”, confira se o raid 42 pertence a este canal. - Mantenha os instantâneos públicos. Todos que podem ver o canal os veem. Nunca coloque dados pessoais em um, e só marque como públicos os arquivos que podem ser mostrados a todos.
- Não vaze URLs de arquivo assinadas. Elas funcionam por uma hora para quem as tiver. Coloque
file_ids nas visualizações e deixe o GameVox assiná-los para cada pessoa. - Guarde os seus nonces para você. Cada um responde à requisição de uma pessoa; o GameVox confere se ele pertence à sua aplicação.
- Prefira imagens hospedadas no GameVox. URLs de imagens de terceiros são marcadas como externas e podem ficar atrás de um clique; elas também avisam o host da imagem quando a tela é mostrada.
- O GameVox, por sua vez, nunca confia na sua visualização: ela é reconstruída a partir de uma lista de permissões, HTML nunca é renderizado, menções nunca notificam, e
open_urlé sóhttpse sempre passa pelo aviso de link externo.
Erros e limites
Respostas REST
400: corpo inválido. Erros de manifesto e de instantâneo nomeiam o campo. Uma resposta sem nonce também é 400.403: o nonce ou o canal pertence a outra aplicação, ou o seu app não está instalado naquele servidor.404: nonce desconhecido ou expirado; canal desconhecido, ou um canal que não é canal de app (instantâneo, invalidação); arquivo desconhecido; nenhum manifesto publicado (GET).409: o arquivo não está em um estado que permita isso.413: corpo acima do limite: manifesto 64 KB (mais 192 KB de traduções), resposta de visualização 256 KB, instantâneo 128 KB, requisição de notificação 64 KB.429: acima de um limite de taxa, com os cabeçalhos de limite de taxa de sempre.
Limites de taxa
- Manifesto: 10 publicações por minuto por aplicação (GET: 30 por minuto).
- Respostas de visualização: 200 por segundo por bot.
- Instantâneo: 6 envios a cada 10 segundos por canal.
- Invalidação: 5 por segundo por canal passam na hora; as demais são agrupadas, não recusadas.
- Arquivos de app: 30 chamadas a cada 10 segundos por rota; uso: 30 por minuto.
- Notificações de app: 20 chamadas a cada 10 segundos por bot, mais os limites por membro e por servidor acima.
- Por pessoa: 20 aberturas e 10 ações a cada 10 segundos. Por aplicação, somando todas as pessoas: 600 requisições a cada 10 segundos. Acima disso, a pessoa é avisada para ir mais devagar ou de que o app está ocupado; o seu bot não é consultado.
O que a pessoa vê
- “O app não respondeu. Tente de novo.” Nenhuma resposta em 10 segundos.
- “O app está offline no momento.” Uma ação enquanto o seu bot está offline.
- “Este app enviou uma tela que o GameVox não conseguiu mostrar.” A visualização foi recusada por inteiro.
- “Esta tela precisa de uma versão mais nova do GameVox.” Uma visualização com outra
versionou outrokind. - O seu próprio texto de
error, quando você envia um.
Um exemplo mínimo de ponta a ponta
Um app de inscrição em raids com uma tela e um formulário, usando HTTP puro. Os ids estão encurtados.
1. Publique o manifesto quando o bot iniciar:
PUT /api/v10/applications/@me/app-manifest
Authorization: Bot <token>
{ "name": "Raid sign-up", "icon": { "builtin": "calendar" }, "home": "home",
"nav": [ { "label": "Home", "route": "home" } ],
"abilities": [ { "key": "signup", "label": "Sign up", "default": "everyone" } ] }
HTTP/1.1 200 OK 2. Obtenha um canal. Um admin adiciona o seu app com Criar canal ▸ App, ou o seu bot cria um:
POST /api/v10/guilds/1491230000000000001/channels
{ "type": 100, "name": "raid-signup" }
HTTP/1.1 201 Created
{ "id": "1491240000000000002", "type": 100, "gamevox_type": "app", ... } 3. Um membro o abre. O seu bot recebe:
{ "op": 0, "t": "APP_VIEW_REQUEST", "d": {
"nonce": "a1...", "kind": "open", "guild_id": "1491230000000000001",
"channel_id": "1491240000000000002", "route": "home", "params": {},
"user": { "id": "1491250000000000003", "username": "ben", ... },
"abilities": ["signup"], "is_manager": false, "locale": "en-US", "platform": "web", ... } } 4. Responda com a tela (em até 10 segundos):
POST /api/v10/applications/@me/app-view-response
Authorization: Bot <token>
{ "nonce": "a1...",
"view": { "version": 1, "title": "Raid sign-up", "route": "home", "blocks": [
{ "type": "text", "markdown": "You are not signed up yet." },
{ "type": "form", "id": "signup",
"fields": [ { "key": "role", "type": "select", "label": "Role", "required": true,
"options": [ { "value": "tank" }, { "value": "healer" }, { "value": "dps" } ] } ],
"submit": { "label": "Sign up", "action": "signup", "ability": "signup" } }
] } }
HTTP/1.1 204 No Content 5. O membro envia o formulário. Você recebe uma ação com os valores:
{ "op": 0, "t": "APP_VIEW_REQUEST", "d": {
"nonce": "b2...", "kind": "action", "route": "home", "action": "signup",
"params": {}, "values": { "role": "healer" },
"channel_id": "1491240000000000002", "user": { "id": "1491250000000000003", ... },
"abilities": ["signup"], ... } } 6. Confira, guarde e responda com um toast e a tela atualizada, e depois avise as outras pessoas para atualizar:
POST /api/v10/applications/@me/app-view-response
{ "nonce": "b2...", "toast": "Signed up as healer",
"view": { "version": 1, "title": "Raid sign-up", "route": "home", "blocks": [
{ "type": "text", "markdown": "You are signed up as **healer**." } ] } }
HTTP/1.1 204 No Content
POST /api/v10/channels/1491240000000000002/app-view/invalidate
{ "routes": ["home"] }
HTTP/1.1 204 No Content Opcional: um instantâneo para quando o bot estiver offline. Ele é público, então mostra a contagem, não quem se inscreveu:
PUT /api/v10/channels/1491240000000000002/app-view/snapshot
{ "view": { "version": 1, "title": "Raid sign-up", "blocks": [
{ "type": "stats", "items": [ { "label": "Signed up", "value": 18 } ] } ] } }
HTTP/1.1 200 OK
{ "warnings": [] } Em uma biblioteca do Discord, leia o evento pelo hook bruto do gateway dela (on_socket_raw_receive no discord.py, o evento raw no discord.js; veja a tabela em Configurações no app). Uma versão em discord.py dos passos 3 a 6:
# discord.py: answer APP_VIEW_REQUEST from the raw gateway hook
# (create the client with enable_debug_events=True)
import json
API = "https://app-api.gamevox.com/api/v10"
ROLES = ("tank", "healer", "dps")
signups = {} # (channel_id, user_id) -> role
def screen(req, note):
return {"version": 1, "title": "Raid sign-up", "route": "home", "blocks": [
{"type": "text", "markdown": note},
{"type": "form", "id": "signup",
"fields": [{"key": "role", "type": "select", "label": "Role", "required": True,
"options": [{"value": r} for r in ROLES]}],
"submit": {"label": "Sign up", "action": "signup", "ability": "signup"}}]}
@client.event
async def on_socket_raw_receive(msg):
packet = json.loads(msg)
if packet.get("t") != "APP_VIEW_REQUEST":
return
req = packet["d"]
key = (req["channel_id"], req["user"]["id"])
reply = {"nonce": req["nonce"]}
if req["kind"] == "action" and req["action"] == "signup":
role = req["values"].get("role")
if "signup" not in req["abilities"]: # enforce, never trust the client
reply["error"] = "You can't sign up in this channel."
elif role not in ROLES: # values are user input
reply["error"] = "Pick a role."
else:
signups[key] = role
reply["toast"] = f"Signed up as {role}"
if "error" not in reply:
mine = signups.get(key)
reply["view"] = screen(req, f"You are signed up as **{mine}**." if mine else "You are not signed up yet.")
await session.post(f"{API}/applications/@me/app-view-response",
headers={"Authorization": f"Bot {TOKEN}"}, json=reply) Os bots do próprio GameVox usam a biblioteca de bots em Go em server/shared-gamebot, que publica o manifesto, responde às requisições dentro de um prazo, valida as visualizações e agrupa as invalidações por você.
Ainda não disponível
- Um tipo
web(uma página em sandbox em vez de um layout) é reservado. Manifestos e visualizações que o usam são recusados. - Envio de documentos e compra de um plano não estão disponíveis.
- Arrastar e soltar nos quadros.