JavaScriptでTwitterのOAuthを取り付けて通信する方法

以下の記事とTwicliのソースをコピペして作成しました

おれにこんな難しいことができるはずがない
TwitterクライアントのOAuth対応(Javascript編) | tomatomax.net
NeoCat/twicli · GitHub

JavaScriptでクロスドメイン通信したい

方法としては2つある

結論としては両方使いました。Firefoxの拡張ならGreeseMonkeyでGM_xmlhttpRquestがあるし、ChromeOperaの拡張だとどこかの設定でOrigin PolicyでAllowすればXMLHttpRquestして返り値を取ってくることができる。でも今回は生でやりとりするから

  • 返り値を求めないPOSTは$.ajax
  • GET参照する場合はJSONP

という形で落ち着きました。*1

JS(jQuery)クロスドメイン通信に関しての質問です。GMや拡張ではない環境(生HTML)でクロスドメイン通信しようとするときに動的にiframeを生成してそこから通信するとかなにかありますが(Twicliを参考にしてます)、そもそもなぜiframeなのですか?

@xxxxx
基本的にはクロスドメイン通信は相手側の許可(というか特別な対応)がないとできません。

ですよね。だからOriginをサイト側で設定したり拡張の設定でAllowする必要がある。でも、じゃあなんでiframeを動的に作ってそこからポストすればSame Originにひっかからないんですか?

@yyyyyy
Same Originにはちゃんと引っかかっていて、レスポンスは受け取れません
Twitterについて、クロスドメインで結果を受け取るにはJSONPしかないですが、JSONPはGETしか送れないので、POSTしか受け取らないAPIにはアクセスできません。おそらくそこで引っかかっていると思います。
結局のところ、TwitterJavaScriptから直接投げるのではなく、自前のサーバーを経由させるのがスタンダードですね。

OAuthをとりつける

手順はよくある通り。厳密には間違えているかもしれない。

  • Consumer Key と Consumer Secretを発行(アプリ)
  • Consumer Key と Consumer SecretをつかってRequst Tokenを発行するURLに問いかけ、Request Tokenと Request Token Secret を取得
  • Twitterで認証
  • Callbackで戻ってくるときにVerifierがクエリについてくるので、それを保存(Callbackを指定しない場合はPINが返ってくる)
  • Consumer Key と Consume Secret と Verifier を使って Access Token と Access Token Secretを取得
  • TwitterにはAccess Tokenやらいままで取得した値をつかって署名してリクエストを送る

各種Tokenを取得する

これがコピペプログラミングの真髄や(ドヤァ
htmlはてきとうにこんな感じにしておく

<!DOCTYPE HTML>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>skz3</title>
  <script type="text/javascript" src="./js/oauth.js"></script>
  <script type="text/javascript" src="./js/sha1.js"></script>
  <script type="text/javascript" src="./js/skz3_oauth.js"></script>
  <link rel="stylesheet" href="./css/skz3.css" />
</head>
<body>
<input type="button" onclick="TwitterOAuth.prototype.getRequestToken()" value="request_tokenを取得する"/>
<form method="GET" action="https://api.twitter.com/oauth/authorize" onsubmit="return TwitterOAuth.prototype.setRequestToken(this)">
  <input type="text" name="oauth_token"/>
  <input type="submit" value="送信っ!"/>
</form>

</body>
</html>

Request Tokenを取得するボタンを押すと、window.openしてレスポンスが返ってくるので、コピペしてもらう(ここでクロスドメイン通信ができるなら自動的に返り値をセットすることなぞ簡単だが、それができないからこうしている)。ちなみにAccess Tokenを取得するときもほぼ全く同じHTMLなので割愛します。
JavaScriptはこんな感じ。CoffeeScriptだけど。あらかじめoauth - API needz authorized? - Google Project HostingのSource→JavaScriptから oauth.js と sha1.js を読み込ませておきます

class TwitterOAuth
    getRequestToken: ->

        accessor =
          consumerSecret: "YOUR CONSUMER SECRET"
          tokenSecret: ''

        message =
          method: "GET"
          action: "https://api.twitter.com/oauth/request_token"
          parameters:
            oauth_signature_method: "HMAC-SHA1"
            oauth_consumer_key: "YOUR CONSUMER KEY"

        OAuth.setTimestampAndNonce(message)
        OAuth.SignatureMethod.sign(message, accessor)
        target = OAuth.addToURL(message.action, message.parameters)

        window.open(target)


    setRequestToken: (tkn) ->
        key = tkn.oauth_token.value
        //正規表現でクエリをとってきてlocalStorageに保存する
        if key.match(/^oauth_token=([^&]+)&oauth_token_secret=([^&]+)/)
            localStorage.setItem("request_token", RegExp.$1)
            localStorage.setItem("request_token_secret", RegExp.$2)
            tkn.oauth_token.value = RegExp.$1
            return true

    setVerifier: (href) ->
        //Callbackで呼ばれるURLが自動的に発火するScript
        //これも正規表現でクエリをとってきたらAccess Token取得ページヘリダイレクト
        if href.match(/oauth_verifier=([^&]+)/)
            localStorage.setItem("verifier",  RegExp.$1)
            location.href = "./access_token.html"

    getAccessToken: ->
        accessor =
          consumerSecret: "YOUR CONSUMER SECRET"
          tokenSecret: localStorage.getItem("request_token_secret") # Request Token Secret

        message =
          method: "GET"
          action: "https://api.twitter.com/oauth/access_token"
          parameters:
            oauth_signature_method: "HMAC-SHA1"
            oauth_consumer_key: "YOUR CONSUMER KEY"
            oauth_token: localStorage.getItem("request_token") # Request Token
            oauth_verifier: localStorage.getItem("verifier")

        OAuth.setTimestampAndNonce(message)
        OAuth.SignatureMethod.sign(message, accessor)
        target = OAuth.addToURL(message.action, message.parameters)

        window.open(target)

    setAccessToken: (form) ->
        key = form.access_token.value
        if (key.match(/^oauth_token=([^&]+)&oauth_token_secret=([^&]+)/))
            localStorage.setItem("access_token", RegExp.$1)
            localStorage.setItem("access_token_secret", RegExp.$2)
            if (key.match(/screen_name=([^&]+)/))
                localStorage.setItem("access_user", RegExp.$1)

これで必要なAccess Token等を取得するところまではできた。次は取得したTokenを使ってクエリに含めてリクエストを送る

リクエストを作成する

こんな感じ

class TwitterBase

    @TWITTER_ROOT = "https://twitter.com/#!/"
    @TWITTER_API = "https://api.twitter.com/1/"

    setOAuth: (method, api, params)->
        accessor =
          consumerSecret: "YOUR CONSUMER SECRET"
          tokenSecret: localStorage.access_token_secret

        message =
            method: method
            action: api
            parameters:
              oauth_consumer_key: "YOUR CONSUMER KEY"
              oauth_token: localStorage.access_token
              oauth_signature_method: "HMAC-SHA1"
              #suppress_response_codes: "True"

        //必要なパラメータはここで構築してSignする必要がある
        for key of params
            message.parameters[key] = params[key]

        OAuth.setTimestampAndNonce(message)
        OAuth.SignatureMethod.sign(message, accessor)
        authed_url= OAuth.addToURL(message.action, message.parameters)
        return authed_url

    //動的にスクリプトを生成してJSONPでコールバックする
    loadJSON: (url) ->
        //jQuery風じゃなかったら別にこれでもいい
        #script = document.createElement("script")
        #script.src = url
        #script.type = "text/javascript"
        #script.charset = "UTF-8"
        #document.body.appendChild(script)
        //jQuery方言風に
        script = $("<script>")
        script.attr('src', url)
        //うまくいかない
        #script.attr('type', "application/json")
        //普段はこれでいいけど、今回はだめ
        #script.attr('type', "text/javascript")
        //これだよ!!!!
        script.attr('type', "application/javascript")
        script.attr('charset', "UTF-8")
        $(document.body).append(script)

ここで重要なのは、OAuthで認証するわけじゃなければJSONPのmimetypeは "text/javascript" でもいいけど、Twitterの場合は "application/javascript"でなくてはならない!!!!!!!!これで半日くらいハマった。どうも401のAuthで落ちるなぁと。レスポンスヘッダみて「へェー知らないパラメータだなァー」と思ってたらこれだよ!!!!
参考::JavaScript関係の媒体型
参考::ちょっとしたメモ - スクリプトのMIMEタイプがRFCとなって公式登録へ
参考::nagenegiのブログ : JavaScriptの正しいscriptタグ
参考::JSONとJSONPのContent-typeに書くMIMEタイプ - kanonjiの日記
実際にリクエストするときはこんな感じでつかう

class API extends TwitterBase

    //POSTで投げる場合は返り値が帰ってこないけどリクエストは通るので$.ajax
    updateStatus: (text, in_reply_to_status_id)->
        params =
            status: text
            in_reply_to_status_id: in_reply_to_status_id
        url = TwitterBase::setOAuth("POST", "#{TwitterBase.TWITTER_API}statuses/update.json", params)
        $.ajax
           type: "POST"
           url: url
           success: ->
               pass
           error: (XMLHttpRequest, textStatus, errorThrown) ->
               console.log textStatus

    //参照系のGETで取得する場合はJSONPでとってきてCallbackする
    getHomeTimeline: ->
        params =
            include_entities: "True"
            callback: "callback"
            count: "50"
        url = TwitterBase::setOAuth("GET", "#{TwitterBase.TWITTER_API}statuses/home_timeline.json", params)
        TwitterBase::loadJSON(url)

//Callbackのサンプル。とりあえずdumpする。実際はこれを元に動的にページを構築する
callback = (json) ->
    for arg in json
        console.log arg

JSONPで受け取ったリクエストを動的に組み立てるサンプルとしてはだいたい前やりました。これはOAuthしないバージョンです。CoffeeScriptじゃなくてJavaScriptです。もっといえば一枚のHTMLにHTML+CSS+JavaScript詰め込んでます。すごく便利だよ。これを誰にでも使えるようにするのが現在進行形のスカンディナヴィア半島3プロジェクトなのだ。

以上

昔は難しすぎて挫折してたけど、まがりなりにもJavaScript生でOAuth突破できたよ!おめでとう!まあ、普通はGreeseMonekyしたりChromeOpera拡張したり、各種言語のライブラリつかって楽するのが普通だけどな!!!!

追記

しかしOperaではPOSTが投げれないのであった

"No Transport"

これを回避するためにjQueryでおまじないを唱えた
参考::PhoneGapアプリでjQuery(1.5以上)のクロスサイトなajax通信を有効にするには|開発部 in ICtriumphs
参考::How to use jquery queries: ajax, get ? - Opera Extensions Development Discussions - Opera Community

$.support.cors = true

しかし今度は

"Security violation"

無理ゲー……。Opera使いとしてはOperaで使えないとかなり困るので、これはどうにかしたいが、探しても無理ゲーなのだが……

*1:POSTするときに動的にiframeを生成してhogehogeするって方法もあるらしいけど、なんだか難しそうだし別に$.ajaxでできたのでいいことにした。それにiframeをつかってもSame Origin ポリシーにはひっかかって返り値を取ってこれないのだから、別に苦労する必要のもないかと。