冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

Unfiltered + HerokuでTwitter ToDoサービスを作る(Twitter APIを使ってみる編)

Twitter ToDoサービス(TwoDo)を作る連載2回目。
すみません、今回はheroku Add-onは使いません。というのも、よくよくドキュメントを見てみたら、使おうとしていたTwitter API用のAdd-onはAPI利用をアドオンを通すことで速度制限の改善をはかる目的で使用されるもののようです。小さいサービスで、そこまで広げる気もないので今回はAdd-onの利用は見送って、Twitter APIの利用に限定したお話にしようかと思います。
Twitter ToDoなので、何はともあれ自分のツイートを取得しなければお話になりません。というわけで今回は、Twitter APIを使って、自分で自分に出したメンションだけ取得してみようと思います。

Twitter4Jをビルド依存性に含める

Twitter APIをそのまま叩くことは当然できますが、世の中にはTwitter APIをラッピングしてくれているライブラリがあります。Twitter4JとかTwitter4JとかTwitter4Jとか。何と言ってもタイプセープに扱えるようになるのがうれしいですね。カターンゼン!
Javaの資産を有効利用できるのはScalaの魅力の一つです。まずは、前回のプロジェクトにTwitter4Jの依存性を追加するところから始めてみましょう。

Twitter4J - A Java library for the Twitter APIMaven統合の章を参考にして、アプリケーションビルド時にTwitter4Jの最新安定板を落としてきてビルドするようにしてみます。
まず、Twitter4Jのリポジトリを取得するためにbuild.sbtのresolveresを以下のように書き換えます。

resolvers += "twitter4j.org Repository" at "http://twitter4j.org/maven2"

続いて、プロジェクトに依存性を追加します。同じくbuild.sbtのlibraryDependenciesを以下のように書き換えます。

libraryDependencies ++= Seq(
   "net.databinder" %% "unfiltered-filter" % "0.5.3",
   "net.databinder" %% "unfiltered-jetty" % "0.5.3",
   "org.fusesource.scalate" % "scalate-core" % "1.5.3",
   "org.fusesource.scalate" % "scalate-util" % "1.5.3" % "test",
   "org.twitter4j" % "twitter4j-core" % "[2.2,)"
)

試しにsbt clean compile consoleした後に、Scalaコンソールから以下のようにタイプしてタブキーを叩いてみましょう。

scala> import t

サジェストされるパッケージの中にtwitter4jがあるはずです。
ここまでやったら、前回の注意事項のとおり、gen-ideaをもう一度叩いて(IDEAのsbtプラグインのコンソールからでも良いです)IDEA用のプロジェクトファイルを再生成します。そうすると、プロジェクトのExternal Libraryにtwitter4jが追加されます。

さて、ローカルでは上手く行きましたが、herokuへのデプロイとビルドはどうでしょうか?unfiltered-scalateの一件以来、外部ライブラリにはだいぶ神経質になってますね。
git push heroku masterを叩いてみるとビルドが上手く行くことがわかります。まずデプロイは問題ないようです。本当にライブラリはダウンロードされてビルドされているのでしょうか?heroku run sbt consoleを叩いて、デプロイしたアプリケーションのコンソールから確認してみましょう。動作は遅いですが、同様にサジェストされることがわかります。

これで、Twitter APIを使う準備が整いました。

Twitterのアプリケーション登録をする

Twitter APIを使ってOAuth認証をしたりアプリケーションを作ったりするためには、アプリケーション登録が必要です。
Twitterのアカウントがあれば、Twitter Application Managementから登録することができます。
新規登録時に必要な情報は以下の3点です。

  1. アプリケーションの名前
  2. アプリケーションの概要
  3. アプリケーションのWeb Site*1

ありがちなライセンス事項への同意やcaptchaなどの入力をして「Create your Twitter application」をクリックするとアプリケーションが登録されます。
アプリケーションのアクセスタイプ(Read onlyかRead and Writeかとか、OAuthのリクエストセッティングとか)などの設定は登録後に変更できるようです。初期値はRead onlyでリクエストセッティングはGETでした。まずはツイートを取得するところから始めるので、初期値のままとします。

Twitter APIを使う

まずは、Twitter4Jを使って直近のツイートを取得して表示させてみましょう。

import unfiltered.request._
import unfiltered.response._
import util.Properties
import twitter4j._

class App extends unfiltered.filter.Plan {
  def intent = {
    // get my last one tweet
    case req@GET(Path(Seg("lasttweet" :: Nil))) => {
      val twitter = new TwitterFactory().getInstance()
      val statuses = twitter.getUserTimeline("mao_instantlife")
      Ok ~> ResponseString(statuses.get(0).getText)
    }
    case GET(_) => Ok ~> ResponseString("Unfiltered on Heroku!")
  }
}

object Web {
  def main(args: Array[String]) {
    val port = Properties.envOrElse("PORT", "8080").toInt
    unfiltered.jetty.Http(port).filter(new App).run
  }
}

上記のソースで実行してみたところ、文字化けしました。
レスポンスヘッダを確認してみます。

HTTP/1.1 200 OK
Content-Length: 42
Server: Jetty(7.5.4.v20111024)

Content-Typeが指定されていないようです。ブラウザのエンコード指定をUTF-8に変更してみたところ、正しく表示することができました。
unfilteredのソースを(全部ではないですが)関係しそうな箇所を読んでみると、デフォルトの文字コードUTF-8で間違いないようなので、レスポンスヘッダを指定してあげれば良さそうです。
Appクラスのintentメソッドを変更します。

class App extends unfiltered.filter.Plan {
  def intent = {
    // get my last one tweet
    case req@GET(Path(Seg("lasttweet" :: Nil))) => {
      val twitter = new TwitterFactory().getInstance()
      val statuses = twitter.getUserTimeline("mao_instantlife")
      // Content-Typeを指定してやる
      // Content-Typeと一緒にcharsetも指定されるようになっている(デフォルトUTF-8)
      Ok ~> HtmlContents ~> ResponseString(statuses.get(0).getText)
    }
    case GET(_) => Ok ~> ResponseString("Unfiltered on Heroku!")
  }
}

実行してみると、文字化けせずにちゃんと表示されます。
レスポンスヘッダも以下のようにContent-Typeとcharsetが指定されています。

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 42
Server: Jetty(7.5.4.v20111024)

ところで、Twitter4JのgetUserTimeLineメソッドの戻り値はResponseList型です。そのままでは、Scalaのコレクションとして扱えないので、foreachやforallなど、リスト処理のためのメソッドが使えません。この辺り、先達諸氏はどうしてるんだろうと思って、こんなことをつぶやきました。

それに対するリプライがこちら。

また一つお利口さんになりました。教わってばかりで申し訳ないです。
というわけで、まずは、先のソースに scala.collection.JavaConverters._ をインポートします。
で、getUserTimeLineしているところを以下のように変更します。

      val statuses = asScalaBufferConverter(twitter.getUserTimeline("mao_instantlife")).asScala

これで、Twitter4Jからの取得結果をScalaのコレクションとして扱うことができるようになります。

自分で自分に出したリプライだけに絞り込む

次は検索APIを使ってみることにします。APIの利用に関しては、以下のサイトを参考にしました。

Twitter API 仕様書 日本語訳 第五十版 (2010年8月12日版)
GET search/tweets | Twitter Developers
The Search API | Twitter Developers

TwoDoでは、「自分が発信した、自分へのリプライで、かつハッシュタグ #SimpleTwoDo がついている*2」ツイートを取得します。
これを検索APIに渡す場合、以下のようなクエリになります。

from:mao_instantlife to:mao_instantlife #SimpleTwoDo

Twitter4JのQueryクラスのコンストラクタ引数に上記を指定してQueryオブジェクトを生成します。

後は、APIに渡すときの取得件数と、取得範囲のフィルタリングです。
上記のクエリを元に生成したQueryオブジェクトに以下をセットします。

query.setRpp(100)
query.setSince("2012-02-12")

ぶっちゃけこの辺りのセッティングは適当です。
どのくらいの頻度で使うか自分としてもよくわからないので、まずはMaxの100件取るようにしています。少し運用してみてからチューニングするかも。
また、sinceで日付をセットしているのは、不必要に過去分のツイートを読み込まないようにするためです(最終的なサービスでは、ユーザの最終アクセス日以降を取得して、未取り込みのもののみ取り込むようにします)。

今回、抽出条件についてはリプライに限定し、メンションは条件に含めていません。条件をあまり複雑にしたくないことと、特にやらなくてもユーザビリティにはあまり影響しないのでは?という判断からです。
TwoDoの仕様として、以下のようなタスクの飛ばし方は認識します。

逆に以下のようなタスクの飛ばし方は認識しません。

メンションは入っていますが、リプライになっていないためです。
もちろん、以下のように他のユーザが飛ばしたタスクは認識しません(ご協力感謝、すますん)。


これでToDoの元ネタを持ってくることはできました。ただし、取ってくるだけで、ユーザごとのタスク状況の永続化などはまだできません。
次は、Twitter APIからOAuth認証を使ったログインとheroku Add-onを使ってMongo-DBへのユーザデータの永続化までやってみたいと思います。

*1:サポート情報を載せてるサイトでも良さそう。私はとりあえず自分のはてダにしました

*2:自分へのリプライを他の用途にも使うことにも考慮して、アプリ用のハッシュタグを用意して用途を一意に絞り込むことを目的としています