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

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

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

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

https://github.com/CORDEA/oauth

# TL;DR
`Twitter API v2`になってもツイートは`OAuth1`です。

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


# TwitterAPI v2
https://qiita.com/TakeshiNickOsanai/items/cf695370dde1ec21dbd6

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

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

https://developer.twitter.com/ja/docs/authentication/oauth-2-0/bearer-tokens

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

https://developer.twitter.com/en/docs/tutorials/postman-getting-started

https://documenter.getpostman.com/view/9956214/T1LMiT5U


# Nimで`authorization`しよう
まずは`BearerTokenの取得`から始めたところ「ツイートは`OAuth1`じゃん！」ということが発覚。

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

https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/quick-start

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

```Nim
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だかわかんねぇんだが？~~

```Nim
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]
```

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

https://github.com/CORDEA/oauth/blob/develop/examples/twitter.nim

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

```Nim
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`になるはずなので違うみたい。

https://qiita.com/zawawahoge/items/58021f9c0dde1f99b611

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

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

```Nim
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)
```
https://github.com/CORDEA/oauth/blob/develop/src/oauth1.nim

ということで、改めて

```Nim
  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]型を定義する](https://zenn.dev/momeemt/articles/nim-result-t-e)
明日(12/7): [【翻訳記事】Pipelines - .Net の新しい IO API のツアーガイド, part 1 -](https://qiita.com/hitoshi44/items/7a05c607a53dcad704f9)