Pinboard にブックマークしたらはてなブックマークに同期するやつを作った。

morygonzalez/pinboard2hatena: Sync はてなブックマーク with Pinboard

なんで Pinboard を使うのか、どうしてはてブに一本化しないのかというと、洋物のサービスを使うときに Pinboard の方が使いやすいから。 IFTTT に Pinboard 連携機能はあってもはてブ連携機能はないし、 Delibar や Reeder や ReadKit も Pinboard には対応しているけどはてブには対応してない。あと Pinboard は広告出ないしホッテントリ的なものもないので気がついたら Amazon で買い物してたとかホッテントリの海に溺れてた、ということも起こらない。とはいえはてブのコメントでキャッキャッウフフはしたい。なのでブックマークするときは両方にしたい。パソコンの Chrome からブックマークするときは Taberareloo でクロスポストできるのだけど、最近 iPhone からブックマークすることが増えて(iPhone 進化し過ぎて仕事するときしかパホコン使わなくなった) Pinboard とはてブにそれぞれブックマークするのがだるかった。そういうわけで Pinboard をメインにしつつはてブに同期を試みた。

はてなスタッフの aereal さんが作ってるはてブ API 用の gem (aereal/hatena-bookmark-restful: A client library for Hatena::Bookmark RESTful API)あって使わせてもらったのだけど、これはそのままだと使えない。タグが複数あるケースに対応してない& User-Agent を送らないので API 側から 401 Unauthorized が返ってくる。なので雑にモンキーパッチした。

はてなブックマークの API は tag を 10 個まで設定できるけど、以下のように Request Body が Encode されるのを期待しているっぽい。

"comment=&tags=ruby&tags=http&url=https%3A%2F%2Fgithub.com%2Flostisland%2Ffaraday"

この様な Request パラメーターを Ruby で表現すると以下のような Hash になると思う。

params = {
  comment: '',
  tags: ['ruby', 'http'],
  url: 'https://github.com/lostisland/faraday'
}

Hash は当然のことながら同じキーを複数持つことはできないから、 tags は配列として表現される。これをこのまま Faraday (Ruby の HTTP クライアント。上述の gem でも使われてる)に渡して Request Body を生成するとエラーになってしまうのだった。

最近の Faraday には encode option が追加されて FlatParamsEncoder というのを選べるようになってた。こいつを使うと配列を value に持つ Hash をシリアライズしたときに tags=foo&tags=bar みたいな形式にしてくれる。加えて User-Agent も載せるようにもした。こんな感じ。

class Hatena::Bookmark::Restful::V1
  def create_bookmark(bookmark_params)
    res = connection.post("/#{api_version}/my/bookmark") {|req|
      req.params = bookmark_params
    }
    attrs = JSON.parse(res.body)
    bookmark = Bookmark.new_from_response(attrs)
  end

  private

  def connection
    @connection ||= Faraday.new(url: 'http://api.b.hatena.ne.jp/') do |conn|
      conn.request :url_encoded
      conn.options.params_encoder = Faraday::FlatParamsEncoder
      conn.request     :oauth, {
        consumer_key:    @credentials.consumer_key,
        consumer_secret: @credentials.consumer_secret,
        token:           @credentials.access_token,
        token_secret:    @credentials.access_token_secret
      }
      conn.headers['User-Agent'] = 'Hatena::Bookmark::Restful Client'
      conn.adapter Faraday.default_adapter
    end
  end
end

なおはてブの API はリクエストパラメーターが不正なとき 400 Bad Request を返すのではなく 401 Unauthorized を返す。のみならず User-Agent なしのリクエストに対しても 401 を返す。一方で OAuth ヘッダーが不正なときは 400 を返す。原因の切り分けがむずかしくなるので、リクエストパラメータが不正なときは 400 を返して欲しいし認証できないときは 401 を返して欲しい。

Archives ページは部分的に React.js を使ってる。これを全部 React にして SPA 化してみたい。そのためには Server Side Rendering が必要。

Ruby で Server Side Rendering するライブラリでは react-rails が有名だけど、 Rails 以外で Server Side Rendering したい。

有名なやつでは airbnb の hypernova というのがあった。これ自体は Node.js で、 hypernova-ruby というクライアントからサーバーを呼び出して Server Side Rendering するというもの。なのでサーバー側に一個プロセスが増える。開発環境でも Node.js でプロセス起動したりしないといけない。仕事で Microservice には食傷気味になってきてるので趣味では Majestic Monolith™ で行きたい。

名前似てるけど Hyper-Ract というやつもあった。これは過激で JavaScript とか HTML を排除して Ruby で全部書こうぜという Opal が React 対応したもの。良いんだけど F/E の進化早すぎ問題に対応できなくなる気がする。数年後には CoffeeScript みたいになりそうな予感がある。スタンダードでシンプルなものを使いたい。

もう一個、スター全然付いてないしメンテナンスもされてなさそうなんだけど、 V8 で React 読み込んで出力するだけという簡単なのもあった。react-ruby-v8 という直球な名前。こんなので十分かもしれない。

Pull Request 、レビューしないといけないものが貯まってしまってつらいと感じることが多かった。そういうつらみを解消するための ruboty プラグイン作った。

morygonzalez/ruboty-check_pr_please

ruboty-cron と組み合わせて

@ruboty [GitHub の Issue のラベル名]のPull Request

という感じで job 登録しておくと、毎時決まった時間になったら Bot が Pull Request のレビューを促す。

便利。

使い方

  1. ruboty の Gemfile に gem 'ruboty-check_pr_please', github: 'morygonzalez/ruboty-check_pr_please' と追加する
    • 定期的に動かすためには ruboty-cron もいる
  2. ruboty の .env に GITHUB_ACCESS_TOKENGITHUB_REPOSITORY='owner/repo' を書く
  3. ruboty をデプロイする

開発時に苦労した点

ruboty 、 rubory とタイポしてしまうこと多くてつらかった。

Fukuoka.rbで Ruby で連続するダブルクオートの扱いについて @nagachika さんに聞いたら面白い話が聞けた。

Lokka のテストで FactoryGril でフィクスチャーデータ作ってるところに、以下のような文字列があった。

  factory :post do
    association :user
    sequence(:title){|n| Test Post #{n}” }
    body <p>Welcome to Lokka!</p><p><a href=“”/admin/“”>Admin login</a> (user / password : test / test)</p>”
    type ‘Post’
    created_at create_time
    updated_at update_time
  end

この body の部分で、ダブルクオートが連続して書いてある場所があって、ここがらみでテストを流してたら失敗する現象に遭遇した。テストに失敗したときのメッセージは以下。

Failures:

  1) Post markup default should == “<p>Welcome to Lokka!</p><p><a href=/admin/>Admin login</a> (user / password : test / test)</p>”
     Failure/Error: it { post.body.should == post.raw_body }
       expected: “<p>Welcome to Lokka!</p><p><a href=/admin/>Admin login</a> (user / password : test / test)</p>”
            got: “<p>Welcome to Lokka!</p><p><a href=\”/admin/\”>Admin login</a> (user / password : test / test)</p>” (using ==)
     # ./spec/unit/post_spec.rb:56:in `block (4 levels) in <top (required)>’

なんか expect の方で HTML 内のダブルクオートが省略されてる。ダブルクオートが連続してるのが怪しいなと思って、文字列内でダブルクオートが連続したら、一つ目の はエスケープ文字列みたいな扱いになるのかなと思った。

しかし実はそうではなくて、 @nagachika さんの説明によると、ダブルクオートが連続した場合、一つ目の " で文字列の終端と判定される。すぐ右隣の " は新しい文字列の開始と見なされ、結果としては文字列として連結されるらしい(C 言語由来の慣習とのこと)。

たとえば、次のような文字列は

<p>Welcome to Lokka!</p><p><a href=“”/admin/“”>Admin login</a> (user / password : test / test)</p>”

“<p>Welcome to Lokka!</p><p><a href=“”/admin/“”>Admin login</a> (user / password : test / test)</p>” という三つの文字列と見なされ、クオートとクオートの間に何も挟まらないので自動的に一つの文字列として連結され、以下のようになる。

<p>Welcome to Lokka!</p><p><a href=/admin/>Admin login</a> (user / password : test / test)</p>”

これ、なぜいままで Lokka でこの状態で CI のテスト通ってたのかわからないけど、今日 wercker で CI の設定していてテストが通らなくてこの問題に気がついた。同じように手元でテスト実行しても落ちた。ひょっとすると Ruby 2.1.0 で落ちるのかも知れない。いずれにせよ "" は使わない方が良さそうなので修正して Pull Request 出そう。

@udzura さんがペーパーボーイ社福岡支部に移ってきたこともあって、約半年振りに Fukuoka.rb をやった。前来てたメンバーに加えて初めて参加した人もいた。今日は今後の活動方針なんかを決めた。毎週開催は負担が大きいので隔週へ、継続が大事なので本読みというよりルビー好きな人が集まっておしゃべりしてるような緩やかな会へ、というような提案があった。ルビーコミッターの @nagachika さんが定例を強く復活させたいとおっしゃっていて熱かった。 Asakusa.rb みたいな感じでやれたらいいなと思う。ただ初めて参加した人からは、場所が特定の会社の会議室だと参加しにくい、 AIP カフェなどのパブリックなスペースで、気軽に参加して発表を聞けるような場を設けてはという意見も出たので来年なんかやりましょうという話になった。

自分はこれまでの Facebook での開催告知とかコミュニケーションがあまり好きじゃなかったので、開催告知は Doorkeeper 、コミュニケーションは Lingr でやることになったのが良かった。

@chikanaga さん、 @udzura さんの二大著名ルビーストのおかげで盛り上がりそうな気配ある。第一期は自分の力不足でポシャってしまったけど、これから良いコミュニティになっていけば良いなと思う。

ということで、毎月第二第四木曜の19時から、グルーブノーツ社とペーパーボーイ社で交互に場所をホストしてやることになりましたので福岡市近辺のルビーストの皆さんはぜひお気軽にお越しください。本とか読んでないのでいきなり来ても大丈夫です。

[1] pry(2.0.0-p195)> require 'open-uri'
[2] pry(2.0.0-p195)> require 'active_support/core_ext/hash/conversions'
[3] pry(2.0.0-p195)> Hash.from_xml(open('http://www.portalshit.net/index.atom').read)
=> {"feed"=>
  {"xmlns"=>"http://www.w3.org/2005/Atom",
   "id"=>"http://www.portalshit.net/",
   "title"=>"portal shit!",
   "updated"=>"2013-06-18T19:07:44+09:00",
   "link"=>
    [{"type"=>"text/html",
      "rel"=>"alternate",
      "href"=>"http://www.portalshit.net/"},
     {"type"=>"application/atom+xml",
      "ref"=>"self",
      "href"=>"http://www.portalshit.net/index.atom"}],
   "entry"=>
    [{"id"=>"tag:www.portalshit.net,2013-06-18T19:04:33+09:00",
      "title"=>"Alfred で Chrome のブックマークを検索する",
      "published"=>"2013-06-18T19:04:33+09:00",
      "updated"=>"2013-06-18T19:07:44+09:00",
      "link"=>
       {"type"=>"html",
        "rel"=>"alternate",
        "href"=>
         "http://www.portalshit.net/2013/06/18/chrome-bookmark-search-with-alfred"},
      "content"=>
       "<p><img src=\"https://resources.portalshit.net/945-chrome-bookmark-search-with-alfred.png\" alt=\"945-chrome-bookmark-search-with-alfred.png (708×295)\
      "author"=>{"name"=>"morygonzalez"}},

で XML の内容を Hash にできる。ActiveSupport 便利。

昨日、こういう Pull Request が Lokka に来てた。

Fix broken spec in ruby 2.0 by 1syo · Pull Request #197 · lokka/lokka

「あれ、Lokka の master ブランチは Ruby 2.0 対応してないんじゃ」と思って Ruby 2.0.0-p0 で試してみたところ案の定 json.gem のインストールに失敗する。しかしものは試しにと思って、最新のパッチレベルの 2.0.0-p195 をインストールしてみたら Ruby 2.0 で Lokka 起動できた。

というわけでポータルシットは Ruby 2.0 で動いております。