くりにっき

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

x_post_sanitizerというgemを作った

x_post_sanitizerとは

github.com

Xのポストに含まれる t.co のような短縮URLを展開したりその他いい感じにするgemです。

だいぶ前に作った https://github.com/sue445/tweet_sanitizer の後継にあたります。

sue445.hatenablog.com

モチベーション

tweet_sanitizerは https://github.com/sferik/twitter-ruby に依存してたのですが、TwitterがXになった後くらいにメンテされなくなって https://github.com/sferik/x-ruby を使うのが推奨されてきました。

しかし僕は https://github.com/yhara/simple_twitter をメンテしている *1からsimple_twitterを使うことが多いです。

そのため、tweet_sanitizerを特定のgemに依存しない形で作り直したのがx_post_sanitizerになります。

tweet_sanitizerにあったテストケースは全部x_post_sanitizerにも移植してパスしてるので互換性はあると思います。

さすがに https://github.com/sferik/twitter-ruby を使ってる人はもういないだろうと思ってtweet_sanitizerの方はこの機会にアーカイブしました。

*1:メンテナになった経緯は https://sue445.hatenablog.com/entry/2023/07/17/232005 を参照

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 を見てください。

Pixelaで自分のツイート数を草化した

こんな感じです。

pixe.la

リポジトリ

github.com

仕組み

Twilogスクレイピングして、CircleCIのcronの仕組を使って定期的(当日分は1時間おき、前日分は1日1回)にツイート数をグラフを更新しています。

スクリプトRubyで書いていますが、定期実行部分に関してはCircleCIとDockerイメージだけで完結しているので実行環境にRubyをインストールする必要はありません。

追記:2018/11/26

1時間おきは多すぎた感があったので今は6時間おき(1日4回)にしてます

Twilogを使ってる理由

  • 特定の日時のツイート数だけを取得するだけならTwitter APIよりTwilogからとってくる方が早いし楽
    • Search APIでも日付を絞り込むことはできるのだが、自分の経験上たまに検索結果が取れなくなることがあるのであまり信用してない *1
    • 特定のユーザのツイートを取得するならuser timeline APIを使うのが確実なのだが直近3200件分しかとれない。
      • 当日分と前日分くらいなら問題ないんだが、それでもuser timetimeで特定の日付を絞ってツイートを取得するのは若干面倒
    • 過去分を一括投入しようとするとRateLimitに引っかかってしまう
    • Twilogであれば https://twilog.org/sue445/stats に全期間(自分が登録した以降全部)のツイート数が入ってるので、何回もAPIを叩くよりもhtmlを1枚スクレイピングする方が圧倒的に楽と判断
  • Twitter APIだと自分でアプリ(APIをたたくためのアクセストークン)を作る必要があってお手軽感がない
  • 過去分投入だけなら公式のツイートダウンロードでもいいんだけど、OSSとして公開しようとすると英語で説明を書くのが面倒だしスクリプト化しづらい

使い方

基本的には https://github.com/sue445/tweet_pixels/blob/master/README.md に書いてる通りにコマンドを実行するだけ。

最初のPixela登録とグラフ作成だけローカルで実行する必要があります。

Rubyでやりたい場合は https://github.com/sue445/tweet_pixels をforkしてclone後に下記のようなコマンドを実行してください。

cp .env.example .env
vi .env
bundle install --path=vendor/bundle

./bin/console
# register
@client.create_user(agree_terms_of_service: true, not_minor: true)

# create graph
@graph.create(name: "Daily tweets", unit: "Tweets", type: "int", color: "sora")

他言語の場合は同じようなことをよしなにやってください。

直近1年分のツイートを一括投入する場合は bundle exec rake update_multi を実行

グラフ作成後はCircleCIのEnvironment Variablesに

  • TWITTER_ID
  • PIXELA_USERNAME
  • PIXELA_TOKEN
  • PIXELA_GRAPH_ID

を登録してください。

f:id:sue445:20181021110424p:plain

Tips

CircleCIでボットを運用する時のノウハウとして、Chat Notificationsで普段使いのチャット(自分の場合Slack)を登録しておくとエラー時にすぐに気付けるので便利です。

f:id:sue445:20181021110928p:plain

1時間に1回にSlackに通知がくると逆にウザいので、自分はFixed/Failed Only(ビルドが失敗から成功に戻った時と、ビルドが失敗した時のみ通知)をよく使っています。

所感

今回のに限ったことじゃないんだけど、CircleCIの設定ファイルに実行環境(利用するDockerイメージ)やcronの設定も含めることができるので、GitHubリポジトリでボットの自動実行環境もセットで配布できるのすっごい便利だ。。。

*1: 過去に実際、一時期自分のツイートだけがSearch APIで取れなくなる事象がありました https://twitter.com/sue445/status/897968605567926272

tweet_sanitizerというgemを作った

退院後の初gem情報です

github.com

どんなgemか?

Twitter APIで取得したツイートの内容から

  • full_text (140文字より長いツイート)があればそれを積極的に使う
  • 短縮URLの t.co を元のURLに戻す
  • ツイートの末尾に含まれてる添付画像などのURLを除去
  • ><&gt;&lt; などにエスケープされているので、エスケープを解除

などのサニタイズ処理を行います

モチベーション

Twittodon のロジックをgem化しています

sue445.hatenablog.com

twittodonのツイートのサニタイズ周りの処理にバグがあったんですが、

という理由でgem化してテストコードをリファクタリングしつつバグ修正しました *1

使い方

tweet_sanitizerを使わない場合

普通に twitter のgemを使った場合

tweet = @client.status("https://twitter.com/github/status/866677968608927744")
tweet.text
#=> "Introducing GitHub Marketplace, a new place to browse and buy integrations using your GitHub account.… https://t.co/dK0Tmcmm72"

tweet_sanitizerを使った場合

using TweetSanitizer::TwitterExtension

extended_tweet = @client.status("https://twitter.com/github/status/866677968608927744", tweet_mode: "extended")
extended_tweet.sanitized_text
#=> "Introducing GitHub Marketplace, a new place to browse and buy integrations using your GitHub account. https://github.com/blog/2359-introducing-github-marketplace-and-more-tools-to-customize-your-workflow"

using TweetSanitizer::TwitterExtension すると生える #sanitized_text でいい感じにサニタイズできます

ツイート取得時の tweet_mode: "extended" がないと前述の full_text がレスポンスで返ってこないので必須です

#status メソッド以外でもツイートを取得しそうなAPI#search とか)のオプションに tweet_mode: "extended" を渡せばたいてい対応してます

参考

Upcoming changes to Tweets — Twitter Developers

詳しくはREADME読んでください

ユースケース

APIで取得したツイートをmastodonなど他のツールに流したり、DBに保存したいような場合に便利かもしれません

TwittodonというTwitterからMastodonに流すボットを作った

ここ最近作ってた便利ツールの共有です

Twittodonとは?

Twitterからの任意のツイートをMastodonに流すためのボットです

github.com

ちなみにTwittodon(ついっとどん)とはTwitter + Mastodonの造語です

サンプル

f:id:sue445:20170601004222p:plain

https://precure.ml/@sue445/85064

  • Twitterの検索クエリにマッチしたツイートをMastodonにトゥートする
  • ツイートに画像が含まれていればMastodonにもアップロードして画像付きのトゥートにする
  • トゥートに元ツイートへのリンクを付与
  • ツイートに短縮URL(t.co)が含まれていた場合、短縮前の元URLの状態でトゥートに含める

といったことをやっています

使い方

TwitterMastodonAPIを使うためのトークンを取得する部分は手動ですが*1Heroku のアカウントがあればその後のサーバへのデプロイはDeploy to Herokuボタンを使ってワンクリックでデプロイできます。

f:id:sue445:20170601005523p:plain

Twitterの検索条件はHeroku Schedulerのジョブに渡せます。

f:id:sue445:20170601005947p:plain

詳しくは https://github.com/sue445/twittodon/blob/master/GETTING_STARTED.md を読んで下さい

Twitter -> Mastodon トゥート同期アプリ」との違い

似たようなサービスで Twitter -> Mastodon トゥート同期アプリ というのがあります。

sync.twi2mstdn.space

Twitter -> Mastodon トゥート同期アプリ」だと連携したTwitterアカウントのツイートを全てMastodonに送るようですが、TwittodonだとMastodonに送るツイートを検索条件で指定することができます。

検索APIを使っているので、自分以外のツイートもMastodonに送れます。

イメージ的にはIFTTTの「Post to Slack when a tweet matches your search term(Twitterの検索条件にマッチしたツイートをSlackに送るアプレット)」のMastodon版を目指して作っています。

ifttt.com

最後に

最初のトークンの取得が手動なのが面倒かもしれませんが*2、ワンクリックでデプロイできるようにしたりTwitterの画像をMastodonに再アップロードなどいろいろ作り込んでいて便利なので是非ご利用ください。

また、インスタンスによってはボット利用が禁止されたり制限されてる場合もあるので節度をもって使うようにしてください。

*1:GETTING_STARTEDに手順は全部書いてる

*2:その手順を簡略化するためのアプリの作成もなくはなかったんですが、これ以上簡単にしすぎるとボットが作られ放題でそれはそれで治安が悪くなりそうな予感があったので、最低限トークンの取得くらい手順書見ながら自力でどうにかできる人だけに使ってほしいという思いでふるいにかけるために現状の仕様にしています

勉強会やアニメの実況におすすめなTwitterクライアントの紹介

Twitterで実況する時に長年愛用してるツイタマについての紹介です *1

俺氏スペック

  • サザエ実況歴6年くらい(初実況が2009年5月)
  • プリキュア実況歴5年くらい(スイート以降)

実況とは?

tsudaる」とも言われてるのでそちらをご参照ください。

d.hatena.ne.jp

個人的な実況要件

  • ハッシュタグで検索した結果を実況用のタイムラインに表示したい
    • 混ざるのでメインのタイムラインとは別に表示したい
    • REST APIだと結果を取得するのにラグがあるのでストリーミングAPIに対応してるとなお可
  • ツイート時にハッシュタグを自動でつけたい
  • 家ではWindows、出先ではMacで実況しているのでできれば両OSで使いたい

ツイタマ for PCについて

ツイタマ - Twitter Browser

上に書いてる自分の実況要件はだいたい満たしていました。

Adobe AIR製なのでWindowsMacの両方で使える

上がWindowsで下がMac。両OSで同じクライアントが使えるのは便利

f:id:sue445:20151220165415p:plain

f:id:sue445:20151220165403p:plain

Windowsだけでいいなら Tween がいい線いっていました。(TLの実況廃人勢もこれをよく使ってる印象)

ストリーミングAPI対応

ONにするとガンガンツイートが流れてきます。

f:id:sue445:20151220165319p:plain

ONにするとガチで流速が速いので番組によってON/OFF適度に切り替えています

検索ワードに対応したフッタを付加できる

これは他のクライアントだとあまり見ない機能です

フッタ文字列を自動にして

f:id:sue445:20151220170526p:plain

上の検索バーに検索ワードを入れると

f:id:sue445:20151220170518p:plain

フッタにその文字列がつきます

f:id:sue445:20151220170532p:plain

検索ワードに OR が入ってる場合、フッタに設定される時には自動で削除されるのが地味に便利。(上の例だと検索ワードが「#sazaesan OR #sazae OR #fujitv OR #サザエさん」なのでフッタは 「#sazaesan #sazae #fujitv #サザエさん」)

検索ワードとフッタを両方設定する必要がないし、実況用のタイムラインとフッタがリンクするのでハッシュタグ間違えて実況ってことがなくなります。べんり

小ネタ

長年アニメ実況してると過去の検索の履歴を消したいことがあるので調べました。

アプリの設定でクリアできないけど設定ファイルを直接編集することで過去の履歴を整理することができました。

  • Windows: C:\Users\<ユーザー名>\AppData\Roaming\softama.twitter.<なんかランダムな文字列>.\Local Store\search_query.xml
  • Mac: /Users/<ユーザー名>/Library/Preferences/softama.twitter.<なんかランダムな文字列>./Local Store/search_query.xml

合わせて読みたい

サザエ実況に関しては過去に知見をまとめてるのでこちらもご参照ください

sue445.hatenablog.com

まとめ

ツイタマ使うと日々の実況がめっちゃ捗ります

*1:ステマではありません

【今月のgem】twitter_retryを作った

github.com

どんなgem?

Twitter APIのエラーハンドリングをいい感じに行うためのgemです

作った経緯

弊社はTwitter APIを使ったアプリがいくつかあるのですが、エラーハンドリング周りが共通化されていなかったのでgemにしました。一応弊社のアプリ2〜3個くらい見て汎用化したつもりです。

使い方

TwitterRetry.with_handing do
  # some twitter API code
  twitter.update("some tweet")
end

with_handing のブロックの中に任意のTwitter APIの処理を書くだけで、

  • 「Your account is suspended and is not permitted to access this feature.(凍結済みユーザのエラー)」が投げられたら TwitterRetry::SuspendedError を投げる
  • 「User is over daily status update limit.(一時的な規制)」 や 「Status is a duplicate.(重複連投)」などが投げられたらエラーは握りつぶす
  • Twitter側の一時的なエラー(Over capacityやInternal errorなど)が投げられたら1秒待って3回までリトライ。それでダメなら TwitterRetry::RetryOverError を投げる

と、いい感じにエラーハンドリングを行います。

追伸

gemを月に1つペースで作ってるのでせっかくなので「今月のgem」というタイトルつけました