shonen.hateblo.jp

やったこと,しらべたことを書く.

gem なしの Ruby で Twitter API にアクセスする

こんな野蛮な事をやっているサイトは他にないだろうと思ってPHPのサイトを参考にしながら書いていたのですが,ググったらあった.

gem が導入できない環境で Twitter API にアクセスするようなコードを書きました.

はじめに・前提

こちらのサイトを参考にしています. Twitter APIの細かい説明などはこちらを参照してください.

syncer.jp

Rubyのバージョンは2.3.0です.

プログラム

次の3つのメソッドを作りました.他は補助的なメソッドです.

  • twitter_oauth_post_requestToken(api_key, api_secret, callback_url)
  • twitter_oauth_get_accessToken(api_key, api_secret, oauth_token, oauth_token_secret, oauth_verifier)
    • 認証情報を基にアクセストークンを取得する.
  • twitter_statuses_userTimeline(api_key, api_secret, oauth_token, oauth_token_secret, screen_name, count)
    • screen_name のタイムラインを count 個取得する.

コードを示します.

require 'uri'
require 'openssl'
require 'base64'
require 'net/http'
require 'json'
require 'securerandom'

def my_escape(str)
    URI.escape(str, /[^a-zA-Z0-9._-]/)
end

def https_post(url, form_data = {}, header = {})
    uri = URI.parse(url)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_PEER

    response = https.start do |https|
        req = Net::HTTP::Post.new(uri, header)
        req.set_form_data(form_data)
        https.request(req)
    end
    return response
end

def https_get(url, form_data = {}, header = {})
    uri = URI.parse(url)
    uri.query = URI.encode_www_form(form_data)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_PEER

    response = https.start do |https|
        req = Net::HTTP::Get.new(uri, header)
        https.request(req)
    end
    return response
end

def twitter_make_signature(request_method, request_url, params, api_secret, api_token_secret)
    params_str = params.sort.map{|k,v| "#{k}=#{v}" }*'&'

    signature_key = "#{my_escape(api_secret)}&#{my_escape(api_token_secret)}"
    signature_data = "#{my_escape(request_method)}&#{my_escape(request_url)}&#{my_escape(params_str)}"

    hash_raw = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, signature_key, signature_data)
    return my_escape(Base64.strict_encode64(hash_raw))
end


def twitter_oauth_post_requestToken(api_key, api_secret, callback_url)
    request_url = "https://api.twitter.com/oauth/request_token"

    time = Time.now()
    params = {
        "oauth_callback" => callback_url,
        "oauth_consumer_key" => api_key,
        "oauth_nonce" => SecureRandom.uuid,
        "oauth_signature_method" => "HMAC-SHA1",
        "oauth_timestamp" => time.to_i.to_s,
        "oauth_version" => "1.0"
    }
    params.each{|k,v| params[k] = my_escape(v.to_s) if k != "oauth_callback" }
 
    params["oauth_signature"] = twitter_make_signature("POST", request_url, params, api_secret, "")

    oauth_header = "OAuth " + params.sort.map{|k,v| "#{k}=#{v}"}*','
    response = https_post(request_url, {}, {"Authorization" => oauth_header})

    case response
    when Net::HTTPSuccess
        results = response.body.split('&').reduce({}){|s,e| k,v=e.split('='); s[k]=v; s }
        return [response.code, results]
    else
        return [response.code, response.body]
    end

end



def twitter_oauth_get_accessToken(api_key, api_secret, oauth_token, oauth_token_secret, oauth_verifier)
    request_url = "https://api.twitter.com/oauth/access_token"

    time = Time.now()
    params = {
        "oauth_consumer_key" => api_key,
        "oauth_token" => oauth_token,
        "oauth_verifier" => oauth_verifier,
        "oauth_nonce" => SecureRandom.uuid,
        "oauth_signature_method" => "HMAC-SHA1",
        "oauth_timestamp" => time.to_i.to_s,
        "oauth_version" => "1.0"
    }
    params.each{|k,v| params[k] = my_escape(v.to_s)}

    params["oauth_signature"] = twitter_make_signature("POST", request_url, params, api_secret, oauth_token_secret)

    oauth_header = "OAuth " + params.sort.map{|k,v| "#{k}=#{v}"}*','
    response = https_post(request_url, {}, {"Authorization" => oauth_header})

    case response
    when Net::HTTPSuccess
        results = response.body.split('&').reduce({}){|s,e| k,v=e.split('='); s[k]=v; s }
        return [response.code, results]
    else
        return [response.code, response.body]
    end
end



def twitter_statuses_userTimeline(api_key, api_secret, oauth_token, oauth_token_secret, screen_name, count)
    request_url = "https://api.twitter.com/1.1/statuses/user_timeline.json"

    params_option = {
        "screen_name" => screen_name,
        "count" => count
    }

    time = Time.now()
    params = {
        "screen_name" => screen_name,
        "count" => count,
        "oauth_consumer_key" => api_key,
        "oauth_token" => oauth_token,
        "oauth_nonce" => SecureRandom.uuid,
        "oauth_signature_method" => "HMAC-SHA1",
        "oauth_timestamp" => time.to_i.to_s,
        "oauth_version" => "1.0"
    }
    params.each{|k,v| params[k] = my_escape(v.to_s)}
    
    params["oauth_signature"] = twitter_make_signature("GET", request_url, params, api_secret, oauth_token_secret)

    oauth_header = "OAuth " + params.sort.map{|k,v| "#{k}=#{v}"}*','
    response = https_get(request_url, params_option, {"Authorization" => oauth_header})

    case response
    when Net::HTTPSuccess
        results = JSON.parse(response.body)
        return [response.code, results]
    else
        return [response.code, response.body]
    end
end

長いね.メソッドだけなので,このコードだけでは動きません.

嵌ったところ

URI.escape

デフォルトの URI.escapePHPrawurlencode とは異なるので,URI.escape(str, /[^a-zA-Z0-9._-]/) とする必要がある.

滅茶苦茶嵌った.

oauth_callback はエスケープしない

request_token する時は callback url をパラメータに含める必要があるが,上で挙げた参考サイトでも触れられている通り,エスケープしない状態でパラメータにセットして signature を作る必要がある.

Base64.strict_encode64

Base64.encode64 ではなく,Base64.strict_encode64 を使う.

メソッド使用例

認証からタイムライン読み出しの流れのコードを載せます.xreaサーバで動作確認しました.

#!/usr/local/bin/ruby
require 'cgi'
require 'cgi/session'

require 'uri'
require 'openssl'
require 'base64'
require 'net/http'
require 'securerandom'
require 'json'

# ==========
# ここに上のメソッド群が入る
# =========

@cgi = CGI.new
@session = CGI::Session.new(@cgi)

begin
    # アプリケーションの情報をここに入れる
    @api_key, @api_secret, @callback = ["APIKEY", "APISECRET", "http://example.com/callback_url"]
    
    case @cgi.request_method
    when 'GET'
        oauth_token = @cgi['oauth_token']
        oauth_verifier = @cgi['oauth_verifier']

        if !oauth_token.empty? && !oauth_verifier.empty?
            # twitter認証ページから戻ってきた
            oauth_token_secret = @session["oauth_token_secret"]

            result_code, result_param = twitter_oauth_get_accessToken(@api_key, @api_secret, oauth_token, oauth_token_secret, oauth_verifier)

            if result_code == "200"
                oauth_token = result_param["oauth_token"]
                oauth_token_secret = result_param["oauth_token_secret"]

                screen_name = result_param["screen_name"]

                tl_c, tl_result = twitter_statuses_userTimeline(@api_key, @api_secret, oauth_token, oauth_token_secret, screen_name, 10)

                if tl_c == "200"
                    print @cgi.header({"status" => "OK",'type' => 'text/plain'})
                    puts "accept!!"
                    p [oauth_token, oauth_verifier]
                    p result_param
                    p tl_c
                    puts JSON.pretty_generate(tl_result)
                    exit
                else
                    print @cgi.header({"status" => "OK",'type' => 'text/plain'})
                    puts "failure: statuses_userTimeline"
                    p [oauth_token, oauth_verifier]
                    p result_param
                    p tl_c,tl_result
                    exit
                end
            else
                print @cgi.header({"status" => result_code.to_i, 'type' => 'text/plain'})
                puts "failure: get_accessToken"
                p [oauth_token, oauth_verifier]
                p result_param
                exit
            end
        end

        if !@cgi["denied"].empty?
            # twitter認証ページをキャンセルした
            print @cgi.header({"status" => result_code.to_i, 'type' => 'text/plain'})
            puts "denied"
            exit
        end
        

        result_code, result_param = twitter_oauth_post_requestToken(@api_key, @api_secret, @callback)

        if result_code == "200"
            oauth_token = result_param["oauth_token"]
            oauth_token_secret = result_param["oauth_token_secret"]

            @session['oauth_token_secret'] = oauth_token_secret

            print @cgi.header({"status" => "REDIRECT", "Location" => "https://api.twitter.com/oauth/authorize?oauth_token=#{oauth_token}"})
            exit

        else
            print @cgi.header({"status" => result_code.to_i, 'type' => 'text/plain'})
            puts "failure: post_requestToken"
            p [result_code, result_param]
            exit
        end

    else
        print @cgi.header({"status" => "NOT_IMPLEMENTED", 'type' => 'text/plain'})
        puts "nop"
        exit
    end

rescue
    print @cgi.header({"status" => "OK", 'type' => 'text/plain'})
    puts "error!"
    tr=$@*"\n"
    puts "#{tr}\n#{$!}"
end

2017/09/21: 本質とは異なる部分の修正