誰得スクリプトで学ぶOAuthとRack(Rubyだよ)

元ネタ

ゆーすけべー先生が先日、以下のような誰得スクリプトを書いておられました。

Facebookのポスト内容をWebアプリケーションとして実行する - ゆーすけべー日記

まじ誰得w しかしよく見てみると勉強になる要素満載で、Rubyで書いたらOAuthとRackの勉強になるぞと思ったのでやってみた

スクリプト企画の全容

しかしそもそもこの謎スクリプト、企画の全容が謎w なので、まずは企画の全容を明らかにする。折角なので先生の著作Webサービスのつくり方に書かれている企画メソッドを借パクしてあてはめてみる。(名前は勝手にfacepostとする)

謎いw

内部設計(のようなもの)

折角なので内部設計(のようなもの)も真面目にやってみる。

1. ユースケース

こいつを最初に書いておくと「あれ?自分何作ってんだっけ?」という状態にならずに済むような気がする。今回の例の場合「誰の役にもたたない」という大変重要な点を見失わずに済む。

2. ユーザーシナリオ

次にユーザーがどういう体験をするのかを簡単に書く。ここまでやれば、大体作るもののイメージが明確になってくる。

3. 技術調査

作るにあたって、技術的に良くわからない箇所があるので、調査する。まず、facebookに投稿した内容を取得するにはどうすればよいのか?というのが良くわからないので、facebookのdeveloper siteを見る。

3-1. Facebook API

投稿に関する情報は以下のページ。どうやらaccess_tokenを取得する必要がある。
https://developers.facebook.com/docs/reference/api/post/

access_tokenを取得する方法は以下のページ。今回はlocalhostを使用するので、Server-side Loginの部分が該当箇所となる。数回リダイレクトしてaccess_tokenを取得することが分かった。
https://developers.facebook.com/docs/concepts/login/login-architecture/

3-2. Rack

今回はWebサービスとしてlocalhostで動かすので、WAFを使うか?使うなら何使うか?という調査・選択が必要。RailsSinatraを使うまでもない規模なので、その大元であるRackを単体で使ってみることにした。Rack単体で使うのは初めてなので、調査。調べてみると、Rackとは

・WebサーバとWAFをつなぐプログラム
・Rackアプリケーションに対してrackupコマンドを実行すれば、Webサービスが立ち上がる
RailsSinatraもRackアプリケーションとして動いている
・Rackアプリケーションとは、以下の用件を満たすもの

・callというメソッドを持っていること
・callメソッドの引数としてWebサーバからのリクエストを受けること
・callメソッドは,次の要素を含むレスポンスを返すること
  ・ステータスコード
  ・レスポンスヘッダ(Hash)
  ・レスポンスボディ(Array)

引用:第23回 Rackとは何か(1)Rackの生まれた背景:Ruby Freaks Lounge|gihyo.jp … 技術評論社

といったことが分かる。といってもよく分からないので、sandboxというフォルダを作って色々いじってみたです。

4. 簡単な仕様

設計の最後に、これから書くプログラムの簡単な仕様を書いた。ざっと以下のような感じ。

facebookの投稿idを引数にrackupするとlocalhost:9292が立ち上がる
・ユーザーがhttp://localhost:9292/にアクセスすると、OAuth認証が行われる
・コールバックURLはhttp://localhost:9292/callbackとする
・ユーザーがfacebookに投稿したコードは標準出力になんらかの文字列を出力するコードであることを期待する
・標準出力に出力された文字列をブラウザに表示する

実装

いよいよ実装。Rackアプリケーションの書き方や、標準入出力の扱い方など結構はまった。けど動いたよ!

config.ru
#coding: utf-8

require 'open-uri'
require 'json'
require 'pit'

config = Pit.get("facebook.com")
app_id = config["app_id"]
app_secret = config["app_secret"]
post_id = ARGV[0]

map '/' do 
  run lambda {|env|
    uri = URI.escape("#{env['REQUEST_URI']}callback")
    redirect_url = "http://www.facebook.com/dialog/oauth?client_id=#{app_id}&redirect_uri=#{uri}&scope=read_stream"
    [302, {'Content-Type' => 'text', 'location' => redirect_url}, []]
  }
end

map '/callback' do 
  run lambda {|env|
    # get access_token
    req = Rack::Request.new(env)
    code = req[:code]
    uri = URI.escape("#{req.base_url}#{req.path}")
    token_url = "https://graph.facebook.com/oauth/access_token?client_id=#{app_id}&redirect_uri=#{uri}&client_secret=#{app_secret}&code=#{code}"
    access_token = ''
    open(token_url) {|f| access_token = f.read}

    # get facebook post
    post_url = "https://graph.facebook.com/#{post_id}?#{access_token}"
    json = ''
    open(post_url) {|f| json = f.read}
    fb_post = JSON.parse(json)
    fb_message = fb_post['message']

    # exec facebook post as code
    # treat stdout as response body
    body = ''
    IO.pipe {|r, w|
      w.instance_eval fb_message
      w.close
      body = r.read
    }

    # create response
    res = Rack::Response.new {|r|
      r.status = 200
      r['Content-Type'] = 'text/html;charset=utf-8'
      r.write body
    }
    res.finish
  }
end

Github

できあがり!

以下のような感じでFacebookに投稿したコードをlocalhostで実行できた!

1. facebookにコードを投稿。その投稿IDを控える

2. 上記config.ruのあるディレクトリで以下のコマンドを実行
$ rackup 控えたID config.ru
3. ブラウザでhttp://localhost:9292/にアクセス
4. ぐははは!

※用法容量をよく守ってお使いください
※元ネタと若干仕様が変わっております

勉強になりましたNE!