Canales de app
Un canal de app es un canal de la lista de canales de un servidor cuyo contenido es un conjunto de pantallas que tu app aporta mientras está en línea: un planificador de raids, una clasificación, un mostrador de tickets, una página de estado del servidor. GameVox guarda dónde está el canal, quién puede verlo y qué puede hacer allí. Tu app es dueña del contenido y genera cada pantalla cuando se le pide.
Resumen
Una app es un lugar con muchas pantallas. Cada pantalla es una ruta, una cadena que define tu app (home, raids, raids/42, roster?sort=ilvl). Quien abre el canal llega a su ruta de inicio (normalmente tu home) y se mueve con tu menú de navegación, con los enlaces y botones de las pantallas, y con Atrás y Adelante.
Qué ven los miembros
- El canal en la lista de canales con tu icono (o uno que eligió el servidor), y un pequeño punto mientras tu app está desconectada.
- Tu menú de navegación, Atrás y Adelante, un botón Inicio y Actualizar, alrededor de la pantalla que devolviste.
- Pantallas que cada cliente dibuja de forma nativa a partir de tu JSON, con el tema propio del cliente, claro y oscuro.
- Mientras tu bot está desconectado: la última instantánea pública que enviaste, marcada como la última actualización, o un aviso de desconexión si no hay ninguna.
Clientes: la app de escritorio (Windows, macOS, Linux), la app web, iOS y Android. La app de Xbox no muestra canales de app. Un cliente demasiado antiguo para conocerlos nunca recibe canales de app en su lista de canales, así que nadie ve un canal de texto vacío.
En qué se diferencia de los comandos de barra y los componentes
- Los comandos de barra y los componentes de mensaje viven en mensajes de un canal de texto y llegan a tu app como
INTERACTION_CREATEcon un token de interacción. Un canal de app no tiene mensajes: publicar en uno se rechaza. - Una petición de vista llega como su propio evento de gateway,
APP_VIEW_REQUEST, y la respondes con una sola llamada REST que lleva un nonce de un solo uso. No hay token de interacción, ni aplazamiento (defer), ni mensajes de seguimiento. Los endpoints de interacciones HTTP no se usan, así que tu app necesita una conexión de gateway activa. - Cada pantalla es personal. GameVox la pide cada vez que un miembro la abre o la usa, y nunca la guarda (solo se guarda la instantánea pública opcional).
- Dentro de la pantalla, tu app define sus propios permisos, llamados habilidades (abilities). La administración del servidor los concede por grupo, y GameVox te dice en cada petición cuáles tiene la persona que mira.
- Es una extensión de GameVox sin equivalente en Discord. Los bots escritos con bibliotecas de Discord siguen funcionando; simplemente nunca reciben el evento.
Cómo encaja todo
Un miembro abre el canal, navega o pulsa un botón
│
▼
GameVox ──── APP_VIEW_REQUEST { nonce, kind, route, user, abilities } ────▶ tu bot
▲
│
tu bot ──── POST /applications/@me/app-view-response { nonce, view } ────▶ GameVox
│
▼
GameVox revisa y limpia la vista, y el cliente la dibuja GameVox comprueba en cada apertura y en cada acción que la persona puede ver el canal, calcula sus habilidades y solo entonces pregunta a tu app. Tu respuesta va solo a esa persona.
Ciclo de vida
- Un servidor instala tu app de la forma habitual (el enlace de instalación o la página de autorización OAuth2).
- En cada arranque, tu bot publica su manifiesto. Una app sin manifiesto no puede tener canales de app.
- Un admin del servidor elige Crear canal ▸ App. La lista muestra las apps instaladas que han publicado un manifiesto, con su nombre, descripción e icono. El nombre del canal se rellena a partir de tu app, y un menú opcional Se abre en lista tus elementos de navegación (por defecto: home). Crear canales requiere el permiso habitual para crear canales. Tu bot también puede crear uno (más abajo).
- En los Ajustes del canal, el admin puede renombrar el canal, cambiar su icono (el de tu app, un emoji o una imagen subida), cambiar dónde se abre y fijar permisos: quién puede ver el canal y qué grupos tienen cada una de tus habilidades.
- Los miembros que pueden ver el canal lo abren, y tu app responde a
APP_VIEW_REQUESTpor cada pantalla que miran. - Si tu app se quita del servidor, sus canales de app se quedan (reinstalarla los recupera) y muestran «Esta app se quitó del servidor», con un botón Eliminar para quienes gestionan. Si la aplicación se elimina, o nunca ha publicado un manifiesto, el canal muestra «Esta app ya no está disponible».
Los canales de app cuentan para el límite de canales de un servidor como cualquier otro canal. Un servidor suele tener un canal por app; se permite un segundo canal para la misma app (por ejemplo, un canal de un equipo de raid que se abre en el planificador de raids) y es la misma app con la misma navegación. Cada petición lleva el channel_id, así que puedes mantener datos separados por canal.
Crear un canal de app desde tu bot
Usa la ruta normal de creación de canales con type 100 y un 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" }
} - Solo para tu propia aplicación, que debe estar instalada en ese servidor y haber publicado un manifiesto. El bot necesita permiso para crear canales, como con cualquier canal.
gamevox_app.start_routees una ruta (vacía o ausente: elhomede tu manifiesto).gamevox_app.icones unemoji, un glifobuiltino unaurlque tu manifiesto ya contiene. Los bots no pueden subir una imagen de canal; solo la administración puede.- Sin
position, el canal va al final de la lista. El nombre tiene de 1 a 100 caracteres. - Quién puede ver el canal y quién tiene cada habilidad lo decide la administración del servidor en los Ajustes del canal.
Un canal de app es un objeto de canal normal con extensiones de GameVox, en las respuestas REST y en los eventos de canal del gateway:
typees 100, fuera del rango de Discord a propósito. Una biblioteca que no conozca el número puede tratarlo como un canal corriente;gamevox_type: "app"es la forma fiable de reconocer uno.GET /channels/{id},GET /guilds/{id}/channels,PATCHyDELETE /channels/{id}funcionan como con otros canales.PATCHtambién aceptagamevox_app: {"start_route", "icon"}, solo en un canal de tu propia aplicación, y requiere permiso para editar canales.
El manifiesto
Cada app publica un manifiesto: su nombre, icono, ruta de inicio, menú de navegación y habilidades. Publícalo en cada arranque con PUT /applications/@me/app-manifest. Sobrescribe el guardado y se puede repetir sin 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 | Notas |
|---|---|
name | Obligatorio, hasta 50 caracteres. Puede tener name_localizations. |
description | Hasta 300 caracteres. Puede tener description_localizations. |
icon | Un icono (ver más abajo). |
home | La ruta de llegada. Por defecto home. |
nav | El menú: hasta 12 elementos de primer nivel y 30 en total. |
abilities | Hasta 25 permisos definidos por la app. |
kind | Debe ser layout (el valor por defecto). Los demás tipos están reservados y se rechazan. |
storage | Opcional. Solo para apps que aceptan subidas de archivos (ver Archivos de app). Una app que no declara almacenamiento no recibe subidas. |
Los elementos de navegación son {label, route, icon?, ability?, children?}. Una etiqueta tiene hasta 40 caracteres. Un elemento con children es un grupo: puede omitir su propia route, y los grupos solo se anidan un nivel. Un elemento con una ability se oculta a quien no la tenga, y esa habilidad debe estar declarada en abilities. Las rutas cumplen ^[A-Za-z0-9._~/?=&%:-]{1,200}$.
Las habilidades son {key, label, description?, default}: una clave que cumpla ^[a-z0-9_]{1,32}$, una etiqueta de hasta 40 caracteres, una descripción de hasta 200 y un default que sea everyone (todos los grupos que pueden ver el canal), managers (los grupos de gestión, ver Contexto por persona) o nobody. La administración puede cambiar el valor por defecto por grupo en cada canal de app.
Los iconos son exactamente uno de {"emoji": "…"} (un solo emoji, dibujado como Twemoji), {"builtin": "…"} (un glifo de GameVox: calendar, trophy, list, chart, shield, sword, map, users, clock, star, flag, book, gift, bolt, home, game) o {"image_data": "<base64>"} (un PNG o WebP cuadrado, de 16 a 2048 píxeles, hasta 128 KB). GameVox analiza y guarda image_data y devuelve {"url": …} en el manifiesto guardado. Una URL que alojes tú se rechaza, porque nunca se mostraría a los miembros; puedes devolver un icono url que hayas leído de GameVox.
La respuesta es 200 con el manifiesto guardado (con los valores por defecto rellenados y las imágenes sustituidas por URL). Un error es un 400 que nombra el campo, por ejemplo nav[2].route is not a valid route. Publicar el mismo manifiesto otra vez no cambia nada. Un manifiesto modificado hace que todos tus canales de app abiertos se actualicen, así que un menú nuevo aparece sin que nadie recargue. GET /applications/@me/app-manifest devuelve lo guardado, o 404 si nunca publicaste uno.
Localizar el manifiesto
Cada texto del manifiesto que lee una persona puede llevar traducciones, igual que un comando slash de Discord lleva name_localizations. Pon un mapa junto al texto que traduce. El campo normal sigue siendo el predeterminado: es lo que ve un miembro cuando el mapa no tiene entrada para su idioma.
{
"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_localizationsydescription_localizationsen el manifiesto (textos de hasta 50 y 300 caracteres),label_localizationsen un elemento de navegación (hasta 40),label_localizationsydescription_localizationsen una habilidad (hasta 40 y 200).- Las claves son códigos de idioma de Discord (
de,fr,pt-BR,es-ES,zh-CN,jay el resto de la lista de Discord). Un idioma para el que Discord no tiene código (ar,he,fa,ms,sk,et,lv,sr,sl,tl,ur,bn,ta,sw,ca) usa su etiqueta de idioma de GameVox. Las claves distinguen mayúsculas y minúsculas, un mapa admite como máximo 64 idiomas y una clave desconocida da un400que nombra el campo. - Un valor en blanco,
nullo igual al texto predeterminado se descarta, así quenullborra un idioma. Un valor que supera el límite del texto da un400, por ejemplonav[2].label_localizations[de] is longer than 40 characters. - Las claves de habilidad, las rutas y los iconos no se localizan. Las vistas no forman parte del manifiesto: renderiza cada vista en el idioma de quien la ve, usando el
localede la petición.
El manifiesto sin sus mapas conserva su límite de 64 KB. Los mapas tienen su propio presupuesto de 192 KB adicionales, así que cabe un manifiesto en 44 idiomas (el documento entero ocupa como máximo 256 KB). Un manifiesto sin mapas sigue limitado a 64 KB como antes.
GameVox resuelve los mapas para cada miembro y envía a los clientes cadenas simples. El idioma es el que el cliente del miembro informó por última vez, el mismo que elige el idioma de las descripciones de los comandos slash. Abarca el nombre y la descripción de la app en la cabecera del canal y en la lista Crear canal, el menú, los nombres y descripciones de las habilidades en el editor de permisos y el nombre de la app en las notificaciones. GET /applications/@me/app-manifest devuelve el manifiesto guardado con sus mapas.
Peticiones y respuestas
Cuando un miembro abre el canal, navega, actualiza o pulsa un botón, tu bot recibe un evento dispatch del gateway (opcode 0) llamado APP_VIEW_REQUEST. No necesita ningún intent. Solo va a una sesión de gateway activa de tu aplicación.
{
"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 | Notas |
|---|---|
nonce | De un solo uso. Devuélvelo con tu respuesta. |
kind | open (el miembro abrió route, navegó hasta ella o la actualizó) o action (pulsó un botón o envió un formulario). |
guild_id, channel_id | El servidor y el canal de app que se está viendo, como snowflakes. Organiza los datos por canal con channel_id. |
route | La ruta que mostrar. Nunca está vacía: si el cliente no envía ninguna, es la ruta de inicio del canal y, si no, tu home. |
params | Los parámetros del enlace o botón que llevó hasta aquí (datos simples, hasta 2 KB). Siempre un objeto. |
action, values | Solo en kind: "action": el nombre de la acción y los valores enviados del formulario (un objeto vacío para un botón simple). |
user | La persona que mira: id (snowflake), username, display_name (hoy igual que el nombre de usuario) y avatar_url (puede estar vacío). |
abilities | Las habilidades que tiene esta persona en este canal. Hazlas cumplir tú. |
is_manager | True para quien es propietario del servidor, un grupo de administración o un grupo con el permiso «Editar servidor» o «Gestionar integraciones». |
locale | El idioma del cliente de la persona (por ejemplo en-US o de); puede estar vacío. Úsalo para responder en su idioma. |
platform | desktop, web, ios o android. |
self_hosted | True cuando el canal está en un servidor autoalojado. |
plan | El plan de la app en este servidor: {key, limits} (ver Planes). |
Responder
Responde con una llamada a POST /applications/@me/app-view-response, autenticada como cualquier otra llamada 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 viewsustituye la pantalla de la persona. En una acción que solo necesita un aviso, omítelo: la persona conserva la pantalla actual.toastes una línea corta (hasta 200 caracteres) que se muestra como aviso emergente. Puede ir junto con unaview.error(hasta 300 caracteres) muestra a la persona un mensaje en lugar de una pantalla. Cuando se fijaerror,viewytoastse ignoran.open_urlpide al cliente que abra una URLhttpstras su aviso habitual de enlace externo. Cualquier otro esquema se descarta.- Si todo va bien, la respuesta es
204. La vista se revisa y se limpia después (ver Qué hace el saneador), así que un204no significa que todos los campos hayan sobrevivido.
Tiempos
- La persona espera 10 segundos. Después ve «La app no respondió. Inténtalo de nuevo.», y una respuesta tardía se descarta. Intenta responder con margen; la biblioteca de bots de GameVox da a cada petición un plazo de 8 segundos.
- El nonce se acepta durante 15 segundos y una sola vez. Una segunda respuesta, o una respuesta después de que caduque, recibe
404. - Si tu aplicación tiene varias sesiones de gateway (shards, o un solapamiento al reiniciar), más de una puede recibir la misma petición. Respóndela una vez; solo cuenta la primera respuesta.
- Tu bot cuenta como en línea mientras su sesión de gateway está conectada y enviando heartbeats. Mientras está desconectado, GameVox no le pregunta: una apertura muestra tu instantánea, y una acción le dice al miembro que la app está desconectada.
El esquema de vista
Una vista es un documento de diseño en JSON: tipo layout, versión 1. Cada cliente la dibuja de forma nativa. Los campos desconocidos se ignoran y los tipos de bloque desconocidos se descartan, así que se pueden añadir campos opcionales nuevos sin romper nada.
{
"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 | Notas |
|---|---|
version | 1 o ausente. Cualquier otra versión muestra «Esta pantalla necesita una versión más reciente de GameVox.» |
kind | layout o ausente. La misma regla. |
title | Hasta 100 caracteres; se muestra en la cabecera. |
subtitle | Hasta 200 caracteres. |
route | La ruta que muestra esta pantalla. Puede ser distinta de la pedida (una redirección); el cliente la anota en su historial. Si la omites, GameVox pone la ruta pedida. |
nav_active | Qué elemento de navegación resaltar. |
breadcrumbs | Hasta 5 {label, route?}, etiquetas de hasta 100 caracteres. |
tabs | Hasta 8 pestañas de subnavegación {label, route, badge?}; se selecciona la pestaña cuya ruta coincide. Etiquetas de hasta 40, insignias de hasta 20. |
actions | Hasta 4 botones de cabecera (ver Botones). |
refresh_seconds | Los clientes vuelven a abrir la pantalla con esta frecuencia mientras está visible y la ventana tiene el foco: de 15 a 3600 segundos. |
blocks | El contenido: como mucho 60 bloques en una lista y 600 bloques en toda la vista. |
Bloques
Cada bloque tiene un type y un id opcional (hasta 64 caracteres). Las notas indican los límites: lo que pase de un límite se recorta o se descarta.
| Tipo | Campos | Notas |
|---|---|---|
heading | text, level | Un encabezado. Texto de hasta 200 caracteres, nivel de 1 a 3 (por defecto 2). |
text | markdown | Un párrafo de markdown, hasta 4.000 caracteres. |
callout | style: info | success | warning | error, markdown | Un recuadro destacado con markdown. El estilo por defecto es info. |
divider | Una línea horizontal. | |
section | title, description, blocks, collapsible, collapsed | Un grupo de bloques con un título opcional (100) y una descripción opcional (200), que opcionalmente se puede plegar. Los contenedores se anidan hasta dos niveles. |
columns | columns: [{width 1-12, blocks}] | Columnas lado a lado en una cuadrícula de 12 unidades de ancho (hasta 4 columnas). En pantallas estrechas se apilan. |
stats | items: [{label, value, delta, trend, color, icon}] | Fichas de estadísticas (hasta 8). Valor de hasta 100 caracteres, variación de hasta 30, tendencia up, down o flat. |
key_value | items: [{label, value, user}] | Pares de etiqueta y valor (hasta 30). Valores de hasta 500 caracteres. Una fila puede ser solo un user, que dibuja la ficha de un miembro. |
table | columns: [{key, label, align, sortable, kind}], rows: [{id, cells, action}], page_size, emptykind: text | number | user | badge | progress | time | icon | image | Una tabla: hasta 10 columnas y 200 filas, que el cliente ordena y pagina (page_size de 1 a 100, por defecto 25). Los valores de las celdas siguen el kind de la columna; celdas de hasta 500 caracteres. Las filas sin id reciben uno. |
list | items: [{id, title, subtitle, user, icon, image, badges, meta, action, buttons}] | Una lista (hasta 100 elementos). Títulos de hasta 200 caracteres, hasta 5 insignias y 5 botones por elemento. |
progress | label, value, max, color | Una barra de progreso. max es 100 por defecto; el valor se mantiene entre 0 y max. |
countdown | label, until | Una cuenta atrás en vivo hasta una fecha y hora RFC 3339. Se descarta sin until. |
timeline | items: [{time, title, markdown, icon, color}] | Una línea de tiempo (hasta 50 entradas). El markdown de cada entrada, de hasta 500 caracteres. |
calendar | mode: month | week | agenda, start, events: [{id, title, start, end, color, action}] | Un calendario (hasta 200 eventos). En pantallas estrechas se muestra por defecto la vista de agenda. Los botones anterior y siguiente envían la acción calendar_nav (ver Acciones). |
chart | kind: bar | line, series: [{name, color, points: [{x, y}]}], x_label, y_label | Un gráfico de barras o de líneas: hasta 5 series de 100 puntos. x es un número o una etiqueta. |
board | columns: [{id, title, cards: [{id, title, subtitle, user, image, badges, action}]}] | Un tablero de columnas y tarjetas: hasta 6 columnas de 50 tarjetas. Sin arrastrar y soltar. |
image | url | file_id, alt, aspect | Una imagen: una URL https o un archivo de app. Texto alternativo de hasta 200 caracteres, aspect como "16:9" o 1.5. |
video | file_id, poster_file_id, title | Un archivo de app de vídeo (solo para apps cuyo plan permite vídeo; ver Archivos de app). |
buttons | items: [Button] | Una fila de botones (hasta 5). |
form | id, fields: [Field], submit: Button | Un formulario (hasta 15 campos). Ver Formularios. |
empty | title, text, icon | Un estado vacío: título (100), texto (200) e icono. |
Valores comunes a varios bloques
- Los colores son
#rrggbbo un token:accent,success,warning,danger,muted. Las insignias son{text, color?}(texto de hasta 30) o una cadena sin más. - Las horas están en formato RFC 3339 y se muestran en la zona horaria de la persona que las ve.
startyenddel calendario también aceptan una fechaYYYY-MM-DD. - Los miembros (
user) son snowflakes de miembro. El cliente dibuja un avatar y el nombre visible, y un clic abre el perfil. - El markdown (
text,callout, entradas de la línea de tiempo) es el markdown del chat de GameVox. El HTML nunca se renderiza. Las menciones como<@id>,<@&id>y<#id>se muestran como nombres y nunca notifican a nadie. Cualquier otro campo de texto es texto plano. - Las imágenes deben ser URL
httpssin credenciales, de hasta 2000 caracteres. GameVox no tiene proxy de imágenes: una imagen que no esté en el almacenamiento de GameVox se conserva pero se marca"external": true, y los clientes la tratan como una imagen de terceros en el chat (sin referrer, sin cookies, quizá tras un clic). Los archivos de app lo evitan.
Qué hace el saneador
Tu vista es contenido de terceros que se dibuja en el cliente de cada miembro, así que GameVox la reconstruye a partir de una lista de permitidos antes de que ningún cliente la vea:
- Los tipos de bloque y los campos desconocidos se descartan.
- El texto pierde el UTF-8 no válido, los caracteres de control y los de anulación bidireccional, y se recorta a su límite.
- Las listas se recortan a su número máximo; los bloques que pasen de 60 en una lista o de 600 en la vista se descartan.
- Los colores, rutas, horas, ids y referencias a miembros que no tengan la forma correcta se vacían.
- Los elementos a los que les falta lo necesario se descartan: un elemento de lista sin título, una entrada de línea de tiempo sin hora, un botón sin etiqueta o sin destino, un formulario sin
ido sinsubmit.action. - Los
paramsde botones y filas deben ser datos simples (cadenas, números, booleanos, listas y objetos, hasta 5 niveles, 50 entradas cada uno, cadenas de hasta 500 caracteres, 2 KB en total), o se descartan junto con su botón. - Una vista solo se rechaza entera cuando no es un objeto JSON, pesa más de 256 KB (128 KB para una instantánea) o tiene otra
versionu otrokind. La persona ve entonces «Esta app envió una pantalla que GameVox no pudo mostrar.»
El endpoint de instantáneas responde con warnings, que lista lo que se cambió. Las respuestas en vivo no. View.Validate() de la biblioteca de bots en Go lista los mismos cambios antes de enviar.
Acciones e interacciones
Botones
Un botón o bien navega (tiene una route) o bien envía una acción a tu app (tiene una 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: obligatorio, hasta 40 caracteres.action: un nombre que cumpla^[A-Za-z0-9._:-]{1,64}$. Al pulsarlo te llegakind: "action"con este nombre y losparamsdel botón.route: navega en su lugar. El cliente abre esa ruta y recibeskind: "open"con losparamsdel botón. Si están los dos, ganaroute.params: un objeto de hasta 2 KB, que se te devuelve exactamente como lo escribiste.style(primary,secondary,success,danger),iconydisabled.ability: oculta el botón a quien no tenga esa habilidad.confirm:{title, text, confirm_label}(80, 300 y 40 caracteres) pregunta al miembro antes de enviar la acción.
Las filas de tabla, los elementos de lista, las tarjetas de tablero y los eventos de calendario aceptan un destino action con las mismas dos formas: {"action": "…", "params": …} o {"route": "…", "params": …}. Los clientes muestran un estado de carga en el control pulsado y nunca envían dos veces la misma pulsación.
Formularios
Un bloque form tiene un id (único en la vista), hasta 15 fields y un botón submit, que debe tener una action. Al enviarlo te llega kind: "action" con la acción y los parámetros del botón de envío, y los valores de los campos en values, indexados por la clave de cada 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: cumple^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$, única en el formulario.type:string,text,number,boolean,select,multiselect,channel,channels,role,roles,color,static,date,datetime,user(selector de miembro) ofile(ver Archivos de app).label(100),help(200),placeholder(100),required,disabled,min,max,step,max_length(hasta 2.000) yvaluepara rellenar de antemano.optionsparaselectymultiselect: hasta 25{value, label?, description?}. Si falta la etiqueta, se muestra el valor.channel_kindsacota un selector de canal. Los selectores de canal y de rol listan los canales y roles del propio servidor; los aporta GameVox, tú no envías ids.
Valores que recibes: cadenas (hasta 2.000 caracteres), números, booleanos o listas de hasta 25 cadenas; como mucho 30 valores y 32 KB en total. Los selectores de canal y de rol dan snowflakes, un selector de miembro da la snowflake del miembro, y un campo de archivo da una lista de objetos de archivo. GameVox solo comprueba la estructura: el cliente aplica required, min, max y las listas de opciones, así que comprueba tú cada valor.
Paginación del calendario: los botones anterior y siguiente de un bloque calendar envían la acción calendar_nav con params {"start": "YYYY-MM-DD", "mode": "month|week|agenda", "id": "<block id>"}. Responde con una vista cuyo calendario muestre ese intervalo.
Mantener las pantallas al día
refresh_secondsen una vista (de 15 a 3600) la vuelve a abrir con un temporizador mientras está visible.POST /channels/{channel_id}/app-view/invalidatecon{"routes": [...]}avisa a todos los que ven el canal de que su pantalla está desfasada. Omiteroutespara actualizar a todos, o indica hasta 20 rutas para actualizar solo a quienes estén en ellas (raidstambién coincide conraids?past=1). Los clientes vuelven a abrir tras un breve retardo aleatorio (de 1 a 3 segundos), así que una ráfaga te cuesta una petición por persona, no una por evento. Como mucho pasan a la vez 5 llamadas por segundo y canal; el resto se agrupa en una más un segundo después. Respuesta:204.PUT /channels/{channel_id}/app-view/snapshotcon{"view": {…}}guarda una pantalla pública, igual para todos los que pueden ver el canal, de hasta 128 KB. Se muestra mientras tu bot está desconectado, marcada como la última actualización. Envía{"view": null}para borrarla (respuesta{"cleared": true}). La respuesta es{"warnings": [...]}. Hasta 6 envíos cada 10 segundos por canal. Nunca pongas nada personal en una instantánea.- Solo la aplicación a la que pertenece un canal puede invalidarlo o fijar su instantánea.
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 por persona
Cada petición describe a una persona, así que las pantallas pueden ser personales: «tus personajes», «tu inscripción», botones que solo ven quienes gestionan.
- Identidad:
user.ides la snowflake del miembro, el mismo id que tu bot ve en los eventos del gateway y en REST. Guarda los datos de cada miembro con él. - Habilidades: para cada habilidad, la persona la tiene si alguno de sus grupos que pueden ver el canal tiene una excepción que la concede, o no tiene excepción y el valor por defecto del manifiesto la cubre (
everyone: todos esos grupos;managers: solo los grupos de gestión;nobody: ninguno). Quien es propietario del servidor tiene todas las habilidades. Un miembro sin grupo pasa al grupo por defecto del servidor. - Los gestores (
is_managery el valor por defectomanagers) son quien es propietario del servidor, los grupos de administración y los grupos con el permiso «Editar servidor» o «Gestionar integraciones». - La navegación se filtra para la persona: los elementos de navegación que requieren una habilidad que no tiene se ocultan. Es una cortesía. Ocultar un botón no es seguridad, y un cliente modificado puede enviar cualquier acción, ruta o parámetros.
- Idioma:
localees el idioma del cliente de la persona. Una pantalla pensada para todos (una instantánea) no puede conocerlo; usa ahí el idioma del servidor. - Miembros en lo que envías: escribe snowflakes de miembro en los campos
user, las celdas de tabla de miembro, los valores de selectores de miembro y las menciones. GameVox las traduce para los clientes y de vuelta; losparamsnunca se tocan.
Archivos de app, planes y notificaciones de app
Archivos de app
Una app puede aceptar subidas (por ejemplo, una captura de una puntuación) mediante un campo de formulario de tipo file, y mostrar archivos guardados en sus pantallas. Los archivos viven en un almacenamiento privado de GameVox, se analizan como los adjuntos del chat y solo se sirven mediante URL firmadas de corta duración.
- Declara el almacenamiento en el manifiesto:
"storage": {"kinds": ["image"], "max_file_bytes"?, "max_total_bytes"?, "max_video_bytes"?}. Sin él, tu app no recibe subidas. Los límites efectivos son los de tu plan, reducidos por lo que declares. - Añade un campo de archivo:
{"type": "file", "key", "label", "accept": ["image"], "max_files": 1 to 10, "required"?}. El cliente sube directamente al almacenamiento y espera al análisis antes de poder enviar el formulario. - Lee los valores: el valor del campo es una lista de
{id, url, name, mime, size, width, height}.urlestá firmada durante una hora; descarga el archivo dentro de tu manejador o llama después aGETsobre él para obtener una URL nueva. - Conserva lo que uses. Una subida está en
pendinghasta que se conserva, y un archivo pendiente se elimina a las 24 horas. Responder sinerrora la acción que lo trajo lo conserva automáticamente.POST /applications/@me/app-files/{id}/keepconserva uno a mano. Un archivo conservado se queda hasta que lo eliminas, vence la retención del plan, la moderación lo bloquea o tu app lleva 30 días quitada del servidor. - Muestra archivos con
{"file_id": "<id>"}en lugar de una URL:file_idde un bloque de imagen,imagede un elemento de lista o de una tarjeta de tablero, una celda de tabla en una columna de tipoimage. GameVox pone en su lugar una URL firmada para la persona. Poner un archivo en una vista es lo que permite que esa persona lo vea. Los ids de otra app o de otro servidor se descartan, y una vista lleva como mucho 100 ids de archivo. - Las instantáneas solo muestran archivos que hayas marcado como públicos:
PATCH /applications/@me/app-files/{id}con{"public_in_snapshot": true}. - Tipos: las imágenes (PNG, JPEG, GIF, WebP) ya funcionan. El vídeo (MP4 con H.264) solo funciona donde GameVox haya concedido a tu app una asignación de vídeo en ese servidor; WebM se rechaza porque no se puede analizar.
documentestá reservado: un manifiesto puede nombrarlo, pero todavía no se acepta ningún tipo de documento.
// 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 (todo bajo /applications/@me/, con token de bot, ids de guild, canal y de quien sube como snowflakes, e ids de archivo como cadenas UUID): GET app-files?guild_id=&state=&page= responde {files, page, has_more} (50 por página); GET, PATCH y DELETE app-files/{id}; POST app-files/{id}/keep; GET app-usage?guild_id= responde {plan, limits, used_bytes, files}. El archivo de otra app es 404; un archivo que aún no se ha subido, se ha eliminado o está bloqueado da 409 al conservarlo.
Planes
Cada petición lleva plan: {key, limits}, los límites de tu app en ese servidor tras aplicar el tope storage de tu manifiesto. Úsalo para limitar tus propios extras (una app construida sobre datos de otra empresa debe seguir las normas de esa empresa sobre funciones de pago). Hoy todas las apps funcionan con el plan free, salvo que el equipo de GameVox fije otro plan para un servidor; todavía no hay forma de comprar un plan.
| Campo | Notas |
|---|---|
max_file_bytes | Tamaño máximo de imagen: 8 MB. |
max_files_per_field | Archivos por campo de archivo: 4. |
max_total_bytes | Almacenamiento por app y servidor: 500 MB. Cuentan tanto los archivos pendientes como los conservados. |
max_uploads_per_user_per_day | Subidas por miembro y día: 50. |
kinds | Tipos permitidos: ["image"], y después acotados por tu manifiesto (vacío si no declaras almacenamiento). |
retention_days | Los archivos conservados se eliminan pasados estos días; 0 los conserva hasta que los elimines. |
max_video_bytes | Tamaño máximo de vídeo; 0 significa sin vídeo. |
Notificaciones de app
Los bots no pueden enviar mensajes directos. En su lugar, una app puede notificar a los miembros (una respuesta a un ticket, un recordatorio de raid) con POST /applications/@me/app-notifications. Los miembros la ven en la campana de notificaciones con el icono de tu app, como aviso emergente y como notificación push en el móvil si permiten las notificaciones de apps. Al tocarla se abre tu canal de app en 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(obligatorio),user_ids(de 1 a 25 snowflakes de miembro),title(obligatorio, hasta 80 caracteres),body(hasta 300, puede estar vacío),route,channel_id(un canal de app de tu app en ese servidor),dedupe_key(hasta 64). Demasiado texto da un400que nombra el límite; el texto nunca se recorta.- El título y el cuerpo son texto plano. Se quitan los caracteres de control y los bidireccionales.
- Al tocarla se abre
channel_idsi lo indicaste y, si no, el primer canal de app de tu app que el miembro pueda ver. Una app sin canal de app en el servidor envía notificaciones que no abren nada. - El mismo
dedupe_keyal mismo miembro dentro de 24 horas sustituye a la notificación anterior (vuelve a aparecer como no leída) en lugar de añadir otra. - La respuesta lista los ids de miembro
deliveredy losskippedcon unreason:not_member(también para un id que GameVox no conoce),muted(el miembro silenció tu app en ese servidor),cannot_view,rate_limitedoerror(reintenta más tarde). Un miembro omitido no consume nada. - Límites: 10 por hora y 30 por día por app y miembro, 500 por hora por app y servidor (un intento rechazado también cuenta), y 20 llamadas cada 10 segundos por bot.
403si tu app no está instalada en ese servidor. - Las notificaciones se conservan 30 días, como mucho 200 por miembro.
Servidores autoalojados
Los canales de app funcionan en servidores autoalojados con los mismos eventos y endpoints. Tu bot sigue conectándose solo a la nube. Las diferencias:
self_hostedestrueenAPP_VIEW_REQUEST.- La instancia del cliente comprueba quién puede ver el canal, calcula las habilidades de la persona a partir de sus propios grupos y luego pide a la nube que te envíe la petición. GameVox confía en la instancia para ello, como con cualquier otro permiso en un servidor autoalojado.
- La instantánea, la invalidación y la creación, edición y eliminación de un canal autoalojado se reenvían a la instancia como otras llamadas autoalojadas:
503cuando la instancia no está conectada,504cuando no responde a tiempo. - Un manifiesto modificado llega a una instancia en aproximadamente un minuto (guarda el manifiesto en caché 60 segundos) en lugar de al momento.
- Cuando la instancia no puede llegar a la nube, los miembros ven tu instantánea (sin los archivos que contenga) o un aviso de desconexión.
- Los archivos de app funcionan como en la nube y se guardan en el almacenamiento de GameVox, no en la instancia. El vídeo necesita una instancia actualizada.
- Para las notificaciones de app, la nube pregunta a la instancia quién puede ver el canal de app. A una instancia antigua o inaccesible no se le pregunta: entonces solo se comprueba la pertenencia al servidor, y un
channel_idque indiques se pasa sin comprobar. - Las traducciones del manifiesto también funcionan en servidores autoalojados, en el idioma que anuncia la conexión de cada miembro. Un servidor que aún no está actualizado muestra el texto predeterminado.
Notas de seguridad para autores de bots
- Aplica las habilidades en cada acción. Comprueba
abilities(y tus propias reglas) antes de cambiar nada. Que el cliente oculte un botón es una cortesía. - Trata la ruta, los parámetros, la acción y los valores como entrada de usuario. Vienen del cliente del miembro. Un cliente modificado puede enviar cualquier ruta, cualquier parámetro y cualquier nombre de acción, y saltarse los campos obligatorios y los límites del formulario. GameVox solo comprueba su forma.
- Confía en los ids que te da GameVox (
guild_id,channel_id,user.id,abilities), no en los ids que vengan en los parámetros. Para actuar sobre «raid 42», comprueba que el raid 42 pertenece a este canal. - Mantén públicas las instantáneas. Todos los que pueden ver el canal las ven. Nunca pongas datos personales en una, y marca como públicos solo los archivos que se puedan mostrar a todos.
- No filtres URL de archivo firmadas. Funcionan durante una hora para quien las tenga. Pon
file_iden las vistas y deja que GameVox las firme para cada persona. - Guárdate tus nonces. Cada uno responde a la petición de una persona; GameVox comprueba que pertenece a tu aplicación.
- Prefiere imágenes alojadas en GameVox. Las URL de imágenes de terceros se marcan como externas y pueden quedar ocultas tras un clic; además, le dicen al servidor de la imagen cuándo se muestra la pantalla.
- GameVox, por su parte, nunca confía en tu vista: se reconstruye a partir de una lista de permitidos, el HTML nunca se renderiza, las menciones nunca notifican, y
open_urlsolo admitehttpsy siempre pasa por el aviso de enlace externo.
Errores y límites
Respuestas REST
400: cuerpo no válido. Los errores de manifiesto y de instantánea nombran el campo. Una respuesta sin nonce también es un 400.403: el nonce o el canal pertenece a otra aplicación, o tu app no está instalada en ese servidor.404: nonce desconocido o caducado; canal desconocido, o un canal que no es de app (instantánea, invalidación); archivo desconocido; ningún manifiesto publicado (GET).409: el archivo no está en un estado que lo permita.413: cuerpo por encima del límite: manifiesto 64 KB (más 192 KB de traducciones), respuesta de vista 256 KB, instantánea 128 KB, petición de notificación 64 KB.429: por encima de un límite de tasa, con las cabeceras de límite de tasa habituales.
Límites de tasa
- Manifiesto: 10 publicaciones por minuto por aplicación (GET, 30 por minuto).
- Respuestas de vista: 200 por segundo por bot.
- Instantánea: 6 envíos cada 10 segundos por canal.
- Invalidación: pasan a la vez 5 por segundo por canal; las demás se agrupan, no se rechazan.
- Archivos de app: 30 llamadas cada 10 segundos por ruta; uso, 30 por minuto.
- Notificaciones de app: 20 llamadas cada 10 segundos por bot, más los límites por miembro y por servidor de arriba.
- Por persona: 20 aperturas y 10 acciones cada 10 segundos. Por aplicación, entre todas las personas: 600 peticiones cada 10 segundos. Por encima de esto se le dice a la persona que vaya más despacio o que la app está ocupada; a tu bot no se le pregunta.
Qué ve la persona
- «La app no respondió. Inténtalo de nuevo.» Sin respuesta en 10 segundos.
- «La app está desconectada ahora mismo.» Una acción mientras tu bot está desconectado.
- «Esta app envió una pantalla que GameVox no pudo mostrar.» La vista se rechazó entera.
- «Esta pantalla necesita una versión más reciente de GameVox.» Una vista con otra
versionu otrokind. - Tu propio texto de
error, cuando envías uno.
Un ejemplo mínimo de principio a fin
Una app de inscripción a raids con una pantalla y un formulario, usando HTTP directo. Los ids están acortados.
1. Publica el manifiesto cuando arranca el bot:
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. Consigue un canal. Un admin añade tu app con Crear canal ▸ App, o tu bot crea uno:
POST /api/v10/guilds/1491230000000000001/channels
{ "type": 100, "name": "raid-signup" }
HTTP/1.1 201 Created
{ "id": "1491240000000000002", "type": 100, "gamevox_type": "app", ... } 3. Un miembro lo abre. Tu bot recibe:
{ "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. Responde con la pantalla (en menos de 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. El miembro envía el formulario. Recibes una acción con los 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. Comprueba, guarda y responde con un aviso y la pantalla actualizada, y luego pide a las demás personas que actualicen:
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: una instantánea para cuando el bot está desconectado. Es pública, así que muestra el recuento, no quién se inscribió:
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": [] } En una biblioteca de Discord, lee el evento desde su hook de gateway en bruto (on_socket_raw_receive en discord.py, el evento raw en discord.js; consulta la tabla de Ajustes dentro de la app). Una versión en discord.py de los pasos 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) Los bots propios de GameVox usan la biblioteca de bots en Go de server/shared-gamebot, que publica el manifiesto, responde a las peticiones dentro de un plazo, valida las vistas y agrupa las invalidaciones por ti.
Aún no disponible
- Un tipo
web(una página en un sandbox en lugar de un diseño) está reservado. Los manifiestos y vistas que lo usen se rechazan. - Las subidas de documentos y la compra de un plan no están disponibles.
- Arrastrar y soltar en los tableros.