はてなAPIをRから使う

はてなAPIをRから使ったときのメモです。認証には種類がいくつかありますが、OAuthによる認証を使います。公式ドキュメントはこのへん。

準備

まずは以下のドキュメントに従ってOAuthのアプリケーションを登録します。

「キー」という項目をOAuthトークンの取得に使うのでメモを取っておきましょう。

f:id:yutannihilation:20170218130039p:plain

トークンの取得

はてなAPIではOAuth1.0の認証を使います。httrパッケージでいつもどおりやるだけです。

library(httr)

hatena_app <- oauth_app("Hatena",
                        key = "<OAuth Consumer Keyの値>",
                        secret = "<OAuth Consumer Secretの値>")

# OAuth1.0なのでrequestも指定
hatena_endpoint <- oauth_endpoint(request = "https://www.hatena.com/oauth/initiate",
                                  authorize = "https://www.hatena.ne.jp/oauth/authorize",
                                  access = "https://www.hatena.com/oauth/token")

hatena_token <- oauth1.0_token(hatena_endpoint, hatena_app)

が、なんか認証がうまくいったと思ったら最後にエラーが出ます…。

#> Error in init_oauth1.0(self$endpoint, self$app, permission = self$params$permission,  : 
#>   Bad Request (HTTP 400).

なんだこれは、とおもってdebug()で調べると、レスポンスに理由が書かれていました。

content(response)
#> $oauth_problem
#> [1] "parameter_rejected"
#> 
#> $oauth_parameters_rejected
#> [1] "oauth_callback"

ドキュメントによると、これは、

parameter_rejected
不要なパラメータが付与されています。oauth_parameters_rejected パラメータに不要なパラメータが「&」区切りでセットされています。
(http://developer.hatena.ne.jp/ja/documents/auth/apis/oauth/problems)

というエラーのようです。はてなのOAuthでは、トークン取得のリクエストにoauth_callbackパラメータが付いているとエラーになるみたいです。ググった感じこれに苦しんでいる人がちらほらいました。

RFC的にはたしかにトークン取得のリクエストにoauth_callbackのパラメータは定義されていません。そういう意味ではhttrの実装が手抜きっぽいんですが、余分なパラメータが付いていた時にどういう扱いをするのが正しいのかはRFCに記述を見つけられませんでした(詳しい方教えてください…)。

ということで誰が悪いのかはわかりませんが、とにもかくにもhttrに手を入れる必要があります。

独自Tokenクラスをつくる

問題はinit_oauth1.0()という関数が常にoauth_callbackパラメータを付けてしまうことです。このパラメータは関数内で定義されている関数、oauth_sig()によって組み立てられます。

  oauth_sig <- function(url, method, token = NULL, token_secret = NULL, private_key = NULL, ...) {
    oauth_header(oauth_signature(url, method, app, token, token_secret, private_key,
        other_params = c(list(...), oauth_callback = oauth_callback())))
    }

(httr/oauth-init.R at 1fc659856602f60ff75eb01903513244e3491ec2 · hadley/httr · GitHub)

これはもうお手上げです。oauth_callbackには何かしら値がセットされるので、そのパラメータ自体を外からコントロールすることはできません。あと、scopeというのも渡さないといけないみたいです。

ということで、以下のようにinit_oauth1.0()自体を置き換える関数を作り、それを使って独自Tokenクラスを作ります。(scopeは外から渡すようにした方がいいけどここでは手を抜きます)

init_oauth1.0_hatena <- function(endpoint, app, permission = NULL,
                          is_interactive = interactive(),
                          private_key = NULL) {
  
  oauth_sig <- function(url, method, token = NULL, token_secret = NULL, private_key = NULL, ...) {
    other_params <- purrr::compact(list(...))
    oauth_header(oauth_signature(url, method, app, token, token_secret, private_key,
                                 other_params = other_params))
  }
  
  # 1. Get an unauthorized request token
  response <- POST(endpoint$request,
                   oauth_sig(endpoint$request, "POST", private_key = private_key,
                             oauth_callback = oauth_callback(),
                             scope = "read_public"))
  stop_for_status(response)
  params <- content(response, type = "application/x-www-form-urlencoded")
  token <- params$oauth_token
  secret <- params$oauth_token_secret
  
  # 2. Authorize the token
  authorize_url <- modify_url(endpoint$authorize, query = list(
    oauth_token = token))
  verifier <- oauth_listener(authorize_url, is_interactive)
  verifier <- verifier$oauth_verifier %||% verifier[[1]]
  
  # 3. Request access token
  response <- POST(endpoint$access,
                   oauth_sig(endpoint$access, "POST", token, secret, oauth_verifier = verifier, private_key = private_key),
                   body = ""
  )
  stop_for_status(response)
  content(response, type = "application/x-www-form-urlencoded")
}


TokenHatena <- R6::R6Class("TokenHatena", inherit = Token1.0, list(
  init_credentials = function(force = FALSE) {
    self$credentials <- init_oauth1.0_hatena(
      self$endpoint,
      self$app,
      permission = self$params$permission,
      private_key = self$private_key
    )
  }
))

このTokenHatenaを直接newすればいいのでこれは必要ではないですが、いちおうトークンをつくる関数も定義しておきます。

oauth1.0_token_hatena <- function(endpoint,
                                  app,
                                  permission = NULL,
                                  as_header = TRUE,
                                  private_key = NULL,
                                  cache = getOption("httr_oauth_cache")) {
  params <- list(permission = permission, as_header = as_header)
  TokenHatena$new(app = app, endpoint = endpoint, params = params,
                  private_key = private_key, cache_path = cache)
}

これを使ってトークンを取得しましょう。

hatena_token <- oauth1.0_token_hatena(hatena_endpoint, hatena_app)

トークンを使ってリクエストしてみる

トークンを使ってリクエストするにはこんな感じです。

GET("http://n.hatena.com/applications/my.json", config(token = hatena_token))

が、なんかこれ、数回に一度のペースで失敗したりするんですよね…。謎。

GET("http://n.hatena.com/applications/my.json", config(token = hatena_token))
#> Response [http://n.hatena.com/applications/my.json]
#>   Date: 2017-02-18 05:14
#>   Status: 401
#>   Content-Type: text/plain; charset=utf-8
#>   Size: 31 B

GET("http://n.hatena.com/applications/my.json", config(token = hatena_token))
#> Response [http://n.hatena.com/applications/my.json]
#>   Date: 2017-02-18 05:14
#>   Status: 200
#>   Content-Type: application/json; charset=utf-8
#>   Size: 163 B

シグネチャがおかしいといわれるので、タイムスタンプあたり?とか思うんですけど、今のところは深追いする気力がわかないのでここでギブアップ。

content(x)
#> [1] "oauth_problem=signature_invalid"