ミニアプリの作り方

ミニアプリはただのWebサイトです。 中身はあなたのサーバーに置いたままで構いません。Vespoは開く場所と身分証明を貸すだけです。

全体の流れ

  1. ① 入口 … 券を1回だけ使って「誰が開いたか」を知る
  2. ② 画面 … SDKを読んで、いまの様子を受け取る
  3. ③ サーバー … 点数・保存・在室はここで確かめる

⚠️ ②と③を混ぜないのが一番大事です。②は速いけれど本人が細工できる、 ③は遅いけれど嘘をつけない。見た目は②、決めごとは③で作ってください。

場所と、部屋の鍵

  • 場所は自分で決めます。Vespoはマップを強制しません。

    保存の place は自由な文字列です。全員が同じ世界なら place="world"、エリア分けなら "zone_3"、一人ずつの個室なら "house_" + user.key。トークの部屋の鍵は、その中の便利な既定値の1つでしかありません。

  • 部屋の鍵が表しているのは「マップ」ではなく「メンバー」です。

    同じトークにいる人だけが同じ鍵になります。誰でも入れる場所で足りるミニアプリには要りません。「このトークの4人だけで」という閉じた集まりを作りたい時に効きます。

  • 鍵は偽れないので、入室の仕組みを自分で作らなくて済みます。

    鍵を渡すのはVespoのサーバーで、中身から部屋をたどることもできません。だから「合言葉が漏れて知らない人が入ってきた」が起きません。招待・入っていいかの判定・追い出しを自作せずに、既にある人間関係をそのまま使えます。

  • ⚠️ 鍵が届くのは「トークから開かれた時」だけです。

    Vespoの設定の一覧から開かれた時は、部屋がないので room は入っていません。同じ人が同じミニアプリを開いても、開いた場所によって届く物が変わります。鍵が無い時に一人用として成り立つように作るか、「トークから開いてください」と案内してください(保存そのものは、どちらから開いても引き継がれます)。

① 入口で券を1回だけ使う(サーバー)

Vespoから開かれると、URLに ?rv_token=… が付きます。⚠️ 券は1回きり・60秒。画面で使うと再読み込みで必ず落ちるので、入口で確かめて自分のCookieに置き換えます。

POST https://www.vespo.app/v1/mini/verify
Authorization: Bearer <あなたのAPIキー>
Content-Type: application/json

{ "token": "URLに付いてきた券" }

→ {
     "user": { "key": "…", "name": "…", "avatarUrl": "…" },
     "room": { "key": "…" },          // scope: room を選んだ時だけ
     "payload": "…",                  // 招待カードから開かれた時の合言葉
     "stateToken": "…"                // scope: call を選んだ時だけ(下で使う)
   }

⚠️ user.key はあなたのミニアプリ専用のID。同じ人でも他のミニアプリでは違う値。
⚠️ 名前とアイコンは「その人がその場で使っている箱」の物。
   同じ人でも開いた場所によって変わることがあります(IDは変わりません)。

② 画面はSDKを1行読むだけ

繋ぎ方が端末で3通りあるのを吸収します。⚠️ SDKは任意で、下の「生の口」でも書けます。

<script src="https://www.vespo.app/sdk/v1.js"></script>
<script>
  Vespo.connect().then(function (rv) {

    // ── 受け取る ───────────────────────────────
    rv.on('state', function (s) {
      // s.inCall / s.with / s.speaking          (⚠️ 画面に届く値なので信用しない)
      // s.levels … 声の大きさ 0〜1。⚠️ 黙っている人は入っていない=無ければ0
    });
    rv.on('visibility', function (d) { /* d.active=false なら音や時間を止める */ });
    rv.on('closing', function () { /* 片付け。⚠️ 保存はここに頼らない */ });

    // ── 人に出す(⚠️ 押した瞬間に呼ぶこと。パソコンは小窓を開くため)──
    document.querySelector('#call').onclick = function () {
      rv.invite({ payload: 'dungeon:42' });      // 誰に送るかは本人が選ぶ
      rv.post({ text: 'レベル10になりました' });  // 文は下書き。本人が直せる
      rv.gift({ code: 'gem' });                  // 贈る。誰に贈るかも本人が選ぶ
    };

    // ── 売る ───────────────────────────────────
    document.querySelector('#buy').onclick = function () {
      rv.buy({ code: 'gem' }).then(function (r) {
        // ⚠️ 返事は端末で2通り。両方書かないと片方で黙って何も起きない
        if (r.opened) return;                    // スマホ=本体がもう開いた
        window.open(r.url, '_blank');            // パソコン=あなたが開く
      });
      // ⚠️ 「買えた」はここでは分からない。あなたのサーバーが
      //    /v1/mini/purchases に聞きに行って、そこで初めて渡す。
    };

    // ── 使えるかは本体が教えてくれる ─────────────
    if (rv.can('voice')) {
      rv.voice({
        volumes: { someKey: 0.3 },
        pan: rv.can('voicePan') ? { someKey: -0.9 } : undefined,
      });
    }
    if (rv.can('haptic')) rv.haptic('hit');      // 震え。⚠️ スマホの板だけ
  });
</script>

型(TypeScript): https://www.vespo.app/sdk/v1.d.ts
⚠️ rv.なんとか() は名前をそのまま流します。本体に機能が増えても入れ替え不要です。

③ SDKを使わない場合(生の口)

やっていることはこれだけです。⚠️ 繋ぎ方が3通りあるので、SDKはこの差を埋めているだけ。

// 受け取る(どの端末でも同じ書き方)
window.addEventListener('message', function (e) {
  // ⚠️ パソコンでは最初の message に「戻り道」のポートが付いてくる。
  //    後から来た方を使い、受け取ったらすぐ返事をする
  if (e.ports && e.ports[0]) {
    port = e.ports[0];
    port.postMessage({ type: 'robverse:ready' });
  }
  var d = e.data;
  if (d && d.type === 'robverse:state') { /* いまの様子 */ }
});

// 送る(3通りのどれか)
function toVespo(msg) {
  if (port) return port.postMessage(msg);                                  // パソコン(別タブ)
  if (window.ReactNativeWebView)                                           // スマホ(板)
    return window.ReactNativeWebView.postMessage(JSON.stringify(msg));
  if (window.parent !== window) return window.parent.postMessage(msg, '*'); // 枠の中
}

toVespo({ type: 'robverse:ready' });                    // 最新をもう一度もらう
toVespo({ type: 'robverse:invite', rid: 'r1', payload: 'dungeon:42' });
// → { type: 'robverse:invite_result', rid: 'r1', ok: true, closed: true }

// 結果を知らせる(「レベル10になりました」など)
toVespo({ type: 'robverse:post', rid: 'r2', text: 'レベル10になりました' });
// → { type: 'robverse:post_result', rid: 'r2', ok: true, closed: true }

toVespo({ type: 'robverse:buy', rid: 'r3', code: 'gem' });
// → { type: 'robverse:buy_result', rid: 'r3', ok: true, opened: true }   スマホ
// → { type: 'robverse:buy_result', rid: 'r3', ok: true, url: '…' }       パソコン

toVespo({ type: 'robverse:gift', rid: 'r4', code: 'gem' });
// → { type: 'robverse:gift_result', rid: 'r4', ok: true, closed: true }
// ⚠️ パソコンでは本体の画面に面が出ます(別タブからは見えない)。
//    SDKの rv.gift() は小窓を開くので、そちらを使ってください。
// ⚠️ 送ったかどうかは返りません(「送るまで閉じない」を作れないようにするため)

④ 点数を決める時は自分のサーバーから

⚠️ 画面に届く値は本人が細工できます。育成・点数・在室は必ずこちらで確かめてください。どれも APIキー + stateToken の両方が要ります。

POST https://www.vespo.app/v1/mini/state   { "stateToken": "…" }
  → { inCall, since, roomKey, with:[…], othersCount, now }
     ⚠️ 元はLiveKitの記録なので嘘をつけません。
     ⚠️ now は**サーバーの時刻**。時間で何かを決める(1日1回・制限時間)なら
        画面の時計ではなくこれを使ってください。端末の時計はずらせます。

POST https://www.vespo.app/v1/mini/save    { "stateToken": "…", "place": "…", "data": {…}, "ifVersion": 3 }
  → { ok: true, version: 4 }   /  版が合わなければ 409(読み直してやり直す)
     ⚠️ place は「あなたが自由に決める文字列」。
        4人で遊ぶ → room.key / 同じ世界 → "world" / 個室 → "house_" + user.key
     ⚠️ data を付けなければ「読む」。1回1MBまで。

POST https://www.vespo.app/v1/mini/place   { "stateToken": "…", "place": "…" }
  → { here: [{ key, name, avatarUrl }] }
     ⚠️ 「居ると伝える」と「居る人を聞く」が1回で済みます。20〜30秒ごとに呼んでください
        (60秒来ないと居ないことになります)。

POST https://www.vespo.app/v1/mini/score   { "stateToken": "…", "board": "score", "value": 1200 }
  → { value: 1200, order: "high", keep: "best", changed: true }
     ⚠️ board は「あなたが自由に決める文字列」。週替わりにしたければ名前に週を入れる
        ("2026w35")。こちらで自動リセットはしません。
     ⚠️⚠️ order(high=大きいほど上 / low=小さいほど上)と keep(best=自己最高 /
        last=いつも上書き)は**そのボードの最初の1回で決まって、あとから変わりません**。
        返ってくる order/keep が実際に効いている物です。
     ⚠️ changed:false は「自己最高を更新できなかった」であって失敗ではありません。

POST https://www.vespo.app/v1/mini/purchases { "stateToken": "…" }
  → { purchases: [{ id, code, name, amountJpy, at, gift }] }
     ⚠️ 支払いが確定した物だけ。決済ページを開いただけの物は返りません。
     ⚠️⚠️ 「買えた」を画面から受け取らないでください。画面の言葉は偽装できます。
     ⚠️ 消える物(回数券・コイン)を売る時は id を自分の表に記録して、
        一度渡した物を二度渡さないようにしてください。
     ⭐ gift:true は「誰かから贈られた物」。持ち主は受け取った人です。

POST https://www.vespo.app/v1/mini/rank    { "stateToken": "…", "board": "score", "scope": "friends" }
  → { me: { rank, value }, total, rows: [{ key, name, avatarUrl, rank, value, me }] }
     ⭐ scope:"friends" = Vespoの友だち + いま開いているトークに居る人。
        **誰が友だちかは渡りません**(こちらで絞ってから順位だけ返します)。
        トークから開いていない時は友だちだけになります。
     ⚠️ scope:"all" は、そのボードに点数を置いた人みんな。
     ⚠️ 自分の行は圏外でも必ず入ります(「あなたは128位」を出せます)。
     ⚠️ 名前とアイコンは name / avatar に同意している時だけ入ります。


POST https://www.vespo.app/v1/mini/friends  { "stateToken": "…" }
  → { friends: [{ key, name, avatarUrl, lastOpenedAt }] }
     ⭐ 友だちのうち**このミニアプリを開いたことがある人**。
        rank は点数を置いた人しか返さないので、「まだ遊んでいないけど入れてはいる
        友だち」を誘いたい時はこちら。
     ⚠️ 友だち=Vespoの友だち+いま開いているトークに居る人(rank と同じ)。
        **誰が友だちかは渡りません**(このミニアプリを開いた人だけが返ります)。

合図の一覧

画面と本体のやりとりはこれだけです。SDKを使う場合は覚えなくて構いません。

向き合図中身
本体 → ミニアプリrobverse:stateいまの様子。通話が始まる・終わると届きます。中に rv.can(使える物の一覧)も入っています。
本体 → ミニアプリrobverse:visibility{ active } アプリが裏へ回ったら false。音やアニメを止めてください(板を畳んでも中は動き続けます)。
本体 → ミニアプリrobverse:closing{ graceMs } もうすぐ終了します。片付け(保存など)をして closing_result を返してください。返すとすぐ閉じます。返さなくても graceMs(いまは500ミリ秒)で必ず閉じます。
ミニアプリ → 本体robverse:ready最新の様子をもう一度もらう。画面ができた直後に1回送ります。
ミニアプリ → 本体robverse:invite{ payload } 仲間を呼ぶ面を出す。送り先を選ぶのは本人です。→ invite_result { closed }。⚠️送ったかどうかは返りません(「送るまで閉じない」を作れないようにするため)。
ミニアプリ → 本体robverse:post{ text } 結果をトークに知らせる面を出す(「レベル10になりました」など)。⚠️text は**下書き**で、本人が直せますし、送らずに閉じることもできます。送り先を選ぶのも本人です。→ post_result { closed }。⚠️送ったかどうかは返りません。
ミニアプリ → 本体robverse:voice{ volumes, pan } 声の大きさと左右。→ voice_result { ok, panApplied }
決まりごとrid と *_result送る時に rid を付けると、同じ rid で <名前>_result が返ります。対応していない名前には ok:false / error:'unsupported' と、使える物の一覧が返ります。

覚えておくこと

  • 終了する時の保存は当てにしない。

    閉じる前に robverse:closing でひと声かけますが、間に合うのは保存1回ぶんくらいです。アプリごと落とされる・ブラウザを閉じられる・電波が切れる、のどれでも合図は届きません。保存は「変わった時にその都度」が本筋で、終了時のはおまけと考えてください。

  • 画面に届く物は信用しない。

    見た目に使うのは自由ですが、点数を決める時は必ず自分のサーバーから /v1/mini/state を呼んでください。

  • APIキーはブラウザに置かない。

    画面のJavaScriptから /v1/* を叩くと、キーが誰にでも見えます。サーバーからだけ呼んでください。

  • 券は1回きり・60秒。

    入口で使って、あとは自分のCookieで動かします。画面で使うと再読み込みのたびに落ちます。

  • invite / gift は rv.call() で呼ばない。

    SDKは名前を素通しする作りなので rv.call('gift', …) でも通ってしまいますが、小窓を開くのはSDKの中の処理なので素通りします。パソコンでは画面に何も出ません。この2つだけは rv.invite() / rv.gift() と名前付きで呼んでください。

  • 買えたかどうかは画面に届きません。

    画面に「買えた」を伝える作りにすると、その言葉を偽装してタダで手に入ります。あなたのサーバーが /v1/mini/purchases に聞きに行って、そこで初めて渡してください。値段もVespoのDBが正本なので、こちらから金額を渡すことはできません(合言葉だけ)。

  • 同時に書く事故は ifVersion で防ぐ。

    同じ place を2人が同時に書くと、付けていない方は黙って消えます。409が返ったら読み直してやり直してください。

動いている見本

みんなの木=通話していた時間で木が育つミニアプリ。①〜③を通しで使っています (身分・場所・様子・保存・招待・声)。Vespoのアプリからためしアプリを開くと遊べます。

⚠️ 育つ時間は見本のサーバーが /v1/mini/state に聞いて決めています。画面で「通話中」を書き換えても木は育ちません。これが③の形です。

ミニアプリを登録する

登録しなくても「テスト」でURLを入れて動かせます(localhost も可)。

SDK: https://www.vespo.app/sdk/v1.js / 型: /sdk/v1.d.ts