Portal do Desenvolvedor
Experimental A plataforma de bots & apps está em desenvolvimento ativo. Servidores auto-hospedados rodam bots, comandos de barra e componentes de mensagem, com algumas diferenças em relação à nuvem. Veja o que é suportado.
← Documentação

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_CREATE com 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

  1. Um servidor instala o seu app do jeito de sempre (o link de instalação ou a página de autorização OAuth2).
  2. A cada inicialização, o seu bot publica o manifesto dele. Um app sem manifesto não pode ter canais de app.
  3. 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).
  4. 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.
  5. Os membros que podem ver o canal o abrem, e o seu app responde a APP_VIEW_REQUEST para cada tela que eles olham.
  6. 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: o home do seu manifesto). gamevox_app.icon é um emoji, um símbolo builtin ou uma url que 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, PATCH e DELETE /channels/{id} funcionam como para os outros canais. PATCH também aceita gamevox_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" }
  ]
}
CampoObservaçõ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_localizations e description_localizations no manifesto (textos de até 50 e 300 caracteres), label_localizations em um item de navegação (até 40), label_localizations e description_localizations em uma habilidade (até 40 e 200).
  • As chaves são códigos de localidade do Discord (de, fr, pt-BR, es-ES, zh-CN, ja e 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á um 400 que nomeia o campo.
  • Um valor em branco, null ou igual ao texto padrão é descartado, então null limpa uma localidade. Um valor acima do limite do texto dá um 400, por exemplo nav[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 locale da 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
      }
    }
  }
}
CampoObservaçõ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
  • view substitui 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 uma view.
  • error (até 300 caracteres) mostra uma mensagem no lugar de uma tela. Quando error está definido, view e toast são ignorados.
  • open_url pede ao cliente que abra uma URL https depois 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 um 204 nã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>." }
  ]
}
CampoObservaçõ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.

TipoCamposObservaçõ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, empty
kind: 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 #rrggbb ou 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. start e end do calendário também aceitam uma data YYYY-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 https sem 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 id ou submit.action.
  • Os params de 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 version ou outro kind. 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 os params do botão.
  • route: navegar em vez disso. O cliente abre essa rota e você recebe kind: "open" com os params do botão. Quando os dois estão definidos, route vence.
  • 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) ou file (veja Arquivos de app).
  • label (100), help (200), placeholder (100), required, disabled, min, max, step, max_length (até 2.000) e value para preencher de antemão.
  • options para select e multiselect: até 25 {value, label?, description?}. Sem rótulo, mostra-se o valor.
  • channel_kinds restringe 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_seconds em uma visualização (de 15 a 3600) a reabre em intervalos enquanto ela está visível.
  • POST /channels/{channel_id}/app-view/invalidate com {"routes": [...]} avisa todos que estão vendo o canal que a tela deles está desatualizada. Deixe routes de fora para atualizar todo mundo, ou liste até 20 rotas para atualizar só quem está nelas (raids também corresponde a raids?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/snapshot com {"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_manager e o padrão managers) 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; os params nunca 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 chame GET nele mais tarde para obter uma URL nova.
  • Guarde o que você usa. Um envio fica pending até ser guardado, e um arquivo pendente é excluído depois de 24 horas. Responder sem error à ação que o trouxe o guarda automaticamente. POST /applications/@me/app-files/{id}/keep guarda 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: o file_id de um bloco de imagem, a image de um item de lista ou cartão de quadro, uma célula de tabela em uma coluna do tipo image. 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.

CampoObservaçõ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 é um 400 que 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_id se 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_key para 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 delivered e os skipped com um reason: 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_limited ou error (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. 403 se 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 é true em APP_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: 503 quando a instância não está conectada, 504 quando 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_id que 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ó https e 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 version ou outro kind.
  • 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.

Componentes de mensagem  ·  ← Todos os docs