App-Kanäle
Ein App-Kanal ist ein Kanal in der Kanalliste eines Servers, dessen Inhalt aus Ansichten besteht, die deine App liefert, solange sie online ist: ein Raidplaner, eine Rangliste, ein Ticketschalter, eine Serverstatusseite. GameVox speichert, wo der Kanal liegt, wer ihn sehen darf und was dort erlaubt ist. Deine App besitzt den Inhalt und rendert jede Ansicht auf Anfrage.
Überblick
Eine App ist ein Ort mit vielen Ansichten. Jede Ansicht ist eine Route, ein String, den deine App festlegt (home, raids, raids/42, roster?sort=ilvl). Wer den Kanal öffnet, landet auf seiner Startroute (normalerweise deinem home) und bewegt sich über dein Navigationsmenü, über Links und Buttons in den Ansichten sowie mit Zurück und Vorwärts.
Was Mitglieder sehen
- Den Kanal in der Kanalliste mit deinem Symbol (oder einem, das der Server gewählt hat) und einem kleinen Punkt, solange deine App offline ist.
- Dein Navigationsmenü, Zurück und Vorwärts, einen Home-Button und Aktualisieren rund um die Ansicht, die du zurückgegeben hast.
- Ansichten, die jeder Client nativ aus deinem JSON zeichnet, im eigenen Theme des Clients, hell und dunkel.
- Solange dein Bot offline ist: den letzten öffentlichen Snapshot, den du gesendet hast, als letzter Stand markiert, oder einen Offline-Hinweis, falls es keinen gibt.
Clients: die Desktop-App (Windows, macOS, Linux), die Web-App, iOS und Android. Die Xbox-App zeigt keine App-Kanäle. Ein Client, der zu alt ist, um sie zu kennen, bekommt App-Kanäle gar nicht erst in seine Kanalliste, sodass niemand einen leeren Textkanal sieht.
Der Unterschied zu Slash-Befehlen und Komponenten
- Slash-Befehle und Nachrichtenkomponenten leben in Nachrichten eines Textkanals und erreichen deine App als
INTERACTION_CREATEmit einem Interaktions-Token. Ein App-Kanal hat überhaupt keine Nachrichten: Posten in einen App-Kanal wird abgelehnt. - Eine View-Anfrage kommt als eigenes Gateway-Ereignis,
APP_VIEW_REQUEST, und du beantwortest sie mit einem REST-Aufruf, der eine Einmal-Nonce mitführt. Es gibt kein Interaktions-Token, kein Zurückstellen (defer), keine Follow-ups. HTTP-Interaktionsendpunkte werden nicht genutzt, deine App braucht also eine aktive Gateway-Verbindung. - Jede Ansicht ist persönlich. GameVox fragt sie jedes Mal an, wenn ein Mitglied sie öffnet oder benutzt, und speichert sie nie (nur der optionale öffentliche Snapshot wird gespeichert).
- Innerhalb der Ansicht legt deine App eigene Berechtigungen fest, sogenannte Fähigkeiten (abilities). Server-Admins vergeben sie pro Gruppe, und GameVox teilt dir bei jeder Anfrage mit, welche davon die betrachtende Person hat.
- Das ist eine GameVox-Erweiterung ohne Discord-Entsprechung. Bots, die gegen Discord-Bibliotheken geschrieben sind, funktionieren weiter; sie erhalten das Ereignis einfach nie.
Wie alles zusammenspielt
Ein Mitglied öffnet den Kanal, navigiert oder drückt einen Button
│
▼
GameVox ──── APP_VIEW_REQUEST { nonce, kind, route, user, abilities } ────▶ dein Bot
▲
│
dein Bot ──── POST /applications/@me/app-view-response { nonce, view } ────▶ GameVox
│
▼
GameVox prüft und bereinigt die View, dann zeichnet der Client sie GameVox prüft bei jedem Öffnen und jeder Aktion, ob die betrachtende Person den Kanal sehen darf, berechnet ihre Fähigkeiten und fragt erst dann deine App. Deine Antwort geht nur an diese eine Person.
Lebenszyklus
- Ein Server installiert deine App auf dem üblichen Weg (über den Installationslink oder die OAuth2-Autorisierungsseite).
- Bei jedem Start veröffentlicht dein Bot sein Manifest. Eine App ohne Manifest kann keine App-Kanäle haben.
- Ein Server-Admin wählt Kanal erstellen ▸ App. Die Liste zeigt installierte Apps, die ein Manifest veröffentlicht haben, mit Name, Beschreibung und Symbol. Der Kanalname wird aus deiner App übernommen, und ein optionales Menü Öffnet bei listet deine Navigationseinträge auf (Standard: home). Zum Erstellen braucht es die übliche Berechtigung, Kanäle zu erstellen. Auch dein Bot kann einen erstellen (siehe unten).
- In den Kanaleinstellungen kann der Admin den Kanal umbenennen, sein Symbol ändern (das deiner App, ein Emoji oder ein hochgeladenes Bild), festlegen, wo er sich öffnet, und Berechtigungen setzen: wer den Kanal sehen darf und welche Gruppen jede deiner Fähigkeiten haben.
- Mitglieder, die den Kanal sehen dürfen, öffnen ihn, und deine App beantwortet
APP_VIEW_REQUESTfür jede Ansicht, die sie aufrufen. - Wird deine App vom Server entfernt, bleiben ihre App-Kanäle bestehen (eine Neuinstallation bringt sie zurück) und zeigen „Diese App wurde vom Server entfernt“, mit einem Löschen-Button für Verwaltende. Wird die Anwendung gelöscht oder hat sie nie ein Manifest veröffentlicht, zeigt der Kanal „Diese App ist nicht mehr verfügbar“.
App-Kanäle zählen wie jeder andere Kanal zum Kanallimit eines Servers. Ein Server hat meist einen Kanal pro App; ein zweiter Kanal für dieselbe App ist erlaubt (zum Beispiel ein Kanal für ein Raidteam, der sich beim Raidplaner öffnet) und ist dieselbe App mit derselben Navigation. Jede Anfrage führt die channel_id mit, du kannst also pro Kanal getrennte Daten halten.
Einen App-Kanal mit deinem Bot erstellen
Nutze die normale Route zum Erstellen von Kanälen mit type 100 und einem optionalen Objekt gamevox_app:
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" }
} - Nur für deine eigene Anwendung, die auf diesem Server installiert sein und ein Manifest veröffentlicht haben muss. Der Bot braucht wie bei jedem Kanal die Berechtigung, Kanäle zu erstellen.
gamevox_app.start_routeist eine Route (leer oder fehlend: dashomedeines Manifests).gamevox_app.iconist einemoji, einbuiltin-Symbol oder eineurl, die dein Manifest bereits enthält. Bots können kein Kanalbild hochladen; das können nur Admins.- Ohne
positionlandet der Kanal am Ende der Liste. Der Name hat 1 bis 100 Zeichen. - Wer den Kanal sehen darf und wer welche Fähigkeit hat, legen die Server-Admins in den Kanaleinstellungen fest.
Ein App-Kanal ist ein normales Kanalobjekt mit GameVox-Erweiterungen, in REST-Antworten und in Gateway-Kanalereignissen:
typeist 100, bewusst außerhalb des Discord-Bereichs. Eine Bibliothek, die die Zahl nicht kennt, behandelt den Kanal eventuell als einfachen Kanal;gamevox_type: "app"ist der verlässliche Weg, einen App-Kanal zu erkennen.GET /channels/{id},GET /guilds/{id}/channels,PATCHundDELETE /channels/{id}funktionieren wie bei anderen Kanälen.PATCHnimmt außerdemgamevox_app: {"start_route", "icon"}an, nur bei einem Kanal deiner eigenen Anwendung, und braucht die Berechtigung, Kanäle zu bearbeiten.
Das Manifest
Jede App veröffentlicht ein Manifest: Name, Symbol, Startroute, Navigationsmenü und Fähigkeiten. Veröffentliche es bei jedem Start mit PUT /applications/@me/app-manifest. Es überschreibt das gespeicherte und kann gefahrlos wiederholt werden.
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" }
]
} | Feld | Hinweise |
|---|---|
name | Erforderlich, bis zu 50 Zeichen. Kann name_localizations haben. |
description | Bis zu 300 Zeichen. Kann description_localizations haben. |
icon | Ein Symbol (siehe unten). |
home | Die Startroute. Standard home. |
nav | Das Menü: bis zu 12 Einträge auf oberster Ebene und 30 insgesamt. |
abilities | Bis zu 25 von der App festgelegte Berechtigungen. |
kind | Muss layout sein (der Standard). Andere Arten sind reserviert und werden abgelehnt. |
storage | Optional. Nur für Apps, die Datei-Uploads annehmen (siehe App-Dateien). Eine App, die keinen Speicher deklariert, bekommt keine Uploads. |
Navigationseinträge sind {label, route, icon?, ability?, children?}. Ein Label hat bis zu 40 Zeichen. Ein Eintrag mit children ist eine Gruppe: Er darf seine eigene route weglassen, und Gruppen lassen sich nur eine Ebene tief verschachteln. Ein Eintrag mit einer ability ist für Personen ohne diese Fähigkeit ausgeblendet, und die Fähigkeit muss in abilities deklariert sein. Routen entsprechen ^[A-Za-z0-9._~/?=&%:-]{1,200}$.
Fähigkeiten sind {key, label, description?, default}: ein Schlüssel, der ^[a-z0-9_]{1,32}$ entspricht, ein Label mit bis zu 40 Zeichen, eine Beschreibung mit bis zu 200 und ein default von everyone (jede Gruppe, die den Kanal sehen darf), managers (verwaltende Gruppen, siehe Kontext pro Person) oder nobody. Admins können den Standard in jedem App-Kanal pro Gruppe überschreiben.
Symbole sind genau eines von {"emoji": "…"} (ein einzelnes Emoji, als Twemoji gezeichnet), {"builtin": "…"} (ein GameVox-Symbol: calendar, trophy, list, chart, shield, sword, map, users, clock, star, flag, book, gift, bolt, home, game) oder {"image_data": "<base64>"} (ein quadratisches PNG oder WebP, 16 bis 2048 Pixel, bis zu 128 KB). GameVox prüft und speichert image_data und gibt im gespeicherten Manifest {"url": …} zurück. Eine URL, die du selbst hostest, wird abgelehnt, weil sie Mitgliedern nie angezeigt würde; du darfst ein url-Symbol zurücksenden, das du von GameVox gelesen hast.
Die Antwort ist 200 mit dem gespeicherten Manifest (Standardwerte ergänzt, Bilder durch URLs ersetzt). Ein Fehler ergibt eine 400-Antwort, die das Feld nennt, zum Beispiel nav[2].route is not a valid route. Dasselbe Manifest erneut zu veröffentlichen, ändert nichts. Ein geändertes Manifest aktualisiert jeden geöffneten App-Kanal von dir, sodass ein neues Menü erscheint, ohne dass jemand neu lädt. GET /applications/@me/app-manifest gibt zurück, was gespeichert ist, oder 404, wenn du nie eines veröffentlicht hast.
Das Manifest lokalisieren
Jeder Text im Manifest, den Menschen lesen, kann Übersetzungen tragen, so wie ein Discord-Slash-Befehl name_localizations trägt. Lege eine Map neben den Text, den sie übersetzt. Das einfache Feld bleibt der Standard: Das sehen Mitglieder, wenn die Map für ihre Sprache keinen Eintrag hat.
{
"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_localizationsunddescription_localizationsam Manifest (Texte bis zu 50 bzw. 300 Zeichen),label_localizationsan einem Navigationseintrag (bis zu 40),label_localizationsunddescription_localizationsan einer Fähigkeit (bis zu 40 bzw. 200).- Schlüssel sind Discord-Locale-Codes (
de,fr,pt-BR,es-ES,zh-CN,jaund der Rest der Discord-Liste). Eine Sprache, für die Discord keinen Code hat (ar,he,fa,ms,sk,et,lv,sr,sl,tl,ur,bn,ta,sw,ca), verwendet ihr GameVox-Sprachkürzel. Schlüssel unterscheiden Groß- und Kleinschreibung, eine Map enthält höchstens 64 Locales, und ein unbekannter Schlüssel ist ein400, der das Feld nennt. - Ein leerer Wert,
nulloder ein Wert, der dem Standardtext entspricht, wird verworfen; mitnulllöschst du also eine Locale. Ein Wert über dem Limit des Textes ist ein400, zum Beispielnav[2].label_localizations[de] is longer than 40 characters. - Fähigkeitsschlüssel, Routen und Symbole werden nicht lokalisiert. Ansichten gehören nicht zum Manifest: Rendere jede Ansicht in der Sprache des Betrachters, anhand des Felds
localeder Anfrage.
Das Manifest ohne seine Maps behält sein Limit von 64 KB. Die Maps bekommen darüber hinaus ein eigenes Budget von 192 KB, sodass ein Manifest in 44 Sprachen hineinpasst (das ganze Dokument hat höchstens 256 KB). Ein Manifest ohne Maps bleibt wie bisher auf 64 KB begrenzt.
GameVox löst die Maps für jedes Mitglied auf und schickt den Clients einfache Strings. Die Sprache ist die, die der Client des Mitglieds zuletzt gemeldet hat, dieselbe, nach der sich auch die Sprache der Slash-Befehl-Beschreibungen richtet. Das betrifft App-Name und Beschreibung in der Kanalkopfzeile und in der Liste „Kanal erstellen“, das Menü, die Namen und Beschreibungen der Fähigkeiten im Berechtigungseditor sowie den App-Namen in Benachrichtigungen. GET /applications/@me/app-manifest liefert das gespeicherte Manifest mit seinen Maps.
Anfragen und Antworten
Wenn ein Mitglied den Kanal öffnet, navigiert, aktualisiert oder einen Button drückt, erhält dein Bot einen Gateway-Dispatch (Opcode 0) namens APP_VIEW_REQUEST. Er braucht keinen Intent. Er geht nur an eine aktive Gateway-Sitzung deiner Anwendung.
{
"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
}
}
}
} | Feld | Hinweise |
|---|---|
nonce | Einmalig verwendbar. Sende sie mit deiner Antwort zurück. |
kind | open (das Mitglied hat route geöffnet, ist dorthin navigiert oder hat sie aktualisiert) oder action (es hat einen Button gedrückt oder ein Formular abgeschickt). |
guild_id, channel_id | Der Server und der betrachtete App-Kanal, als Snowflakes. Ordne kanalbezogene Daten über channel_id zu. |
route | Die anzuzeigende Route. Nie leer: Sendet der Client keine, ist es die Startroute des Kanals, sonst dein home. |
params | Die Parameter des Links oder Buttons, der hierher geführt hat (einfache Daten, bis zu 2 KB). Immer ein Objekt. |
action, values | Nur bei kind: "action": der Aktionsname und die übermittelten Formularwerte (ein leeres Objekt bei einem einfachen Button). |
user | Die betrachtende Person: id (Snowflake), username, display_name (derzeit identisch mit dem Benutzernamen) und avatar_url (kann leer sein). |
abilities | Die Fähigkeiten, die diese Person in diesem Kanal hat. Setze sie selbst durch. |
is_manager | True für den Server-Eigentümer, eine Admin-Gruppe oder eine Gruppe mit der Berechtigung „Server bearbeiten“ oder „Integrationen verwalten“. |
locale | Die Client-Sprache der Person (zum Beispiel en-US oder de); kann leer sein. Nutze sie, um in ihrer Sprache zu antworten. |
platform | desktop, web, ios oder android. |
self_hosted | True, wenn der Kanal auf einem selbst gehosteten Server liegt. |
plan | Der Tarif der App auf diesem Server: {key, limits} (siehe Tarife). |
Antworten
Antworte mit einem Aufruf von POST /applications/@me/app-view-response, authentifiziert wie jeder andere Bot-Aufruf (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 viewersetzt die Ansicht der Person. Bei einer Aktion, die nur einen Toast braucht, lass es weg: Die Person behält die aktuelle Ansicht.toastist eine kurze Zeile (bis zu 200 Zeichen), die als Toast angezeigt wird. Er kann zusammen mit einerviewgesendet werden.error(bis zu 300 Zeichen) zeigt der Person statt einer Ansicht eine Meldung. Isterrorgesetzt, werdenviewundtoastignoriert.open_urlbittet den Client, nach seiner üblichen Abfrage für externe Links einehttps-URL zu öffnen. Jedes andere Schema wird verworfen.- Erfolg ist
204. Die View wird danach geprüft und bereinigt (siehe Was der Sanitizer macht), ein204bedeutet also nicht, dass jedes Feld erhalten geblieben ist.
Zeitverhalten
- Die Person wartet 10 Sekunden. Danach sieht sie „Die App hat nicht geantwortet. Versuch es noch einmal.“, und eine verspätete Antwort wird verworfen. Antworte möglichst deutlich innerhalb dieser Zeit; die GameVox-Bot-Bibliothek gibt jeder Anfrage eine Frist von 8 Sekunden.
- Die Nonce wird 15 Sekunden lang und nur einmal angenommen. Eine zweite Antwort oder eine Antwort nach Ablauf bekommt
404. - Hält deine Anwendung mehrere Gateway-Sitzungen (Shards oder eine Überlappung beim Neustart), kann mehr als eine dieselbe Anfrage erhalten. Beantworte sie einmal; nur die erste Antwort zählt.
- Dein Bot gilt als online, solange seine Gateway-Sitzung verbunden ist und Heartbeats sendet. Solange er offline ist, fragt GameVox ihn nicht: Ein Öffnen zeigt deinen Snapshot, und bei einer Aktion erfährt das Mitglied, dass die App offline ist.
Das View-Schema
Eine View ist ein JSON-Layoutdokument: Art layout, Version 1. Jeder Client zeichnet sie nativ. Unbekannte Felder werden ignoriert und unbekannte Blocktypen verworfen, sodass neue optionale Felder hinzukommen können, ohne bestehende Implementierungen zu beeinträchtigen.
{
"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>." }
]
} | Feld | Hinweise |
|---|---|
version | 1 oder fehlend. Jede andere Version zeigt „Diese Ansicht braucht eine neuere Version von GameVox.“ |
kind | layout oder fehlend. Gleiche Regel. |
title | Bis zu 100 Zeichen, in der Kopfzeile angezeigt. |
subtitle | Bis zu 200 Zeichen. |
route | Die Route, die diese Ansicht zeigt. Sie kann von der angefragten abweichen (eine Weiterleitung); der Client trägt sie in seinen Verlauf ein. Lässt du sie weg, setzt GameVox die angefragte Route ein. |
nav_active | Welcher Navigationseintrag hervorgehoben wird. |
breadcrumbs | Bis zu 5 {label, route?}, Labels bis zu 100 Zeichen. |
tabs | Bis zu 8 Unternavigations-Tabs {label, route, badge?}; der Tab, dessen Route passt, ist ausgewählt. Labels bis zu 40, Badges bis zu 20. |
actions | Bis zu 4 Buttons in der Kopfzeile (siehe Buttons). |
refresh_seconds | Clients öffnen die Ansicht in diesem Abstand neu, solange sie sichtbar ist und das Fenster den Fokus hat: 15 bis 3600 Sekunden. |
blocks | Der Inhalt: höchstens 60 Blöcke in einer Liste und 600 Blöcke in der gesamten View. |
Blöcke
Jeder Block hat einen type und eine optionale id (bis zu 64 Zeichen). Die Hinweise nennen die Grenzen: Alles, was eine Grenze überschreitet, wird gekürzt oder verworfen.
| Typ | Felder | Hinweise |
|---|---|---|
heading | text, level | Eine Überschrift. Text bis zu 200 Zeichen, Ebene 1 bis 3 (Standard 2). |
text | markdown | Ein Absatz in Markdown, bis zu 4.000 Zeichen. |
callout | style: info | success | warning | error, markdown | Ein hervorgehobener Kasten mit Markdown. Der Stil ist standardmäßig info. |
divider | Eine horizontale Linie. | |
section | title, description, blocks, collapsible, collapsed | Eine Gruppe von Blöcken mit optionalem Titel (100) und optionaler Beschreibung (200), wahlweise einklappbar. Container lassen sich zwei Ebenen tief verschachteln. |
columns | columns: [{width 1-12, blocks}] | Nebeneinanderliegende Spalten in einem 12er-Raster (bis zu 4 Spalten). Auf schmalen Bildschirmen werden sie gestapelt. |
stats | items: [{label, value, delta, trend, color, icon}] | Kennzahlenkacheln (bis zu 8). Wert bis zu 100 Zeichen, Delta bis zu 30, Trend up, down oder flat. |
key_value | items: [{label, value, user}] | Paare aus Label und Wert (bis zu 30). Werte bis zu 500 Zeichen. Eine Zeile darf nur aus einem user bestehen, das einen Mitglieds-Chip zeichnet. |
table | columns: [{key, label, align, sortable, kind}], rows: [{id, cells, action}], page_size, emptykind: text | number | user | badge | progress | time | icon | image | Eine Tabelle: bis zu 10 Spalten und 200 Zeilen, vom Client sortiert und seitenweise angezeigt (page_size 1 bis 100, Standard 25). Zellwerte folgen dem kind der Spalte; Zellen bis zu 500 Zeichen. Zeilen ohne id bekommen eine. |
list | items: [{id, title, subtitle, user, icon, image, badges, meta, action, buttons}] | Eine Liste (bis zu 100 Einträge). Titel bis zu 200 Zeichen, bis zu 5 Badges und 5 Buttons pro Eintrag. |
progress | label, value, max, color | Ein Fortschrittsbalken. max ist standardmäßig 100; der Wert wird zwischen 0 und max gehalten. |
countdown | label, until | Ein Live-Countdown bis zu einem Zeitpunkt nach RFC 3339. Wird ohne until verworfen. |
timeline | items: [{time, title, markdown, icon, color}] | Eine Zeitleiste (bis zu 50 Einträge). Markdown pro Eintrag bis zu 500 Zeichen. |
calendar | mode: month | week | agenda, start, events: [{id, title, start, end, color, action}] | Ein Kalender (bis zu 200 Termine). Schmale Bildschirme zeigen standardmäßig die Agenda-Ansicht. Die Buttons Zurück und Weiter senden die Aktion calendar_nav (siehe Aktionen). |
chart | kind: bar | line, series: [{name, color, points: [{x, y}]}], x_label, y_label | Ein Balken- oder Liniendiagramm: bis zu 5 Reihen mit 100 Punkten. x ist eine Zahl oder ein Label. |
board | columns: [{id, title, cards: [{id, title, subtitle, user, image, badges, action}]}] | Ein Board aus Spalten und Karten: bis zu 6 Spalten mit je 50 Karten. Kein Drag-and-drop. |
image | url | file_id, alt, aspect | Ein Bild: eine https-URL oder eine App-Datei. Alt-Text bis zu 200 Zeichen, aspect wie "16:9" oder 1.5. |
video | file_id, poster_file_id, title | Eine Video-App-Datei (nur für Apps, deren Tarif Video erlaubt; siehe App-Dateien). |
buttons | items: [Button] | Eine Reihe von Buttons (bis zu 5). |
form | id, fields: [Field], submit: Button | Ein Formular (bis zu 15 Felder). Siehe Formulare. |
empty | title, text, icon | Ein Leerzustand: Titel (100), Text (200) und Symbol. |
Werte, die in mehreren Blöcken vorkommen
- Farben sind
#rrggbboder ein Token:accent,success,warning,danger,muted. Badges sind{text, color?}(Text bis zu 30) oder ein einfacher String. - Zeiten folgen RFC 3339 und werden in der Zeitzone der Person angezeigt.
startundendim Kalender nehmen auch ein DatumYYYY-MM-DDan. - Mitglieder (
user) sind Mitglieds-Snowflakes. Der Client zeigt einen Avatar und den Anzeigenamen an, und ein Klick öffnet das Profil. - Markdown (
text,callout, Zeitleisteneinträge) ist GameVox-Chat-Markdown. HTML wird nie gerendert. Erwähnungen wie<@id>,<@&id>und<#id>werden als Namen angezeigt und benachrichtigen niemanden. Jedes andere Textfeld ist reiner Text. - Bilder müssen
https-URLs ohne Zugangsdaten sein, bis zu 2000 Zeichen. GameVox hat keinen Bild-Proxy: Ein Bild, das nicht im GameVox-Speicher liegt, bleibt erhalten, wird aber mit"external": truemarkiert, und Clients behandeln es wie ein Drittanbieterbild im Chat (kein Referrer, keine Cookies, eventuell erst nach einem Klick). App-Dateien vermeiden das.
Was der Sanitizer macht
Deine View ist Drittanbieterinhalt, der im Client jedes Mitglieds angezeigt wird; deshalb baut GameVox sie anhand einer Allowlist neu auf, bevor irgendein Client sie sieht:
- Unbekannte Blocktypen und unbekannte Felder werden verworfen.
- Aus Text werden ungültiges UTF-8, Steuerzeichen und Zeichen zur Überschreibung der Schreibrichtung entfernt, und er wird auf sein Limit gekürzt.
- Listen werden auf ihre Höchstanzahl gekürzt; Blöcke nach dem 60. in einer Liste oder nach dem 600. in der View werden verworfen.
- Farben, Routen, Zeiten, IDs und Mitgliedsverweise, die nicht ihrer Form entsprechen, werden geleert.
- Einträge, denen etwas Nötiges fehlt, werden verworfen: ein Listeneintrag ohne Titel, ein Zeitleisteneintrag ohne Zeit, ein Button ohne Label oder Ziel, ein Formular ohne
idodersubmit.action. paramsvon Buttons und Zeilen müssen einfache Daten sein (Strings, Zahlen, Booleans, Listen und Objekte, bis zu 5 Ebenen, je 50 Einträge, Strings bis zu 500 Zeichen, insgesamt 2 KB), sonst werden sie samt ihrem Button verworfen.- Eine View wird nur dann als Ganzes abgelehnt, wenn sie kein JSON-Objekt ist, größer als 256 KB ist (128 KB bei einem Snapshot) oder eine andere
versionoder ein andereskindhat. Die Person sieht dann „Diese App hat eine Ansicht gesendet, die GameVox nicht anzeigen konnte.“
Der Snapshot-Endpunkt antwortet mit warnings, die auflisten, was geändert wurde. Live-Antworten tun das nicht. View.Validate() der Go-Bot-Bibliothek listet dieselben Änderungen auf, bevor du sendest.
Aktionen und Interaktionen
Buttons
Ein Button navigiert entweder (er hat eine route) oder sendet eine Aktion an deine App (er hat eine 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: erforderlich, bis zu 40 Zeichen.action: ein Name, der^[A-Za-z0-9._:-]{1,64}$entspricht. Ein Druck darauf sendet dirkind: "action"mit diesem Namen und denparamsdes Buttons.route: stattdessen navigieren. Der Client öffnet diese Route, und du erhältstkind: "open"mit denparamsdes Buttons. Sind beide gesetzt, gewinntroute.params: ein Objekt bis zu 2 KB, das du genau so zurückbekommst, wie du es geschrieben hast.style(primary,secondary,success,danger),icon,disabled.ability: blendet den Button für Personen ohne diese Fähigkeit aus.confirm:{title, text, confirm_label}(80, 300 und 40 Zeichen) fragt das Mitglied, bevor die Aktion gesendet wird.
Tabellenzeilen, Listeneinträge, Board-Karten und Kalendertermine nehmen ein action-Ziel in denselben zwei Formen an: {"action": "…", "params": …} oder {"route": "…", "params": …}. Clients zeigen am gedrückten Element einen Ladezustand und senden denselben Druck nie zweimal.
Formulare
Ein form-Block hat eine id (eindeutig in der View), bis zu 15 fields und einen submit-Button, der eine action haben muss. Das Absenden schickt dir kind: "action" mit Aktion und Parametern des Absenden-Buttons sowie den Feldwerten in values, jeweils unter dem Feldschlüssel.
{
"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: entspricht^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$, eindeutig im Formular.type:string,text,number,boolean,select,multiselect,channel,channels,role,roles,color,static,date,datetime,user(Mitglieds-Picker) oderfile(siehe App-Dateien).label(100),help(200),placeholder(100),required,disabled,min,max,step,max_length(bis zu 2.000) undvaluezum Vorausfüllen.optionsfürselectundmultiselect: bis zu 25{value, label?, description?}. Fehlt das Label, wird der Wert angezeigt.channel_kindsschränkt einen Kanal-Picker ein. Kanal- und Rollen-Picker führen die eigenen Kanäle und Rollen des Servers auf; GameVox liefert sie, du sendest keine IDs.
Werte, die du erhältst: Strings (bis zu 2.000 Zeichen), Zahlen, Booleans oder Listen mit bis zu 25 Strings; höchstens 30 Werte und insgesamt 32 KB. Kanal- und Rollen-Picker liefern Snowflakes, ein Mitglieds-Picker liefert die Snowflake des Mitglieds, und ein Dateifeld liefert eine Liste von Dateiobjekten. GameVox prüft nur die Form: required, min, max und Optionslisten setzt der Client durch, prüfe also jeden Wert selbst.
Im Kalender blättern: Die Buttons Zurück und Weiter eines calendar-Blocks senden die Aktion calendar_nav mit params {"start": "YYYY-MM-DD", "mode": "month|week|agenda", "id": "<block id>"}. Antworte mit einer View, deren Kalender diesen Zeitraum zeigt.
Ansichten aktuell halten
refresh_secondsin einer View (15 bis 3600) öffnet sie per Timer neu, solange sie sichtbar ist.POST /channels/{channel_id}/app-view/invalidatemit{"routes": [...]}teilt allen, die den Kanal betrachten, mit, dass ihre Ansicht veraltet ist. Lassroutesweg, um alle zu aktualisieren, oder nenne bis zu 20 Routen, um nur Personen auf diesen Routen zu aktualisieren (raidspasst auch aufraids?past=1). Clients öffnen nach einer kurzen zufälligen Verzögerung (1 bis 3 Sekunden) neu, ein Schwall kostet dich also eine Anfrage pro Person, nicht eine pro Ereignis. Höchstens 5 Aufrufe pro Sekunde und Kanal gehen sofort durch; der Rest wird eine Sekunde später zu einem weiteren zusammengefasst. Antwort:204.PUT /channels/{channel_id}/app-view/snapshotmit{"view": {…}}speichert eine öffentliche Ansicht, gleich für alle, die den Kanal sehen dürfen, bis zu 128 KB. Sie wird angezeigt, solange dein Bot offline ist, als letzter Stand markiert. Sende{"view": null}, um sie zu löschen (Antwort{"cleared": true}). Die Antwort ist{"warnings": [...]}. Bis zu 6 Übertragungen pro 10 Sekunden und Kanal. Lege nie etwas Persönliches in einen Snapshot.- Nur die Anwendung, zu der ein Kanal gehört, kann ihn invalidieren oder seinen Snapshot setzen.
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": [] } Kontext pro Person
Jede Anfrage beschreibt eine betrachtende Person, Ansichten können also persönlich sein: „deine Charaktere“, „deine Anmeldung“, Buttons, die nur Verwaltende sehen.
- Identität:
user.idist die Snowflake des Mitglieds, dieselbe ID, die dein Bot in Gateway-Ereignissen und über REST sieht. Speichere mitgliedsbezogene Daten darunter. - Fähigkeiten: Eine Person hat eine Fähigkeit, wenn irgendeine ihrer Gruppen, die den Kanal sehen dürfen, entweder eine Überschreibung hat, die sie gewährt, oder keine Überschreibung hat und der Standard des Manifests sie abdeckt (
everyone: jede solche Gruppe;managers: nur verwaltende Gruppen;nobody: keine). Der Server-Eigentümer hat jede Fähigkeit. Ein Mitglied ohne Gruppe fällt auf die Standardgruppe des Servers zurück. - Verwaltende (
is_managerund der Standardmanagers) sind der Server-Eigentümer, Admin-Gruppen und Gruppen mit der Berechtigung „Server bearbeiten“ oder „Integrationen verwalten“. - Navigation wird für die Person gefiltert: Navigationseinträge, die eine fehlende Fähigkeit voraussetzen, sind ausgeblendet. Das dient nur dem Komfort. Einen Button auszublenden ist keine Sicherheit, und ein veränderter Client kann jede Aktion, jede Route und beliebige Parameter senden.
- Sprache:
localeist die Client-Sprache der Person. Eine Ansicht für alle (ein Snapshot) kann sie nicht kennen; nutze dort die Sprache des Servers. - Mitglieder in dem, was du sendest: Schreibe Mitglieds-Snowflakes in
user-Felder, Mitglieds-Tabellenzellen, Werte von Mitglieds-Pickern und Erwähnungen. GameVox übersetzt sie für die Clients und zurück;paramswerden nie angefasst.
App-Dateien, Tarife und App-Benachrichtigungen
App-Dateien
Eine App kann über ein Formularfeld vom Typ file Uploads annehmen (zum Beispiel einen Screenshot eines Ergebnisses) und gespeicherte Dateien in ihren Ansichten zeigen. Dateien liegen in privatem GameVox-Speicher, werden wie Chat-Anhänge gescannt und nur über kurzlebige signierte URLs ausgeliefert.
- Speicher im Manifest deklarieren:
"storage": {"kinds": ["image"], "max_file_bytes"?, "max_total_bytes"?, "max_video_bytes"?}. Ohne das bekommt deine App keine Uploads. Die geltenden Grenzen sind die deines Tarifs, abgesenkt durch das, was du deklarierst. - Ein Dateifeld hinzufügen:
{"type": "file", "key", "label", "accept": ["image"], "max_files": 1 to 10, "required"?}. Der Client lädt direkt in den Speicher hoch und wartet auf den Scan, bevor das Formular gesendet werden kann. - Die Werte lesen: Der Wert des Felds ist eine Liste von
{id, url, name, mime, size, width, height}.urlist eine Stunde lang signiert; hol die Datei in deinem Handler ab oder rufe sie später perGETauf, um eine frische URL zu bekommen. - Behalte, was du nutzt. Ein Upload ist
pending, bis er behalten wird, und eine ausstehende Datei wird nach 24 Stunden gelöscht. Beantwortest du die Aktion, die sie mitbrachte, ohneerror, wird sie automatisch behalten.POST /applications/@me/app-files/{id}/keepbehält eine von Hand. Eine behaltene Datei bleibt, bis du sie löschst, die Aufbewahrungsfrist des Tarifs abläuft, die Moderation sie sperrt oder deine App seit 30 Tagen vom Server entfernt ist. - Dateien anzeigen mit
{"file_id": "<id>"}statt einer URL:file_idin einem Bildblock,imagebei einem Listeneintrag oder einer Board-Karte, eine Tabellenzelle in einer Spalte der Artimage. GameVox setzt für die Person eine signierte URL ein. Erst dadurch, dass eine Datei in einer View steht, darf diese Person sie sehen. IDs einer anderen App oder eines anderen Servers werden verworfen, und eine View trägt höchstens 100 Datei-IDs. - Snapshots zeigen nur Dateien, die du als öffentlich markiert hast:
PATCH /applications/@me/app-files/{id}mit{"public_in_snapshot": true}. - Arten: Bilder (PNG, JPEG, GIF, WebP) funktionieren heute. Video (MP4 mit H.264) funktioniert nur, wo GameVox deiner App auf diesem Server ein Video-Kontingent gewährt hat; WebM wird abgelehnt, weil es sich nicht scannen lässt.
documentist reserviert: Ein Manifest darf es nennen, aber noch wird kein Dokumenttyp angenommen.
// 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 (alles unter /applications/@me/, Bot-Token, Guild-, Kanal- und Uploader-IDs als Snowflakes, Datei-IDs als UUID-Strings): GET app-files?guild_id=&state=&page= antwortet mit {files, page, has_more} (50 pro Seite); GET, PATCH und DELETE app-files/{id}; POST app-files/{id}/keep; GET app-usage?guild_id= antwortet mit {plan, limits, used_bytes, files}. Die Datei einer anderen App ergibt 404; eine Datei, die gelöscht, gesperrt oder noch nicht hochgeladen ist, ergibt beim Behalten 409.
Tarife
Jede Anfrage führt plan: {key, limits} mit, die Grenzen deiner App auf diesem Server nach der storage-Obergrenze deines Manifests. Nutze das, um eigene Extras freizuschalten (eine App, die auf Daten eines anderen Unternehmens aufbaut, muss dessen Regeln für kostenpflichtige Funktionen befolgen). Derzeit läuft jede App im Tarif free, sofern das GameVox-Team für einen Server keinen anderen Tarif gesetzt hat; einen Tarif kaufen kann man noch nicht.
| Feld | Hinweise |
|---|---|
max_file_bytes | Größtes Bild: 8 MB. |
max_files_per_field | Dateien pro Dateifeld: 4. |
max_total_bytes | Speicher pro App und Server: 500 MB. Ausstehende und behaltene Dateien zählen beide. |
max_uploads_per_user_per_day | Uploads pro Mitglied und Tag: 50. |
kinds | Erlaubte Arten: ["image"], dann durch dein Manifest eingeschränkt (leer, wenn du keinen Speicher deklarierst). |
retention_days | Behaltene Dateien werden nach so vielen Tagen gelöscht; 0 behält sie, bis du sie löschst. |
max_video_bytes | Größtes Video; 0 bedeutet kein Video. |
App-Benachrichtigungen
Bots können keine Direktnachrichten senden. Stattdessen kann eine App Mitglieder mit POST /applications/@me/app-notifications benachrichtigen (eine Antwort auf ein Ticket, eine Raid-Erinnerung). Mitglieder sehen das in der Benachrichtigungsglocke mit dem Symbol deiner App, als Toast und als mobile Push-Benachrichtigung, wenn sie App-Benachrichtigungen erlauben. Ein Tippen darauf öffnet deinen App-Kanal bei 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(erforderlich),user_ids(1 bis 25 Mitglieds-Snowflakes),title(erforderlich, bis zu 80 Zeichen),body(bis zu 300, darf leer sein),route,channel_id(ein App-Kanal deiner App auf diesem Server),dedupe_key(bis zu 64). Zu viel Text ergibt ein400, das die Grenze nennt; Text wird nie gekürzt.- Titel und Body sind reiner Text. Steuerzeichen und Zeichen zur Schreibrichtung werden entfernt.
- Ein Tippen öffnet
channel_id, wenn du eine angegeben hast, sonst den ersten App-Kanal deiner App, den das Mitglied sehen darf. Eine App ohne App-Kanal auf dem Server sendet Benachrichtigungen, die nichts öffnen. - Derselbe
dedupe_keyan dasselbe Mitglied innerhalb von 24 Stunden ersetzt die frühere Benachrichtigung (sie erscheint wieder als ungelesen), statt eine weitere hinzuzufügen. - Die Antwort listet die Mitglieds-IDs in
deliveredund die übersprungenen inskippedmit einemreasonauf:not_member(auch für eine ID, die GameVox nicht kennt),muted(das Mitglied hat deine App auf diesem Server stummgeschaltet),cannot_view,rate_limitedodererror(später erneut versuchen). Ein übersprungenes Mitglied verbraucht nichts. - Grenzen: 10 pro Stunde und 30 pro Tag pro App und Mitglied, 500 pro Stunde pro App und Server (auch ein abgelehnter Versuch zählt) und 20 Aufrufe pro 10 Sekunden pro Bot.
403, wenn deine App auf diesem Server nicht installiert ist. - Benachrichtigungen werden 30 Tage aufbewahrt, höchstens 200 pro Mitglied.
Selbst gehostete Server
App-Kanäle funktionieren auf selbst gehosteten Servern mit denselben Ereignissen und Endpunkten. Dein Bot verbindet sich weiterhin nur mit der Cloud. Die Unterschiede:
self_hostedist inAPP_VIEW_REQUESTauftruegesetzt.- Die Instanz der Kundschaft prüft, wer den Kanal sehen darf, berechnet die Fähigkeiten der Person aus ihren eigenen Gruppen und bittet dann die Cloud, dir die Anfrage zu senden. GameVox vertraut der Instanz darin, wie bei jeder anderen Berechtigung auf einem selbst gehosteten Server.
- Snapshot, Invalidieren sowie Erstellen, Bearbeiten und Löschen eines selbst gehosteten Kanals werden wie andere selbst gehostete Aufrufe an die Instanz weitergeleitet:
503, wenn die Instanz nicht verbunden ist,504, wenn sie nicht rechtzeitig antwortet. - Ein geändertes Manifest erreicht eine Instanz innerhalb von etwa einer Minute (sie speichert das Manifest 60 Sekunden zwischen) statt sofort.
- Wenn die Instanz die Cloud nicht erreicht, sehen Mitglieder deinen Snapshot (Dateien darin werden weggelassen) oder einen Offline-Hinweis.
- App-Dateien funktionieren wie in der Cloud und liegen im GameVox-Speicher, nicht auf der Instanz. Video braucht eine aktualisierte Instanz.
- Für App-Benachrichtigungen fragt die Cloud die Instanz, wer den App-Kanal sehen darf. Eine ältere oder nicht erreichbare Instanz wird nicht gefragt: Dann wird nur die Mitgliedschaft geprüft, und eine angegebene
channel_idwird ungeprüft weitergereicht. - Manifest-Übersetzungen funktionieren auch auf selbst gehosteten Servern, in der Sprache, die die Verbindung des jeweiligen Mitglieds meldet. Eine Box, die noch nicht aktualisiert ist, zeigt den Standardtext.
Sicherheitshinweise für die Bot-Entwicklung
- Setze Fähigkeiten bei jeder Aktion durch. Prüfe
abilities(und deine eigenen Regeln), bevor du etwas änderst. Dass der Client einen Button ausblendet, ist eine Gefälligkeit. - Behandle Route, Parameter, Aktion und Werte als Benutzereingabe. Sie kommen aus dem Client des Mitglieds. Ein veränderter Client kann jede Route, beliebige Parameter und jeden Aktionsnamen senden und die Pflichtfelder und Grenzen des Formulars überspringen. GameVox prüft nur ihre Form.
- Vertraue den IDs, die GameVox dir gibt (
guild_id,channel_id,user.id,abilities), nicht den IDs in Parametern. Um auf „Raid 42“ zu reagieren, prüfe, ob Raid 42 zu diesem Kanal gehört. - Halte Snapshots öffentlich. Alle, die den Kanal sehen dürfen, sehen sie. Lege nie persönliche Daten hinein, und markiere nur Dateien als öffentlich, die allen gezeigt werden dürfen.
- Gib keine signierten Datei-URLs preis. Sie funktionieren eine Stunde lang für alle, die sie haben. Lege
file_ids in Views und lass GameVox sie pro Person signieren. - Behalte deine Nonces für dich. Jede beantwortet die Anfrage einer Person; GameVox prüft, dass sie zu deiner Anwendung gehört.
- Bevorzuge Bilder, die bei GameVox liegen. Bild-URLs von Drittanbietern werden als extern markiert und eventuell hinter einem Klick verborgen; außerdem erfährt der Bild-Host, wann die Ansicht angezeigt wird.
- GameVox seinerseits vertraut deiner View nie: Sie wird anhand einer Allowlist neu aufgebaut, HTML wird nie gerendert, Erwähnungen benachrichtigen nie, und
open_urlist nurhttpsund läuft immer über die Abfrage für externe Links.
Fehler und Grenzen
REST-Antworten
400: ungültiger Body. Fehler bei Manifest und Snapshot nennen das Feld. Eine Antwort ohne Nonce ist ebenfalls ein 400.403: Die Nonce oder der Kanal gehört zu einer anderen Anwendung, oder deine App ist auf diesem Server nicht installiert.404: unbekannte oder abgelaufene Nonce; unbekannter Kanal oder ein Kanal, der kein App-Kanal ist (Snapshot, Invalidieren); unbekannte Datei; kein Manifest veröffentlicht (GET).409: Die Datei ist nicht in einem Zustand, der das erlaubt.413: Body über der Grenze: Manifest 64 KB (plus 192 KB Übersetzungen), View-Antwort 256 KB, Snapshot 128 KB, Benachrichtigungsanfrage 64 KB.429: Rate-Limit überschritten, mit den üblichen Rate-Limit-Headern.
Rate-Limits
- Manifest: 10 Veröffentlichungen pro Minute pro Anwendung (GET 30 pro Minute).
- View-Antworten: 200 pro Sekunde pro Bot.
- Snapshot: 6 Übertragungen pro 10 Sekunden pro Kanal.
- Invalidieren: 5 pro Sekunde pro Kanal gehen sofort durch; weitere werden zusammengefasst, nicht abgelehnt.
- App-Dateien: 30 Aufrufe pro 10 Sekunden pro Route; Nutzung 30 pro Minute.
- App-Benachrichtigungen: 20 Aufrufe pro 10 Sekunden pro Bot, dazu die oben genannten Grenzen pro Mitglied und pro Server.
- Pro Person: 20 Öffnungen und 10 Aktionen pro 10 Sekunden. Pro Anwendung, über alle Personen hinweg: 600 Anfragen pro 10 Sekunden. Darüber hinaus wird der Person gesagt, dass sie langsamer machen soll oder dass die App ausgelastet ist; dein Bot wird nicht gefragt.
Was die Person sieht
- „Die App hat nicht geantwortet. Versuch es noch einmal.“ Keine Antwort innerhalb von 10 Sekunden.
- „Die App ist gerade offline.“ Eine Aktion, während dein Bot offline ist.
- „Diese App hat eine Ansicht gesendet, die GameVox nicht anzeigen konnte.“ Die View wurde als Ganzes abgelehnt.
- „Diese Ansicht braucht eine neuere Version von GameVox.“ Eine View mit einer anderen
versionoder einem anderenkind. - Dein eigener
error-Text, wenn du einen sendest.
Ein minimales Beispiel von Anfang bis Ende
Eine App für Raid-Anmeldungen mit einer Ansicht und einem Formular, mit reinem HTTP. IDs sind gekürzt.
1. Das Manifest veröffentlichen, wenn der Bot startet:
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. Einen Kanal bekommen. Ein Admin fügt deine App über Kanal erstellen ▸ App hinzu, oder dein Bot erstellt einen:
POST /api/v10/guilds/1491230000000000001/channels
{ "type": 100, "name": "raid-signup" }
HTTP/1.1 201 Created
{ "id": "1491240000000000002", "type": 100, "gamevox_type": "app", ... } 3. Ein Mitglied öffnet ihn. Dein Bot erhält:
{ "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. Mit der Ansicht antworten (innerhalb von 10 Sekunden):
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. Das Mitglied sendet das Formular ab. Du erhältst eine Aktion mit den Werten:
{ "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. Prüfen, speichern, antworten mit einem Toast und der aktualisierten Ansicht, dann die anderen Personen zum Aktualisieren auffordern:
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 Optional: ein Snapshot für die Zeit, in der der Bot offline ist. Er ist öffentlich, zeigt also die Anzahl, nicht, wer sich angemeldet hat:
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": [] } In einer Discord-Bibliothek liest du das Ereignis über ihren rohen Gateway-Hook (on_socket_raw_receive in discord.py, das Ereignis raw in discord.js; siehe die Tabelle unter In-App-Einstellungen). Eine discord.py-Version der Schritte 3 bis 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) Die eigenen Bots von GameVox nutzen die Go-Bot-Bibliothek in server/shared-gamebot, die für dich das Manifest veröffentlicht, Anfragen innerhalb einer Frist beantwortet, Views validiert und Invalidierungen bündelt.
Noch nicht verfügbar
- Der Typ
web(eine Seite in einer Sandbox statt eines Layouts) ist reserviert. Manifeste und Views damit werden abgelehnt. - Uploads von Dokumenten und der Kauf eines Tarifs sind nicht verfügbar.
- Drag-and-drop auf Boards.