223 Software

主に勉強したことなどを記録するログです。 最近はRuby, Railsなどが多め。

heroku - 223 Software

InstagramのReal-time APIを使ってみた

Tokyo Real-time Photosというアプリを公開しました。
Nekostagramに代表される、Instagram系サービスが人気です。やや波に乗り遅れた感はありますが、僕も何か作ってみようと思い立ちチャレンジしました。

これは何?

Instagramで東京付近の写真がアップされると、リアルタイムで画面に表示されるだけのアプリです。具体的には以下の青丸の範囲です。(未公開の管理側です。)
InstagramのReal-timeAPIを使っています。

技術的には?

信頼のsinatra + Herokuで作っています。リアルタイムに写真が通知される部分はPusherで、Redisをキャッシュ + DB用途で使っています。
こういった外部APIを使った簡易的なサービスにはsinatraは最適ですね。
ソースコードはこちら:https://github.com/satococoa/Tokyo-Real-time-Photos

Real-time APIについて

まずInstagramのReal-time APIを僕が使った範囲でちょっと説明します。
利用手順としては3ステップになります。
  1. 条件を指定して購読申し込みを行う
  2. 購読申し込みに対してInstagramから確認を受ける
  3. Instagramからの通知を受け取る
それぞれコードを見ながらいきます。
1. 条件を指定して購読申し込みを行う
post '/admin/subscriptions' do
  require_admin # ここではとりあえず無視してOKです。
  client = Instagram.client(:access_token => session[:access_token])
  obj = client.create_subscription(:client_id => Instagram.options[:client_id],
                                   :client_secret => Instagram.options[:client_secret],
                                   :object => 'geography',
                                   :aspect => 'media',
                                   :lat => params[:lat],
                                   :lng => params[:lng],
                                   :radius => 5000,
                                   :callback_url => subscript_url)
  # subscript_urlは更新の通知を受け取るURLです。
  data = {:lat => params[:lat], :lng => params[:lng], :radius => 5000}
  REDIS.set("subscription:#{obj['object_id']}", data.to_json)
  # create_subscriptionの返り値からobject_idを受け取り、緯度・経度・半径の情報と一緒に保存
  obj
end
これは管理画面で地図上の点をクリックしたときにAjaxで呼び出されるコードです。
create_subscriptionメソッドを使って、クリックした点を中心とした半径5km圏内の更新に対して購読を申し込みます。するとInstagramから購読申し込みに対しての確認アクセスが来ますので、それに答えてあげるのが次のコードです。
2. 購読申し込みに対してInstagramから確認を受ける
get '/subscription/callback' do
  params[:'hub.challenge']
end
(本当はそのアクセスがほんとにInstagramから来ているのか、ちゃんと検証すべきでしょうけれども、やってません;;)
さきほど:callback_urlで指定したURLにInstagramからGETのアクセスが来ます。そしたらパラメータのhub.challengeをそのまま返してあげてください。これで購読完了です。

Herokuでやる方へ注意:この際、先ほどの購読申し込みのPOSTリクエストが、購読確認のGETリクエストに対してレスポンスを返すまで終了しません。つまり、同時に2アクセスは最低さばかないといけない、ということです。
仕方ないので、僕はこのときだけDynoを2個にしました。ハマりました。
3. Instagramからの通知を受け取る
post '/subscription/callback' do
  #略
end
略し過ぎでごめんなさい。でも普通にcallback_urlにしたURLにPOSTのリクエストが来るだけです。
ただ、ちょっと注意したいのがここで実際に写真データが送られてくるわけではないことです。

購読申し込みが成功すると、create_subscriptionメソッドは:object_idを含むオブジェクトを返してきます。
そして、実際に写真がUPされてアプリ側にInstagramから通知が来るときには、その:object_idを含むオブジェクトを送ってくるので、それをもとにInstagramからAPIを使って画像を持ってくる必要があります。
(・・・ですよね、多分。しっかり調べた訳じゃないのでちょっと自信がないのです。)

ということで、詳しくは github上のコードを参考にしてみてください。
https://github.com/satococoa/Tokyo-Real-time-Photos

最後に制限事項

Herokuからアドオンとして利用できるPusherを使っていますが、これが1日に1万回までしかリクエストを受け付けてくれません。
時間帯によって写真がアップされる頻度が違っているためによくわかりませんが、もしかすると簡単に1万回は超えてしまうかもしれません。
自動で写真が表示されなくなったら察してください。

時間帯によって、一定枚数の写真をキャッシュすればいいのかもしれませんが、とりあえずはInstagramからPOSTが来るたびにリアルタイムに通知していってみようかと思います。

以上です。お楽しみいただけると幸いです!

Map Chasingというアプリを作りました

「第0回 HTML5 プログラミング&クリエイティブコンテスト」に応募するため、Map Chasingというアプリを作り、公開しました。
このアプリの簡単な紹介と技術的な説明を少し書きたいと思います。

コンテストについて

html5-developers-jpというHTML5の普及・促進・情報共有を目的とするコミュニティが主催するプログラミングコンテストです。
応募対象は川柳/キャラクターデザイン/テーマに沿った作品/自由課題があるのですが、僕は自由課題部門に応募してみました。「HTML5, CSS3及び周辺技術の将来性を感じさせるもの」という要件でしたので、個人的に興味があったWebSocketを使って何かリアルタイムな仕組みが作りたいなぁ、ということで。
コンテストについての参考URL: 「第 0 回 HTML5 プログラミング&クリエイティブ・コンテスト」のお知らせ

作成したアプリについて

(スクリーンキャスト、ちょっと見づらいですね。初なのでご勘弁ください。)

Twitterアカウントでログインし、地図上で出題されるミッションに答えていく、という単純なアプリです。
現在実装されているミッションは山手線の駅を探すミッションのみです。
ちなみに最初に自分のアイコンが表示される場所ですが、「東京近辺のランダムな地点」としています。Geolocationで現在地を取得することもできますが、ねぇ・・・。

同時にログインしている人全員に同じミッションが提示されます。ミッションをクリアするとクリアした人にポイントが入り(名前の後ろの数字がスコアです。)、次のミッションが提示されます。特に排他処理はしていませんので、一番最初の人がスコアをゲットした直後、次の問題が出題される前でしたらほぼ同着の方もスコアをゲットできるはずです。

同時にログインしている人が増えるとちょっとした競争になって面白いです。もちろん、ミッションもユーザーのアイコンの移動も全ユーザーでWebSocketを使って通知されていますので、運がよければ(?)他の方のアイコンが動いているのも見ることができます。

Map Chasingというアプリ名の通りユーザー同士で追いかけっこを楽しめるアプリにしよう、というプランのもとで作り始めたのですが、時間的な制約(締め切り的な意味で)と同時にログインする人が多くないと追いかけっこはゲームが成り立たないからという考えのもと、現状のようになりました。
ソースコード: https://github.com/satococoa/map_chasing

技術的な説明

Herokuを使っています。WebSocketの部分はPusherに丸投げで、ユーザーの情報(Twitterからもらった情報や、スコアなど)はRedis To Goを使っています。
本当はこの程度のアプリではredisを使う必要は全くなく、PostgreSQLやSQLite3でも十分かとは思いますが、どうせやるなら新しい技術を・・・という考えです。
ちなみにRailsではなくてSinatraです。あと、無駄にjQuery mobileです。これも今後のための練習です。
WebSocket
途中まで、Pusherを使わずにem-websocketを使ってサンプル程度ですがまずはちょっとWebSocketを試してみました。
require 'em-websocket'
require 'logger'

EventMachine.run {
  @logger = Logger.new(STDERR)
  @channel = EM::Channel.new
  
  EventMachine::WebSocket.start(:host => '0.0.0.0', :port => 8080) do |ws|
    ws.onopen {
      sid = @channel.subscribe{|msg| ws.send msg}
      @logger.debug "#{sid} connected!"
      
      ws.onmessage {|msg|
        @logger.debug "<#{sid}>: #{msg}"
        @channel.push msg
      }
      ws.onclose {
        @channel.unsubscribe(sid)
        @logger.debug "#{sid} disconnected!"
      }
    }
  end
}
やってることは、メッセージがブラウザから送られてきたらそのメッセージをブラウザに送り返してやる、というそれだけです。で、それをJavaScriptで受けて色々します。
試しに簡単なチャットを実装したところでふと気づいたのですが、herokuで公開するつもりであったことをすっかり忘れていました。herokuのアドオンとして使えるものを・・・ということでPusherを使うことに。
Pusher
Pusherの仕組みはWebSocketとはちょっと変わっていて、メッセージの送信はRESTで(たいてい)サーバーから送り、クライアントはJavaScriptでメッセージを受け取りほげほげする、という流れです。
詳しくは公式のページをご覧いただくとして、おおざっぱに言えばこんなイメージです。サーバー側はちゃんとgemがあるので、そちらを使います。$ gem install pusherですね。クライアント側もJSのライブラリがあり、簡単に使えるようになっています。

メッセージのやり取りをRubyコード、JavaScriptコードで以下のように記述します。Map Chasing上でアイコンを動かしたときの例です。
Ruby(メッセージの送信)
Pusher['map-chasing'].trigger('move', user.to_hash, params[:socket_id])
JavaScript(メッセージの受信)
pusher.bind('move', function(data) {
  if (Users[0].uid == data.uid) return false;
  var uid = data.uid;
  if (!!Users[uid]) {
    var pos = new google.maps.LatLng(data.lat, data.long);
    Users[uid].move(pos);
  } else {
    Users[data.uid] = new User(data);
    Users[data.uid].appear(map);
  }
});
たったこれだけです。非常に簡単ですね。送信、受信ともチャネルを指定できます。僕は今回は"map-chasing"というチャネル一つだけを使いましたが、例えばMMOで言うサーバーのように複数のワールドに分けるために使ったり、チャットチャネルとゲームのイベント送信(キャラクターの移動とか?)チャネルに分ける、など色々な使い道がありそうです。
イベントも任意に定義できますのでソースの見通しがよくなって素敵です。
"move"はアイコンの移動、"appear"はアイコンの出現、"score"は得点時、というように使い分けました。
あと送信側のtrigger()メソッドの第 3引数ですが、以下のケースに対応するためのものです。
  1. Aさんがアイコンを移動("move")しました。
    Aさんのブラウザ上では既にAさんのアイコンは移動済みになる。ドラッグした訳なので。
  2. "move"イベントがPusherに送信されます。
  3. BさんやCさんのブラウザ上では、Pusherから受信した"move"イベントを処理し、AさんのアイコンをJavaScriptで移動させてあげます。
  4. ・・・Aさんのブラウザ上では2.で送信されたイベント(=自分で送信したイベント)を受け取る必要がない!
PusherにJavaScriptで接続した際にsocket_idが取得できます。そのsocket_idをブラウザからPOSTするときにパラメータとしてサーバーに渡してあげてその値を第3引数にすることにより、余分なイベントを受信せずに済むようになります。
Pusherに接続し、socket_idを取得するコードは以下。
var pusher = new Pusher(window.pusher_key);
pusher.bind('pusher:connection_established', function(event){
  socket_id = event.socket_id;
});
ちょっと余談になりますが、Pusherと接続するときにはapp_id, key, secretが必要となります。heroku上では、本番環境用に既にそれらがセットされた状態になっており、さらに開発用のapp_id, key, secretも最初から用意していてくれます。
ただし、開発環境上ではそれらをオブジェクトに与える処理は自分で書かなくてはなりません。
configureメソッドで:development, :productionに処理を分けて、:development用の設定時に例えばymlファイルに書いた設定を読み込む、などの対応が必要かと思います。ソースからその部分を抜粋しました。
configure :production do
  enable :sessions
  use OmniAuth::Builder do
    provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
  end
  uri = URI.parse(ENV['REDISTOGO_URL'])
  Ohm.connect(:host => uri.host, :port => uri.port, :password => uri.password)
end

configure :development do
  enable :sessions
  config = YAML::load_file('config.yml')
  use OmniAuth::Builder do
    provider :twitter, config['twitter']['key'], config['twitter']['secret']
  end
  Ohm.connect
  Pusher.app_id = config['pusher']['app_id']
  Pusher.key = config['pusher']['key']
  Pusher.secret = config['pusher']['secret']
end
OmniAuth
今回の開発にあたって、実は一番感動したのはOmniAuthでした。
手順はたったの3ステップ。
  1. useブロックで使用するプロバイダごとの設定。(上記ソース抜粋参照)
  2. get '/auth/:provider/callback'処理を書く。
  3. '/auth/:provider'にアクセスさせる。
    リダイレクトなり、リンククリックなりご自由に。
これだけです。あっという間でした。get '/auth/:provider/callback'は以下のようになります。
get '/auth/twitter/callback' do
  auth_hash = request.env['omniauth.auth']
  login(auth_hash)
  redirect '/'
end
request.env['omniauth.auth']ハッシュの中にTwitterからもらってきた値が含まれているので、それを使ってログイン処理やユーザーの登録処理を行います。
※ loginメソッドの中で、初アクセスユーザーについてはRedisに情報を登録しています。
ENV['TWITTER_KEY']ですが、以下のようにしてherokuに環境変数を登録できます。localではymlを読むようにしちゃっています。
$ heroku config:add TWITTER_KEY=my_twitter_key
Redis
前から興味があったので使ってみました。heroku上ではRedis To Goというアドオンを使っています。一応WebSocketを用いたリアルタイムなアプリなので、処理が早いという噂のredis、使わざるを得ませんね。
redis自体はredisドキュメント日本語訳を読んで学びました。
Rubyからredisを使う際は、Ohmというライブラリを使いました。Ohmで保存されたユーザーのデータですが、面白い構造をしています。
redis> keys *
1. "User:2"
2. "User:uid:MTQ2MDA4Njk="
3. "User:all"
4. "User:1:_indices"
5. "User:2:_indices"
6. "User:uid:NzU3NTI1NjM="
7. "User:id"
8. "User:1"
9. "current_question"
redis> hgetall User:1
1. "uid"
2. "14600869"
3. "nickname"
4. "satococoa"
5. "image"
6. "http://a2.twimg.com/profile_images/1211141886/gravatar_normal.png"
7. "lat"
8. "35.6911965175872"
9. "long"
10. "139.77053279826234"
11. "created"
12. "2011-02-11 16:05:11 +0900"
13. "modified"
14. "2011-02-12 02:07:46 +0900"
15. "score"
16. "87"
redis> get User:id
"2"
User:idは主キーのためのauto_incrementなintegerが格納され、User:1, User:2というようなハッシュに登録されたデータが入っています。

まとめ

出来上がったアプリ自体はそう新しい感じではないのですが(とはいえ、複数人で同時に遊ぶと結構面白いとは思います)、WebSocketに関しては非常に未来を感じました。
作ってみて初めてわかる、この「わくわく感」です。面白いことがたくさんできそうだなぁという感触を得ることができました。

もう一つ感じたのが、開発者には今後ますます知識とモラルが必要になるなぁ、と言うことです。
どういうことかというと、いざやろうと思えば、僕はこういったアプリを通じてユーザーの現在地とTwitterアカウント、さらには特定の地域に関しての土地勘があるか否か、という情報を手に入れることができてしまいます。(最初はGeolocationも使おうと思っていたのですが、それはプライバシー的にまずい、と思ってやめた訳です。)
また、TwitterではなくてFacebookのアカウントでログインするように作ることも容易です。そうすると友人のデータや誕生日なども手に入ってしまいます。
もちろん接続の許可をTwitterなりFacebookからは求められ、そのときにユーザーは拒否することもできるのですが、深く考えずに「なんだかよくわからないから」と許可してしまうユーザーも少なからずいると思います。
その辺を悪用するようなサービスが多発した場合には、せっかく盛り上がってきたソーシャルなインターネットの世界への信頼が一気に損なわれてしまいます。肝に銘じて開発しなきゃなぁ、と感じた次第です。

そんなことを感じるきっかけと、楽しい開発のきっかけをくださったこのコンテストに感謝しています。ありがとうございました。

長くなってしまいました。ここまで読んでくださった方、どうもありがとうございました。せっかくなのでMap Chasingで10点くらい取るまで遊んでください。

heroku + sinatraでメールフォームを作ってみる

Salesforceに買収されたこともあり、最近またherokuが注目を浴びています。

多くのrubyistの間では数年前(?)からおなじみですが、これを機に知ったという方もいらっしゃると思います。
herokuを使うことでPHP並み、いやそれ以上に気軽にWebアプリケーションを作ることができます。ぜひその気軽さを知っていただきたいと思い、heroku + sinatraで簡単なメールフォームを作ってみました。
(ruby, git, sinatra, hamlあたりは一応ある程度知っているという前提。あまり難しいことはしていないので、全く知らなくても読めると思います。)

URL: http://simple-form.heroku.com/
github: https://github.com/satococoa/simple_form

キモとなるところを少し解説します。

ライブラリ(gem)の設定

herokuにデプロイした際に、必要なライブラリがちゃんとインストールされ、requireされるようにします。

Gemfile
source :rubygems
gem 'sinatra'
gem 'haml'
gem 'mail'
config.ru
require 'rubygems'
require 'bundler'

Bundler.require

require './app.rb'
run Sinatra::Application
Gemfileに書いたライブラリをインストールするには、以下のようにします。
$ bundle install --path bundle

これで、Gemfile.lockというファイルと、.bundle/, bundle/というディレクトリができます。
Gemfile.lockはgitでリポジトリに入れてください。.bundle/とbundle/は入れる必要はありません。

ローカルでの開発

では、肝心なアプリケーションをapp.rbに書きます。メール関連の設定をしているのは以下のコードです。
configure do
  set :root, File.dirname(__FILE__)
  set :haml => {:escape_html => true}
  set :mailto, ENV['MAILTO'] || 'YOUR_MAIL_ADDRESS' # ここで送信先を設定
end

configure :development do # 開発時用の設定
  Mail.defaults do
    delivery_method :smtp, {
      :address              => 'smtp.gmail.com',
      :port                 => 587,
      :domain               => 'localhost:9393',
      :user_name            => 'YOUR_NAME@gmail.com',
      :password             => 'YOUR_PASSWORD',
      :authentication       => :plain,
      :enable_starttls_auto => true,
    }
  end
end

configure :production do # heroku上で使われる設定
  Mail.defaults do
    delivery_method :smtp, {
      :address              => 'smtp.sendgrid.net',
      :port                 => 25,
      :domain               => ENV['SENDGRID_DOMAIN'],
      :user_name            => ENV['SENDGRID_USERNAME'],
      :password             => ENV['SENDGRID_PASSWORD'],
      :authentication       => 'plain',
      :enable_starttls_auto => true,
    }
  end
end
sinatraでの開発方法については特に書きません。shotgunというgemを使うと楽です。
また、開発時にはgmailのsmtpを使ってメールを送信するようにしてみました。YOUR_NAMEやYOUR_PASSWORDをご自分のものに書き換えてください。set :mailto, ENV['MAILTO'] || 'YOUR_MAIL_ADDRESS'のYOUR_MAIL_ADDRESSも同様に、メールの送信先を入力してください。

ENV['...']というところは環境変数です。heroku上で自動的に設定されるものもありますし、自分で任意に設定することもできます。ここではENV['MAILTO']が自分で設定したもの、ENV['SENDGRID...']がSendGridというアドオンを追加したときに自動的に設定されたものです。

Herokuにデプロイ

では、ここから実際にherokuとやり取りします。(ソースコードはgitでコミットをすませておいてください。)

$ gem install heroku
$ heroku create simple-form #など、適当なアプリ名。あとで変えられます。初めての場合はここでメールアドレスやパスワードを聞かれたりします。
$ heroku config:add MAILTO='YOUR_MAIL_ADDRESS'
$ heroku addons:add sendgrid:free # 追加する前にクレジットカードの認証が必要だったはずです。(sendgrid:freeは無料で使えます。)
$ heroku config # 環境変数が見られます。
$ git push heroku master # このコマンドでデプロイ。
$ heroku open

これだけです。これで、herokuへデプロイが完了し、自動的にアプリケーションがデフォルトのブラウザで開かれたはずです。

DBの操作などは他にもたくさん記事がありますので、敢えて解説記事の若干少なめなメールフォームを作ってみました。環境変数の設定やアドオンの追加なども(さらっと)盛り込んでみましたので参考になれば幸いです。