C#用のVRChat API SDKを使おう

# はじめに
タイトル通りです。VRChatのWEB APIをC#から使ってみました。
他の言語のSDKもありますが多分同じ使い方のはずです。

# 結論
- SDKはnugetからインストールする
- ユーザエージェントはテキトーでOK
- メソッド化されていないAPIにもアクセス可能
- ログイン時は二段階認証が必要
    - ただしasync/awaitはバグで使えない
- 実はVRChatのアカウントが無くてもAPIにアクセス可能
    - APIキーを使う
    - コンストラクタではなくプロパティからAPIキーを指定する

# いきさつ
なんか使ってみたくなっただけです。

# どうする
とりあえず[公式ドキュメント](https://vrchatapi.github.io/docs/api/)を読んでみます。
普通のREST APIですがSDK経由でアクセスすれば認証を簡単に出来るようです。
nugetは[こちら](https://www.nuget.org/packages/VRChat.API)。執筆時でのバージョンは1.17.6です。
検証条件は.NET 8.0です。.NET Frameworkでも同じコードになるはずです。

## ログインする
[SDKのGitHub](https://github.com/vrchatapi/vrchatapi-csharp)に使い方が書かれていました。
サンプルコードをそのまま使いましょう。簡略化したコードは下記です。
``` csharp
void Auth()
{
    var config = new Configuration
    {
        Username = "****",
        Password = "****",
        UserAgent = "Babylon/34.0", //ユーザエージェントは適当でOK
    };
    var client = new ApiClient();
    var authApi = new AuthenticationApi(client, client, config);

    var resp = authApi.GetCurrentUserWithHttpInfo();
    //2段階認証確認
    if (resp.RawContent.Contains("emailOtp"))
    {
        //コードの入力方法は自分で作ってね
        var s = Microsoft.VisualBasic.Interaction.InputBox("2段階認証コードを入れて、どうぞ", "2段階認証", "114514", -1, -1);
        authApi.Verify2FAEmailCode(new TwoFactorEmailCode(s));
    }
    else
    {
        //ここの条件と使い方はよく分からん
        //authApi.Verify2FA(new TwoFactorAuthCode("931810"));
    }
    //ここでログイン成功
    var currentUser = authApi.GetCurrentUser();
    Console.WriteLine(currentUser.DisplayName);
    //あとはご自由に
    var userApi = new UsersApi(client, client, config);
    var worldApi = new WorldsApi(client, client, config);
}
```

## バグ
2段階認証の非同期メソッドをコールすると401エラーが返ってきます。
[プロジェクトのissue](https://github.com/vrchatapi/vrchatapi-csharp/issues/14)によると放置されているようです。知らね。
``` csharp
//ここで401エラーで例外になる
await authApi.Verify2FAEmailCodeAsync(new TwoFactorEmailCode(s));
```


## ログインしない
認証情報の管理とかCookieとか実にめんどくさいのでログインなしで実行したいですね。
メソッドは用意されていませんがAPIキーを使います。
APIキーは公式ドキュメントに堂々と書かれていますので確認してください。
下記はログイン時のコードを改造したコードです。
``` csharp
async Task WithoutAuthAsync()
{
    var config = new Configuration
    {
        //ユーザ情報を消した
        UserAgent = "Mozilla/5.0"
    };
    config.AddApiKey("apiKey", "JlE5Jldo5Jibnk5O5hTx6XVqsJu4WJ26");
    var client = new ApiClient();
    
    var worldApi = new WorldsApi(client, client, config);
    //ワールド情報の取得は認証不要らしい
    await worldApi.GetWorldAsync("wrld_...");
}
```

## ワールドのサムネイルをダウンロードしてみる
得られたワールド情報にサムネイルのURIがあります。
サムネイルのAPIはライブラリにありませんが、先のコードを改造してサムネイルをダウンロードしてみます。
得られた画像はWPFの`Image`コントロールに使えます。
``` csharp
private async Task<BitmapImage> GetWorldThumbAsync(string worldId)
{
    var config = new Configuration
    {
        //ユーザ情報を消した
        UserAgent = "Babylon/34.0"
    };
    config.AddApiKey("apiKey", "JlE5Jldo5Jibnk5O5hTx6XVqsJu4WJ26");
    var client = new ApiClient();
    var worldApi = new WorldsApi(client, client, config);
    
    //ここから先は認証不要
    var worldInfo = await worldApi.GetWorldAsync(worldId);
    var option = new RequestOptions
    {
        Operation = "Iced.Tea" //適当なオペレーション名を与える（内部では使ってない）
    };
    //内部で使っているクライアントオブジェクトがあるのでそのまま使う
    var thumb = await worldApi.AsynchronousClient.GetAsync<byte[]>(worldInfo.ThumbnailImageUrl, option, config);
    
    //PNGのバイト配列から画像オブジェクトを取得
    var image = new BitmapImage();
    using (var stream = new MemoryStream(thumb.Data))
    {
        image.BeginInit();
        image.StreamSource = stream;
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.EndInit();
    }
    return image;
}
```

# おわりに
用途が限られているからか、中途半端に実装されている感は否めない。
どのエンドポイントでログインが必要かは知らん。自分で調べてね。