UnityでTwitterRestAPIをOAuthの勉強がてら自家実装でたたいてみた

## Pref.
UnityからTwitterのREST-APIを自家実装でたたけるように，TwitterのOAuth認証について勉強しました∩(ﾟ∀ﾟ∩)

今回この記事では，TwitterのOauth認証とは？というところから，Oauth認証の方法，UnityからOauth認証を行いAPIを叩いてみるところまでやってます．

(ライブラリ使えばかんたんなのに...って言うのは置いておいて．勉強のためw)

<font color="Green">GitHubにサンプルプロジェクトを置いておいたので，よかったら見ていってね</font>
[https://github.com/nmxi/UnityTwitterRestAPI](https://github.com/nmxi/UnityTwitterRestAPI)

## Env.
Unity2018.1.5f1
Twitter REST-API 2018/06/28現在
(今年の8月を過ぎてもUser Streams APIは使っていないので，この方法で行けるはず...)

## Contents
### 1.OAuth認証とは
APIを使う上でTwitterの場合はセキュリティ関係のあれこれのためにOAuth認証をしなければなりません．
そのOAuthとは...

<font color="Green">OAuth (オー オース) は、権限の認可(authorization)を行うためのオープンスタンダードである。 2016年現在の最新の標準は、2012年にRFCとして発行されたOAuth 2.0である(RFC6749、RFC6750)</font>
[※Wikipedia引用](https://ja.wikipedia.org/wiki/OAuth)

つまり権限許可のための認証の仕組みのことですね．この文章には現在2.0が標準と書かれていますが，TwitterはOAuth1.0を利用しています．
1.0から2.0の変更点は自分もよく分かっていませんが，セキュリティ向上が行われたというわけではなく仕組みが変わったくらいらしいですので，Twitterの認証の仕方が古いというわけではないみたいです．

### 2.TwitterでのOAuth認証の方法
どのようなことをやりたいか定義しないと文章が書きづらいので，今回

* リクエストメソッドはGET
* 例としてTwitter APIの中でもSearch APIを使う

としたいと思います．

Twitter APIを使って過去にTweetされたデータに検索をかけるわけなのですが，まず以下の図を見てください．

![Untitled Diagram (3).png](https://qiita-image-store.s3.amazonaws.com/0/181807/12209600-90dd-25bf-117a-b576bba77fa0.png)

簡単ですね．ClientからTwitterのサーバにRequestをGETで送り，Twitterから結果が返される．
ただそれだけです．このClientとは今回に置き換えるとUnityで作るものです．
そして1章の説明であったOAuthの仕組みはこのRequestデータの中に含みます．

RequestデータをGETで送るのですがそのRequest HeaderにOAuth認証に使う署名(signature)を入れたりして
認証させます．上の図よりも詳しい図が以下の図です．

![Untitled Diagram (4).png](https://qiita-image-store.s3.amazonaws.com/0/181807/5e267649-df2f-ca88-12cf-3d129baab614.png)

GETでリクエストを送る際にURLの後ろにクエリストリングを記述し同時にAuthorization HeaderにOAuthのための認証のあれこれを
貼り付けてリクエストを送ると，TwitterからJSONで結果が返ってきます．
この図のJSONはコード215，認証失敗ですね（笑
正しければ検索の結果がJSONで返ってきます．
もし上の図のGETの中身のParameterとはなんぞや？Headerとはなんぞや？という方は，
以下の説明に進む前に，この辺りの記事を読まれてから進んだほうが良いかもしれません．

* [GETとPOSTの違いについて](https://qiita.com/kanataxa/items/522efb74421255f0e0a1)
* [HTTP（GET・POST）について](https://qiita.com/7968/items/4bf4d6f28284146c288f)

記事を執筆して下さった方ありがとうございます．
また，このツールを使い，学習するとめちゃくちゃHeaderとはなんぞや？という質問に効くと思います．
[Advanced REST client](https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo)

### 3.実際のリクエスト作成手順
ではもっとクローズアップしていきます．ここから面倒です．

まず「APIキー」や「アクセストークン」といった用語がこれから出てきますので簡単に説明します．

それらは<font color="red">TwitterのAPIを利用するための鍵</font>です．
[Twitterの開発者用ページ](https://developer.twitter.com/#)でどんなアプリを作るかなどの登録を行い，「APIキー」や「アクセストークン」といったものが発行されるので，そちらを先に行っておきましょう．

開発者ページの右上の方から新規アプリを作成していき，最終的に以下のような画面たどり着きます．

![f762d8heiuqw.png](https://qiita-image-store.s3.amazonaws.com/0/181807/c877dd06-78d6-e5a8-b029-f65935f2431e.png)

この画面にある4つの文字列をこの後使うので，メモっておいて下さい．

Consumer APi Keys
・API Key(APIキー)
・API Secret Key(APIシークレット)

Access token & access token secret
・Access token(アクセストークン)
・Access token secret(アクセストークンシークレット)

---

2章の最初でも定義しましたが，この章ではもっと詳しく今回やりたいことを定義します．

* リクエストメソッドはGETを使う
* Twitter APIの中でもTweet検索をするためのSearch APIを使う
* 検索ワードは"#Unity"
* 検索範囲は全て（言語制限なし，地域制限なし）
* 取得するツイートは最も新しいTweetから上位20Tweet

OAuth1.0で認証する際のリクエストは以下のようなファーマットでなければなりません．

![OAuth_requestParam_Example.png](https://qiita-image-store.s3.amazonaws.com/0/181807/7c2f2b03-a411-ab96-3d62-e4fbd38f8687.png)

ちなみに普段ブラウザでURLバーに書いてるのは<font color="blue">RequestURL</font>の部分のみ．<font color="green">Request Header</font>の部分は，何かツールを使ったりしない限り書き換えたりすることはできないです．（このため上にリンクを載せたツールを使ったり，Curlコマンドを使ったりしてテストしなければならない！）

<font color="blue">Request URL</font>の中身から説明していきます．
まず前半の<font color="Orange">https://api.twitter.com/1.1/search/tweets.json</font>というのはTwitter APIを使って「検索」を行うときにアクセスするURLです．今回は「検索」をしたいのでこのURLにアクセスしますがやりたいことによってこのURLは変わります．

続いてRequest URLの中の「？」よりも後の部分は「<font color="red">クエリストリング</font>」と呼びますが．クエリストリングには今回の検索で使うパラメータを指定します．
<font color="Purple">Count=20</font>というのは今回の「検索」の場合最新検索結果から上位20件を表示という意味で，<font color="Purple">q=〇〇</font>というのは検索ワードが〇〇という意味になります．（後述しますがこの検索ワード〇〇はURLエンコードされている必要があります．）

例として「検索」の場合以下のようなものなどがパラメータに指定できます．

|パラメータ|例|説明|
|---|---|---|
|q|q=Unity|検索ワード(このパラメータは必須)|
|lang|lang=ja|検索する対象の地域の言語コード|
|result_type|result_type=popular|取得するTweetの種類|
|count|count=10|検索結果の上位何件分のTweetを取得するか|
|include_entities|include_entities=false|Tweetオブジェクト内にentitiesを含むか否か|

今回のやりたいことの定義に従うと<font color="green">リクエストURL</font>は以下のようになります．

```
https://api.twitter.com/1.1/search/tweets.json?count=20&q=%23Unity
```
っとここで「#」が「%23」になっとるぞーいって思った方，これはリクエストURLに含んではいけない「#」という文字を，含んでも良い文字に変換しています．URLエンコードといいますが，下で説明しています．

---

では次に<font color="Green">Request Header</font>の中身を説明していきます．
こちらは結構初心者からすると複雑です...＼(^o^)／

まず後に出てくる単語で初心者だと「なんぞや？」となりやすいものを説明しておきます．

<b><font color="red">1.UnixTime(ユニックスタイム)</font></b>
1970/1/1 00:00:00から経過している秒数のことです．例として2018-07-30 13:47:33のUnixTimeは
<font color="red">1532958453といった感じになります．ぱっと見，数字の羅列です．</font>

<b><font color="red">2.Nonce(ナンス)</font></b>
number used onceの略で一度しか使われない数字(文字列)という意味があります．
唯一無二な文字列なわけですが，この記事では<font color="red">UnixTimeの数字の後ろに"N"をつけたものを利用します．</font>

<b><font color="red">3.URLエンコード</font></b>
URLには含んでは行けない文字が規定されています．その文字をエンコードすることにより，URLに使っても良い文字列にする作業のことをURLエンコードといいます．

|URLエンコード前|URLエンコード後|
|---|---|
|認証|%E8%AA%8D%E8%A8%BC|

こんな感じに%〇〇%〇〇みたいに変換されます．

<b><font color="red">4.ハッシュ</font></b>
ハッシュとは、簡単にいってしまうと「暗号化技術」のことです．ここでは深く説明しませんが，様々なハッシュ化アルゴリズムが存在していて，代表的なものにmd5やshaなどのものが存在します．

---

Request Headerの中では予めTwitterのサーバ側で<font color="red">「Request Headerの中にはAuthorization ヘッダーが存在して，その中には<b>oauth_nonce</b>と<b>oauth_signature_method</b>と....の情報を入れてね！」</font>といった感じに<font color="red">とても細かく</font>決められています．

クエリストリングもそうですが，この中身が<font color="red">1つでも間違うと</font>Bad Autentication data.みたいな返答が返ってきてしまうわけです...

<font color="Green">Request Header</font>の中身の話に戻ります．

TwitterのOauth1.0で認証する際のRequest Headerには以下の情報が<font color="red">必ず</font>含まれなければなりません．

Request Headerに必要なヘッダー
<font color="red">Authorization</font>

Authorizationヘッダーに必要な情報

|Key名|入れる情報(value)|
|---|---|
|Oauth oauth_consumer_key|APIキー|
|oauth_nonce|Nonce．UnixTimeにNをつけた文字列|
|<font color="red">oauth_signature</font>|URLエンコード済みの認証用文字列<font color="red">(これを作るのが一番面倒)</font>|
|oauth_signature_method|文字列"SMAC-SHA1"(細かくここでは説明しません)|
|oauth_timestamp|UnixTime|
|oauth_token|アクセストークン|
|oauth_version|文字列"1.0"(認証バージョンの設定，ここでは1.0を選択)|

これらのデータをそれぞれ揃えていくことがRequest Headerを組み立てる作業になるわけですが，
上の表の3番目の<font color="red">oauth_signature以外は</font>殆どが文字列を入れていくだけなので，深く考えることはありません．

<font color="red">oauth_signature</font>は以下の複雑な手順を踏んで作成した認証用の文字列を入れなければなりません．
ここからはその<font color="red">oauth_signature</font>に入れる文字列の作り方について説明します．

---

認証用の文字列を作っていくわけなのですが，<font color="red">3つの材料</font>が必要です．

![784ewfew.png](https://qiita-image-store.s3.amazonaws.com/0/181807/b2eaf2c9-59aa-1a64-c5d9-0588b4ce3ba3.png)

順番に作り方を書いていきます．

<font color="green">1.URLエンコード済みの「リクエストメソッド」を作成</font>
リクエストメソッドは3章のはじめに記述したように<font color="red">"GET"</font>を使うので，<font color="red">文字列"GET"をURLエンコードしたものが<b>URLエンコード済みの「リクエストメソッド」</b>になります．</font>

<font color="green">2.URLエンコード済みの「リクエストURL」を作成</font>
これも3章のはじめで説明した部分にありますが，リクエストURL，つまり今回ならば「検索」を行うので"https://api.twitter.com/1.1/search/tweets.json" にアクセスを行います．
よって<font color="red">文字列"https://api.twitter.com/1.1/search/tweets.json" をURLエンコードしたものが<b>URLエンコード済みの「リクエストURL」</b>になります．</font>

<font color="green">3.URLエンコード済みの「パラメータ文字列」を作成</font>
ここが認証文字列を作るための最終関門です．

まず以下の情報を入手します．

|Key名|入れる情報(value)|
|---|---|
|oauth_nonce|Nonce．UnixTimeにNをつけた文字列|
|oauth_signature_method|文字列"SMAC-SHA1"(細かくここでは説明しません)|
|oauth_timestamp|UnixTime|
|oauth_consumer_key|アクセストークン|
|oauth_version|文字列"1.0"(認証バージョンの設定，ここでは1.0を選択)|
|q|クエリ文字列(クエリストリングに記述したものと一緒)|

これらの情報をKeyの名前順に昇順ソートを行い
Key=Value&Key=Value&Key=...に組み直します．

こんな感じの文字列になります．

```
oauth_consumer_key=oooooo&oauth_nonce=1532958453N&oauth_....
```

その組みなおした文字列をURLエンコードし

以下のような%を含んだ文字列が<font color="red"><b>URLエンコード済みの「パラメータ文字列」</b></font>になります．

```
oauth_consumer_key%3Doooooo%26oauth_nonce%3D1532958453N%26oauth_....
```
---
3つの材料が揃いました．

これらを
<font color="green"><b>リクエストメソッド, リクエストURL, パラメータ文字列<b></font>
の順に<font color="red">&</font>でつなぎます．

![3f8huiwenjk.png](https://qiita-image-store.s3.amazonaws.com/0/181807/f58a0fa9-7ff3-f8d8-9dd5-53edb8a000b5.png)

つないだらこんな感じになります．

```
GET&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fsearch%2Ftweets.json&oauth_consumer_key%3Doooooo%26oauth_nonce%3D1532958453N%26oauth_....
```

そして最後にHMAC-SHA1方式のハッシュ値に変換します(ハッシュ化)．
ハッシュ値に変換するときには「ハッシュ化するデータ」を「salt(塩)と呼ばれるデータ」を使ってハッシュ化します．

今回の場合は
<font color="red">ハッシュ化するデータ</font>には3つの材料をつなげた文字列を使い，
<font color="red">salt(塩)と呼ばれるデータ</font>にはAPIシークレットとアクセストークンシークレットを"&"で繋いだ文字列を使います．

ハッシュ化します．イメージはこんな感じ．

![7482yhiuenjc.png](https://qiita-image-store.s3.amazonaws.com/0/181807/78f10eda-c99f-b3dd-2744-ad7754bbd3c1.png)


ハッシュ値が出てきたらそれをBase64でエンコードします．
これで認証用文字列が完成しました．ﾎｯ (´･ω･`)


![4f2ejnk.png](https://qiita-image-store.s3.amazonaws.com/0/181807/4e82f2e8-540c-4621-cd1d-192d297ceb04.png)

---
ではもうひと頑張りして，続きをやっていきましょう．

上で記述した表を，もう一度以下に記述します．

<font color="red">Authorizationヘッダーに必要な情報</font>

|Key名|入れる情報(value)|
|---|---|
|Oauth oauth_consumer_key|APIキー|
|oauth_nonce|Nonce．UnixTimeにNをつけた文字列|
|<font color="red">oauth_signature</font>|URLエンコード済みの認証用文字列<font color="red">(完成した！)</font>|
|oauth_signature_method|文字列"SMAC-SHA1"(細かくここでは説明しません)|
|oauth_timestamp|UnixTime|
|oauth_token|アクセストークン|
|oauth_version|文字列"1.0"(認証バージョンの設定，ここでは1.0を選択)|

全てのKeyのValueに何を入れればよいかが分かりました．
ではAuthorizationヘッダーを組み立てていきましょう．

Authorizationヘッダーに必要な情報の表の順に
<font color="red">Key=Value,Key=Value,Key=...</font>のように文字列を繋げていきます．
今までは"&"を使って繋げていましたが，ここでは",(カンマ)"を使います．

そして完成した文字列を<font color="red">Request HeaderのAuthorizationヘッダー</font>に格納します．

これでTwitterのAPIをつかって文字列検索を行うためのリクエストデータが完成しました．
4章ではこのやり方を使って，Unityからリクエストを送って結果を受け取れるようにします．

### 4.サンプルプログラム for Unity
このスクリプトの「TwitterSearch」関数をボタンなどから呼ぶと，Twitterに「#Unity」というハッシュタグ付きツイートの検索を行い，検索結果の上位20件をJsonで返されるので，Debug.Logでコンソール上に表示します．

<font color="red">「TwitterSearch」関数を呼ぶ前にAPIキーなどの鍵とトークンを入れることを忘れないようにしてくださいね
ヾ(｡>﹏<｡)ﾉﾞ</font>

入力した後にエラーコード「401」が返ってきたら，APIキーなどに空白文字が入っていないかを見てみて下さい．
直る場合があります．

```TwitterCommentSearcher.cs
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Security.Cryptography;
using System.Net;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;

/// <summary>
/// Twitterから特定の文字列を含むツイートを検索し，結果をJsonで取得する
/// </summary>
public class TwitterCommentSearcher : MonoBehaviour {

    [SerializeField] private string _apiKey;
    [SerializeField] private string _apiSecret;
    [SerializeField] private string _accessToken;
    [SerializeField] private string _accessTokenSecret;

    [SerializeField] private int _count;    //一度に取得するtweeet数
    [SerializeField] private string _searchWord;    //検索文字列

    private string _requestURL = "https://api.twitter.com/1.1/search/tweets.json";
    private string _requestMethod = "GET";

    private Dictionary<string, string> _dic;

    private string _oauthSignature; //baase64でエンコードされたsignature

    private WWW www;

    private long _lastFetchTweetID; //最後に取得したTweetのID

    void Awake() {
        //検索ワードに#Unityを代入
        _searchWord = "#Unity";

        //Encode searchWord
        _searchWord = LargeCharUrlEncode(_searchWord);
    }

    public void TwitterSearch() {
        StartCoroutine(Fetch());
    }

    private IEnumerator Fetch() {
        //Fetch UnixTime
        var baseDt = new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0), TimeSpan.Zero);
        long unixTime = (DateTimeOffset.Now - baseDt).Ticks / 10000000;

        //Encode Signature_key
        string signatureKey = _apiSecret + "&" + _accessTokenSecret;

        //for Nonce
        string nonce = unixTime.ToString() + "N";

        //必要なパラメータを列挙
        var _dicTmp = new SortedDictionary<string, string>() {
        {"oauth_nonce", nonce},
        {"oauth_signature_method", "HMAC-SHA1" },
        {"oauth_timestamp", unixTime.ToString()},
        {"oauth_consumer_key", _apiKey},
        {"oauth_token", _accessToken},
        {"oauth_version", "1.0"},
        {"count", _count.ToString()},
        {"q" , _searchWord}    //クエリ
        };

        //ソートを行い_dicに詰めいていく
        _dic = new Dictionary<string, string>();
        foreach (KeyValuePair<string, string> pair in _dicTmp) {
            _dic.Add(pair.Key, pair.Value);
        }

        //_dicをキー=値&キー=値&の順に組み立てる
        string paramStr = "";
        foreach (KeyValuePair<string, string> pair in _dic) {
            paramStr += "&" + pair.Key + "=" + pair.Value;
        }
        paramStr = paramStr.Remove(0, 1);

        //Debug.Log(paramStr);

        //paramStrをURLエンコード
        string encodedRequestParams = LargeCharUrlEncode(paramStr);
        //Debug.Log(encodedRequestParams);

        //リクエストメソッドをエンコード
        string encodedRequestMethod = LargeCharUrlEncode(_requestMethod);

        //リクエストURLをエンコード
        string encodedRequestURL = LargeCharUrlEncode(_requestURL);

        string signatureData = encodedRequestMethod + "&" + encodedRequestURL + "&" + encodedRequestParams;

        //Debug.Log(signatureData);

        //HMAC-SHA1方式のハッシュ値に変換
        _oauthSignature = HashingHMACSHA1(signatureKey, signatureData);

        ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(OnRemoteCertificateValidationCallback);

        string url = "https://api.twitter.com/1.1/search/tweets.json?count=" + _count.ToString() + "&q=" + _searchWord;
        //Debug.Log(url);
        HttpWebRequest hwr = (HttpWebRequest)WebRequest.Create(url);

        string authorizationHeaderParams = "Oauth oauth_consumer_key=" + _dic["oauth_consumer_key"] + "," +
                                            "oauth_nonce=" + _dic["oauth_nonce"] + "," +
                                            "oauth_signature=" + WWW.EscapeURL(_oauthSignature) + "," +
                                            "oauth_signature_method=HMAC-SHA1," +
                                            "oauth_timestamp=" + _dic["oauth_timestamp"] + "," +
                                            "oauth_token=" + _dic["oauth_token"] + "," +
                                            "oauth_version=1.0";

        //Debug.Log(authorizationHeaderParams);

        hwr.Headers.Add("Authorization", authorizationHeaderParams);

        hwr.Method = "GET";

        HttpWebResponse res = (HttpWebResponse)hwr.GetResponse();   //fetch

        Stream receiveStream = res.GetResponseStream();

        StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8);

        Encoding ascii = Encoding.ASCII;
        string responseJsonStr = readStream.ReadToEnd();

        Debug.Log(responseJsonStr);

        res.Close();
        readStream.Close();

        yield break;
    }

    public string HashingHMACSHA1(string key, string dataToSign) {
        Byte[] secretBytes = UTF8Encoding.UTF8.GetBytes(key);
        HMACSHA1 hmac = new HMACSHA1(secretBytes);

        Byte[] dataBytes = UTF8Encoding.UTF8.GetBytes(dataToSign);
        Byte[] calcHash = hmac.ComputeHash(dataBytes);
        String calcHashString = Convert.ToBase64String(calcHash);
        return calcHashString;
    }

    //信頼できないSSL証明書を「問題なし」にする
    private bool OnRemoteCertificateValidationCallback(
      System.Object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors) {
        return true;
    }

    /// <summary>
    /// URLエンコードを大文字で返す
    /// </summary>
    /// <param name="inputText"></param>
    /// <returns></returns>
    private string LargeCharUrlEncode(string inputText) {
        var escapedStr = WWW.EscapeURL(inputText);

        string sdTmp = "";
        for (int i = 0; i < escapedStr.Length; i++) {
            var c = escapedStr[i];
            if (c == '%') {
                sdTmp += c;
                c = escapedStr[i + 1];
                c = c.ToString().ToUpper()[0];
                sdTmp += c;
                c = escapedStr[i + 2];
                c = c.ToString().ToUpper()[0];
                sdTmp += c;
                i = i + 2;
            } else {
                sdTmp += c;
            }
        }

        return sdTmp;
    }
}
```

## Reference

- https://syncer.jp/Web/API/Twitter/REST_API/GET/search/tweets/