OAuth gemだけでTwitter APIを使ってみる

Twitter gemは非常に優秀ですので、低レイヤーなOAuth gemを使ってTwitter APIにアクセスする必要はまずありません。
ただし、「Twitterからの応答をちゃんと返してくれているのだろうか?」と疑問があったときに、gemを介さずに直接アクセスする手段を知っておくことは大切なことだと思います。*1

APIエンドポイント

REST API v1.1 Resources | Twitter Developers
で確認してください。

必要なもの

$ gem install oauth

しておきます。
ライブラリとして

require 'json'
require 'oauth'

が必要です。
なおTwitter APIの返答がJSONなのでライブラリを入れています。

例を挙げておきます(もちろんニセモノ)。

consumer_key        = '9Gu1FiE1IfKi32bR8Fw'
consumer_secret     = 'QUnRDVcl5FNCZ9C1j80OAjUJlguMNVyqbdrOrnjtQ'
access_token        = '2194235326-sm2SUkVRHMJ1CdwYG1f04klQMlxc4i05tkSbnZa'
access_token_secret = 'oairK8FMvLZ8dqEVrCgh2brhyL0SCd5wpdkIoNxz62Jdi'

取得方法はtwitter OAuthのアクセストークンを取得 - 別館 子子子子子子(ねこのここねこ)を参考にしてください。

consumer key/secretの認証

consumer = OAuth::Consumer.new(
  consumer_key, 
  consumer_secret, 
  site:'https://api.twitter.com/'
)
=> #<OAuth::Consumer:0x007fa234d15410
 @key="9Gu1FiE1IfKi32bR8Fw",
 @options=
  {:signature_method=>"HMAC-SHA1",
   :request_token_path=>"/oauth/request_token",
   :authorize_path=>"/oauth/authorize",
   :access_token_path=>"/oauth/access_token",
   :proxy=>nil,
   :scheme=>:header,
   :http_method=>:post,
   :oauth_version=>"1.0",
   :site=>"https://api.twitter.com/"},
 @secret="QUnRDVcl5FNCZ9C1j80OAjUJlguMNVyqbdrOrnjtQ">

consumerインスタンスaccess token/secretを使ってAPIアクセスするためのインスタンスを作る

endpoint = OAuth::AccessToken.new(consumer, access_token, access_token_secret)
=> #<OAuth::AccessToken:0x007fa234dfad30
 @consumer=
  #<OAuth::Consumer:0x007fa234d15410
   @http=#<Net::HTTP api.twitter.com:443 open=false>,
   @key="9Gu1FiE1IfKi32bR8Fw",
   @options=
    {:signature_method=>"HMAC-SHA1",
     :request_token_path=>"/oauth/request_token",
     :authorize_path=>"/oauth/authorize",
     :access_token_path=>"/oauth/access_token",
     :proxy=>nil,
     :scheme=>:header,
     :http_method=>:post,
     :oauth_version=>"1.0",
     :site=>"https://api.twitter.com/"},
   @secret="QUnRDVcl5FNCZ9C1j80OAjUJlguMNVyqbdrOrnjtQ",
   @uri=#<URI::HTTPS:0x007fa234d63228 URL:https://api.twitter.com/>>,
 @params={},
 @secret="oairK8FMvLZ8dqEVrCgh2brhyL0SCd5wpdkIoNxz62Jdi",
 @token="2194235326-sm2SUkVRHMJ1CdwYG1f04klQMlxc4i05tkSbnZa">

GETの例:@twitterapiのツイート履歴を見る

GET statuses/user_timeline | Twitter Developers
に載っている例です。APIのエンドポイントURLは次の通りです。

GET https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=twitterapi&count=2

アカウント名がtwitterapiのツイートを2つ取得する、ということです。パラメータに関しては上のリンクから確認してください。

アクセスする
response = endpoint.request(:get, 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=twitterapi&count=2')
=> #<Net::HTTPOK 200 OK readbody=true>

なお、 endpoint.request(:get, URL) と書きましたが、 endpoint.get(URL) で良いようです(このほうが読みやすい)。

返答の内容を確認

返答はNet::HTTPOKクラスのオブジェクトですのでresponse.bodyでbodyが取得できます。JSONなのでJSON.parse(response.body)で戻せます。

result = JSON.parse(response.body)
=> [{"created_at"=>"Wed May 21 18:29:30 +0000 2014",
  "id"=>469183215714828288,
  "id_str"=>"469183215714828288",
  "text"=>
   "RT @TwitterDev: Developer Spotlight: Botanicalls via @jbulava  https://t.co/
  "source"=>"web",
  "truncated"=>false,
  "in_reply_to_status_id"=>nil,
  "in_reply_to_status_id_str"=>nil,
  "in_reply_to_user_id"=>nil,
  "in_reply_to_user_id_str"=>nil,
  "in_reply_to_screen_name"=>nil,
  "user"=>
   {"id"=>6253282,
    "id_str"=>"6253282",
    "name"=>"Twitter API",
    "screen_name"=>"twitterapi",
    "location"=>"San Francisco, CA",
    "description"=>
     "The Real Twitter API. I tweet about API changes, service issues and happil
    "url"=>"http://t.co/78pYTvWfJd",
    "entities"=>
     {"url"=>
       {"urls"=>
         [{"url"=>"http://t.co/78pYTvWfJd",
           "expanded_url"=>"http://dev.twitter.com",
           "display_url"=>"dev.twitter.com",
           "indices"=>[0, 22]}]},
      "description"=>{"urls"=>[]}},
    "protected"=>false,
    "followers_count"=>2173311,
    "friends_count"=>48,
    "listed_count"=>12689,
    "created_at"=>"Wed May 23 06:01:13 +0000 2007",
    "favourites_count"=>26,
    "utc_offset"=>-25200,
    "time_zone"=>"Pacific Time (US & Canada)",
    "geo_enabled"=>true,
    "verified"=>true,
    "statuses_count"=>3504,
    "lang"=>"en",
    "contributors_enabled"=>false,
    "is_translator"=>false,
    "is_translation_enabled"=>false,
    "profile_background_color"=>"C0DEED",
    "profile_background_image_url"=>
     "http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d
    "profile_background_image_url_https"=>
     "https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3
    "profile_background_tile"=>true,
    "profile_image_url"=>
     "http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal
    "profile_image_url_https"=>
     "https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_norma
    "profile_banner_url"=>
     "https://pbs.twimg.com/profile_banners/6253282/1347394302",
    "profile_link_color"=>"0084B4",
    "profile_sidebar_border_color"=>"C0DEED",
    "profile_sidebar_fill_color"=>"DDEEF6",
    "profile_text_color"=>"333333",
    "profile_use_background_image"=>true,
    "default_profile"=>false,
    "default_profile_image"=>false,
    "following"=>false,
    "follow_request_sent"=>false,
    "notifications"=>false},
  "geo"=>nil,
  "coordinates"=>nil,
  "place"=>nil,
  "contributors"=>nil,
  "retweeted_status"=>
   {"created_at"=>"Wed May 21 18:29:05 +0000 2014",
    "id"=>469183110723031041,
    "id_str"=>"469183110723031041",
    "text"=>
     "Developer Spotlight: Botanicalls via @jbulava  https://t.co/EtJEF3XFOt",
    "source"=>
     "<a href=\"http://twitter.com/tweetbutton\" rel=\"nofollow\">Tweet Button</
    "truncated"=>false,
    "in_reply_to_status_id"=>nil,
    "in_reply_to_status_id_str"=>nil,
    "in_reply_to_user_id"=>nil,
    "in_reply_to_user_id_str"=>nil,
    "in_reply_to_screen_name"=>nil,
    "user"=>
     {"id"=>2244994945,
      "id_str"=>"2244994945",
      "name"=>"TwitterDev",
      "screen_name"=>"TwitterDev",
      "location"=>"Internet",
      "description"=>
       "Developers and Platform Relations @Twitter. We are developer advocates. 
      "url"=>"https://t.co/66w26cd6ZO",
      "entities"=>
       {"url"=>
         {"urls"=>
           [{"url"=>"https://t.co/66w26cd6ZO",
             "expanded_url"=>"https://dev.twitter.com/",
             "display_url"=>"dev.twitter.com",
             "indices"=>[0, 23]}]},
        "description"=>{"urls"=>[]}},
      "protected"=>false,
      "followers_count"=>5789,
      "friends_count"=>1177,
      "listed_count"=>99,
      "created_at"=>"Sat Dec 14 04:35:55 +0000 2013",
      "favourites_count"=>116,
      "utc_offset"=>-25200,
      "time_zone"=>"Pacific Time (US & Canada)",
      "geo_enabled"=>false,
      "verified"=>true,
      "statuses_count"=>374,
      "lang"=>"en",
      "contributors_enabled"=>false,
      "is_translator"=>false,
      "is_translation_enabled"=>false,
      "profile_background_color"=>"FFFFFF",
      "profile_background_image_url"=>
       "http://abs.twimg.com/images/themes/theme1/bg.png",
      "profile_background_image_url_https"=>
       "https://abs.twimg.com/images/themes/theme1/bg.png",
      "profile_background_tile"=>false,
      "profile_image_url"=>
       "http://pbs.twimg.com/profile_images/431949550836662272/A6Ck-0Gx_normal.p
      "profile_image_url_https"=>
       "https://pbs.twimg.com/profile_images/431949550836662272/A6Ck-0Gx_normal.
      "profile_banner_url"=>
       "https://pbs.twimg.com/profile_banners/2244994945/1396995246",
      "profile_link_color"=>"0084B4",
      "profile_sidebar_border_color"=>"FFFFFF",
      "profile_sidebar_fill_color"=>"DDEEF6",
      "profile_text_color"=>"333333",
      "profile_use_background_image"=>false,
      "default_profile"=>false,
      "default_profile_image"=>false,
      "following"=>false,
      "follow_request_sent"=>false,
      "notifications"=>false},
    "geo"=>nil,
    "coordinates"=>nil,
    "place"=>nil,
    "contributors"=>nil,
    "retweet_count"=>12,
    "favorite_count"=>10,
    "entities"=>
     {"hashtags"=>[],
      "symbols"=>[],
      "urls"=>
       [{"url"=>"https://t.co/EtJEF3XFOt",
         "expanded_url"=>
          "https://blog.twitter.com/2014/developer-spotlight-botanicalls",
         "display_url"=>"blog.twitter.com/2014/developer…",
         "indices"=>[47, 70]}],
      "user_mentions"=>
       [{"screen_name"=>"jbulava",
         "name"=>"Jon Bulava",
         "id"=>987121,
         "id_str"=>"987121",
         "indices"=>[37, 45]}]},
    "favorited"=>false,
    "retweeted"=>false,
    "possibly_sensitive"=>false,
    "lang"=>"en"},
  "retweet_count"=>12,
  "favorite_count"=>0,
  "entities"=>
   {"hashtags"=>[],
    "symbols"=>[],
    "urls"=>
     [{"url"=>"https://t.co/EtJEF3XFOt",
       "expanded_url"=>
        "https://blog.twitter.com/2014/developer-spotlight-botanicalls",
       "display_url"=>"blog.twitter.com/2014/developer…",
       "indices"=>[63, 86]}],
    "user_mentions"=>
     [{"screen_name"=>"TwitterDev",
       "name"=>"TwitterDev",
       "id"=>2244994945,
       "id_str"=>"2244994945",
       "indices"=>[3, 14]},
      {"screen_name"=>"jbulava",
       "name"=>"Jon Bulava",
       "id"=>987121,
       "id_str"=>"987121",
       "indices"=>[53, 61]}]},
  "favorited"=>false,
  "retweeted"=>false,
  "possibly_sensitive"=>false,
  "lang"=>"en"},
 {"created_at"=>"Fri May 16 17:13:22 +0000 2014",
  "id"=>467352118966235136,
  "id_str"=>"467352118966235136",
  "text"=>
   "Forthcoming tweak to streaming API error codes from June https://t.co/HbQsmb
  "source"=>
   "<a href=\"http://itunes.apple.com/us/app/twitter/id409789998?mt=12\" rel=\"n
  "truncated"=>false,
  "in_reply_to_status_id"=>nil,
  "in_reply_to_status_id_str"=>nil,
  "in_reply_to_user_id"=>nil,
  "in_reply_to_user_id_str"=>nil,
  "in_reply_to_screen_name"=>nil,
  "user"=>
   {"id"=>6253282,
    "id_str"=>"6253282",
    "name"=>"Twitter API",
    "screen_name"=>"twitterapi",
    "location"=>"San Francisco, CA",
    "description"=>
     "The Real Twitter API. I tweet about API changes, service issues and happil
    "url"=>"http://t.co/78pYTvWfJd",
    "entities"=>
     {"url"=>
       {"urls"=>
         [{"url"=>"http://t.co/78pYTvWfJd",
           "expanded_url"=>"http://dev.twitter.com",
           "display_url"=>"dev.twitter.com",
           "indices"=>[0, 22]}]},
      "description"=>{"urls"=>[]}},
    "protected"=>false,
    "followers_count"=>2173311,
    "friends_count"=>48,
    "listed_count"=>12689,
    "created_at"=>"Wed May 23 06:01:13 +0000 2007",
    "favourites_count"=>26,
    "utc_offset"=>-25200,
    "time_zone"=>"Pacific Time (US & Canada)",
    "geo_enabled"=>true,
    "verified"=>true,
    "statuses_count"=>3504,
    "lang"=>"en",
    "contributors_enabled"=>false,
    "is_translator"=>false,
    "is_translation_enabled"=>false,
    "profile_background_color"=>"C0DEED",
    "profile_background_image_url"=>
     "http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d
    "profile_background_image_url_https"=>
     "https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3
    "profile_background_tile"=>true,
    "profile_image_url"=>
     "http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal
    "profile_image_url_https"=>
     "https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_norma
    "profile_banner_url"=>
     "https://pbs.twimg.com/profile_banners/6253282/1347394302",
    "profile_link_color"=>"0084B4",
    "profile_sidebar_border_color"=>"C0DEED",
    "profile_sidebar_fill_color"=>"DDEEF6",
    "profile_text_color"=>"333333",
    "profile_use_background_image"=>true,
    "default_profile"=>false,
    "default_profile_image"=>false,
    "following"=>false,
    "follow_request_sent"=>false,
    "notifications"=>false},
  "geo"=>nil,
  "coordinates"=>nil,
  "place"=>nil,
  "contributors"=>nil,
  "retweet_count"=>34,
  "favorite_count"=>38,
  "entities"=>
   {"hashtags"=>[],
    "symbols"=>[],
    "urls"=>
     [{"url"=>"https://t.co/HbQsmbkLmQ",
       "expanded_url"=>"https://dev.twitter.com/discussions/28457",
       "display_url"=>"dev.twitter.com/discussions/28…",
       "indices"=>[57, 80]}],
    "user_mentions"=>[]},
  "favorited"=>false,
  "retweeted"=>false,
  "possibly_sensitive"=>false,
  "lang"=>"en"}]

POSTの例:ツイートする

POST statuses/update | Twitter Developers
に載っている例です。APIのエンドポイントURLは

POST https://api.twitter.com/1.1/statuses/update.json

で、ツイートする内容は

POST Data	 status=Maybe%20he%27ll%20finally%20find%20his%20keys.%20%23peterfalk

とありますが、どちらかといえば日本語が使えるか気になりますので

tweet messageには 日本語が使えます。

という文をツイートしてみましょう。

アクセスする

ツイート文はPOSTデータに入れるのですが、以下のようにハッシュで渡します。

response = endpoint.request(:post, 'https://api.twitter.com/1.1/statuses/update.json', status: 'tweet messageには 日本語が使えます。')
=> #<Net::HTTPOK 200 OK readbody=true>

endpoint.get(URL) と同様にこちらも endpoint.post(URL, hash) と書いたほうが読みやすいですね。

返答の内容を確認
result = JSON.parse(response.body)
=> {"created_at"=>"Thu May 22 11:11:28 +0000 2014",
 "id"=>4694353711895674xx,
 "id_str"=>"4694353711895674xx",
 "text"=>"tweet messageには 日本語が使えます。",
 (以下略)
ツイート結果


半角スペースも日本語も問題なく使えています。

まとめ

require 'json'
require 'oauth'

consumer_key        = '9Gu1FiE1IfKi32bR8Fw'
consumer_secret     = 'QUnRDVcl5FNCZ9C1j80OAjUJlguMNVyqbdrOrnjtQ'
access_token        = '2194235326-sm2SUkVRHMJ1CdwYG1f04klQMlxc4i05tkSbnZa'
access_token_secret = 'oairK8FMvLZ8dqEVrCgh2brhyL0SCd5wpdkIoNxz62Jdi'

consumer = OAuth::Consumer.new(
  consumer_key, 
  consumer_secret, 
  site:'https://api.twitter.com/'
)
endpoint = OAuth::AccessToken.new(consumer, access_token, access_token_secret)

# GET
response = endpoint.request(:get, <API_ENDPOINT_URL>)
result = JSON.parse(response.body)

# POST
response = endpoint.request(:post, <API_ENDPOINT_URL>, {<PARAMETER_HASH>})
result = JSON.parse(response.body)

OAuth::Consumer#requestメソッドを使った例

すこし使いづらいのですが、こちらのやりかたを先に見つけたので、メモしておきます。
上のやり方ではOAuth::AccessToken#initialize*2を作ってから取り扱っている
Method: OAuth::ConsumerToken#initialize ― Documentation for oauth (0.4.7)
のですが、この例ではconsumerインスタンスを直接扱います。
Method: OAuth::Consumer#request ― Documentation for oauth (0.4.7)
念のためにconsumerインスタンスを再掲しておきます。

consumer = OAuth::Consumer.new(
  consumer_key, 
  consumer_secret, 
  site:'https://api.twitter.com/'
)
GET
access_token_pair = OAuth::Token.new(access_token, access_token_secret)
response = consumer.request(:get, <API_ENDPOINT_URL>, access_token_pair)
result = JSON.parse(response.body)
POST
response = consumer.request(:post, <API_ENDPOINT_URL>, access_token_pair, {}, {<PARAMETER_HASH>})
result = JSON.parse(response.body)

なお、POSTの際には引数の空ハッシュ({})が必要です。
Method: OAuth::Consumer#request ― Documentation for oauth (0.4.7)

*1:つまり、そういう事があったわけで

*2:そこから呼ばれたOAuth::ConsumerToken#initialize