日記帳

プログラミングのことをつぶやく日記です。

Twitterのメンションに反応するRubyスクリプトを書いた

この記事は下記の記事の続きです。

leokun0210.hatenablog.com

Twitter botを作成していましたが、メンションに反応するために必要なUser Streams APIが2018年に終わっていました。したがってTwitter APIで実現させなければなりません。そこで今回は自前でメンションに反応するスクリプトを作成しました。このスクリプトは、cronで1分ごとに実行しています。前回に引き続きtwitter gemを使用します。

Twitterの“User Streams API”が完全終了 ~それでもリアルタイム更新を楽しむ方法 - やじうまの杜 - 窓の杜

require "twitter"
require "redis"

class Card
#省略
end

def get_image(client)
#省略
end

client = Twitter::REST::Client.new do |config|
  config.consumer_key = ""
  config.consumer_secret = ""
  config.access_token = ""
  config.access_token_secret = ""
end

redis = Redis.new

client.mentions_timeline.each do |tweet|
  tweet_id = tweet.id
  unless redis.get(tweet_id)
    pos = tweet.text =~ /ドロー|draw|Draw/
    if pos
      retry_count = 0
      card = nil

      while true
        card = get_image(client)
        if card || retry_count > 5
          break
        end
        retry_count += 1
      end

      if card
        client.update_with_media("@#{tweet.user.screen_name} あなたが引いたカードは『#{card.name}", card.img_uri, in_reply_to_status: tweet)
      end
    end
  end
  redis.set(tweet_id, tweet.user.screen_name)
end

メンションされたツイートのタイムラインを取得します

Twitter::REST::Client#mentions_timeline を呼び出して、メンションされたツイートのタイムラインを取得します。optionで取得件数を制限できるようですが、今回はしませんでした。

client.mentions_timeline.each do |tweet|

Redisで過去に返したメンションを記録します

下記の部分はRedisを用いて、過去にメンションされて返信済みのツイートか判定しています。DBを構築するほどではなかったので、Redisで手軽にできないかどうか模索してい見ました。unless redis.get(tweet_id)で、過去に返信したツイートを取得できなかった場合は、返信を行った後にredis.set(tweet_id, tweet.user.screen_name)でRedisのキーにツイートIDを記録します。Valueはなんでもよいので適当にしています。

redis = Redis.new
# 省略
  unless redis.get(tweet_id)
# 省略
  redis.set(tweet_id, tweet.user.screen_name)

正規表現で返信するワードを制限します

Twitter::Tweet#textでツイートの内容を取得できます。すべてのメンションされたツイートに反応するのではなく、特定のワードのみに反応したいので、正規表現を利用しています。ここでは「ドロー」「draw」「Draw」の3つの単語に反応するようにします。

    pos = tweet.text =~ /ドロー|draw|Draw/
    if pos
#省略
    end

メンション元のユーザIDを取得します

tweet.user.screen_nameは、メンション元のユーザIDです。したがって、このユーザIDにメンションします。

        client.update_with_media("@#{tweet.user.screen_name} あなたが引いたカードは『#{card.name}", card.img_uri, in_reply_to_status: tweet)

これぐらいの規模であればRDBではなく、Redis(KVS)で十分だと思いましたが、予想通りでした。特にスクリプトを実行するうえで不便はしてません。またこれぐらいの軽い処理を行うときはKVSを用いるかもしれません。

遊戯王OCGのカードを一枚ランダムにツイートさせる

このようにランダムにカードを画像付きで一枚つぶやくTwitter botRubyで作成します。コードは以下です。Twitterの投稿部分については特に説明を記述しません。このRubyスクリプトをcronで12:00 JSTに実行しています。

require "twitter"
require "net/http"
require "open-uri"
require "nokogiri"

def post(client)
  begin
    uri = URI.parse("https://db.ygoprodeck.com/api/v7/randomcard.php")

    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    res = https.start {
      https.get(uri.request_uri)
    }
    card = JSON.parse res.body
    uri = URI.parse(card["card_images"][0]["image_url"])
    img = uri.open

    name = card["name"].gsub(" ", "_")
    card_uri = "https://yugioh.fandom.com/wiki/#{name}"
    html = URI.open(card_uri).read

    # 取得したhtmlをNokogiriでパースする
    doc = Nokogiri::HTML.parse(html)
    name_ja = doc.xpath("//th[contains(., '(base)')]")[0].parent.children[1].children[1].text

    client.update_with_media("今日の最強カードは『#{name_ja}", img)
  rescue
    false
  end
end

client = Twitter::REST::Client.new do |config|
  config.consumer_key = ""
  config.consumer_secret = ""
  config.access_token = ""
  config.access_token_secret = ""
end

while true
  break if post(client)
end

カード画像を取得する

GET https://db.ygoprodeck.com/api/v7/randomcard.php はランダムでカードを一枚返却してくれるJSON APIです。Net::HTTPを使用してリクエストして、返却されたJSONをparseしています。["card_images"][0]["image_url"]にカード画像のuriがあるので、それをopenします。

    uri = URI.parse("https://db.ygoprodeck.com/api/v7/randomcard.php")

    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    res = https.start {
      https.get(uri.request_uri)
    }
    card = JSON.parse res.body
    uri = URI.parse(card["card_images"][0]["image_url"])
    img = uri.open

カード名の日本語名を取得する

GET https://db.ygoprodeck.com/api/v7/randomcard.phpで取得するカード名は英語です。そのため下記のコードが、カード名を日本語に翻訳します。まずカード名のスペースを_(アンダースコア)に変換します。https://yugioh.fandom.com/wiki/[変換後の英語カード名]で日本語のカード名を取得することができます。カード情報のHTMLを取得した後にNokogiriを用いてパースします。カードの日本語名は<th>タグのJapanese (base)が値のとき、それのいとこの要素に日本語のカード名を持っています。そのためコードはdoc.xpath("//th[contains(., '(base)')]")[0].parent.children[1].children[1].text のように魔術的な書き方をしています。ここはもっと良い書き方があると思います。興味があれば、該当HPのdomの構造を見てみてください。

    name = card["name"].gsub(" ", "_")
    card_uri = "https://yugioh.fandom.com/wiki/#{name}"
    html = URI.open(card_uri).read

    # 取得したhtmlをNokogiriでパースする
    doc = Nokogiri::HTML.parse(html)
    name_ja = doc.xpath("//th[contains(., '(base)')]")[0].parent.children[1].children[1].text

例外処理

下記のコードでは、Twitterに画像を投稿するpostメソッドがtrueになるまでwhileで繰り返しています。理由はGET https://db.ygoprodeck.com/api/v7/randomcard.phpトークンカードやラッシュデュエルのカードの情報を返却するためです。https://yugioh.fandom.com/wiki/[英語のカード名]は、TCG、OCGのformatのカードのみ対応していると推測されるため、404が返却されて例外が発生します。したがってpostメソッドで例外処理を行います。postメソッドが正常実行できるまで繰り返し実行します。

while true
  break if post(client)
end

今後の開発予定

メンションをしてくれたアカウントにランダムに1枚、カード画像をリプライしたいと考えます。

WSLのUbuntu LTSの初期設定一覧

私は、最近WSLの初期設定を2台分行いました。それときに実行したコマンドを下記に記載します。

zsh

普段使用しているシェルはzshなので、導入します。

sudo apt install zsh
sudo chsh -s /usr/bin/zsh

oh-my-zsh

sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

zsh-syntax-highlighting

cd ~/.oh-my-zsh/custom/plugins
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

rbenv

aptで入れられるrbenvのバージョンが古いためか、新しいrubyバージョンが入れられません。したがってbuildを行います。

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src
~/.rbenv/bin/rbenv init
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

peco

pecoの新しめのバージョンを動かすと、私が利用しているスクリプトが動かなくなってしまいました。そのため古めのpecoを入れています。

# ホームディレクトリ
cd ~/
# tarファイルをダウンロード
sudo wget "https://github.com/peco/peco/releases/download/v0.5.1/peco_linux_386.tar.gz"
# 解凍
sudo tar xzvf peco_linux_386.tar.gz
cd peco_linux_386
# 実行権限変更
sudo chmod +x peco
# PATHの通っている所にpecoを配置
sudo cp peco /usr/local/bin
# バージョン確認
peco --version

starship

curl -sS https://starship.rs/install.sh | sh

anyframe

mkdir ~/.zsh
cd ~/.zsh
git clone https://github.com/mollifier/anyframe

ghq

sudo add-apt-repository -y ppa:longsleep/golang-backports
sudo apt update -y 
sudo apt install -y golang-go
go install github.com/x-motemen/ghq@latest
# ghqはシェルを改めて立ち上げた

dockerがtmuxで動かない

dokcerがtmuxで動きませんでした。したがって、dcokerディレクトリの実行権限を付与してあげる必要があります。

# https://stackoverflow.com/questions/51766179/inside-tmux-docker-commands-wont-work-without-sudo
$ sudo chown "$USER":"$USER" /home/"$USER"/.docker -R
$ sudo chmod g+rwx "/home/$USER/.docker" -R

ネットワークドライブ(Z:)をwslから見えるようにする

ネットワークドライブ(Z:)をwslから見えるようにするため、mountします。

# /mnt/nasというディレクトリはあらかじめ作成しておく。
# またWIndows側でzドライブにnasを割り当てている。
sudo mount -t drvfs 'z:' /mnt/nas/

ファイルを一か所にまとめるワンライナー

PixivでDLしたjpg,png,gifファイルを一か所にまとめるワンライナー

findコマンドの -name オプションで検索対象のファイルを指定します。nl コマンドでファイルのリストに変換します。awkコマンドで、デリミタ . で分割して拡張子を抜き出し、ファイルをリネームします。(そうしないと同一ファイルがあるので失敗する)awkで組み立てたファイルの移動先をxargsでmvコマンドに渡します。-n オプションは引数の数を表しています。

find ./  -name "*.jpg" -or -name "*.png" -or -name "*.gif" | nl | awk -F'[.]' '{ printf("\".%s.%s\" ./_matome/%02d_.%s\n", $2, $3, $1, $3) }' | xargs -n 2 mv

6学期も終わった

6学期目の成績が出ました。結構よかったので、皆に感謝です。大学が始まってから仕事と勉学のバランスを考えていたけど、気持ち的には5:5ぐらいの配分だったけど、仕事と勉学の結果を見てから考えると2:8ぐらいの力の入れようでした。単純な時間ではなく気力の配分です。下手したら1:9ぐらいで仕事は完全に余力でしかやっていない気もします。それぐらい仕事は全く進捗が出せていません。仕事は学費のため、最低限生きるためぐらいの気持ちまで意識が落ちてしまっている感が否めなくて、マズローの承認欲求の一番下の場所まで移動してしまっています。仕事が楽しいか?という問いかけ以前という気もします。

MTGのプロプレイヤーのヤスさんは、ゲームデザイナーも兼業して両方とも成功を収めているけど、1年スパンで仕事の量を配分しているらしい。僕も配分したいけど、1年のうち10か月は授業があるので、仕事に集中できる期間は賞味2か月ぐらい。どうしたもんかなという気持ちです。最近つらい時に、なぜ自分は普通の人に比べて、こんなに苦しい生活をしているのかと思ってしまうときがあります。マイナスの感情なので、この感情を外に向けてしまって攻撃的になることは避けたいので、深呼吸して気持ちを落ち着けたいと思います。

7学期から研究室配属があるので仕事と勉学の配分が0.5:9.5とかになりそうな気もします。何とかしたいという気持ちもありますが、3年間やってみて思ったのは、多分無理かなという気持ちがあります。前向きにとらえて、それだけ力を入れられるものがあるということは良いことだということにしておきます。