OculusGoからTwitter認証する

[Oculus Rift Advent Calendar 2018](https://qiita.com/advent-calendar/2018/oculus-rift)の15日目です。

以前から作っていたUnityC#用のTwitter API ライブラリに、先日、認可周りのアップデートを入れました。
https://github.com/toofusan/Twity

自分もこれまでOauthのことはよくわかっておらず、アップデートによっていろいろ勉強になったのでそのへんのことを書きつつ、OculusGoでの実装例を書いていきます。



# はじめに
UnityからTwitter APIを利用する際、認証の方法は大きくわけて2つあります。それぞれできることが違い、かつ認証時のユーザー体験も異なるため、適宜どちらにするか選びます。

### 3-legged authorization
主体となるユーザーの特定が必要な場合に利用します。
例：ツイートする、ホームタイムラインを見る、DMを見たり送信したりする
後者のApplication-only authenticationに比べてできることが多い一方、一度ブラウザを通してTwitterの画面を表示し、ユーザーの認可を得る必要があります。また、Streaming APIについてもこちらを使う必要があります。

### Application-only authentication
主体となるユーザーが特定されない場合に利用します。
例：特定ユーザーのツイートを取得する、ツイートを検索する、トレンドを見る
できることは限られますが、ユーザーの認可を必要としないため、サクッと認証を終わらせることができます。

# ユーザー認証が必要な場合（3-legged authorization)
OculusGoでやってみた感じはこんなのです。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Oculus GoでもTwitter認証できるようになったー<br>ブラウザからアプリへ一発で戻れるようになったらさらに快適だな <a href="https://t.co/B9WsBkV5Gt">pic.twitter.com/B9WsBkV5Gt</a></p>&mdash; 人生 (@toofu__) <a href="https://twitter.com/toofu__/status/1049144476696899585?ref_src=twsrc%5Etfw">2018年10月8日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

体験として最適かと言われるとそうではない…数字覚えなきゃいけないし単純にめんどくさいし…
本当は認可時にそのままアプリに戻ってこれるといいんですが、AndroidのIntentとかがよくわかっておらず後回しにしています。


まず `consumer_key`と `consumer_secret` を使い、 `request_token`と`request_token`を手に入れます。`https://api.twitter.com/oauth/authorize?oauth_token={手に入れたrequest_token}` がユーザーに認可を行ってもらうためのURLとなるため、そこにブラウザで遷移してもらう必要があります。Oculus Goでは `Application.OpenURL(url)` でブラウザを起動することができるので、それを使います。

``` c#:リクエストトークンの取得
public class EventHandler : MonoBehaviour {

  [SerializeField] private string consumer_key;
  [SerializeField] private string consumer_secret;

  void Start () {
    Twity.Oauth.consumerKey = consumer_key;
    Twity.Oauth.consumerSecret = consumer_secret;
    GenerateRequestToken();
  }

  void GenerateRequestToken() {
    StartCoroutine(Twity.Client.GenerateRequestToken(RequestTokenCallback));
  }

  // Twityライブラリではリクエストトークンを取得した時点でOauth.authorizeURLに認可用URLが格納されます
  void RequestTokenCallback(bool success) {
    if (!success) return;
    Application.OpenURL(Twity.Oauth.authorizeURL);
  }

}
```

で、ユーザーがブラウザ上で認可してくれたら数字が表示されるので、その数字を入力するUIを作ります（省略）。その数字を使って`access_token`を取得します。

```c#:アクセストークンの取得
public class EventHandler : MonoBehaviour {
  void GenerateAccessToken() {
    StartCoroutine(Twity.Client.GenerateAccessToken({ユーザーが入力した数字}, AccessTokenCallback));
  }

  void AccessTokenCallback(bool success) {
    if (!success) return;
    // success == trueの時点でOauth.accessToken、Oauth.accessTokenSecret、Oauth.screenNameにはそれぞれの情報が格納されています。
  }

}
```

これで認可が完了しました。が、このままだとアプリを起動するたびに毎回認可しなおさないといけないので、必要な情報を端末に保存しておくなどの処理を入れます（`access_token`には期限がありません）



```c#:端末に情報を保存したりログイン判別をしたり
public class EventHandler : MonoBehaviour {
  [SerializeField] private string consumer_key;
  [SerializeField] private string consumer_secret;
  private string access_token;
  private string access_token_secret;
  private string screen_name;

  void Start () {
    Twity.Oauth.consumerKey = consumer_key;
    Twity.Oauth.consumerSecret = consumer_secret;
    StartAuthorization();
  }

  void StartAuthorization() {
    if (CheckAuthorizationInfo()) {
      // access_tokenとaccess_token_secretが端末内に保存されている場合（＝認可済の場合）
      Twity.Oauth.accessToken = access_token;
      Twity.Oauth.accessTokenSecret = access_token_secret;
      Twity.Client.screenName = screen_name;

      // access_tokenが何らかの原因で正しくないものになっていないか確認するため、試しにユーザー情報を取得する
      Dictionary<string, string> p = new Dictionary<string, string> {
        {"screen_name", Twity.Oauth.screenName}
      };
      StartCoroutine(Twity.Client.Get("users/show", p, (bool success, string response) => {
        if (!success) {
          // 何らかの原因でaccess_token等がおかしい可能性があるので、一旦削除して最初からやりなおす
          PlayerPrefs.DeleteAll();

        } else {
          // 認可は成功している

        }
      });
    } else {
      // まだ認可されていない場合、端末内にrequest_tokenがあるかどうかをチェックする
      if (CheckRequestToken()) {
        // 端末内にrequest_tokenがある場合、
        Twity.Oauth.requestToken = request_token;
        Twity.Oauth.requestTokenSecret = request_token_secret;

        // この状態はpin入力のシーンである場合があるので、入力用のUIを表示する
        
      } else {
        // まだ何もない状態なので、request_tokenの取得を行う
        GenerateRequestToken()

      }
    }
  }

  // 端末内にaccess_tokenとaccess_token_secretが保存されているか確認する
  bool CheckAuthorizationInfo() {
    access_token = PlayerPrefs.GetString("access_token", "");
    access_token_secret = PlayerPrefs.GetString("access_token_secret", "");
    screen_name = PlayerPrefs.GetString("screen_name", "");
    if (string.IsNullOrEmpty(access_token) || string.IsNullOrEmpty(access_token_secret) || string.IsNullOrEmpty(screen_name)) {
      return false;
    } else {
      return true;
    } 
  }

  // 端末内にrequest_tokenとrequest_token_secretが保存されているか確認する
  bool CheckRequestToken() {
    request_token = PlayerPrefs.GetString("request_token", "");
    request_token_secret = PlayerPrefs.GetString("request_token_secret", "");
    if (string.IsNullOrEmpty(request_token) || string.IsNullOrEmpty(request_token_secret)) {
      return false;
    } else {
      return true;
    }
  }

}

```

# ユーザー認証が不要の場合（Application-only authentication）
こちらは`request_token`などは必要なく、`consumer_key`と`consumer_secret`をもとに`bearer_token`を取得したら、それをもとにAPIへリクエストを送ることができます。

```c#:Application-only-authentication
public class EventHandler : MonoBehaviour {

  [SerializeField] private string consumer_key;
  [SerializeField] private consumer_secret;

  void Start () {
    Twity.Oauth.consumerKey = consumer_key;
    Twity.Oauth.consumerSecret = consumer_secret;
    StartCoroutine(Twity.Client.GetOauth2BearerToken(OnGetBearerToken));
  }

  void OnGetBearerToken(bool success) {
    if (!success) return;

    // この時点で認証完了。試しに特定のユーザー情報を出力する
    GetUserInfo("toofu__");
  }
    
  void GetUserInfo(string screen_name) {
    Dictionary<string, string> p = new Dictionary<string, string> {
      { "screen_name", screen_name}
    };
    StartCoroutine(Twity.Client.Get("users/show", p, OnGetUserInfo));
  }
    
  void OnGetUserInfo(bool success, string response) {
    if (!success) return;
    Debug.Log(response);
  }
}

```

という感じで、特にユーザーに特別な操作をしてもらう必要もなく、サクッと認証が完了します。ツイッタークライアントと言えるほどの機能はつけられませんが、ちょっとツイッターの情報をアプリ内に表示したい、といった場合には十分使えるのではないでしょうか。
（自分はこれで「ツイートが流れるVRChatのワールドが作れるのでは？」とか考えていたのですが、調べたところVRChatはそもそもカスタムスクリプトが使えないので不可能そう）