Unlit Sphere

Let’s discover a new world with Unity

【Unity】TwitterRestAPIを使ってツイートの検索を行う

Pref.

UnityからTwitterのREST-APIを自家実装でたたけるように,TwitterOAuth認証について勉強しました∩(゚∀゚∩)

今回この記事では,TwitterOauth認証とは?というところから,Oauth認証の方法,UnityからOauth認証を行いAPIを叩いてみるところまでやってます.

この記事を読んで応用すれば自分でTwitterクライアントをUnityで作れます(白目) (ライブラリ使えばかんたんなのに...って言うのは置いておいて.勉強のためw)

GitHubにサンプルプロジェクトを置いておいたので,よかったら見ていってね 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とは...

OAuth (オー オース) は、権限の認可(authorization)を行うためのオープンスタンダードである。 2016年現在の最新の標準は、2012年にRFCとして発行されたOAuth 2.0である(RFC6749、RFC6750) ※Wikipedia引用

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

2.TwitterでのOAuth認証の方法

どのようなことをやりたいか定義しないと文章が書きづらいので,今回

としたいと思います.

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

Untitled Diagram (3).png

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

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

Untitled Diagram (4).png

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

記事を執筆して下さった方ありがとうございます. また,このツールを使い,学習するとめちゃくちゃHeaderとはなんぞや?という質問に効くと思います. Advanced REST client

3.実際のリクエスト作成手順

ではもっとクローズアップしていきます.ここから面倒です.

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

それらはTwitterAPIを利用するための鍵です. Twitterの開発者用ページでどんなアプリを作るかなどの登録を行い,「APIキー」や「アクセストークン」といったものが発行されるので,そちらを先に行っておきましょう.

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

f762d8heiuqw.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

ちなみに普段ブラウザでURLバーに書いてるのはRequestURLの部分のみ.Request Headerの部分は,何かツールを使ったりしない限り書き換えたりすることはできないです.(このため上にリンクを載せたツールを使ったり,Curlコマンドを使ったりしてテストしなければならない!)

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

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

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

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

今回のやりたいことの定義に従うとリクエストURLは以下のようになります.

https://api.twitter.com/1.1/search/tweets.json?count=20&q=%23Unity

っとここで「#」が「%23」になっとるぞーいって思った方,これはリクエストURLに含んではいけない「#」という文字を,含んでも良い文字に変換しています.URLエンコードといいますが,下で説明しています.


では次にRequest Headerの中身を説明していきます. こちらは結構初心者からすると複雑です...\(^o^)/

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

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

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

3.URLエンコード URLには含んでは行けない文字が規定されています.その文字をエンコードすることにより,URLに使っても良い文字列にする作業のことをURLエンコードといいます.

URLエンコード URLエンコード
認証 %E8%AA%8D%E8%A8%BC

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

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


Request Headerの中では予めTwitterのサーバ側で「Request Headerの中にはAuthorization ヘッダーが存在して,その中にはoauth_nonceoauth_signature_methodと....の情報を入れてね!」といった感じにとても細かく決められています.

クエリストリングもそうですが,この中身が1つでも間違うとBad Autentication data.みたいな返答が返ってきてしまうわけです...

Request Headerの中身の話に戻ります.

TwitterのOauth1.0で認証する際のRequest Headerには以下の情報が必ず含まれなければなりません.

Request Headerに必要なヘッダー Authorization

Authorizationヘッダーに必要な情報

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

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

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


認証用の文字列を作っていくわけなのですが,3つの材料が必要です.

784ewfew.png

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

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

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

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

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

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エンコード

以下のような%を含んだ文字列がURLエンコード済みの「パラメータ文字列」になります.

oauth_consumer_key%3Doooooo%26oauth_nonce%3D1532958453N%26oauth_....

3つの材料が揃いました.

これらを リクエストメソッド, リクエストURL, パラメータ文字列 の順に&でつなぎます.

3f8huiwenjk.png

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

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

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

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

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

7482yhiuenjc.png

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

4f2ejnk.png


ではもうひと頑張りして,続きをやっていきましょう.

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

Authorizationヘッダーに必要な情報

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

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

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

そして完成した文字列をRequest HeaderのAuthorizationヘッダーに格納します.

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

4.サンプルプログラム for Unity

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

「TwitterSearch」関数を呼ぶ前にAPIキーなどの鍵とトークンを入れることを忘れないようにしてくださいね ヾ(。>﹏<。)ノ゙

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

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