kenkovlog

Haskell, Python, Vim, ...

HaskellでOAuthとTwitter API

ゴールデンウィークの課題としていたHaskellでOAuth。
jsonの解析はまだ行えていませんが、OAuthを使ってjsonを取得するところまでできたので、その事をまとめます。

TwitterでOAuthを使う基本的な流れについてはtwitter developersのauthenticationの項
http://dev.twitter.com/pages/auth
を見てもらうことにして、ここでは各部分について実際のコードを書きます。
標準で入っていないモジュールがありますので、その際にはcabalなどを使っていれてもらえればと思います。

OAuthを使うのに一番の山場はsignatureの生成です。
僕もここでかなり苦戦しました。
Signatureの生成には次のコードを書きました。
Haskellらしくないコードの書き方かもしれませんが、
どのような手順でSignatureを生成しているのかわかるように書いています。

-- Signature.hs
-- signatureを生成する関数を提供するモジュールSignature
module Signature (
  makeSignature, 
  ConsumerKey,
  ConsumerSecret,
  AccessToken,
  AccessTokenSecret,
  URL,
  Parameter
) where

import Data.Word (Word8(..))
import Data.List
import Network.HTTP (RequestMethod, urlEncode)
-- Base64を使うために次のモジュールをインポートする
import qualified Codec.Binary.Base64 as B64
-- HMAC-SHA1を使うために次のモジュールをインポートする
import Data.Digest.Pure.SHA (hmacSha1, bytestringDigest, showDigest)
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8

type ConsumerKey = String
type ConsumerSecret = String
type AccessToken = String
type AccessTokenSecret = String

type URL = String
type Parameter = [(String, String)]

-- signatureを生成する関数
makeSignature :: URL -> RequestMethod -> ConsumerSecret -> AccessTokenSecret -> Parameter -> String
makeSignature url method cSecret aSecret param =
  str6
  where
    -- keyの作成
    key = urlEncode cSecret ++ "&" ++ urlEncode aSecret
    -- keyを秘密鍵として、signatureBaseStringで作成したsignature base stringのHMAC-SHA1を取得する
    str4 :: [Word8]
    str4 = L.unpack $ bytestringDigest $ hmacSha1 (L8.pack key) (L8.pack $ makeSignatureBaseString url method param)
    -- str4をBase64エンコードする
    str5 :: String
    str5 = B64.encode str4
    -- str5をURLエンコードする
    str6 = urlEncode str5

-- signature base stringを生成する関数
makeSignatureBaseString :: URL -> RequestMethod -> Parameter -> String
makeSignatureBaseString url method param =
  str3
  where
    -- メソッド(GET, POST)とURLをエンコードした文字列を"&"で連結する
    str1 = show method ++ "&" ++ urlEncode url
    -- paramのキー1=paramの値1&....&paramのキーn=paramの値n
    -- 【重要】パラメータはソートしておかないといけない
    str2 = intercalate "&" $ map concatPair (sort param)
    -- str2をURLエンコードする
    str2' = urlEncode str2
    -- str1とstr2'を"&"で連結する
    str3 = str1 ++ "&" ++ str2'

    concatPair :: (String, String) -> String
    concatPair (x, y) = x ++ "=" ++ y

次にリクエストを作成するコードを書きます。

-- OAuth module

module OAuth (
  OAuth (..),
  oauthRequest
) where

-- 上で作成したSignatureモジュールのインポート
import  Signature

import Codec.Binary.UTF8.String (encodeString, utf8Encode)
import Network.HTTP 
import Network.URI
import Network.HTTP.Proxy (parseProxy)
import Network.Browser (browse, request, setProxy, request)
import Data.Maybe
import Data.List
import System.Time (ClockTime(..), getClockTime)
import Control.Applicative ((<$>))
import System.Random (randomRIO)

-- OAuthデータ型の定義
data OAuth = OAuth {
     consumerKey :: String,
     consumerSecret :: String,
     accessToken :: String,
     accessTokenSecret :: String
     } deriving (Show, Eq)

-- 乱数の作成
randomInt :: IO Int
randomInt = randomRIO (0, maxBound::Int)

-- システム時間の取得
getUnixTime :: IO Integer
getUnixTime = getUnixTime' <$> getClockTime
  where
    getUnixTime' (TOD i _) = i

-- oauthを使ってリクエストを作成する関数
oauthRequest :: OAuth -> URL -> RequestMethod -> [(String, String)] -> IO Request_String
oauthRequest oauth url method param = do
  -- 乱数の取得
  nonce <- show <$> randomInt
  -- システム時間の取得	     
  unixTime <- show <$> getUnixTime
  let
    -- パラメータをURlエンコードする
    param' = parameterUrlEncode param
    -- 次のパラメータもURLエンコードする
    oauthParam = parameterUrlEncode
                 [("oauth_consumer_key", consumerKey oauth), 
                  ("oauth_nonce", nonce),
                  ("oauth_signature_method", "HMAC-SHA1"),
                  ("oauth_timestamp",  unixTime), 
                  ("oauth_token", accessToken oauth),
                  ("oauth_version", "1.0")]
    -- Signature.makeSignatureを使ってsignatureを作成する		  
    signature = makeSignature url 
                              method
                              (consumerSecret oauth) 
                              (accessTokenSecret oauth) 
                              (param' ++ oauthParam)
    -- URLに付けるパラメータの作成
    urlParam =  sort (("oauth_signature", signature): (param' ++ oauthParam))
    -- URLの作成
    oauthURL = url ++ "?" ++
               intercalate "&" (map concatParam urlParam)

    concatParam :: (String, String) -> String
    concatParam (x, y) = x ++ "=" ++ y

    parameterUrlEncode :: [(String, String)] -> [(String, String)]
    parameterUrlEncode = map $ \(x, y) -> (urlEncode x, urlEncode y)
  -- リクエストの作成
  return $  Request {
             rqURI = fromJust $ parseURI oauthURL,
             rqMethod = method,
             rqHeaders = [],
             rqBody = ""
            }

これでリクエストを作成する関数ができたので、実際に使ってみます。

ここでひとつはまったのは、日本語のツイート。
日本語のツイートには、
Codec.Binary.UTF8.String.encodeString
をつかいます。

-- OAuthTwitter.hs

module OAuthTwitter where

-- 上で作成したモジュールOAuthのインポート
import OAuth
-- 日本語をツイートするためのUTF8モジュールのインポート
import Codec.Binary.UTF8.String (encodeString)
import Network.HTTP 
import Network.HTTP.Proxy (parseProxy)
import Network.Browser (browse, request, setProxy, request)
import Data.Maybe
import Data.List

-- user timelineの取得
userTimeline :: OAuth -> [(String, String)] -> IO Request_String
userTimeline oauth param =  
 oauthRequest oauth userTimelineURL GET param

-- friends timelineの取得
friendsTimeline :: OAuth -> IO Request_String
friendsTimeline oauth = do
  oauthRequest oauth friendsTimelineURL GET []

-- ツイートする
update :: OAuth -> String -> IO Request_String
update oauth status =
  oauthRequest oauth updateURL POST [("status", status)]

-- リストの取得
listTimeline :: OAuth -> String -> String -> IO Request_String
listTimeline oauth userName listName =
  oauthRequest oauth ("http://api.twitter.com/1/" ++ userName ++ "/lists/" ++ listName ++ "/statuses.json") GET []

-- 
-- Define URL and oauth for Twitter
--
requestTokenURL = "http://api.twitter.com/oauth/request_token"
accessTokenURL = "http://api.twitter.com/oauth/access_token"

updateURL = "http://api.twitter.com/1/statuses/update.json"
userTimelineURL = "http://api.twitter.com/1/statuses/user_timeline.json"
friendsTimelineURL = "http://api.twitter.com/1/statuses/friends_timeline.json"

cKey = "****"
cSecret = "****"
aToken = "****"
aSecret = "****"

oauth = OAuth {
          consumerKey = cKey,
          consumerSecret = cSecret,
          accessToken = aToken, 
          accessTokenSecret = aSecret
}

main :: IO ()
main = do
  -- rq <- userTimeline oauth [("id", "screenName")]
  -- rq <- friendsTimeline oauth

  -- 日本語を使うために、encodeStringを使います。
  rq <- update oauth $ encodeString "テストツイート"
  -- rq <- listTimeline oauth "screenName" "listName"
  (uri, res) <- browse $ do
  	 -- プロキシを使うには次をコメントアウトする
         -- setProxy . fromJust $ parseProxy "proxy.server:8080"
         request $ rq
  putStrLn $ rspBody res

参考にしたページは、
twitter developers Authentication】
http://dev.twitter.com/pages/auth
【EAGLE雑記】
http://d.hatena.ne.jp/eagletmt/20100820/1282253083
です。

あと、OAuthを理解、実装するにあたって、次の本が大変参考になりました。

Twitter API プログラミング

Twitter API プログラミング