Rich Presence
アクティビティとは、その人が今していることです。たとえば Tibia をプレイ中で、その下に Roshamuul で狩り中、レベル 412 エリートナイト、画像、タイマーが並びます。GameVox は Discord のアクティビティオブジェクトと Discord の設定方法を採用しているため、Discord Rich Presence 向けに書かれたコードはほとんどそのまま動きます。表示する名前は GameVox が決め、それ以外の内容はすべて GameVox が検査します。
概要
入口は 4 つ、出口は 1 つです。
- ボット自身のステータス: Identify の
presenceとゲートウェイのオペコード 3。Discord とまったく同じです。 - サーバーから設定するユーザーのアクティビティ: ボットトークンを使った REST 呼び出しで、アプリを許可したユーザー(
activities.writeスコープ)が対象です。これには GameVox によるアプリの承認が必要です。 - ユーザーのコンピューター上のプログラムが設定するユーザーのアクティビティ: デスクトップアプリのローカル RPC で、Discord RPC の IPC プロトコルに対応しています。
- GameVox 自身のゲーム検出。これは制御できませんが、あなたのアクティビティと統合されることがあります。
- 出口:
GUILD_PRESENCESインテントを持つボットは、メンバーのステータスやアクティビティが変わるとPRESENCE_UPDATEを受け取ります。
表示される場所: サーバーのメンバー一覧の名前の下、プロフィールカード(画像、詳細、状態、パーティー、タイマー、ボタン)、DM のヘッダー、フレンド一覧。デスクトップ、ウェブ、iOS、Android のすべてで表示されます。
表示される内容
1 人のユーザー(またはボット)に表示されるのは、最大で カスタムステータス(種類 4)1 つとその他のアクティビティ 1 つです。複数のソースが同時に何かを示した場合、ローカル RPC がアプリより、アプリがボット自身のアクティビティより、そしてそのいずれもが検出されたゲームより優先されます。GameVox が検出したのと同じゲームのアクティビティは、検出結果と統合されます。詳細、状態、画像はあなたのものが使われ、ゲーム名と、あなたが送らなかった場合の開始時刻は GameVox のものが使われます。別のもののアクティビティは、検出されたゲームを置き換えます。ユーザーはまだカスタムステータスを設定できませんが、ボットは設定できます。
アクティビティオブジェクト
フィールドは Discord のものです。送れる内容は送り手によって異なります(各セクションを参照)。すべての内容は整えられます。制御文字と方向オーバーライド文字は削除され、改行は空白になり、連続する空白は 1 つにまとめられ、テキストは下記の上限で切り詰められます。
| フィールド | 備考 |
|---|---|
name | 最大 128 文字。ボットは自分で名前を決めます。ユーザーのアクティビティでは GameVox が名前を決め、あなたの指定は無視されます(「名前」を参照)。 |
type | 種類の表を参照してください。許可される種類はソースによって異なります。 |
url | ストリーミングのみ。しかも twitch.tv または youtube.com の https リンクに限ります。 |
timestamps | ミリ秒または秒単位の Unix 時刻(1011 未満の値は秒として読まれます)。start はカウントアップ、end はカウントダウンします。2000 年より前の場合、start が 1 日を超えて先の場合、end が start より前の場合は拒否されます。 |
details, state | それぞれ最大 128 文字。カードの 1 行目と 2 行目です。 |
status_display_type | 1 は 1 行表示で名前の代わりに状態を表示し(「レベル 412 エリートナイトをプレイ中」)、2 は詳細を表示します。0 または省略時は名前を表示します。 |
emoji | カスタムステータスのみ。Unicode の絵文字(名前、最大 32 文字)。カスタム絵文字の ID は破棄されます。 |
party | id は最大 128 文字、size は [current, max] で 0 ≤ current ≤ max ≤ 10000。「2 / 5」のように表示されます。サイズが不正な場合はサイズだけが破棄されます。 |
assets | Rich Presence タブであなたのアプリケーションにアップロードした画像のキー(a-z 0-9 _ - の 1〜32 文字)と、最大 128 文字のホバーテキスト。URL と mp: の値は破棄されます。「画像」を参照してください。 |
buttons | 最大 2 個で、それぞれ {label, url}。ラベルは最大 32 文字、https の URL は最大 512 文字。承認済みアプリとファーストパーティのアプリのみです。メンバーは GameVox の外部リンク警告を経てボタンを開きます。自分のカードではボタンは表示されません。 |
アクティビティ全体は、整えた後の JSON で 4096 バイトに収まる必要があります。リンク(url、ボタンの URL、details_url、state_url)は https で、公開ホスト名を使い、ユーザー名、IP アドレス、ポートを含んではいけません。application_id、flags、instance、secrets、sync_id、その他送られたものはすべて置き換えられるか破棄されます。
アクティビティの種類
| 種類 | 表示 |
|---|---|
0 | {name}をプレイ中 |
1 | {details or name}を配信中 |
2 | {name}を再生中 |
3 | {name}を視聴中 |
4 | {emoji} {state} |
5 | {name}で対戦中 |
1 行表示は GameVox が閲覧者の言語に翻訳します。あなたのテキストは翻訳されません。
ボットのステータス
Discord と同じ方法で設定します。Identify(オペコード 2)の presence フィールドと、変更するたびにゲートウェイのオペコード 3(Presence Update)です。discord.js の client.user.setPresence()、discord.py の change_presence() などはそのまま動きます。
{
"op": 2,
"d": {
"token": "<bot token>",
"intents": 513,
"properties": { "os": "linux", "browser": "my-bot", "device": "my-bot" },
"presence": {
"since": null,
"activities": [ { "name": "12 Tibia worlds", "type": 3 } ],
"status": "online",
"afk": false
}
}
} {
"op": 3,
"d": {
"since": null,
"activities": [ { "name": "Custom Status", "type": 4, "state": "/help for commands" } ],
"status": "dnd",
"afk": false
}
}
// discord.js
client.user.setPresence({
activities: [{ name: 'raid signups', type: ActivityType.Watching }],
status: 'idle',
});
# discord.py
await client.change_presence(activity=discord.Game(name="VALORANT"), status=discord.Status.online) GameVox が使うもの
status:online、idle、dnd、invisible、offlineのいずれか。退席中と取り込み中はボットのステータスの点が変わります。invisible と offline では、ボットはアクティビティなしのオフラインとして表示されますが、ボット自体は接続したまま動作し続けます。ステータスがなければオンライン扱いで、それ以外の値なら更新全体が無視されます。activities: 各アクティビティで使われるのはname、type、state、url(ストリーミング)、絵文字の名前(カスタム)だけです。それ以外はすべて破棄されます。リストはカスタムステータス 1 つとその他のアクティビティ 1 つに絞られます。- カスタムステータス(種類 4)には
stateか絵文字が必要です。名前は何でも構いません(Discord は「Custom Status」を送ります)。 sinceとafkは受け付けますが無視されます。presenceのない Identify はボットを通常のオンラインに戻します。Resume ではボットが持っていたステータスが維持されます。
上限
- オペコード 3 は接続ごとに 20 秒あたり 5 回までです。上限を超えると、GameVox は最新の更新を保持し、枠が空いた時点で適用します。これを理由に接続が切られることはありません。
- クライアントを開いているメンバーには 30 秒以内に変更が届きます。GameVox はボットごとに 30 秒に最大 1 回しか更新を送らないためです。後から開いたメンバー一覧には現在のステータスが表示されます。
- 同じプレゼンスを再送しても何も変わりません。
ステータスはメンバー一覧のボット名の下とボットのプロフィールに表示され、ボットが参加しているすべてのサーバー(セルフホストサーバーを含む)のメンバーに見えます。他のボットには届きません。
アプリからユーザーのアクティビティを設定する
あなたのサーバーは、ユーザーのコンピューターで何も動かさずに、ユーザーがあなたのゲームで何をしているかを表示できます。たとえばゲームサーバー自身のデータを使えます。これは GameVox 独自の拡張で、Discord にはこのエンドポイントはありません。
必要なもの
- 承認。 GameVox はこの機能を使うアプリを手作業で承認します。承認されるまで、すべての PUT はコード 50001 付きの 403 を返します。GameVox サポートにお問い合わせください。GameVox 自身のファーストパーティアプリはすでに承認済みです。
- ユーザーの同意。 ユーザーは通常の OAuth2 同意画面(GameVox でログイン)で、
activities.writeスコープ付きであなたのアプリを認可します。ユーザーは設定からいつでもこれを取り消せ、その時点であなたのアプリからのアクティビティはすぐに消えます。 - ユーザーのスイッチ。 「アプリにアクティビティを設定させる」をオフにしたユーザーには何も表示されません。その場合も PUT は 204 を返すため、あなたには区別できません。
https://gamevox.com/oauth2/authorize/?client_id=1491200000000000000
&response_type=code
&scope=identify%20activities.write
&redirect_uri=https%3A%2F%2Fexample.com%2Fgamevox%2Fcallback
&state=8f2c1a 設定と消去
ボットトークンで認証します。{user_id} はユーザーの Snowflake で、ボットが他の場所で見るものと同じです。
PUT https://app-api.gamevox.com/api/v10/applications/@me/users/1491250000000000003/activity
Authorization: Bot <token>
Content-Type: application/json
{
"activity": {
"type": 0,
"details": "Hunting in Roshamuul",
"state": "Level 412 Elite Knight",
"timestamps": { "start": 1761868000000 },
"party": { "size": [2, 5] },
"assets": {
"large_image": "roshamuul", "large_text": "Roshamuul",
"small_image": "knight", "small_text": "Elite Knight"
},
"buttons": [ { "label": "View character", "url": "https://example.com/characters/412" } ]
},
"ttl_seconds": 300
}
HTTP/1.1 204 No Content
DELETE https://app-api.gamevox.com/api/v10/applications/@me/users/1491250000000000003/activity
Authorization: Bot <token>
HTTP/1.1 204 No Content GameVox の読み取り方
ttl_seconds: 更新がない場合にアクティビティが残る時間で、60〜900 秒(既定 300。範囲外の値は範囲内に収められます)。期限が切れる前に現在の状態を再度 PUT してください。サービスが止まれば、アクティビティは自然に消えます。nameは無視されます。GameVox は、あなたのアプリが連携しているゲームの名前を、なければアプリの名前を表示し、常にあなたのアプリ「経由」であることを付け加えます。- 種類 0、1、2、3、5。カスタムステータス(4)は拒否されます。
- PUT は、そのユーザーに対するあなたの以前のアクティビティを置き換えます。何も変えない更新は TTL を延ばすだけです。種類と名前が変わらない間は
created_atも変わらないため、カードが新しく見えることはありません。 - 画像キーはあなたのアプリケーションのアセットでなければなりません。不明なキーは破棄され、テキストはそのまま表示されます。
- DELETE はそのユーザーに対するあなたのアプリのアクティビティだけを削除し、許可は不要です。アクティビティがあったかどうかにかかわらず 204 を返します。
エラー
| HTTP | コード | 備考 |
|---|---|---|
204 | 成功。ユーザーが「アプリにアクティビティを設定させる」をオフにしている場合もこの応答です(何も保存されません)。 | |
400 | 50035 | ボディが JSON でない、activity がないかオブジェクトでない、ttl_seconds が整数でない、またはアクティビティが拒否された場合。errors.activity.<field>._errors[0].code が理由を示します: ACTIVITY_TYPE_INVALID、ACTIVITY_NAME_REQUIRED、ACTIVITY_TIMESTAMPS_INVALID、ACTIVITY_TOO_LARGE、ACTIVITY_INVALID、BASE_TYPE_REQUIRED、NUMBER_TYPE_COERCE。 |
403 | 50001 | アプリがユーザーのアクティビティについて承認されていない、またはユーザーがアプリに activities.write を許可していない(または取り消した)場合。 |
404 | 10002 | 不明なアプリケーション。 |
404 | 10013 | 不明なユーザー(その Snowflake は GameVox のユーザーではありません)。 |
413 | ボディが 16 KB を超えています。 | |
429 | レート制限中。ボディは Discord と同じ形で、retry_after を含みます。 |
HTTP/1.1 400 Bad Request
{
"code": 50035,
"message": "Invalid Form Body",
"errors": {
"activity": {
"type": {
"_errors": [
{ "code": "ACTIVITY_TYPE_INVALID",
"message": "This activity type is not allowed for an application. Use 0, 1, 2, 3 or 5." }
]
}
}
}
} 上限
- ユーザーごとに 20 秒あたり 5 リクエスト。PUT と DELETE は別々に数えます。
- アプリケーションごとに、全ユーザー合計で 1 秒あたり 50 リクエスト。
画像
アプリケーションの Rich Presence タブで画像をアップロードし、それぞれにキーを付けます(1〜32 文字の英字、数字、_、-。アプリごとに一意で、大文字と小文字は区別しません)。PNG、JPEG、WebP で最大 1 MB。アップロードはすべて、使われる前に検査されます。アクティビティでは、キーを assets.large_image または assets.small_image に入れます。
- GameVox は、アクティビティが属するアプリケーションのアセットの中からキーを探します。REST エンドポイントとボットではあなたのアプリ、RPC ではローカルのクライアント ID が表すアプリです。それ以外は参照できません。
- 大きい画像がない場合、GameVox はそのゲーム用の自前のアートを、なければあなたのアプリのアイコンを表示します。小さい画像は大きい画像の上にしか表示されません。
- 削除したアセットは数分以内にカードから消えます。
- 画像の URL、Discord の
mp:メディアプロキシの値、他のアプリのキーは破棄されます。
アプリのアセットを一覧する
アセット一覧は Discord と同じく公開されているので、ツールからどのキーが存在するか確認できます。認証は不要です。
GET https://app-api.gamevox.com/api/v10/oauth2/applications/1491200000000000000/assets
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
[
{ "id": "6f1c2e3a-…", "name": "roshamuul", "type": 1 },
{ "id": "9b0d4c7e-…", "name": "knight", "type": 1 }
] - 画像アセットのみで、古い順です。
idはアセットの ID、nameはそのキー、typeは常に1です。 - 不明なアプリケーションにはコード 10002 付きの 404 が返ります。
- 応答は 5 分間キャッシュされるため、新しいアップロードが表示されるまでそれだけかかることがあります。IP ごとに 1 分あたり 30 リクエスト。
デスクトップのローカル RPC
ユーザーのコンピューター上のプログラム(ゲーム、ランチャー、メディアプレーヤー)は、GameVox のデスクトップアプリ(Windows、macOS、Linux)を通じてユーザーのアクティビティを設定できます。デスクトップアプリは Discord RPC の IPC プロトコルに対応しているため、Discord Game SDK やコミュニティ製の Rich Presence ライブラリのフレーミングがそのまま動きます。ウェブ、iOS、Android のアプリにはローカル RPC はありません。
接続先
- Windows: 名前付きパイプ
\\.\pipe\gamevox-ipc-0(-0から-9まで試してください)。 - macOS と Linux:
$XDG_RUNTIME_DIR、なければ$TMPDIR/gamevox-<uid>/にある Unix ソケットgamevox-ipc-0から-9。 - 接続できるのは同じ OS ユーザーとして動いているプログラムだけです。ネットワーク上では何も待ち受けておらず、WebSocket や HTTP のトランスポートもありません。
- ユーザーは GameVox の設定でローカル RPC をオフにできます(既定はオン)。
Discord RPC 向けに作られたプログラムのための互換モード
Discord のパイプ名しか知らないプログラムでも、ユーザーが GameVox の設定で互換モードをオンにすれば(既定はオフ)、GameVox を見つけられます。このとき GameVox は discord-ipc-0 から -9 でも応答しますが、それは Discord が動いていない間だけです。Discord が起動するとすぐに譲り、10 秒ごとに再確認します。可能なら自分で gamevox-ipc-N に接続してください。ほとんどのライブラリはパイプ名を設定でき、そうすれば互換モードは不要です。
フレーミング
各フレームは 8 バイトのヘッダー(オペコードとペイロード長を、符号なし 32 ビットのリトルエンディアン整数で)と、それに続くその長さの UTF-8 JSON です。64 KB を超えるフレームを受け取ると接続が閉じられます。
| オペコード | 備考 |
|---|---|
0 HANDSHAKE | 最初のフレーム。最初のフレームでのみ使います。 |
1 FRAME | コマンドとその応答。 |
2 CLOSE | どちらの側からでも閉じられます。GameVox が接続を拒否するときは {code, message} を送ります。 |
3 PING | GameVox は同じペイロードの PONG で応答します。 |
4 PONG | PING への応答。 |
ハンドシェイクと READY
接続から 10 秒以内に {"v": 1, "client_id": "…"} を送ってください。client_id はアプリケーション ID(15〜21 桁)です。GameVox は、サインイン中のユーザー(そのユーザーの GameVox Snowflake)を含む READY 配信で応答します。不正なハンドシェイクにはコード 4000 の CLOSE で応答します。
→ op 0 HANDSHAKE
{ "v": 1, "client_id": "1491200000000000000" }
← op 1 FRAME
{
"cmd": "DISPATCH",
"evt": "READY",
"nonce": null,
"data": {
"v": 1,
"config": {
"cdn_host": "files.gamevox.com",
"api_endpoint": "//app-api.gamevox.com/api",
"environment": "production"
},
"user": {
"id": "1491250000000000003", "username": "ben", "discriminator": "0",
"global_name": "Ben", "avatar": null, "bot": false, "flags": 0, "premium_type": 0
}
}
} SET_ACTIVITY
応答には、GameVox が受け付けた形のアクティビティが入っています。"activity": null で消去され、接続を閉じても消去されます。ローカルのアクティビティは 1 つだけで、最後に設定したプログラムがそれを所有します。
→ op 1 FRAME
{
"cmd": "SET_ACTIVITY",
"nonce": "7c1f0a52",
"args": {
"pid": 4242,
"activity": {
"type": 0,
"details": "Hunting in Roshamuul",
"state": "Level 412 Elite Knight",
"timestamps": { "start": 1761868000 },
"assets": { "large_image": "roshamuul", "large_text": "Roshamuul" },
"party": { "id": "p1", "size": [2, 5] }
}
}
}
← op 1 FRAME
{
"cmd": "SET_ACTIVITY",
"evt": null,
"nonce": "7c1f0a52",
"data": {
"id": "a41c09e2f3b8d715", "name": "Tibia", "type": 0, "url": null,
"created_at": 1761868800000,
"timestamps": { "start": 1761868000000 },
"application_id": "1491200000000000000",
"details": "Hunting in Roshamuul", "state": "Level 412 Elite Knight", "emoji": null,
"party": { "id": "p1", "size": [2, 5] },
"assets": { "large_image": "roshamuul", "large_text": "Roshamuul" },
"flags": 0
}
} | コマンド | 備考 |
|---|---|
SET_ACTIVITY | アクティビティを設定または消去します(上記を参照)。種類 0、2、3、5 のみ。GameVox が拒否したものには evt: "ERROR" とコード 4000 で応答します。 |
SUBSCRIBE | join、spectate、ready のイベントを購読する SDK が動き続けるよう、受け付けだけ行います。GameVox がこれらのイベントを配信することはありません。 |
UNSUBSCRIBE | SUBSCRIBE と同じです。 |
* | それ以外(AUTHORIZE、AUTHENTICATE、GET_GUILDS、ボイスやロビーのコマンドなど)には evt: "ERROR"、コード 4006、「Not supported by GameVox」で応答します。 |
同意と識別
- ユーザーがあなたのプログラムを許可するまで、何もコンピューターの外に出ません。初回に GameVox は許可する、後で、今後も許可しないのどれにするか尋ねます。回答は実行ファイルとクライアント ID ごとに記憶されます。その間も SET_ACTIVITY は通常どおり応答し、アクティビティは待機します。
- あなたのプログラムが GameVox の検出したゲームで、かつクライアント ID が GameVox 上でそのゲームに連携している場合、GameVox は尋ねずに許可します。
- GameVox はあなたが誰であるかを、OS(パイプの反対側にあるプロセスとその実行ファイル)から知ります。
args.pidやペイロード内の他の情報から知ることは決してありません。 - 名前を決めるのはあなたではなく GameVox です。GameVox 上でクライアント ID が連携しているゲームまたはアプリ、なければ実行ファイルがそのゲームである場合に GameVox が検出したゲーム、なければカタログ上でその実行ファイルに対応するゲーム、それもなければ「アプリ」です。アクティビティの
nameは無視されます。 - ボタンが表示されるのは、クライアント ID が承認済みまたはファーストパーティの GameVox アプリのものである場合だけです。画像はそのアプリのアセットから解決されます。
- テキストは GameVox のサーバーでも再度検査され、不適切な詳細や状態は削除されます。
上限: 接続ごとに 20 秒あたり 5 回の SET_ACTIVITY で、間隔は 500 ミリ秒以上。それより速い更新はまとめられ、最新のものが優先されます。同時に接続できるプログラムは最大 16 個です。
メンバーのプレゼンスを受け取る
GUILD_PRESENCES インテントがあれば、ボットが参加しているサーバーのメンバーがステータスやアクティビティを変えたときに、Discord と同じ形の PRESENCE_UPDATE を受け取ります。
{
"op": 0,
"t": "PRESENCE_UPDATE",
"d": {
"user": { "id": "1491250000000000003" },
"guild_id": "1491230000000000001",
"status": "online",
"activities": [ {
"id": "a41c09e2f3b8d715", "name": "Tibia", "type": 0, "url": null,
"created_at": 1761868800000,
"timestamps": { "start": 1761868000000 },
"application_id": "1491200000000000000",
"details": "Hunting in Roshamuul", "state": "Level 412 Elite Knight", "emoji": null,
"assets": { "large_image": "roshamuul", "large_text": "Roshamuul" },
"buttons": [ "View character" ],
"flags": 0
} ],
"client_status": { "desktop": "online" }
}
} statusはonline、idle、dnd、offlineのいずれかです。オンライン状態を隠しているメンバーはアクティビティなしのofflineになります。client_statusには常にdesktopが入ります。- サーバーのメンバーが見るものと同じものが見えます。メンバーがこのサーバーと共有していないゲーム、表示を選んでいない成人向けゲームは含まれず、詳細をオフにしたメンバーからは名前と種類だけが届きます。
- アクティビティは、
idとcreated_atを持つ Discord のオブジェクトです。buttonsはラベルのみで、ボタンの URL や GameVox 独自のフィールドが届くことはありません。 - 届くのは変更だけです。GUILD_CREATE やメンバーチャンクにはプレゼンスは含まれません。変更はメンバーごとに約 2 秒間まとめられます。
- ボット自身のステータスは他のボットには送られません。
セルフホストサーバー
セルフホストサーバーでは次のようになります。
- ボットのステータスは同じように動き、サーバーのメンバーに表示されます。
- デスクトップのローカル RPC も同じように動きますが、サーバーはクライアント ID を照会できません。アクティビティが検出されたゲームのものならそのゲーム名を、そうでなければ「アプリ」と表示します。ボタンはなく、画像は GameVox 自身のファイルホストのものだけです。
- REST エンドポイントでアプリから設定したアクティビティは、GameVox のクラウド(プロフィール、フレンド、クラウドのサーバー)に表示され、セルフホストサーバーには表示されません。
PRESENCE_UPDATEも同じように動き、サーバー自身が送ります。
未対応のもの
- 参加、観戦、参加リクエスト(
secrets、party.privacy、ロビー)と、それに伴う RPC イベント。 - 画像の URL と Discord のメディアプロキシ(
mp:)の画像。代わりにアセットをアップロードしてください。 - WebSocket や HTTP 経由の RPC と、RPC の認可(AUTHORIZE / AUTHENTICATE)。
- ユーザーのカスタムステータスと、ヘッドレスセッション。