【VRChat】OSCを使ってDiscordからチャットボックスに入力する

# OSCを使ってDiscordからチャットボックスに入力する
こんにちは。VRChat始めたてのぽよたんぴです。
VRChatにはマイクがなくても会話できる「チャットボックス」という機能があります。

ですが、文字を打ってる間は移動などができません（当然ですが）
そこで、VRChatにはOSC[^1]という、外部アプリからVRChatを制御できるAPIのようなものがあります。
今回はOSCとDiscordを使用して、スマホ版Discordから入力した文字をチャットボックスに送信するプログラムを作成していきます。

# 必要なもの
- VRChat
- Node.js
- Discord

# OSCを有効化
VRChatを開き、アクションメニューを開きます
<details><summary>アクションメニューの開き方</summary>

- デスクトップ: Rキー
- Viveコントローラー: メニューボタン長押し
- Oculus Touch: B、Y長押し
- Indexコントローラー: B長押し

参考: [リンク](https://vrchatjp.playing.wiki/d/%C1%E0%BA%EE%CA%FD%CB%A1#footer-footnote1)
</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/f78f5251-c3b8-e6b5-3818-192e4b1546a9.png)
アクションメニューから「オプション」を選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/71606ff4-bb00-67fd-8032-73b21a33b3bf.png)
「OSC」を選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/e7878c88-661d-76b1-38f5-67d45c7e5c64.png)
「有効」をONにします

# Discord BOTを新規作成
[Discord Developer Portal](https://discord.com/developers/applications)をブラウザ開きます。

:::note
ブラウザでDiscordのアカウントにログインしている必要があります
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/56e43d56-e48d-feaf-fa09-1657e566fe59.png)
右上の「New Application」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/f884617c-728a-e861-9db6-56dd65c714ec.png)
お好きなアプリケーション名を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/3723a3bd-6714-6726-781f-8209bd6ef3f0.png)
左のサイドバーから「BOT」をクリックし、
TOKENの項目にある「Reset Token」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/41770282-e19f-d239-3121-ff55c589d607.png)
「本当にBOTのトークンをリセットする？」という確認画面が出てくるので、
「Yes, Do it!」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/aea261b9-84d1-5976-1866-142bdba10cc6.png)

ここで二段階認証を求められます。(以下2FAと略します)
「あれ、2FAなんて設定してないよ？」っていう方も居ると思います。
実はこの2FAの入力が画面、実は2FAの入力画面ではありません ~~（え？）~~
2FAを設定して**いない**場合は「**Enter your password**」(パスワードを入力)
2FAを設定して**いる**場合は「**6-digit authentication code**」(6桁のコードを入力)
となっていることに注意してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/bcd33de7-98af-2054-400c-096144275c0a.png)
認証コード（またはパスワード）を入力するとこのようにTokenという文字列が出てくると思いますので、忘れないようにメモしておいてください。
これはプログラムからBOTにログインするときに使うもので、
**トークンさえあれば誰でもBOTにログインできてしまうので、他人に公開してはいけません。**

:::note warn
トークンは一度しか表示されないので、紛失してしまったら再生成しなければなりません。
トークンをリセットすると、元々あったトークンは無効になります。
:::

その後、少し下の方にある「Message Content Intent」という項目を有効化してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901368/7e888258-79e1-f78c-4d32-03f629620bc5.png)

この設定は「Botがメッセージの内容を取得できるようにしますか？」という項目です。
**この設定を忘れると、この後にプログラムからログインするときに失敗します。**

# プログラムの実装
ここからは、Node.jsを使用してプログラムを書いていきます。

### 依存関係をインストール
以下のコマンドで必要なライブラリを入れます。
```bash
$ npm i discord.js node-osc
```
(今回はnpmを使っていますが、yarn[^2]やnpm[^3]などを使うのをおすすめします)

### config.jsonを作成
できるだけトークンなどはコード上にハードコードしないほうがいいので、
別のjsonファイルに書いていきます。

内容は適宜置き換えてください。
[ユーザーID・チャンネルIDを取得する方法](https://support.discord.com/hc/ja/articles/206346498-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC-%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC-%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8ID%E3%81%AF%E3%81%A9%E3%81%93%E3%81%A7%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%89%E3%82%8C%E3%82%8B)
```json:config.json (例)
{
  "oscPort": 9000,
  "allowedUsers": ["ユーザーID"],
  "discordToken": "トークン",
  "channelId": "DiscordのチャンネルID"
}
```

### コーディング
`index.js`というファイルを作成し、ここにコードを書いていきます。

まずライブラリや先程作ったconfig.jsonを読み込みます。
```js
const { Client: DiscordClient, IntentsBitField } = require("discord.js");
const { Client: OSCClient, Message } = require("node-osc");
const config = require("./config.json");

//許可されたユーザー一覧（Discord）
const allowedUserIds = new Set(config.allowedUsers);
```
VRChatにメッセージを送信できるユーザーを制限するために、
configに許可されているユーザーID一覧を書き、
`Set`を使用して重複を取り除いています。

#### 変数定義・初期化

タイピング表示に使う変数を定義します。
```js
let typingTimeoutId;
let isTyping = false;
```

OSCクライアントを初期化します。
```js
//OSCクライアントを初期化
const oscClient = new OSCClient("127.0.0.1", config.oscPort);
```

Discordクライアントも初期化します。
```js
//Discordクライアントの初期化
const discordClient = new DiscordClient({
    intents: [
        IntentsBitField.Flags.Guilds, //サーバー一覧を受信
        IntentsBitField.Flags.GuildMessages, //サーバーのメッセージを受信
        IntentsBitField.Flags.GuildMessageTyping, //タイピングを受信
        IntentsBitField.Flags.MessageContent //メッセージの内容を受信
    ]
});
```
※ここでIntentsを指定しておかないと、メッセージの内容やタイピングの受信ができません。

Discordにログインした際に、BOTの名前をログに出すようにします。
```js
discordClient.on("ready", () => {
    console.log(`[Discord] Logined as ${discordClient.user.username}`)
})
```

### タイピング状態の更新
Discordでタイピングした際に`isTyping`を`true`にし、
10秒以内に再度タイピングのイベントが発生しなければ`isTyping`を`false`にするコードを書いていきます。
```js
//Discordでタイピングを開始した時のイベント
discordClient.on("typingStart", (event) => {
    if(!allowedUserIds.includes(event.user.id)) return;

    //タイマーが重複しないように、すでにあるタイマーを解除
    clearTimeout(typingTimeoutId);
    
    //10秒後にタイピングを解除
    typingTimeoutId = setTimeout(() => {
        isTyping = false;
    }, 10000);
    
    isTyping = true;
});
```

#### 定期的にタイピング状態を送信
OSC経由で`/chatbox/typing`(パラメータは`bool`)に送信することで、
チャットボックスのタイピング中の表示を変更できます。

パラメータにタイピングしているかしていなかの状態を指定します。
```js
//毎秒(1000msごとに)実行
setInterval(() => {
    //VRChatに送信
    oscClient.send(new Message("/chatbox/typing", isTyping));
}, 1000);
```
#### チャットボックスへ送信
ここが一番重要な部分です。
`/chatbox/input`(パラメータは`string, bool, bool`)に送信することで、
チャットボックスへの入力ができます。

1つ目のパラメータには内容を指定します。
2つ目のパラメータがtrueの場合はキーボード画面を表示せずに即座に送信されます。
3つ目のパラメータをfalseにすると通知の効果音を鳴らしません。
```js
//Discordにメッセージを送信した時のイベント
discordClient.on("messageCreate", (event) => {
    //フィルタ
    if(!event.member || !allowedUserIds.includes(event.member?.id)) return;
    if(event.channelId != config.channelId) return;
    //タイピング状態を解除
    isTyping = false;
    //VRChatに送信
    oscClient.send(new Message("/chatbox/input", event.content, true, true));
    console.log(`[Send] ${event.content}`);
});
```

#### Discordにログイン
最後にDiscordにログインする処理を書きます。
```js
//Discordにログイン
discordClient.login(config.discordToken);
```

# 実行
以下のコマンドで起動できます。
```bash
$ node index.js
```
`[Discord] Logined as BOTの名前` と表示されたらDiscordへのログイン成功です。
先程指定したチャンネルIDのチャンネルにメッセージを打ち、
チャットボックスに反映されるか確認してみてください。

全体のコードはこちらに置いてあります。
https://github.com/poyotanp/osc-discord-vrc/blob/master/index-node.js

[^1]: https://docs.vrchat.com/docs/osc-overview
[^2]: https://yarnpkg.com/
[^3]: https://pnpm.io/ja/