アプリチャンネル
アプリチャンネルは、サーバーのチャンネル一覧に並ぶチャンネルで、その中身はアプリがオンラインの間に提供する一連の画面です。レイドプランナー、ランク戦のラダー、チケット窓口、サーバーのステータスページなどが作れます。GameVox が保存するのは、チャンネルの場所、閲覧できる人、できる操作だけです。中身の所有者はあなたのアプリで、すべての画面をリクエストに応じて描画します。
概要
アプリは、多くの画面を持つ 1 つの場所です。各画面はルートで、アプリが定義する文字列です(home、raids、raids/42、roster?sort=ilvl)。チャンネルを開いたメンバーは開始ルート(通常は home)に着き、ナビゲーションメニュー、画面内のリンクやボタン、「戻る」と「進む」で移動します。
メンバーに見えるもの
- チャンネル一覧に、アプリのアイコン(またはサーバーが選んだアイコン)付きでチャンネルが表示されます。アプリがオフラインの間は小さな点が付きます。
- あなたが返した画面の周りに、ナビゲーションメニュー、「戻る」と「進む」、ホームボタン、更新ボタンが表示されます。
- 画面は、あなたの JSON から各クライアントがネイティブに、クライアント自身のテーマ(ライトとダーク)で描画します。
- ボットがオフラインの間は、最後にプッシュした公開スナップショットが最終更新として表示されます。スナップショットがなければオフラインの案内が表示されます。
対応クライアント: デスクトップアプリ(Windows、macOS、Linux)、ウェブアプリ、iOS、Android。Xbox アプリはアプリチャンネルを表示しません。アプリチャンネルを知らない古いクライアントには、チャンネル一覧にアプリチャンネルが一切送られないため、空のテキストチャンネルが見えてしまうことはありません。
スラッシュコマンドやコンポーネントとの違い
- スラッシュコマンドとメッセージコンポーネントはテキストチャンネルのメッセージの中にあり、インタラクショントークン付きの
INTERACTION_CREATEとしてアプリに届きます。アプリチャンネルにはメッセージが一切なく、投稿は拒否されます。 - ビューのリクエストは独自のゲートウェイイベント
APP_VIEW_REQUESTとして届き、使い捨ての nonce を付けた REST 呼び出し 1 回で応答します。インタラクショントークンも、遅延応答も、フォローアップもありません。HTTP のインタラクションエンドポイントは使われないため、アプリにはゲートウェイへの常時接続が必要です。 - すべての画面は閲覧者ごとのものです。GameVox はメンバーが画面を開いたり操作したりするたびに問い合わせ、画面を保存することはありません(保存されるのは任意の公開スナップショットだけです)。
- 画面の中では、アプリがアビリティと呼ぶ独自の権限を定義します。サーバー管理者がグループごとに付与し、GameVox は閲覧者が持つアビリティをリクエストのたびに伝えます。
- これは GameVox 独自の拡張で、Discord に相当する機能はありません。Discord のライブラリで書かれたボットはそのまま動き、このイベントを受け取らないだけです。
全体の流れ
メンバーがチャンネルを開く、移動する、またはボタンを押す
│
▼
GameVox ──── APP_VIEW_REQUEST { nonce, kind, route, user, abilities } ────▶ あなたのボット
▲
│
あなたのボット ──── POST /applications/@me/app-view-response { nonce, view } ────▶ GameVox
│
▼
GameVox がビューを検査して整え、クライアントが描画 GameVox は、開くたび、アクションのたびに閲覧者のチャンネル閲覧権限を確認し、アビリティを計算してから、初めてアプリに問い合わせます。応答はその閲覧者 1 人だけに届きます。
ライフサイクル
- サーバーが通常の方法(インストールリンクまたは OAuth2 の認可ページ)でアプリをインストールします。
- ボットは起動のたびにマニフェストを公開します。マニフェストのないアプリはアプリチャンネルを持てません。
- サーバー管理者がチャンネルを作成 ▸ アプリを選びます。一覧には、マニフェストを公開済みのインストール済みアプリが、名前、説明、アイコン付きで表示されます。チャンネル名はアプリから入力され、任意の開始画面メニューにはナビゲーション項目が並びます(既定: home)。チャンネルの作成には通常のチャンネル作成権限が必要です。ボットから作成することもできます(後述)。
- チャンネル設定では、管理者がチャンネル名の変更、アイコンの変更(アプリのアイコン、絵文字、アップロードした画像)、開始画面の変更、権限の設定(チャンネルを閲覧できる人と、各アビリティを持つグループ)を行えます。
- チャンネルを閲覧できるメンバーがそれを開き、アプリはメンバーが見るすべての画面について
APP_VIEW_REQUESTに応答します。 - アプリがサーバーから削除されても、そのアプリチャンネルは残り(再インストールで元に戻ります)、「このアプリはサーバーから削除されました」と表示され、管理権限を持つ人には削除ボタンが出ます。アプリケーション自体が削除された場合や、マニフェストを一度も公開していない場合は、「このアプリは利用できなくなりました」と表示されます。
アプリチャンネルも、他のチャンネルと同じくサーバーのチャンネル数の上限に数えられます。通常はアプリごとに 1 チャンネルですが、同じアプリの 2 つ目のチャンネルも作れます(たとえばレイドプランナーで開くレイドチーム用チャンネル)。中身は同じアプリで、ナビゲーションも同じです。すべてのリクエストに channel_id が含まれるので、チャンネルごとに別のデータを持てます。
ボットからアプリチャンネルを作成する
通常のチャンネル作成ルートを、type を 100 にし、任意の 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" }
} - 対象は自分自身のアプリケーションだけで、そのサーバーにインストール済みかつマニフェストを公開済みである必要があります。他のチャンネルと同様に、ボットにはチャンネル作成権限が必要です。
gamevox_app.start_routeはルートです(空または省略時はマニフェストのhome)。gamevox_app.iconはemoji、builtinのグリフ、またはマニフェストがすでに持っているurlのいずれかです。ボットはチャンネル画像をアップロードできません。アップロードできるのは管理者だけです。positionがなければ、チャンネルは一覧の末尾に置かれます。名前は 1〜100 文字です。- チャンネルを閲覧できる人と、各アビリティを持つ人は、サーバー管理者がチャンネル設定で決めます。
アプリチャンネルは、GameVox の拡張を含む通常のチャンネルオブジェクトです。REST の応答でもゲートウェイのチャンネルイベントでも同じです。
typeは 100 で、意図的に Discord の範囲外にしています。この番号を知らないライブラリは普通のチャンネルとして扱うことがあるため、確実に見分けるにはgamevox_type: "app"を使ってください。GET /channels/{id}、GET /guilds/{id}/channels、PATCH、DELETE /channels/{id}は他のチャンネルと同じように動きます。PATCHはgamevox_app: {"start_route", "icon"}も受け付けますが、自分のアプリケーションのチャンネルに限られ、チャンネル編集権限が必要です。
マニフェスト
各アプリは 1 つのマニフェストを公開します。名前、アイコン、ホームルート、ナビゲーションメニュー、アビリティです。起動のたびに PUT /applications/@me/app-manifest で公開してください。保存済みのものを上書きし、何度繰り返しても安全です。
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" }
]
} | フィールド | 備考 |
|---|---|
name | 必須。最大 50 文字。name_localizations を持てます。 |
description | 最大 300 文字。description_localizations を持てます。 |
icon | アイコン(後述)。 |
home | 最初に開くルート。既定は home。 |
nav | メニュー。トップレベルの項目は最大 12 個、全体で最大 30 個。 |
abilities | アプリ定義の権限を最大 25 個。 |
kind | layout(既定)でなければなりません。他の種類は予約済みで、拒否されます。 |
storage | 任意。ファイルのアップロードを受け付けるアプリのみ(アプリファイルを参照)。ストレージを宣言しないアプリはアップロードを受け取れません。 |
ナビゲーション項目は {label, route, icon?, ability?, children?} です。ラベルは最大 40 文字。children を持つ項目はグループで、自身の route を省略でき、グループの入れ子は 1 段までです。ability を持つ項目は、そのアビリティを持たない閲覧者には表示されず、そのアビリティは abilities で宣言されている必要があります。ルートは ^[A-Za-z0-9._~/?=&%:-]{1,200}$ に一致する必要があります。
アビリティは {key, label, description?, default} です。キーは ^[a-z0-9_]{1,32}$ に一致し、ラベルは最大 40 文字、説明は最大 200 文字、default は everyone(チャンネルを閲覧できるすべてのグループ)、managers(マネージャーグループ。閲覧者ごとのコンテキストを参照)、nobody のいずれかです。管理者は、アプリチャンネルごと、グループごとに既定値を上書きできます。
アイコンは次のいずれか 1 つです。{"emoji": "…"}(絵文字 1 つ。Twemoji で描画)、{"builtin": "…"}(GameVox のグリフ: calendar、trophy、list、chart、shield、sword、map、users、clock、star、flag、book、gift、bolt、home、game)、または {"image_data": "<base64>"}(正方形の PNG または WebP、16〜2048 ピクセル、最大 128 KB)。GameVox は image_data をスキャンして保存し、保存済みマニフェストでは {"url": …} を返します。自分でホストする URL は、メンバーに表示されることがないため拒否されます。GameVox から読み取った url のアイコンは送り返して構いません。
応答は 200 で、保存されたマニフェスト(既定値を補完し、画像を URL に置き換えたもの)が返ります。誤りがあればフィールド名を示した 400 になります(例: nav[2].route is not a valid route)。同じマニフェストを再公開しても何も変わりません。マニフェストが変わると、開かれているあなたのアプリチャンネルがすべて更新されるため、誰も再読み込みしなくても新しいメニューが表示されます。GET /applications/@me/app-manifest は保存内容を返し、一度も公開していなければ 404 を返します。
マニフェストのローカライズ
マニフェストの中で人が読むテキストはすべて、Discord のスラッシュコマンドが name_localizations を持つのと同じように、翻訳を持てます。翻訳するテキストのそばにマップを置いてください。通常のフィールドが既定のままで、マップに閲覧者の言語のエントリがないときにメンバーが目にするのはこれです。
{
"name": "Raid Planner",
"name_localizations": { "de": "Raidplaner", "pt-BR": "Planejador de raids", "ja": "レイドプランナー" },
"nav": [
{ "label": "Roster", "route": "roster",
"label_localizations": { "de": "Aufstellung", "fr": "Effectif", "es-ES": "Plantilla" } }
],
"abilities": [
{ "key": "manage", "label": "Manage raids", "default": "managers",
"label_localizations": { "de": "Raids verwalten" },
"description": "Create, edit and cancel raids",
"description_localizations": { "de": "Raids erstellen, bearbeiten und absagen" } }
]
} - マニフェストの
name_localizationsとdescription_localizations(テキストはそれぞれ最大 50 文字と 300 文字)、ナビゲーション項目のlabel_localizations(最大 40)、アビリティのlabel_localizationsとdescription_localizations(それぞれ最大 40 と 200)。 - キーは Discord のロケールコードです(
de,fr,pt-BR,es-ES,zh-CN,ja、そして Discord の一覧の残り)。Discord にコードがない言語(ar,he,fa,ms,sk,et,lv,sr,sl,tl,ur,bn,ta,sw,ca)には、その GameVox の言語タグを使います。キーは大文字と小文字を区別し、1 つのマップに入れられるロケールは最大 64 個で、不明なキーはフィールド名を示す400になります。 - 空の値、
null、既定のテキストと同じ値は破棄されるので、nullでロケールを消せます。テキストの上限を超える値は400です。例:nav[2].label_localizations[de] is longer than 40 characters。 - アビリティのキー、ルート、アイコンはローカライズされません。ビューはマニフェストの一部ではありません。リクエストの
localeを使って、閲覧者の言語でビューを描画してください。
マップを除いたマニフェストは従来どおり 64 KB が上限です。マップにはそれとは別に 192 KB の予算があるので、44 言語のマニフェストも収まります(ドキュメント全体で最大 256 KB)。マップのないマニフェストは従来どおり 64 KB までです。
GameVox はメンバーごとにマップを解決し、クライアントにはただの文字列を送ります。言語は、メンバーのクライアントが最後に報告したもので、スラッシュコマンドの説明の言語を決めるのと同じものです。チャンネルヘッダーと「チャンネルを作成」の一覧にあるアプリ名と説明、メニュー、権限エディターにあるアビリティの名前と説明、通知のアプリ名が対象です。GET /applications/@me/app-manifest は保存されたマニフェストをマップ付きで返します。
リクエストと応答
メンバーがチャンネルを開く、移動する、更新する、またはボタンを押すと、ボットは APP_VIEW_REQUEST という名前のゲートウェイ配信(オペコード 0)を受け取ります。Intent は不要です。配信先は、あなたのアプリケーションの接続中のゲートウェイセッションだけです。
{
"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
}
}
}
} | フィールド | 備考 |
|---|---|
nonce | 使い捨てです。応答に付けて送り返してください。 |
kind | open(メンバーが route を開いた、そこへ移動した、または更新した)または action(ボタンを押した、またはフォームを送信した)。 |
guild_id, channel_id | 閲覧中のサーバーとアプリチャンネル(Snowflake)。チャンネルごとのデータは channel_id をキーにしてください。 |
route | 表示するルート。空になることはありません。クライアントが何も送らない場合はチャンネルの開始ルート、それもなければ home です。 |
params | ここへ来るきっかけになったリンクやボタンの params(プレーンなデータ、最大 2 KB)。常にオブジェクトです。 |
action, values | kind: "action" のときのみ。アクション名と送信されたフォームの値(単なるボタンでは空のオブジェクト)。 |
user | 閲覧者。id(Snowflake)、username、display_name(現在はユーザー名と同じ)、avatar_url(空の場合あり)。 |
abilities | この閲覧者がこのチャンネルで持つアビリティ。これらに基づく制御はご自身で行ってください。 |
is_manager | サーバーオーナー、管理者グループ、または「サーバーを編集」か「連携を管理」の権限を持つグループの場合に true。 |
locale | 閲覧者のクライアントの言語(例: en-US や de)。空の場合があります。閲覧者の言語で応答するのに使ってください。 |
platform | desktop、web、ios、android のいずれか。 |
self_hosted | チャンネルがセルフホストサーバーにある場合に true。 |
plan | このサーバーでのアプリのプラン: {key, limits}(プランを参照)。 |
応答する
POST /applications/@me/app-view-response を 1 回呼んで応答します。認証は他のボット呼び出しと同じです(Authorization: Bot <token>)。
POST /api/v10/applications/@me/app-view-response
Authorization: Bot <token>
Content-Type: application/json
{
"nonce": "2f6c1a9e-4b7d-4c1e-9a53-0d8e7f6a5b41",
"toast": "Signed up as healer",
"view": { "version": 1, "title": "Mythic raid", "route": "raids/42", "blocks": [ ... ] }
}
HTTP/1.1 204 No Content viewは閲覧者の画面を置き換えます。トーストだけで済むアクションでは省略してください。閲覧者には今の画面がそのまま残ります。toastはトーストとして表示される短い 1 行(最大 200 文字)です。viewと一緒に送れます。error(最大 300 文字)は、画面の代わりに閲覧者へメッセージを表示します。errorがあるとviewとtoastは無視されます。open_urlは、通常の外部リンク確認を経たうえでhttpsの URL を開くようクライアントに求めます。それ以外のスキームは破棄されます。- 成功すると
204が返ります。ビューはその後に検査され整えられる(「サニタイザーの処理」を参照)ため、204はすべてのフィールドが残ったという意味ではありません。
タイミング
- 閲覧者は 10 秒待ちます。それを過ぎると「アプリが応答しませんでした。もう一度お試しください。」と表示され、遅れた応答は破棄されます。十分余裕をもって応答してください。GameVox のボットライブラリは各リクエストに 8 秒の期限を設けています。
- nonce が受け付けられるのは 15 秒間、1 回だけです。2 回目の応答や期限切れ後の応答には
404が返ります。 - アプリケーションが複数のゲートウェイセッション(シャードや再起動時の重なり)を持つ場合、同じリクエストを複数のセッションが受け取ることがあります。応答は 1 回にしてください。有効なのは最初の応答だけです。
- ゲートウェイセッションが接続されハートビートを送っている間、ボットはオンラインとみなされます。オフラインの間 GameVox はボットに問い合わせません。開くとスナップショットが表示され、アクションではアプリがオフラインであることがメンバーに伝えられます。
ビューのスキーマ
ビューは JSON のレイアウト文書で、kind は layout、version は 1 です。各クライアントがネイティブに描画します。未知のフィールドは無視され、未知のブロックタイプは破棄されるため、互換性を壊さずに新しい任意フィールドを追加できます。
{
"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>." }
]
} | フィールド | 備考 |
|---|---|
version | 1 または省略。それ以外のバージョンでは「この画面を表示するには新しいバージョンの GameVox が必要です。」と表示されます。 |
kind | layout または省略。規則は同じです。 |
title | 最大 100 文字。ヘッダーに表示されます。 |
subtitle | 最大 200 文字。 |
route | この画面が表すルート。リクエストされたものと違っても構いません(リダイレクト)。クライアントはこれを履歴に記録します。省略すると GameVox がリクエストされたルートを入れます。 |
nav_active | ハイライトするナビゲーション項目。 |
breadcrumbs | {label, route?} を最大 5 個。ラベルは最大 100 文字。 |
tabs | サブナビゲーションのタブ {label, route, badge?} を最大 8 個。ルートが一致するタブが選択されます。ラベルは最大 40 文字、バッジは最大 20 文字。 |
actions | ヘッダーのボタンを最大 4 個(ボタンを参照)。 |
refresh_seconds | 画面が表示されウィンドウにフォーカスがある間、クライアントはこの間隔で画面を開き直します。15〜3600 秒。 |
blocks | 中身。1 つのリストに最大 60 ブロック、ビュー全体で最大 600 ブロック。 |
ブロック
すべてのブロックは type と、任意の id(最大 64 文字)を持ちます。上限は備考欄のとおりで、上限を超えた分は切り詰められるか破棄されます。
| 種類 | フィールド | 備考 |
|---|---|---|
heading | text, level | 見出し。テキストは最大 200 文字、レベルは 1〜3(既定 2)。 |
text | markdown | Markdown の段落。最大 4,000 文字。 |
callout | style: info | success | warning | error, markdown | Markdown を含む強調ボックス。スタイルの既定は info。 |
divider | 水平線。 | |
section | title, description, blocks, collapsible, collapsed | 任意のタイトル(100)と説明(200)を持つブロックのグループ。折りたたみ可能にもできます。コンテナの入れ子は 2 段まで。 |
columns | columns: [{width 1-12, blocks}] | 12 分割のグリッド上に横並びにする列(最大 4 列)。狭い画面では縦に積まれます。 |
stats | items: [{label, value, delta, trend, color, icon}] | 統計タイル(最大 8 個)。値は最大 100 文字、差分は最大 30 文字、トレンドは up、down、flat。 |
key_value | items: [{label, value, user}] | ラベルと値のペア(最大 30 個)。値は最大 500 文字。行を user だけにすると、メンバーのチップが描画されます。 |
table | columns: [{key, label, align, sortable, kind}], rows: [{id, cells, action}], page_size, emptykind: text | number | user | badge | progress | time | icon | image | テーブル。最大 10 列、200 行で、並べ替えとページ分けはクライアントが行います(page_size は 1〜100、既定 25)。セルの値は列の kind に従い、セルは最大 500 文字。id のない行には自動で付与されます。 |
list | items: [{id, title, subtitle, user, icon, image, badges, meta, action, buttons}] | リスト(最大 100 項目)。タイトルは最大 200 文字、項目ごとにバッジ最大 5 個、ボタン最大 5 個。 |
progress | label, value, max, color | プログレスバー。max の既定は 100。値は 0 から max の間に収められます。 |
countdown | label, until | RFC 3339 の時刻までのライブカウントダウン。until がなければ破棄されます。 |
timeline | items: [{time, title, markdown, icon, color}] | タイムライン(最大 50 エントリ)。エントリの Markdown は最大 500 文字。 |
calendar | mode: month | week | agenda, start, events: [{id, title, start, end, color, action}] | カレンダー(最大 200 イベント)。狭い画面では既定でアジェンダ表示になります。前へ・次へのボタンはアクション calendar_nav を送ります(アクションを参照)。 |
chart | kind: bar | line, series: [{name, color, points: [{x, y}]}], x_label, y_label | 棒グラフまたは折れ線グラフ。最大 5 系列、各 100 点。x は数値またはラベル。 |
board | columns: [{id, title, cards: [{id, title, subtitle, user, image, badges, action}]}] | 列とカードのボード。最大 6 列、各 50 枚。ドラッグ&ドロップはありません。 |
image | url | file_id, alt, aspect | 画像。https の URL またはアプリファイル。代替テキストは最大 200 文字、aspect は "16:9" や 1.5 など。 |
video | file_id, poster_file_id, title | 動画のアプリファイル(プランで動画が許可されているアプリのみ。アプリファイルを参照)。 |
buttons | items: [Button] | ボタンの行(最大 5 個)。 |
form | id, fields: [Field], submit: Button | フォーム(最大 15 フィールド)。フォームを参照。 |
empty | title, text, icon | 空の状態。タイトル(100)、テキスト(200)、アイコン。 |
ブロック共通の値
- 色は
#rrggbbまたはトークン(accent、success、warning、danger、muted)です。バッジは{text, color?}(テキストは最大 30 文字)または単なる文字列です。 - 時刻は RFC 3339 で、閲覧者のタイムゾーンで表示されます。カレンダーの
startとendはYYYY-MM-DDの日付も受け付けます。 - メンバー(
user)はメンバーの Snowflake です。クライアントがアバターと表示名を描画し、クリックするとプロフィールが開きます。 - Markdown(
text、callout、タイムラインのエントリ)は GameVox のチャット用 Markdown です。HTML は決して描画されません。<@id>、<@&id>、<#id>などのメンションは名前として描画され、誰にも通知されません。その他のテキストフィールドはすべてプレーンテキストです。 - 画像は資格情報を含まない
httpsの URL で、最大 2000 文字です。GameVox には画像プロキシがないため、GameVox のストレージにない画像は残されますが"external": trueの印が付き、クライアントはチャット内のサードパーティ画像と同様に扱います(リファラーなし、Cookie なし、クリックが必要な場合あり)。アプリファイルならこれを避けられます。
サニタイザーの処理
ビューはすべてのメンバーのクライアントで描画されるサードパーティのコンテンツなので、GameVox はどのクライアントに届く前にも、許可リストに基づいてビューを組み立て直します。
- 未知のブロックの種類と未知のフィールドは破棄されます。
- テキストからは不正な UTF-8、制御文字、双方向オーバーライド文字が取り除かれ、上限で切り詰められます。
- リストは最大件数で切り詰められます。1 つのリストで 60 個目、ビュー全体で 600 個目を超えたブロックは破棄されます。
- 形式に合わない色、ルート、時刻、ID、メンバー参照は消去されます。
- 必要なものが欠けた項目は破棄されます。タイトルのないリスト項目、時刻のないタイムラインのエントリ、ラベルや遷移先のないボタン、
idやsubmit.actionのないフォームなどです。 - ボタンや行の
paramsはプレーンなデータ(文字列、数値、真偽値、リスト、オブジェクト。入れ子は 5 段まで、各 50 エントリまで、文字列は最大 500 文字、全体で 2 KB)でなければならず、そうでなければボタンごと破棄されます。 - ビュー全体が拒否されるのは、JSON オブジェクトでない場合、256 KB(スナップショットは 128 KB)を超える場合、
versionかkindが異なる場合だけです。このとき閲覧者には「このアプリが GameVox で表示できない画面を送信しました。」と表示されます。
スナップショットのエンドポイントは、変更内容を列挙した warnings を返します。ライブの応答では返しません。Go のボットライブラリの View.Validate() を使えば、送信前に同じ変更内容を確認できます。
アクションとインタラクション
ボタン
ボタンは移動する(route を持つ)か、アプリにアクションを送る(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: 必須。最大 40 文字。action:^[A-Za-z0-9._:-]{1,64}$に一致する名前。押されると、この名前とボタンのparamsを付けたkind: "action"が届きます。route: 代わりに移動します。クライアントがそのルートを開き、ボタンのparamsを付けたkind: "open"が届きます。両方が指定されている場合はrouteが優先されます。params: 最大 2 KB のオブジェクト。書いたとおりそのまま返されます。style(primary、secondary、success、danger)、icon、disabled。ability: そのアビリティを持たない閲覧者にはボタンを表示しません。confirm:{title, text, confirm_label}(80、300、40 文字)。アクションを送る前にメンバーに確認します。
テーブルの行、リストの項目、ボードのカード、カレンダーのイベントは、同じ 2 つの形の action ターゲットを取ります。{"action": "…", "params": …} または {"route": "…", "params": …} です。クライアントは押されたコントロールに処理中の状態を表示し、同じ操作を 2 回送ることはありません。
フォーム
form ブロックは id(ビュー内で一意)、最大 15 個の fields、submit ボタンを持ち、送信ボタンには action が必須です。送信すると、送信ボタンのアクションと params、およびフィールドキーをキーとするフィールドの値を values に入れた kind: "action" が届きます。
{
"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:^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$に一致し、フォーム内で一意。type:string、text、number、boolean、select、multiselect、channel、channels、role、roles、color、static、date、datetime、user(メンバーピッカー)、file(アプリファイルを参照)。label(100)、help(200)、placeholder(100)、required、disabled、min、max、step、max_length(最大 2,000)、初期値を入れるvalue。selectとmultiselectのoptions:{value, label?, description?}を最大 25 個。ラベルがなければ値が表示されます。channel_kindsはチャンネルピッカーを絞り込みます。チャンネルとロールのピッカーにはそのサーバー自身のチャンネルとロールが並びます。用意するのは GameVox なので、ID を送る必要はありません。
受け取る値: 文字列(最大 2,000 文字)、数値、真偽値、または最大 25 個の文字列のリスト。値は最大 30 個、全体で 32 KB までです。チャンネルとロールのピッカーは Snowflake を、メンバーピッカーはメンバーの Snowflake を、ファイルフィールドはファイルオブジェクトのリストを返します。GameVox が確認するのは形だけです。required、min、max、選択肢の一覧はクライアントが強制するものなので、すべての値をご自身で確認してください。
カレンダーのページ送り: calendar ブロックの前へ・次へのボタンは、params {"start": "YYYY-MM-DD", "mode": "month|week|agenda", "id": "<block id>"} を付けてアクション calendar_nav を送ります。その範囲を表示するカレンダーを含むビューで応答してください。
画面を最新に保つ
- ビューの
refresh_seconds(15〜3600)は、画面が表示されている間、タイマーで画面を開き直します。 {"routes": [...]}を付けたPOST /channels/{channel_id}/app-view/invalidateは、チャンネルを見ている全員に画面が古くなったことを伝えます。routesを省略すると全閲覧者を更新し、最大 20 個のルートを指定するとそのルートを見ている閲覧者だけを更新します(raidsはraids?past=1にも一致します)。クライアントは短いランダムな遅延(1〜3 秒)の後に開き直すため、まとめて発生しても、コストはイベントごとではなく閲覧者ごとに 1 リクエストです。チャンネルごとに 1 秒あたり 5 回までがすぐに処理され、残りは 1 秒後の 1 回にまとめられます。応答:204。{"view": {…}}を付けたPUT /channels/{channel_id}/app-view/snapshotは、チャンネルを見られる全員に共通の公開画面(最大 128 KB)を保存します。ボットがオフラインの間、最終更新として表示されます。消去するには{"view": null}を送ります(応答は{"cleared": true})。応答は{"warnings": [...]}です。チャンネルごとに 10 秒あたり 6 回までプッシュできます。スナップショットに個人的な情報は決して入れないでください。- チャンネルを無効化したりスナップショットを設定したりできるのは、そのチャンネルが属するアプリケーションだけです。
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": [] } 閲覧者ごとのコンテキスト
すべてのリクエストは 1 人の閲覧者を表すので、画面を個人向けにできます。「あなたのキャラクター」「あなたの参加登録」、マネージャーにだけ見えるボタンなどです。
- 本人の識別:
user.idはメンバーの Snowflake で、ボットがゲートウェイイベントや REST で見るのと同じ ID です。メンバーごとのデータはこれをキーにしてください。 - アビリティ: 各アビリティについて、閲覧者が属するグループのうちチャンネルを閲覧できるもののいずれかが、それを許可する上書きを持つか、上書きがなくマニフェストの既定値に含まれる場合(
everyone: 該当するすべてのグループ、managers: マネージャーグループのみ、nobody: なし)、閲覧者はそのアビリティを持ちます。サーバーオーナーはすべてのアビリティを持ちます。グループのないメンバーは、サーバーの既定グループに属するものとして扱われます。 - マネージャー(
is_managerとmanagersの既定値)は、サーバーオーナー、管理者グループ、「サーバーを編集」か「連携を管理」の権限を持つグループです。 - ナビゲーションは閲覧者に合わせて絞り込まれ、持っていないアビリティが必要な項目は隠されます。これは表示上の配慮にすぎません。ボタンを隠すことはセキュリティではなく、改変されたクライアントはどんなアクション、ルート、params でも送れます。
- 言語:
localeは閲覧者のクライアントの言語です。全員向けの画面(スナップショット)ではこれを知ることができないので、サーバーの言語を使ってください。 - 送信内容に含めるメンバー:
userフィールド、メンバーのテーブルセル、メンバーピッカーの値、メンションにはメンバーの Snowflake を書いてください。GameVox がクライアント向けに変換し、また元に戻します。paramsには一切手を加えません。
アプリファイル、プラン、アプリ通知
アプリファイル
アプリは種類 file のフォームフィールドでアップロード(スコアのスクリーンショットなど)を受け付け、保存したファイルを画面に表示できます。ファイルは GameVox の非公開ストレージに置かれ、チャットの添付ファイルと同様にスキャンされ、常に有効期間の短い署名付き URL 経由でのみ配信されます。
- ストレージを宣言する: マニフェストに
"storage": {"kinds": ["image"], "max_file_bytes"?, "max_total_bytes"?, "max_video_bytes"?}を書きます。これがないとアプリはアップロードを受け取れません。実際の上限はプランの上限を、宣言した値まで引き下げたものです。 - ファイルフィールドを追加する:
{"type": "file", "key", "label", "accept": ["image"], "max_files": 1 to 10, "required"?}。クライアントはストレージに直接アップロードし、スキャンが終わるまでフォームを送信できません。 - 値を読む: フィールドの値は
{id, url, name, mime, size, width, height}のリストです。urlの署名は 1 時間有効です。ハンドラー内でファイルを取得するか、後でGETを呼んで新しい URL を得てください。 - 使うものは保持する。 アップロードは保持されるまで
pendingで、pending のファイルは 24 時間後に削除されます。そのファイルを含むアクションにerrorなしで応答すると自動で保持されます。POST /applications/@me/app-files/{id}/keepで手動でも保持できます。保持したファイルは、削除するまで、プランの保存期間が切れるまで、モデレーターがブロックするまで、またはアプリがサーバーから削除されて 30 日経つまで残ります。 - ファイルを表示するには、URL の代わりに
{"file_id": "<id>"}を使います。画像ブロックのfile_id、リスト項目やボードカードのimage、種類imageの列のテーブルセルです。GameVox が閲覧者向けの署名付き URL に差し替えます。ファイルをビューに入れることが、その閲覧者にファイルを見せる条件です。他のアプリや他のサーバーの ID は破棄され、1 つのビューに含められるファイル ID は最大 100 個です。 - スナップショットには公開の印を付けたファイルだけが表示されます。
{"public_in_snapshot": true}を付けてPATCH /applications/@me/app-files/{id}を呼んでください。 - 種類: 画像(PNG、JPEG、GIF、WebP)は現在利用できます。動画(H.264 の MP4)は、GameVox がそのサーバーでアプリに動画の利用枠を付与している場合にのみ使えます。WebM はスキャンできないため拒否されます。
documentは予約済みで、マニフェストに書くことはできますが、文書の種類はまだ受け付けていません。
// 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(すべて /applications/@me/ 配下、ボットトークン、サーバー・チャンネル・アップロード者の ID は Snowflake、ファイル ID は UUID 文字列): GET app-files?guild_id=&state=&page= は {files, page, has_more} を返します(1 ページ 50 件)。GET、PATCH、DELETE app-files/{id}。POST app-files/{id}/keep。GET app-usage?guild_id= は {plan, limits, used_bytes, files} を返します。他のアプリのファイルは 404、まだアップロードされていない、削除済み、またはブロック済みのファイルを保持しようとすると 409 です。
プラン
すべてのリクエストには plan: {key, limits} が含まれます。これは、マニフェストの storage による上限を適用した後の、そのサーバーでのアプリの上限です。独自の追加機能の制限に使ってください(他社のデータを使うアプリは、有料機能に関するその会社の規則に従う必要があります)。現在、GameVox のスタッフがサーバーに別のプランを設定しない限り、すべてのアプリは free プランで動きます。プランを購入する手段はまだありません。
| フィールド | 備考 |
|---|---|
max_file_bytes | 画像の最大サイズ: 8 MB。 |
max_files_per_field | ファイルフィールドあたりのファイル数: 4。 |
max_total_bytes | サーバーごと・アプリごとのストレージ: 500 MB。保留中と保持済みのファイルの両方が数えられます。 |
max_uploads_per_user_per_day | メンバーごとの 1 日のアップロード数: 50。 |
kinds | 許可される種類: ["image"]。さらにマニフェストで絞り込まれます(ストレージを宣言しなければ空)。 |
retention_days | 保持したファイルはこの日数の後に削除されます。0 なら削除するまで保持されます。 |
max_video_bytes | 動画の最大サイズ。0 は動画なしを意味します。 |
アプリ通知
ボットはダイレクトメッセージを送れません。代わりにアプリは POST /applications/@me/app-notifications でメンバーに通知を送れます(チケットへの返信、レイドのリマインダーなど)。メンバーには、アプリのアイコン付きで通知ベルに表示され、トーストとしても表示され、アプリ通知を許可していればモバイルのプッシュ通知としても届きます。タップするとアプリチャンネルが 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(必須)、user_ids(メンバーの Snowflake を 1〜25 個)、title(必須、最大 80 文字)、body(最大 300 文字、空でも可)、route、channel_id(そのサーバーにあるあなたのアプリのアプリチャンネル)、dedupe_key(最大 64 文字)。テキストが長すぎると上限を示した400になります。テキストが切り詰められることはありません。- タイトルと本文はプレーンテキストです。制御文字と双方向文字は取り除かれます。
- タップすると、指定があれば
channel_idが、なければメンバーが閲覧できるあなたのアプリの最初のアプリチャンネルが開きます。サーバーにアプリチャンネルがないアプリの通知は、タップしても何も開きません。 - 24 時間以内に同じメンバーへ同じ
dedupe_keyで送ると、新しい通知を追加するのではなく以前の通知を置き換えます(再び未読として表示されます)。 - 応答には、
deliveredのメンバー ID と、reason付きのskippedのメンバー ID が並びます。理由はnot_member(GameVox が知らない ID も含む)、muted(メンバーがそのサーバーであなたのアプリをミュートしている)、cannot_view、rate_limited、error(後で再試行)のいずれかです。スキップされたメンバーの分は何も消費しません。 - 上限: メンバーごと・アプリごとに 1 時間 10 件、1 日 30 件。サーバーごと・アプリごとに 1 時間 500 件(拒否された試行も数えます)。ボットごとに 10 秒あたり 20 回の呼び出し。アプリがそのサーバーにインストールされていなければ
403。 - 通知は 30 日間、メンバーごとに最大 200 件保持されます。
セルフホストサーバー
アプリチャンネルはセルフホストサーバーでも、同じイベントとエンドポイントで動作します。ボットの接続先はこれまでどおりクラウドだけです。違いは次のとおりです。
APP_VIEW_REQUESTのself_hostedがtrueになります。- 顧客のサーバーが、チャンネルを閲覧できる人を確認し、自身のグループから閲覧者のアビリティを計算したうえで、リクエストをあなたに送るようクラウドに依頼します。セルフホストサーバーの他のすべての権限と同様に、GameVox はこの判定を顧客のサーバーに委ねます。
- セルフホストのチャンネルに対するスナップショット、無効化、チャンネルの作成・編集・削除は、他のセルフホスト向けの呼び出しと同様に顧客のサーバーへ転送されます。サーバーが接続されていなければ
503、時間内に応答しなければ504です。 - マニフェストの変更は、すぐにではなく 1 分ほどで顧客のサーバーに届きます(マニフェストを 60 秒キャッシュするため)。
- 顧客のサーバーがクラウドに到達できないとき、メンバーにはスナップショット(中のファイルは除外)かオフラインの案内が表示されます。
- アプリファイルはクラウドと同じように動作し、顧客のサーバーではなく GameVox のストレージに保存されます。動画には更新済みの顧客サーバーが必要です。
- アプリ通知では、クラウドが顧客のサーバーにアプリチャンネルを閲覧できる人を問い合わせます。古いサーバーや到達できないサーバーには問い合わせません。その場合はメンバーであるかどうかだけが確認され、指定した
channel_idは確認されずにそのまま渡されます。 - マニフェストの翻訳はセルフホストサーバーでも動作し、各メンバーの接続が知らせる言語が使われます。まだ更新されていないサーバーでは既定のテキストが表示されます。
ボット開発者向けのセキュリティ上の注意
- すべてのアクションでアビリティを確認して制限を適用してください。 何かを変更する前に
abilities(と独自の規則)を確認してください。クライアントがボタンを隠すのは表示上の配慮にすぎません。 - route、params、action、values はユーザー入力として扱ってください。 これらはメンバーのクライアントから届きます。改変されたクライアントは、どんなルート、params、アクション名でも送れ、フォームの必須項目や上限も無視できます。GameVox が確認するのは形だけです。
- GameVox が渡す ID を信頼してください(
guild_id、channel_id、user.id、abilities)。params の中の ID は信頼しないでください。「レイド 42」を操作するなら、レイド 42 がこのチャンネルのものであることを確認してください。 - スナップショットは公開のままにしてください。 チャンネルを閲覧できる全員が見ます。個人データは決して入れず、全員に見せてよいファイルだけに公開の印を付けてください。
- 署名付きのファイル URL を漏らさないでください。 URL を持っている人なら誰でも 1 時間使えます。ビューには
file_idを入れ、閲覧者ごとの署名は GameVox に任せてください。 - nonce は外に出さないでください。 各 nonce は 1 人の閲覧者のリクエストに応答するためのもので、GameVox はそれがあなたのアプリケーションのものであることを確認します。
- GameVox でホストされた画像を優先してください。 サードパーティの画像 URL は外部扱いになり、クリックしないと表示されない場合があります。また、画面が表示されたことが画像のホストに伝わります。
- GameVox の側でも、あなたのビューを決して信頼しません。ビューは許可リストに基づいて組み立て直され、HTML は決して描画されず、メンションは通知されず、
open_urlはhttpsのみで必ず外部リンクの確認を経由します。
エラーと上限
REST の応答
400: ボディが不正です。マニフェストとスナップショットのエラーではフィールド名が示されます。nonce のない応答も 400 になります。403: nonce またはチャンネルが別のアプリケーションのものであるか、アプリがそのサーバーにインストールされていません。404: 不明または期限切れの nonce。不明なチャンネル、またはアプリチャンネルでないチャンネル(スナップショット、無効化)。不明なファイル。マニフェストが未公開(GET)。409: ファイルがこの操作を許す状態にありません。413: ボディが上限を超えています。マニフェスト 64 KB(翻訳用にさらに 192 KB)、ビューの応答 256 KB、スナップショット 128 KB、通知のリクエスト 64 KB。429: レート制限を超えました。通常のレート制限ヘッダーが付きます。
レート制限
- マニフェスト: アプリケーションごとに 1 分あたり 10 回の公開(GET は 1 分あたり 30 回)。
- ビューの応答: ボットごとに 1 秒あたり 200 回。
- スナップショット: チャンネルごとに 10 秒あたり 6 回のプッシュ。
- 無効化: チャンネルごとに 1 秒あたり 5 回まですぐに処理されます。それを超えた分は拒否されずにまとめられます。
- アプリファイル: ルートごとに 10 秒あたり 30 回の呼び出し。使用量は 1 分あたり 30 回。
- アプリ通知: ボットごとに 10 秒あたり 20 回の呼び出し。加えて、上記のメンバーごと・サーバーごとの上限。
- 閲覧者ごと: 10 秒あたり 20 回のオープンと 10 回のアクション。アプリケーションごと(全閲覧者の合計): 10 秒あたり 600 リクエスト。これを超えると、閲覧者には少し待つよう、またはアプリが混み合っていると伝えられ、ボットには問い合わせが行きません。
閲覧者に表示されるもの
- 「アプリが応答しませんでした。もう一度お試しください。」10 秒以内に応答がない場合。
- 「アプリは現在オフラインです。」ボットがオフラインの間のアクション。
- 「このアプリが GameVox で表示できない画面を送信しました。」ビュー全体が拒否された場合。
- 「この画面を表示するには新しいバージョンの GameVox が必要です。」
versionかkindが異なるビュー。 - あなたが
errorを送った場合は、そのテキスト。
最小限のエンドツーエンドの例
画面 1 つとフォーム 1 つのレイド参加登録アプリを、生の HTTP で作ります。ID は短縮しています。
1. マニフェストを公開する(ボットの起動時):
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. チャンネルを用意する。 管理者が「チャンネルを作成 ▸ アプリ」でアプリを追加するか、ボットが作成します:
POST /api/v10/guilds/1491230000000000001/channels
{ "type": 100, "name": "raid-signup" }
HTTP/1.1 201 Created
{ "id": "1491240000000000002", "type": 100, "gamevox_type": "app", ... } 3. メンバーが開く。 ボットが受け取るもの:
{ "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. 画面で応答する(10 秒以内):
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. メンバーがフォームを送信する。 値を含むアクションが届きます:
{ "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. 確認、保存、応答。 トーストと更新後の画面で応答し、他の閲覧者に再読み込みするよう伝えます:
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 任意: スナップショット(ボットがオフラインのとき用)。公開されるので、誰が登録したかではなく人数を表示します:
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": [] } Discord のライブラリでは、生のゲートウェイフックからイベントを読みます(discord.py では on_socket_raw_receive、discord.js では raw イベント。アプリ内設定の表を参照)。手順 3〜6 の discord.py 版:
# 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) GameVox 自身のボットは server/shared-gamebot にある Go のボットライブラリを使っています。マニフェストの公開、期限付きでのリクエストへの応答、ビューの検証、無効化のまとめ送りを代わりに行います。
まだ利用できないもの
webの種類(レイアウトではなくサンドボックス化されたページ)は予約済みです。これを使うマニフェストとビューは拒否されます。- 文書のアップロードと、プランの購入はできません。
- ボード上のドラッグ&ドロップ。