Discord.js v14 でステータスをモバイルにしたい！えっ、プロパティはなくなった…？

# TL; DR

:::note info
2024年11月3日現在、この手法は使用可能です。
:::

:::note warn
本記事のコードをコピーした記事に、バグを利用しているとの記述がありますが、バグや非推奨オプションではありません。
:::

```ts
const client = new Discord.Client({
    ws: {
        buildStrategy: manager => new (class MobileSimpleShardingStrategy extends SimpleShardingStrategy {
            constructor(manager) {
                manager.options.identifyProperties = {
                    os: "ios",
                    device: "device",
                    browser: "Discord iOS"
                };
                super(manager);
            }
        })(manager)
    }
});
```

# モバイルステータスって？

こういうやつ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309889/01e48dd8-48b6-2684-701e-6c08f2bd9257.png)

実はこれ、スマホだとモバイルとして表示されないんですよね。なぜでしょう…？

## 技術的な内容

実はこれは、接続元デバイスから判定しており、その情報はDiscord Gatewayに接続する際[^2]、identifyに自己申告する形で含まれています[^3]。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309889/15d956ba-bd9a-b668-8325-0a40abdb1fb1.png)

そのため、identifyを変更すればよいわけです。要は User-Agent 書き換えのようなものですね。

# 本題

## v13まで

v13[^1]までは `Client` のコンストラクタに指定するオプションで指定できていました。

https://renorari.net/blog/%E3%80%90%E8%A3%8F%E6%8A%80%E3%80%91discord-js%E3%81%A7%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%81%AE%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB%E8%A1%A8%E7%A4%BA/

```ts:v13 時点での指定方法
const client = new Discord.Client({
    ws: {
        properties: { $browser: "Discord iOS" }
    }
});
```

しかし、いつからかこのオプションが削除され、変更できなくなりました。
また、直接的な関係はありませんが、Discord API v10においても `$browser` 短縮プロパティが削除されました[^3]。
そのため、この方法はレガシーな方法といえるでしょう。

## v14では

しかし、v14ではそもそも `Client` のオプションの `ws` に関連する設定が見当たりません。

ではどうすればいいのかというと、**ソースを読んでコールツリーを頭の中で生成し、抜け道を探します**（力技）[^4]。

### 前提

Discord.js において、WebSocketに実際に接続する役割は `@discordjs/ws` というライブラリが担っています。このライブラリと、Discord.js 本体どちらにも `WebSocketManager`・`WebSocketShard` が存在するので、[^7]今回は前者を `WSWebSocketManager`・`WSWebSocketShard` と呼称します。

今回は TypeScript で動作する環境で `Client` オブジェクトのコンストラクタのオプションのみで、identify オブジェクトを変更することが目的です。

### コールツリー
1. コード[^5]を読むと、実際に送信される `properties` は `<WSWebSocketShard>.strategy.options.identifyProperties` を参照しています[^6]。
2. `<WSWebSocketShard>.strategy` については、 `IContextFetchingStrategy` が使用されています[^8]。この変数は、コンストラクタによって代入されています[^9]。
3. `WSWebSocketShard` は、`<SimpleShardingStrategy>.spawn` にて生成されています[^10]。
4. この `SimpleShardingStrategy` は、`<WSWebSocketManager>.strategy` に存在しています[^11]。以後、型は `IShardingStrategy` となります。
5. `<WSWebSocketManager>.strategy` は、 `<WSWebSocketManager>.options.buildStrategy` 関数によって生成されています[^12]。
6. `<WSWebSocketManager>.options` は、デフォルトが指定されているものの `options` で上書きされています[^13]。
7. `WSWebSocketManager` は、 `<WebSocketManager>.connect` にて生成されています。
8. `wsOptions.buildStrategy` は、 `ws.buildStrategy` を参照しています[^15]。
9. `ws` は、 `this.client.options` を参照しています。この先は自明であると思いますが、 `Client` のコンストラクタに指定できるオプションが参照されています。

### 結論

つまり、`Client` のコンストラクタのオプションの、 `ws.buildStrategy` 関数にカスタムクラスを一枚挟んで、受け取る `manager` (`WSWebSocketManager`) の `options` を変更すればいいということです。

なお、TypeScript と言っておきながら manager は下記コードでは `any` 型です。`@discordjs/ws` をインポートして解決することはできますが、私はめんどくさいので `any` でつぶしています。ごめんね。

```ts
const client = new Discord.Client({
    ws: {
        buildStrategy: manager => new (class MobileSimpleShardingStrategy extends SimpleShardingStrategy {
            constructor(manager) {
                manager.options.identifyProperties = {
                    os: "ios",
                    device: "device",
                    browser: "Discord iOS"
                };
                super(manager);
            }
        })(manager)
    }
});
```

# おわりに

Discord.js はドキュメントがふわふわしてたり分かれていたり互換性がなかったり…

TypeScript / JavaScript 信者なので、一番使いやすいライブラリではあるんですが、なんか絶妙な使いたくなさがありますね。

[^1]: 不明瞭な情報です。古いコード記載されていたのはこの方式で、その時のDiscord.jsのバージョンがv13だった記憶があるからです。よく調べてません、今使えないものは使えないんで。
[^2]: https://discord.com/developers/docs/topics/gateway#connection-lifecycle
[^3]: https://discord.com/developers/docs/topics/gateway-events#identify-identify-connection-properties
[^4]: 2時間かかりました。
[^5]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketShard.ts#L460
[^6]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketShard.ts#L446
[^7]: お前、嘘だろ…ネーミングセンス…………
[^8]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketShard.ts#L117
[^9]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketShard.ts#L129
[^10]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/strategies/sharding/SimpleShardingStrategy.ts#L29
[^11]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketManager.ts
[^12]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketManager.ts#L231
[^13]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/ws/src/ws/WebSocketManager.ts#L230
[^14]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/discord.js/src/client/websocket/WebSocketManager.js#L160
[^15]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/discord.js/src/client/websocket/WebSocketManager.js#L159
[^16]: https://github.com/discordjs/discord.js/blob/bfc7bb55641c60d4d67e57c27c9d1e63b6f30c9b/packages/discord.js/src/client/websocket/WebSocketManager.js#L137