くりにっき

フルスタックキュアエンジニアです

Twitter botをAPI v2移行する(Ruby編)

https://github.com/sue445/pribirthdaybotTwitter API v2移行した時のメモ

tl;dr;

移行先のgem

自分が当時調べた範囲だとAPI v2対応しているAPIクライアントのgemは下記の2つしかなかったです。

github.com

github.com

ちなみに前者のsimple_twitterは最近メンテナになりました。

sue445.hatenablog.com

その他に必要になりそうなgem

TwitterのOAuth2のリフレッシュトークンからアクセストークンを取得するのに必要だったgem

github.com

ウェブアプリでTwitterのOAuth2認証したい場合に必要なgem

github.com

botAPI v2対応する時の注意点

アクセストークン(厳密にはベアラートークン)には有効期限がある(約2時間)ので、botで利用する時には実行時にリフレッシュトークンからトークンリフレッシュを行い毎回新しいアクセストークンを生成する必要があるのですが、一度利用したリフレッシュトークンは使えなくなるのでトークンリフレッシュの時に返ってくるリフレッシュトークンをDBとかに保存しておく必要があります。(このリフレッシュトークンを次回実行時に使う)

追記: 2023/07/21 10:40

これ書いた後に知ったんだけどAPI v2を使う上でOAuth2は必須ではない(一部のエンドポイントはOAuth 1.0aのアクセストークンとかでTwitter API v2が使えるらしい *1 )ので、OAuth1.0aであればリフレッシュトークンは考えなくてよさそう

手順

リフレッシュトークンを作成する

一番最初にリフレッシュトークンを取得するところだけは手で作る必要があります。

自分用にはこういうアプリをローカルで動かして認証するようにしていました。

github.com

Twitter Appについているデフォルトのトークンでもいいんですが、Appを管理しているTwitterアカウントとbotTwitterアカウントが異なる場合にはこっちの方がいいと思います。( https://developer.twitter.com/ への登録は電話番号認証が必要なのでなるべく1つのTwitterアカウントに集約させたい)

権限はTwitter botの要件であればこれで十分。 *2

# https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code
TWITTER_V2_SCOPE = "tweet.read tweet.write users.read offline.access"

リフレッシュトークンを使う場合には最低限 offline.access は必須。他は用途に応じて調整してください。

このアプリをローカルで動かせば下記のようなjsonが返ってくるので、ここにあるtokenとrefresh_tokenを使います。

{
    "provider": "twitter2",
    "uid": "14540215",
    "info": {
        "name": "sue445",
        "email": null,
        "nickname": "sue445",
        "description": "フルスタックキュアエンジニア。 プリキュア/サザエさん/プリパラ/プリ☆チャン/プリマジ/オヤジギャグコミッタ 【警告】上級者向けアカウント。フォローすると実況ですごい勢いでTLが埋まるよ!【危険】",
        "image": "https://pbs.twimg.com/profile_images/378800000394619528/fb8a746f6955be08134050ba2adaa1f2_normal.png",
        "urls": {
            "Website": "https://t.co/fZVVmoHpLx",
            "Twitter": "https://twitter.com/sue445"
        }
    },
    "credentials": {
        "token": "XXXXXXXXXXXXXXXXXXXXXX",
        "refresh_token": "XXXXXXXXXXXXXXXXXXXXXX",
        "expires_at": 1689828440,
        "expires": true
    },
    "extra": {
        "raw_info": {
            "data": {
                "verified": true,
                "protected": false,
                "description": "フルスタックキュアエンジニア。 プリキュア/サザエさん/プリパラ/プリ☆チャン/プリマジ/オヤジギャグコミッタ 【警告】上級者向けアカウント。フォローすると実況ですごい勢いでTLが埋まるよ!【危険】",
                "entities": {
                    "url": {
                        "urls": [
                            {
                                "start": 0,
                                "end": 23,
                                "url": "https://t.co/fZVVmoHpLx",
                                "expanded_url": "http://sue445.hatenablog.com/",
                                "display_url": "sue445.hatenablog.com"
                            }
                        ]
                    }
                },
                "name": "sue445",
                "location": "Tokyo / Japan",
                "id": "14540215",
                "profile_image_url": "https://pbs.twimg.com/profile_images/378800000394619528/fb8a746f6955be08134050ba2adaa1f2_normal.png",
                "url": "https://t.co/fZVVmoHpLx",
                "created_at": "2008-04-26T05:33:43.000Z",
                "public_metrics": {
                    "followers_count": 2859,
                    "following_count": 1678,
                    "tweet_count": 456027,
                    "listed_count": 182
                },
                "username": "sue445"
            }
        }
    }
}

botでのAPI v2移行

自分がsimple_twitterのメンテナをしてるってことでsimple_twitterを例に出しますが全体的な流れはtweetkitでもそんなに変わらないと思います。

Gemfile に下記を追加。

gem "simple_twitter"
gem "twitter_oauth2"

こういうclassを追加 *3

# frozen_string_literal: true

class TwitterClient
  API_ENDPOINT = "https://api.twitter.com/2"

  class Error < StandardError
  end

  # @return [Hash]
  def get_me
    simple_twitter_client.get("#{API_ENDPOINT}/users/me")
  end

  # @param [String] text
  def post_tweet(text)
    simple_twitter_client.post("#{API_ENDPOINT}/tweets", json: { text: text })
  end

  private

  # @return [String]
  def access_token
    return @access_token if @access_token

    oauth2 = TwitterOAuth2::Client.new(
      identifier: ENV["TWITTER_V2_CLIENT_ID"],
      secret:     ENV["TWITTER_V2_CLIENT_SECRET"],
    )

    # FIXME: refresh_tokenをDBなどから取得する
    oauth2.refresh_token = load_refresh_token

    ret = oauth2.access_token!

    # FIXME: refresh_tokenをDBなどに保存する
    save_refresh_token(ret.refresh_token)

    @access_token = ret.access_token
  end

  # @return [SimpleTwitter::Client]
  def simple_twitter_client
    @simple_twitter_client ||= SimpleTwitter::Client.new(bearer_token: access_token)
  end
end

ここでポイントになるのがrefresh_tokenの読み込みと保存です。

priborthdaybotだと実行環境にCloud Functionsを使っているのでFirestoreに保存するようにしていますが、ここはアプリの要件によって変わると思います。(だいたいはRDBやKVSになるはず)

具体的な作業内容は https://github.com/sue445/pribirthdaybot/pull/61/files を見てください。