# ガイド: Discord・他のボットへの中継

> Discord のテキストチャンネルに発言を流すだけなら、プログラムは要りません。Discord サーバーの管理者が管理画面で「発言をテキストチャンネルにも流す」を選ぶと、文字おこしくんが、発言を話した人の名前とアイコンでテキストチャンネルに投稿します（使い方の[発言をテキストチャンネルにも流す](../../help/index.md#relay)）。

あなたのボットは、文字起こしの結果を、別の Discord チャンネル・Slack・Matrix・翻訳や要約の処理などに流せます。あなたのボットはサーバー側で動くので、API キーを安全に持てます。

## 形

1. あなたのボット（サーバー側）が、HTTP（API キー）で動いているセッションを探し、`transcript:read` のトークンを発行します。Discord サーバーの管理者が文字おこしくんの Webhook の送信先を登録し、送る出来事に「文字起こしの開始」を選んでいれば、ボットはセッションを探す代わりに、文字おこしくんが送る `session.started` を受け取ってトークンを発行できます（[Webhook リファレンス](../webhooks.md#store-transcripts-off)）。
2. ボットが、WebSocket `/v1/subscribe` で発話を受け取ります。
3. ボットが、受け取った発話を転送先（Discord の Webhook・Slack・翻訳 API …）に送ります。

このページの「Discord の Webhook」は Discord のチャンネルにメッセージを投稿するための Discord の仕組みです。Discord の Webhook では、あなたのボットが Discord へメッセージを送ります。文字おこしくんの Webhook（[Webhook リファレンス](../webhooks.md)）では、文字おこしくんがあなたのボットへ文字起こしの開始と終了を知らせます。

## 押さえどころ

1. **`partial` は流さない。** `partial` は数百 ms ごとに書き換わるので、チャットに流すと荒らしになります。`final` から流します。
2. **`refined` は「前に流した発言の修正」として扱う。** 同じ `utteranceId` の `final` を既に送っていたら、新しいメッセージではなく**編集**（Discord なら Edit Message）にします。`utteranceId` → 転送先のメッセージ ID の表を持ってください。`refined` は `final` の数秒後に来ることも、来ないこともあります。
3. **`revision` を比べる。** 再接続すると、`welcome.recent` で同じ発話がもう一度届きます。送信済みの `revision` 以下の改訂が届いたら、ボットは何もしません（二重投稿を防ぐ）。
4. **転送先のレート制限を守る。** Discord の Webhook などには送信の上限があります。発話が多いときは、数秒ぶんの発話をまとめて 1 通にするなど、キューで送る量を平らにしてください。
5. **期限の前にトークンを取り直す。** 再接続の前に、API キーで新しいトークンを発行してから使います（[トークンの期限](../reconnection.md#token-expiry)）。
6. **終わりを検知する。** `session_ended` が来たら、そのセッションの購読をやめて、次のセッションを探しに戻ります。Discord サーバーの管理者が文字おこしくんの Webhook の送信先を登録し、送る出来事に「文字起こしの開始」を選んでいれば、ボットは次のセッションが始まったことを `session.started` で知ることができます。

## 例: Discord の Webhook に中継する（Node 22）

参照実装 [mojiokoshi-subscribe.js](../examples/mojiokoshi-subscribe.js) を使った例です。この例のボットは、Discord の Webhook の `?wait=true` で送信したメッセージの ID を受け取り、`refined` が来たら、そのメッセージを編集します。

```js
import { subscribe, speakerName } from "./mojiokoshi-subscribe.js";

const WEBHOOK = process.env.DISCORD_WEBHOOK_URL; // https://discord.com/api/webhooks/<id>/<token>
const posted = new Map(); // utteranceId → { messageId, revision }
const queue = [];

function relay(state, message) {
  if (message.type !== "utterance") return;
  const u = message.utterance;
  if (u.stage === "partial" || u.text.trim() === "") return;
  const sent = posted.get(u.utteranceId);
  if (sent !== undefined && sent.revision >= u.revision) return; // 再接続の welcome.recent などで重複した
  queue.push({ utteranceId: u.utteranceId, revision: u.revision, content: `**${speakerName(state, u)}**: ${u.text}` });
}

// 転送先のレート制限を守るため、1 秒に 1 件ずつ送る（編集も 1 件と数える）
setInterval(async () => {
  const item = queue.shift();
  if (item === undefined) return;
  const sent = posted.get(item.utteranceId);
  const body = JSON.stringify({ content: item.content, allowed_mentions: { parse: [] } }); // メンションさせない
  const headers = { "content-type": "application/json" };
  if (sent === undefined) {
    const response = await fetch(`${WEBHOOK}?wait=true`, { method: "POST", headers, body });
    if (response.ok) posted.set(item.utteranceId, { messageId: (await response.json()).id, revision: item.revision });
  } else {
    await fetch(`${WEBHOOK}/messages/${sent.messageId}`, { method: "PATCH", headers, body });
    sent.revision = item.revision;
  }
}, 1000);

subscribe({
  url: process.env.MOJIOKOSHI_SUBSCRIBE_URL,
  token: process.env.MOJIOKOSHI_TOKEN,
  onChange: relay,
  onStatus: (status, detail) => console.error(status, detail?.code ?? ""),
});
```

- この例は `allowed_mentions: { parse: [] }` で、発言の中の `@everyone` などが実際のメンションにならないようにしています。発言は文字おこしくんが利用者の音声から作った文字列なので、転送先でも「信頼できない入力」として扱ってください。
- ボットを長く動かすなら、`onStatus` に `failed`（`unauthorized` など）が届いたときに、API キーで新しいトークンを発行し、そのトークンで `subscribe()` を呼び直します。

## 翻訳・要約に流すとき

- `final` が届いたら翻訳し、`refined` が来たら訳し直して置き換えます（`refined` の方が句読点・表記が正確です）。
- 要約は `session_ended` の後に HTTP の書き出し（[記録と書き出し](archive.md)）で全文を取ってから作ると確実です。WebSocket は切断中の発話を取り戻せません。
- 発言を外部の API に送る前に、送ることを参加者に伝えてあるかと、利用規約に反しないかを確かめてください（[利用規約](../../legal/terms.html)）。

## コーディングエージェント向けのプロンプト

```text
文字おこしくんの Subscribe WebSocket API から発話を受け取り、Discord の Webhook に中継する Node 22 のボットを書いてください。
仕様: https://moji.kaitoranma.com/docs/llms.txt 、ガイド: https://moji.kaitoranma.com/docs/guides/bot-relay.md
参照実装 examples/mojiokoshi-subscribe.js を使う。

- partial は送らない。final を新しいメッセージとして送り、同じ utteranceId の refined（revision が大きい）は同じメッセージを編集する。utteranceId → { messageId, revision } を持つ。
- 送信済みの revision 以下の改訂は無視する（再接続の welcome.recent で重複して届く）。
- 送信はキューにして 1 秒に 1 件まで。allowed_mentions は空にする。
- 設定（Subscribe の URL、トークン、API キー、Discord の Webhook の URL、文字おこしくんの Webhook の署名のシークレット）は環境変数から読む。API キー・トークン・シークレットをログに出さない。
- session_ended で止まる。failed（unauthorized）になったら新しいトークンを取り直す関数を呼ぶ（取り方は差し替え可能にする）。
- 次のセッションは、文字おこしくんの Webhook の session.started（署名を生の本文で確かめる。https://moji.kaitoranma.com/docs/webhooks.md）で知るか、API キーで GET /v1/sessions?status=active を読んで探す（どちらにするかは差し替え可能にする）。
- 転送の判断（新規 / 編集 / 無視）を純粋関数にして、偽のメッセージ列でテストする。
```
