LoginSignup
3
1

More than 1 year has passed since last update.

Nim で TwitterAPIv2 (OAuth1) 叩こうと思ってミスった話

Last updated at Posted at 2021-12-06

概要

Twitter API v2をNimで叩こうとして苦戦した話。
この記事は、Nim Advent Calendar 2021の6日目です。

なんとなく「CLIでツイートしよ」と軽く思って調べているとTwitter API v2がプライマリAPIになっているということで、試してみるか……
と思ってNimで試行錯誤しました。

サクッと作りたかったので、とりえあず今回はOAuthで叩いてくれるライブラリがあったのでこちらを使用しました。

TL;DR

Twitter API v2になってもツイートはOAuth1です。

また上記ライブラリは HttpClient.headerを読み込んでくれないようなのでちゃんと引数extraHeaders: HttpHeadersを設定しましょう。

TwitterAPI v2

V1との違いに関しては、こちらの記事で詳細な説明があります。
重要な点としてOAuth1OAuth2が混在している点に注意してください。

OAuth1ではTwitterのAPI_KEYAPI_KEY_SECRETを使用していましたが、OAuth2ではその2つを組み合わせてBearer Tokenとして使うようです。
生成方法はこちらにありました。

今回はTweetのPOSTですのでPostmanくんで最初に施行してみます。

Nimでauthorizationしよう

まずはBearerTokenの取得から始めたところ「ツイートはOAuth1じゃん!」ということが発覚。

とりあえず、ツイートするためにクイックスタートを貼っておきます。

気を取り直して、OAuth1用のヘッダーを設定。
さらに開発用のアカウントと別のアカウントでツイートしたかったのでPIN認証してverifierも取ってきます。

type TwitterClient* = ref object
  apiKey: string
  consumerKey: string
  consumerSecret: string
  accessToken: string
  accessSecret: string
  uri*: string
  authorization: string
  header: string

proc newTwitterClient*(): TwitterClient =
  let v = TwitterClient(apiKey: "Bearer " & os.getEnv("TWITTER_BEARER"),
                        consumerKey: os.getEnv("TWITTER_API_KEY"),
                        consumerSecret: os.getEnv("TWITTER_API_SECRET_KEY"),
                        accessToken: os.getEnv("TWITTER_USER_ACCESS_TOKEN"),
                        accessSecret: os.getEnv("TWITTER_USER_ACCESS_SECRET"),
                        header: "application/json",
                        uri: "https://api.twitter.com/2/tweets")
  result = v

こんな感じで各キーを持ったTwitter型作りました。
何が何のKeyだかわかんねぇんだが?

proc twitterOAuth*(twitter: TwitterClient) : array[2, string] =
  let client = newHttpClient()
  let requestToken = client.getOAuth1RequestToken("https://api.twitter.com/oauth/request_token",
                               twitter.consumerKey,
                               twitter.consumerSecret,
                               isIncludeVersionToHeader = true)
  if requestToken.status == "200 OK":
    var response = parseResponseBody requestToken.body
    let
      requestToken = response["oauth_token"]
      requestTokenSecret = response["oauth_token_secret"]
    echo "Access the url, please obtain the verifier key."
    echo getAuthorizeUrl(authorizeUrl, requestToken)
    echo "Please enter a verifier key (PIN code)."
    let
      verifier = readLine stdin
      accessToken = client.getOAuth1AccessToken(accessTokenUrl,
                                                twitter.consumerKey, twitter.consumerSecret, requestToken, requestTokenSecret,
                                                verifier, isIncludeVersionToHeader = true)
    if accessToken.status == "200 OK":
      response = parseResponseBody accessToken.body
      let
        accessToken = response["oauth_token"]
        accessTokenSecret = response["oauth_token_secret"]
      result = [accessToken, accessTokenSecret]

ほぼ下記からの引用です。
サンプルが豊富なライブラリ、すき。

さてこれでツイートする準備はできました!
実行してみましょう。

proc tweet*(twitter: TwitterClient, tweet: string) : Response =
  let 
    twit = %* { "text": tweet }
    client = newHttpClient()
  client.headers = newHttpHeaders({ "Content-Type": "application/json" })
  result = client.oAuth1Request(tweetUrl, twitter.consumerKey, twitter.consumerSecret,
                       twitter.accessToken, twitter.accessSecret,
                       isIncludeVersionToHeader = true, httpMethod = HttpPOST, body= $twit)

ところが、401 Authorization Requiredと言われる。
Twitterにはツイートをする権限が必要だったりするのでそれかなぁ、と思っていろいろ調べた。
その場合403 Forbiddenになるはずなので違うみたい。

そもそもログインができていない模様。

HttpClient.headerOAuthライブラリに渡す

さて上記のコードの問題はログインできてないとのことなので、まず認証周りを確認したんですが問題なし。
結論から言うと、client.headers = newHttpHeaders({ "Content-Type": "application/json" })
この一行がoAuth1Requestには設定されていないようでした。

proc oAuth1Request(client: HttpClient | AsyncHttpClient,
    url, consumerKey, consumerSecret: string,
    callback, token, verifier: string = "", tokenSecret = "",
    isIncludeVersionToHeader = false, httpMethod = HttpGET,
    extraHeaders: HttpHeaders = nil, body = "",
    nonce: string = "", realm: string = ""): Future[Response | AsyncResponse] {.multisync.} =
    let
        timestamp = int(round(epochTime()))
        nonce = if len(nonce) == 0: createNonce() else: nonce
        params = OAuth1Parameters(
            realm: realm,
            consumerKey: consumerKey,
            nonce: nonce,
            signatureMethod: signatureMethod,
            timestamp: $timestamp,
            isIncludeVersionToHeader: isIncludeVersionToHeader,
            callback: callback,
            token: token,
            verifier: verifier
        )
        signature = getSignature(httpMethod, url, body, params, consumerSecret, tokenSecret)

    params.signature = percentEncode(signature)
    let header = getOAuth1RequestHeader(params, extraHeaders)
    result = await client.request(url, httpMethod = httpMethod, headers = header, body = body)

ということで、改めて

  result = client.oAuth1Request(tweetUrl, twitter.consumerKey, twitter.consumerSecret,
                       twitter.accessToken, twitter.accessSecret,
                       isIncludeVersionToHeader = true, httpMethod = HttpPOST, extraHeaders = client.headers, body= $twit)

これで無事ツイートすることができました。

感想

Nimはライブラリのコードが読みやすいものが多い印象。
一方よく比較されることのあるPythonはバッテリー内蔵ということでどうも内部のコードを読むという作業を億劫にしてしまいがちでした。

また、TwitterAPIv2のエラーメッセージはちょいちょい不親切なので、沼にハマると大変でした。

昨日(12/5): NimでResult[T, E]型を定義する
明日(12/7): 【翻訳記事】Pipelines - .Net の新しい IO API のツアーガイド, part 1 -

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1