Ruby+User Stream APIで無言リプライに高速返信するbotを作りました

Twitter User Stream API でタイムラインを表示するサンプル - でぶぬる日記
というエントリを先日書いたのですが、もう少し実用的なプログラムを作ってみました。

このbotを動かしているTwitterアカウントに対して無言@を送信すると、「random.txt」ファイルに書かれたダジャレのうちランダムで一つを選択して自動で返信してくれます。

通常のREST APIによるbotプログラムの場合、一定間隔でAPIをコールしてタイムラインを取得するような作りになる為、@を送ってからリプライが返ってくるまでに多少のタイムラグが出てしまいます。一方、User Stream APIを使用する場合は、タイムラインをほぼリアルタイムで読み取ることが出来る為、高速で応答することが出来るみたいです。

ちなみにダジャレのテキストファイルはこれを使っています。

今回の実装における注意点を列挙しておきます。

User-Agentヘッダをちゃんと設定しておいたほうが良さげ

実装における必須事項ではなさげですが、以下のように書いてあるので設定しておいたほうが無難でしょう。
http://dev.twitter.com/pages/user_streams

The User-Agent HTTP header must be set to include the application’s version. This will be critical in diagnosing issues with your client. If your environment precludes setting this the User-Agent field, you must then set an X-User-Agent field.

trackキーワードを付与しておく

User Stream APIにもtrackキーワードが実は指定出来るようです。
"track=[Twitterアカウントのscreen_name]" というパラメータを付与しておくことで、screen_nameを含むツイートがStream内に混ざってくるようになり、「自分がフォローしていないユーザからの@」も(高い確率で)検出出来るようになるでしょう。

なお、trackパラメータは厳密にはpostで送る必要があるそうですが、現状URLに付与して送信しても動作しているので、簡易的にそのような実装にしています。

再接続処理を実装しておく

前回のサンプルでは省略してしまっていたのですが、まともにUser Stream APIを使ったbotを運用しようと思った場合、Twitterとの接続が切れた際の再接続処理を実装しておく必要があります。

今回のプログラムでは単純に、あらゆる例外発生時に強制的に再接続するという手抜きな実装にしてしまいました。
これだと永久にリトライしてしまうので、本来であればより真面目に実装すべきところではあるかもしれません。

Rubyのnet/httpで発生するTimeout::Errorは、rescueで明示的に指定しないと拾えないようなので、注意が必要です。
参考: Timeout::Errorに注意 - dreammindの日記

ソースコード

# -*- coding: utf-8 -*-

require 'rubygems'
require 'net/https'
require 'oauth'
require 'json' if RUBY_VERSION < '1.9.0'

class DajareBot

  # 適当なものを定義すること
  CONSUMER_KEY        = "xxxxxxxx"
  CONSUMER_SECRET     = "xxxxxxxx"
  ACCESS_TOKEN        = "xxxxxxxx"
  ACCESS_TOKEN_SECRET = "xxxxxxxx"

  # botを動かすTwitterアカウントのscreen_nameを定義すること
  MY_SCREEN_NAME = "xxxxxxxx"

  # botのUser-Agentを指定
  BOT_USER_AGENT = "Auto Dajare Reply Program 1.0 @aquarla"

  # テキストファイルの置き場所を指定
  RANDOM_FILE_PATH = "./random.txt"

  # 証明書のパスを指定
  HTTPS_CA_FILE_PATH = './verisign.cer'

  def initialize
    @consumer = OAuth::Consumer.new(
      CONSUMER_KEY,
      CONSUMER_SECRET,
      :site => 'http://twitter.com'
    )
    @access_token = OAuth::AccessToken.new(
      @consumer,
      ACCESS_TOKEN,
      ACCESS_TOKEN_SECRET
    )
    # ファイルからダジャレ一覧を読み込み
    open(RANDOM_FILE_PATH) do |file|
      @dajares = file.readlines.collect{|line| line.strip}
    end
  end

  # Stream APIの呼出処理
  def connect
    uri = URI.parse("https://userstream.twitter.com/2/user.json?track=#{MY_SCREEN_NAME}")

    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    https.ca_file = HTTPS_CA_FILE_PATH
    https.verify_mode = OpenSSL::SSL::VERIFY_PEER
    https.verify_depth = 5

    https.start do |https|
      request = Net::HTTP::Get.new(uri.request_uri)
      request["User-Agent"] = BOT_USER_AGENT
      request.oauth!(https, @consumer, @access_token)

      buf = ""
      https.request(request) do |response|
        response.read_body do |chunk|
          buf << chunk
          while (line = buf[/.+?(\r\n)+/m]) != nil
            begin
              buf.sub!(line,"")
              line.strip!
              status = JSON.parse(line)
            rescue
              break
            end

            yield status
          end
        end
      end
    end
  end

  def run
    loop do
      begin
        connect do |json|
          if json['text']
            user = json['user']
            # 無言リプライを検出したら、発言元に対してダジャレをツイートする
            if (json['text'].match(/^@#{MY_SCREEN_NAME}\s*$/))
              @access_token.post('/statuses/update.json', 
                'status' => "@#{user['screen_name']} #{random_dajare}", 
                'in_reply_to_status_id' => json['id'])
            end
          end
        end
      rescue Timeout::Error, StandardError # Timeout::Errorも明示的に捕捉する必要あり
        puts "Twitterとの接続が切れた為、再接続します"
      end
    end
  end

  def random_dajare
    @dajares[rand(@dajares.size)]
  end
end

if $0 == __FILE__
  DajareBot.new.run
end