Sinatra で Google Reader の共有アイテムをつぶやくアプリを作ってみた

Google Reader の共有アイテムは PubSubHubbub というプロトコルに対応しており、リアルタイムで更新の通知を受け取る事ができます。

これを利用したアプリに、Reader2Twitter があります。
Google Reader の共有アイテムを追加すると、その内容を即座に Twitter へつぶやいてくれます。

ただ、Reader2Twitter は結構な頻度で止まってしまいます。
そこで、Sinatra で同じようなアプリを作ってみました。

PubSubHubbub の Subscriber の構築

PubSubHubbub は、

  • Publisher (コンテンツの発行者)
  • Subscriber (コンテンツの購読者)
  • Hub (両者の仲介役)

で構成されます。

Publisher は、Hub を介して Subscriber に新着フィードをプッシュします。

ここでは、3 者のうちの Subscriber を作ってみます。
(残り 2 つは Google から提供されているので、それを使います。)

PubSubHubbub の詳細についてはググッていただくとして、簡単に言うと、

Subscriber は、

  1. 対応する Publisher (フィード) の URL を
  2. Hub に対して購読要求し
  3. 認証手続きが済むと
  4. Hub から新着フィードの publish を受けることができる

というものです。

この 1 - 4 の流れを追って書いていきたいと思います。

1. Publisher (フィード) の URL

少しわかりにくいですが、URL は以下のように調べられます。

リーダー設定 -> フォルダとタグ -> 共有設定
と辿ります。

カスタム URL の下側のアドレスにアクセスします。

この "Atom フィード" の URL です。

2. 購読要求

Google の提供している Hub には、購読要求のための Web インタフェースがあるので、これを使います。
Hub - Subscription debug

  • Callback にアプリを動かす URL
  • Topic に 1 で調べた URL
  • Verify token にパスワード
  • HMAC secret に新着フィード署名用のキー

を入力します。

(Callback の URL に .rb が付いているのは、Ruby CGI で作っていた頃の名残です。)

3. 認証手続き

2 で "Do it" ボタンを押すと、"Callback" で指定した URL に、HTTP GET で認証手続きが飛んできます。
手続きに必要なパラメータは、URL のクエリストリングに格納されてくるので、適切に判断し、レスポンスを返します。

# 購読要求時の入力情報
greaderid = '01234567890123456789'
verify_token = 'homuhomu'

get '/sub.rb' do
  # 各パラメータの取得
  p_mode = params['hub.mode']
  p_topic = params['hub.topic']
  p_challenge = params['hub.challenge']
  p_verify_token = params['hub.verify_token']

  if p_mode == 'subscribe' && p_topic.include?(greaderid) && p_verify_token == verify_token then
    # レスポンスボディにチャレンジを格納
    p_challenge
  else
    # サーバエラーを返す
    status 500
  end
end

上記が、認証手続きに対応するためのコードです。

"greaderid" には、(1) で調べた URL の番号部分を指定してください。

"Topic" で指定した URL、"Verify token" で指定したパスワードが共に正しければ、クエリストリングの hub.challenge をボディに格納してレスポンスを返します。
この認証手続きは、定期的に行われます。

4. 新着フィードの受信

新着フィードは、HTTP POST されます。
ボディにフィードの xml が格納されているので、これを解析して使います。

# 購読要求時の入力情報
secret = 'foofoo'

post '/sub.rb' do
  # リクエストボディの取得
  payload = request.body.read

  # X_Hub_Signature ヘッダの値を取得
  hub_sig = request.env['HTTP_X_HUB_SIGNATURE']
  # HMAC-SHA1 の計算
  my_sig = OpenSSL::HMAC::hexdigest(OpenSSL::Digest::SHA1.new, secret, payload)

  if hub_sig.include?(my_sig) then
    # xml の解析
    doc = REXML::Document.new(payload.gsub('<br>', '<br />'))
    title = doc.elements['/feed/entry/title'].text
    link = doc.elements['/feed/entry/link[@rel="alternate"]'].attributes['href']
    # コメントはない場合もあるので分けて処理
    comment_ele = doc.elements['/feed/entry/gr:annotation/content']
    comment = comment_ele ? comment_ele.text : ''

    # つぶやく
    t = Tweet.new
    begin
      t.tweet(title, link, comment)
    rescue
      true
    end
  end

  # 成功を返す
  status 200
end

上記がフィードを処理する部分です。

購読要求時、"HMAC secret" に入力した値をキーにして、X-Hub-Signature ヘッダに、ペイロードの HMAC-SHA1 ハッシュが付加されます。
ヘッダのハッシュと、自分で計算したハッシュが一致する場合、続く処理を行います。
ハッシュが違っていても、status 200 を返す必要があるので、注意。

さいごに

subscriber の実装についての記事は、調べてみるとちょくちょく見つかります。
でも、publish 時の署名についてまで言及しているものは少なかったので、書いてみました次第です。

Reader2Twitter さん今までお世話になりました。
感謝の気持ちを込めて、Donate しておきました。:D

参考