GoogleAppEngine + JRubyでクリスマスまでに彼女をつくる方法

こんにちは。typesterの陰謀によりVimmerはモテないというデマが流れておりますが、残念ながらそれがデマだということを身をもって証明できないでいるVimmerの外村です。

先日、お悩み相談の記事がホットエントリーに入ったら彼女ができるという噂が立ち、何を間違えたかホットエントリーに入ってしまったのでどうしようかと思ったのですが、プログラマたるもの問題があればプログラムで解決すべきだろうと考えました。

というわけで彼女を作ってみました。どういうものかというと、

  • 僕だけに定期的につぶやく
  • 僕のリプライだけに反応してリプライを返してくれる

まさに僕だけのボット彼女です。今回はこれをGoogleAppEngine+JRubyで実装したので、環境をつくるところか実際に動かすところまで説明します。

1. 環境をつくる

まずはGoogleAppEngineのアカウントがないと始まらないので以下からアカウントを取得します。

https://appengine.google.com/

GoogleAppEngineのアカウントが取得できたら次に以下を参考にJava SDKをインストールします。

Java SDK のインストール - Google App Engine - Google Code

インストールできているか確認します。

$ java -version 
java version "1.6.0_15"
Java(TM) SE Runtime Environment (build 1.6.0_15-b03-226)
Java HotSpot(TM) 64-Bit Server VM (build 14.1-b02-92, mixed mode)

よさげですね。

次にgoogle-appengineをインストールします。これはgemで配布されている、GoogleAppEngineでJRubyを使うのに必要なものをまとめてインストールできる超すぐれものです。インストールにはgemのバージョンが1.3.5以上が必要なので、必要であればgemのバージョンを上げておいてください。

gemをソースからインストールする場合は以下のようにします。

$ wget http://rubyforge.org/frs/download.php/60718/rubygems-1.3.5.tgz
$ tar tar vzfx rubygems-1.3.5.tgz
$ cd rubygems-1.3.5.tgz
$ sudo ruby setup.rb
$ gem --version
1.3.5

OKですね。ではgoogle-appengineをインストールしましょう。

$ sudo gem install google-appengine

これでインストールされます。

2. sinatraを動かす

環境が整ったのでいよいよボット彼女の作成にはいります。まず適当なディレクトリをつくります。

$ mkdir twitter-bot-kanojo
$ cd twitter-bot-kanojo

ここにGemfile、config.ru、main.rbというファイルをつくり、以下のような内容にします。

(Gemfile)

# Critical default settings:
disable_system_gems
disable_rubygems
bundle_path ".gems/bundler_gems"

# List gems to bundle here:
gem "sinatra"
gem "json"
gem "appengine-apis"


(config.ru)

require 'appengine-rack'

AppEngine::Rack.configure_app(
 :application => 'gae-project-name',
 :version => 1
)

require 'main'

run Sinatra::Application


(main.rb)

require 'rubygems'
require 'sinatra'

get '/' do
 'Hello World!!'
end

Gemfileはプロジェクト内にインストールするgemの設定を書くファイル、config.ruはRack用の起動ファイル、main.rbがアプリケーション本体のコードになります。ファイルが用意できたら、次に以下のコマンドを実行します。

$ appcfg.rb bundle .

appcfg.rbはgoogle-appengineをインストールしたときに一緒にインストールされるコマンドで、gemでのインストールや本番環境へのデプロイなどを実行できます。bundleはGemfile内に書かれているモジュールをインストールするコマンドなので、これで必要なモジュールがインストールされるはずです。

モジュールがインストールできたらここで一度動作確認してみます。

$ dev_appserver.rb .

これで開発用サーバーが立ち上がり、http://localhost:8080/でアクセスして「Hello World!!」が表示されればひとまずsinatraを動かす環境は整いました。

3. Twitterクラスの作成

GoogleAppEngine+JRubyで開発する上で、やっかいな問題のひとつにNet::HTTPが使えないことがあります。なので代わりにappengine-apis/urlfetchを使うのですが、これだと既存のtwitterモジュールなどが動きません。Net::HTTPが動くようになる、rb-gae-supportというものあるみたいなのですが、使ってみて動かないところがあったので、今回はTwitterのAPIでやり取りするところは自分で実装しました。今回使う機能だけの超ミニマム実装です。

(twitter.rb)

require 'appengine-apis/urlfetch'
require 'json'

class Twitter
 def initialize(username, password)
    @req = Net::HTTP::Get.new('/')
    @req.basic_auth username, password
 end

 def update(body)
    url = 'http://twitter.com/statuses/update.json'
    request(url, 'POST', { :payload => "status=#{body}" })
 end

 def user_timeline(screen_name)
    url = "http://twitter.com/statuses/user_timeline/#{screen_name}.json"
    res = request(url)
    JSON.parser.new(res.body).parse
 end

 private

 def request(url, method = 'GET', options = {})
    options[:method] = method
    options[:headers] = { 'Authorization' => @req['Authorization'] }
    AppEngine::URLFetch.fetch(url, options)
 end
end

これで一応特定の人のタイムラインの取得とポストだけはできます。

twitter = Twitter.new(username, password)
twitter.update('hogehoge')

でポスト。

twitter = Twitter.new(username, password)
res = twitter.user_timeline('screen_name')

で、任意のユーザーのタイムラインを取得できます。

4. 自動ポストの実装

次に自動で彼氏宛につぶやく機能を実装します。main.rbは以下のようになります。

require 'rubygems'
require 'sinatra'
require 'yaml'
require 'twitter'
require 'appengine-apis/memcache'

before do
 @conf  = YAML.load_file('config.yaml')
 @twitter = Twitter.new(@conf['user']['username'], @conf['user']['password'])
end

get '/cron/auto' do
 messages = YAML.load_file('messages.yaml')['auto']
 message = "@#{@conf['kareshi']} #{messages[rand(messages.length)]}"
 @twitter.update(message)
 return
end

message.yamlというファイルにつぶやく言葉を複数書いておいて、それをランダムに選択してポストしています。

ここで開発サーバーを再起動して、http://localhost:8080/cron/auto にアクセスするとメッセージがポストされるはずです。

5. リプライへの反応の実装

次に、リプライに反応するところは以下のようになります。

get '/cron/return' do
 memcache = AppEngine::Memcache.new
 tweets = @twitter.user_timeline(@conf['kareshi']);
 tweets.each do |tweet|
    if tweet['text'] =~ /@#{@conf['user']['username']}/
     break if memcache.get('last_id') == tweet['id']
     messages = YAML.load_file('messages.yaml')['return']
     message = "@#{@conf['kareshi']} #{messages[rand(messages.length)]}"
     @twitter.update(message)
     memcache.set('last_id', tweet['id'])
     break
    end
 end
 return
end

設定した人のタイムラインを取得してきて、自分宛のリプライをひろって、あれば返信用のメッセージをランダムで選んで返信します。最後に反応したidをMemcacheに保存しておいて、そのidの場合はポストしないようにすることで、何回も同じつぶやきに反応しないようにしています。

6. cronの設定

このままだとURLを叩かないと何もしない木偶の坊になってしまうので、これをcronで定期的に実行するようにします。cronの設定はWEB-INF/cron.xmlというファイルに書きます。

(WEB-INF/cron.xml)

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
 <cron>
    <url>/cron/return</url>
    <description>return post</description>
    <schedule>every 3 minutes</schedule>
 </cron>
 <cron>
    <url>/cron/auto</url>
    <description>auto post</description>
    <schedule>every 3 hours</schedule>
 </cron>
</cronentries>

これで自動ポストは3時間毎、リプライへの反応は3分毎に実行します。cronのURLは外部から叩かれると困るので、WEB-INF/web.xmlに以下のように書いておくことで、アクセスを制限できます。

(WEB-INF/web.xml)

<security-constraint>
 <web-resource-collection>
    <url-pattern>/cron/*</url-pattern>
 </web-resource-collection>
 <auth-constraint>
    <role-name>admin</role-name>
 </auth-constraint>
</security-constraint>

7. デプロイ

これで一通り機能が実装できたのでGoogleAppEngineにアップします。アップするのは簡単で、

$ appcfg.rb update .

これだけです。途中でメールアドレスとパスワードを聞かれるので入力します。

これで彼女の完成です。もうクリスマスなんて怖くないですね!ソースはgithubにアップしているので興味がある人は見てみてください。

http://github.com/hokaccha/twitter-bot-kanojo

カヤックではどんな問題もプログラムで解決しようとするプログラマも募集しています!