概要
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との違いに関しては、こちらの記事で詳細な説明があります。
重要な点としてOAuth1
とOAuth2
が混在している点に注意してください。
OAuth1
ではTwitterのAPI_KEY
とAPI_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.header
をOAuth
ライブラリに渡す
さて上記のコードの問題はログインできてないとのことなので、まず認証周りを確認したんですが問題なし。
結論から言うと、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 -