( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

ついったーのStreaming API ChirpUserStreamとWebSocketを組み合わせてみた

最近の流れでは、時代はHTML5なんですかね?


そんなわけで、前にちょっとやってみたJetty7のWebSocketと、ついったーのStreaming API ChirpUserStreamを組み合わせて簡単なWebベースのついったクライアントを書いてみました。


スクリーンショット:
f:id:yuroyoro:20100510185832p:image


動いているところ:


動作している動画を見てもらえば分かるんですが、Streamingで受信したイベントを上から"落とす"ようなUIにしてみました。
TLをちゃんと読むのではなく、ぼーっとみてるようなコンセプトです。実際に使うならこんなUIしんどいでしょうけどまぁ実験あぷりなんで。


(もしスクリーンショットや動画に自分のPOSTが表示されていて、消して欲しい場合は、お手数ですがコメントお願いします。)


ChirpUserStreamなんで、他の人がふぁぼったりフォローしたりという情報も落ちてきますよ。


ソースはこんなんです。

Server側:

package com.yuroyoro.websocket

import java.io.{InputStream, IOException}
import java.net.{Authenticator, PasswordAuthentication, URL, HttpURLConnection}
import javax.servlet.http._
import org.eclipse.jetty.websocket._
import org.eclipse.jetty.websocket.WebSocket.Outbound

class ChirpUserStreamsServlet extends WebSocketServlet {

  override def doGet(req:HttpServletRequest, res:HttpServletResponse ) =
    getServletContext.getNamedDispatcher("default").forward(req, res)

  override def doWebSocketConnect(req:HttpServletRequest, protocol:String ) =
    new ChirpUserStreamsWebSocket

  class ChirpUserStreamsWebSocket extends WebSocket {
    private val chirpUserStreamingURL = "http://chirpstream.twitter.com/2b/user.json"

    // BASIC認証orz
    private def setBasicAuth( username:String, passwd:String ) =
      Authenticator.setDefault( new Authenticator {
        override def getPasswordAuthentication =
          new PasswordAuthentication(username,  passwd.toCharArray)
      })

    private def connectSteaming( url:String ) = {
      val urlConn = new URL( url ).openConnection match {
        case con:HttpURLConnection => {
          con.connect()
          con.getResponseCode match {
            case 200 => { }
            case c =>
              throw new RuntimeException( "can't connect to %s : StatusCode = %s" format ( url, c))
          }
          con
        }
      }
      urlConn.getInputStream
    }

    def consume(in:InputStream)( f:Option[String]=>Unit){
      val buf = new Array[Byte](1024)
      var remains:String = ""
      try{
        // InputStreamから1行読んでfにわたす
        for(i <- Stream.continually(in.read(buf)).takeWhile(_ != -1)){
          val str = remains + new String(buf,  0,  i)
          remains = ( "" /: str){ (s,c) =>
            if( c == '\n'){
              f( Some(s) )
              ""
            }
            else s + c
          }
        }
     }
     catch{ case e:IOException => }
     finally{ in.close }
    }

    var outbound:Outbound = _

    override def onConnect(outbound:Outbound ) = this.outbound = outbound

    override def onMessage(frame:Byte, data:Array[Byte], offset:Int, length:Int ) = {}

    override def onMessage(frame:Byte, data:String ) = {
      // usernameとpasswdが送られてくるので切り出す
      val m = Map( data split('&') map{  _.split('=') match { case Array(k,v) => (k,v)}} : _* )
      m.get("username") foreach { u => m.get("passwd") foreach { p =>
        streaming( frame, u, p )
      }}
    }

    def streaming( frame:Byte, username:String, passwd:String ) = {
      setBasicAuth( username, passwd )  // BASIC認証設定する
      // ChirpUserStreamに接続
      val con = connectSteaming( chirpUserStreamingURL )
      // JSONをクライアントに送る
      consume( con ) { o => o match{
        case Some( s ) => {
          println(s)
          println("----------------------- %s Bytes. -------" format( s.length ) )
          outbound.sendMessage( frame , s )
        }
        case None =>
      }}
    }

    override def onDisconnect = {}

  }
}

InputStreamから一行JSONを読んだら、consumeメソッドの第2引数の f:Option[String]=>Unit)な関数オブジェクトに渡してます。その関数オブジェクトでクライアントにJSONをプッシュ!してる感じですね。


ChirpUserStreamsは今のところBasic認証のみでOAuthはサポートしてないんだって。
まだちゃんとローンチされてないAPIだからかな。


クライアント:

<!doctype html>
<html>
  <head>
  <meta charset="UTF-8">
  <title>WebSocket Twitter ChirpUserStreams </title>

   <link href="./style.css" media="screen, projection" rel="stylesheet" type="text/css">

</head>
<body>

  <form id="account">
    <span class="title">WebSocket Twitter ChirpUserStreams </span>
    username:<input name="username" type="text"/>
    password:<input name="passwd" type="password"/>
    <input id="start_button" type="submit" value="Start!"/>
    <input id="stop_button" type="button" value="Stop!"/>
  </form>

  <hr/>

  <section id="content"></section>

  <script src="http://www.google.com/jsapi"></script>
  <script>google.load("jquery", "1.4.1")</script>
  <script>
    var ws = new WebSocket("ws://localhost:8080/");

    var lean = {
      i:0,
      next:function(){
        var n = this.i * 320;
        this.i = (this.i + 1) % 5;
        return n;
      }
    }

    var createUserInfo = function ( json ){
      var e = $("<span/>").addClass("user_info")
        .append( $("<span/>" ).addClass("profile_img")
          .append($("<img/>" ).attr("src" , json.profile_image_url ) ))
        .append( $("<a/>" ).addClass("user_name")
          .attr("href", "http://twitter.com/" + json.screen_name )
          .append( json.screen_name + "(" + json.name + ")" ));

      return e;
    };

    var pushEvent = function( e ) {
        $(e).hide()
        $('#content').prepend( e );
        $(e).css({
            position : "absolute",
            top : 50,
            left: 20 + lean.next()
        });
        $(e).show("normal",
          function(){
            $(e).animate({"top" : "+=1200px"},15000,
               function(){ $(e).hide();$(e).remove();})
            }
        );

    };

    var start = function(){
      if( !ws ){
        ws = new WebSocket("ws://localhost:8080/");
      }

      ws.onmessage = function(m) {
        var json = $.parseJSON( m.data );

        if( json.text ){
          var e = $("<div/>").addClass("event").addClass("posted")
            .append( createUserInfo ( json.user ) )
            .append( $("<span/>" ).addClass("event_name").append( " : posted" ) )
            .append( $("<hr/>" ) )
            .append( $("<span/>" ).addClass("message")
                  .append( json.text ));
          pushEvent( e );
        }
        if( json.event ) {
          var t = $("<div/>" ).addClass("target")
            .append( createUserInfo ( json.target ) );
          if( json.target_object ) {
            t.append("<br/>")
              .append( $("<span/>" ).addClass("message")
              .append( json.target_object.text ));
          }

          var e = $("<div/>").addClass("event").addClass( json.event )
            .append( createUserInfo ( json.source) )
            .append( $("<span/>" ).addClass("event_name").append( " : " + json.event ) )
            .append( $("<hr/>" ))
            .append( t );

          pushEvent( e );
        }
      };

      ws.send( $("form").serialize() );
    };

    var stop = function(){
      if( ws ) {
        ws.close();
        ws = null;
      }
    };

    $('#start_button').click( function(){start();return false;});
    $('#account').submit( function(){start();return false;});
    $('#stop_button').click( function(){ stop();return false;} );
    $(window).unload( stop );

  </script>
</body>

ws.onmessageに登録したfunctionで、サーバーからプッシュされたJSONをパースして、DOMを組み立ててjQueryでアニメーションして落としてます。
このクライアントの方が作るの大変だったorz


動作するプロジェクトはGitHubに置いてあります。
git clone して、以下のコマンドで起動しますよ(要maven)。


scala-websocket/ChirpUserStreams at master · yuroyoro/scala-websocket · GitHub

mvn clean package scala:run -DaddArgs="target/websocket.chirpuserstreams-1.0/|8080"


参考情報:
Jetty7のWebSocketをScalaから使う - ゆろよろ日記
Page not found | Twitter Developers
Twitterの新しいStreaming API「ChirpUserStreams」がすごすぎる件 - すぎゃーんメモ